Author:相忠良
Email: ugoood@163.com
起始于:June 8, 2018
最后更新日期:June 11, 2018java
声明:本笔记依据传智播客方立勋老师 Java Web 的授课视频内容记录而成,中间加入了本身的理解。本笔记目的是强化本身学习所用。如有疏漏或不当之处,请在评论区指出。谢谢。
涉及的图片,文档写完后,一次性更新。mysql
发sql时,把多个sql放在Start transaction
和commit
之间便可。spring
试验准备:sql
create table account( id int primary key auto_increment, name varchar(40), money float )character set utf8 collate utf8_general_ci; insert into account(name,money) values('aaa',1000); insert into account(name,money) values('bbb',1000); insert into account(name,money) values('ccc',1000);
如今,a向b转帐100元,操做以下:数据库
start transaction; update account set money=money-100 where name='aaa';
关掉链接,从新登陆数据库查看,aaa 帐户的 money 仍是 1000。
只有下面这样才行:apache
start transaction; update account set money=money-100 where name='aaa'; update account set money=money+100 where name='bbb'; commit;
执行到 commit,上面2条sql才算真正执行,而不是回滚,这就是事务(控制多条sql做为总体执行)。编程
rollback
能够手动回滚,而不是异常时,事务在数据库中自动回滚。设计模式
当Jdbc程序向数据库得到一个Connection对象时,默认状况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可以使用下列语句:
JDBC控制事务语句:服务器
Connection.setAutoCommit(false);
至关于 start transactionConnection.rollback();
rollbackConnection.commit();
commit程序中控制事务的例子以下:并发
public class Demo1 { /** a--->b 100 */ public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try{ conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); //start transaction; String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; st = conn.prepareStatement(sql1); st.executeUpdate(); int x = 1/0; // <-- 产生异常 st = conn.prepareStatement(sql2); st.executeUpdate(); conn.commit(); // commit }finally{ JdbcUtils.release(conn, st, rs); } } }
手动回滚,按下面例子,只想从第二条sql开始回滚,方法就是:
Savepoint
;例子以下:
public class Demo2 { public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; Savepoint sp = null; // 回滚点对象 try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // start transaction; String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; String sql3 = "update account set money=money+100 where name='ccc'"; st = conn.prepareStatement(sql1); st.executeUpdate(); sp = conn.setSavepoint(); // <-- 2. 设置回滚点 st = conn.prepareStatement(sql2); st.executeUpdate(); int x = 1 / 0; // <-- 1. 产生异常 st = conn.prepareStatement(sql3); st.executeUpdate(); conn.commit(); // commit } catch (Exception e) { e.printStackTrace(); conn.rollback(sp); // <-- 3. 回滚 conn.commit(); // <-- 4. 手动回滚后,必定要记得提交事务 } finally { JdbcUtils.release(conn, st, rs); } } }
若一个数据库号称支持事务,那它必然支持 ACID;反过来讲,若某数据库支持 ACID,那这个数据库也是支持事务的。
脏读:指一个事务读取了另一个事务未提交的数据。(最危险)
故事:这是很是危险的,假设 A 向 B 转账 100 元,对应 sql 语句以下所示:
update account set money=money+100 while name='b';
update account set money=money-100 while name='a';
当第 1 条 sql 执行完,第 2 条还没执行(A 未提交时),若是此时 B 查询本身的账户,就会发现本身多了 100 元钱。若是 A 等 B 走后再回滚,B 就会损失 100 元。
下面介绍的不可重复读和幻读,有些状况下是没问题的,但有时会有问题。
不可重复读:在一个事务内读取表中的某一行数据,屡次读取结果不一样。 也指读表中同一条数据,结果不一样。
故事:中国人民银行生成开启生成报表这个事务,报送克强总理1000亿RMB,在报送近平主席前,生成报表这个事务未结束期间,有客户存了200亿RMB并该客户完成了他的事务,如今又生成近平主席的报表显示为1200亿。问题出现了:两位领导要打架的。困惑就是:哪次查询时是准确的呢? 这就是不可重复读所产生的问题。
虚读(幻读):是指在一个事务内读取到了别的事务插入的数据,致使先后读取不一致。 也指所读的表的记录数在变化。
故事:人口普查系统正生成报表,开启了一个事务。该系统在这个事务中需生成多个报表。可能发生这样的事:生成第一个报表,显示中国有10亿人,但生成第二个报表期间,有人往数据库中插入了数据,统计结果显示有11亿人。困惑来了:到底以哪一个为准呢?这就是幻读产生的问题。
根据上节介绍的,若无隔离性,数据库可能出现的三种问题,针对问题的解决,提出了事务隔离级别。隔离级别的提出,主要在解决问题的基础上,尽量的不过多损失数据库性能。
数据库共定义了四种隔离级别:
事务隔离性的设置语句:
set transaction isolation level
设置事务隔离级别select @@tx_isolation
查询当前事务隔离级别方立勋老师开启了2个mysql客户端,进行了模拟。模拟过程这里不表述了。
编程序时,得到的 connection:
编程中,用JDBC设置隔离级别: conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
示例代码:
public class Demo3 { public static void main(String[] args) throws SQLException, InterruptedException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; Savepoint sp = null; try{ conn = JdbcUtils.getConnection(); //mysql repeatable read conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); conn.setAutoCommit(false); //start transaction; String sql = "select * from account"; conn.prepareStatement(sql).executeQuery(); Thread.sleep(1000*20); conn.commit(); }finally{ JdbcUtils_DBCP.release(conn, st, rs); } } }
下图展现了无数据库链接池时的缺点:
下图是有链接池的情形:
有链接池后,数据库就没必要为每一个用户建立链接,而仅仅在一开始生成一些链接(假如20个),并将这些链接放入链接池,其余用户只从池中拿链接,用完后还到池中。(这个故事主要考虑,数据库本身建立1个链接需消耗不少资源,10万用户申请,就建立10万次链接,数据库自己作本职工做就很繁忙,再去频繁地建立若此多的连接,数据库极有可能被累死!咱们要作的是尽可能减轻数据库服务器的负担。)
故事:
咱们但愿执行conn.close();
时,链接还回链接池,但事实是conn是mysql提供的连接,执行close方法时,那个链接将还给mysql,而不是链接池。
当发现对象的方法不够咱们用时,咱们需加强那个方法。办法有:
一般子类的方式不可行,缘由是很难将父类对象信息导入子类对象中,除非父类对象封装的信息极少。
包装设计模式步骤(我本身的经验,想象一下BufferedReader
的用法,就是用构造函数接收被包装对象):
包装模式例子:
class MyConnection implements Connection{ // step 1 private Connection conn; // step 2 public MyConnection(Connection conn){ // step 3 this.conn = conn; } public void close(){ // step 4 list.add(this.conn); } // step 5 @Override public void commit() throws SQLException{ this.conn.commit(); // 调用的是 mysql 提供的 commit 方法 } @Override public void clearWarnings() throws SQLException{ this.conn.clearWarnings(); // 调用的是 mysql 提供的 clearWarnings 方法 } /* ... ... 后面不想加强的方法均照 step 5 处理,极有可能代码量超大,这也是包装模式处理此类问题的缺点 */ }
使用经包装(装饰)后的conn对象:
MyConnection my = new MyConnection(conn);
当咱们用my
这个连接对象时,它的close方法就是咱们本身写的方法了。
下面代码时动态代理方式(这里仅作个记录):
proxyConn = (Connection) Proxy.newProxyInstance(this.getClass() .getClassLoader(), conn.getClass().getInterfaces(), new InvocationHandler() { // 此处为内部类,当close方法被调用时将conn还回池中,其它方法直接执行 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("close")) { pool.addLast(conn); return null; } return method.invoke(conn, args); } });
数据源 = 数据库链接池
常见开源数据库链接池有:
若想用 Apache DBCP,应用程序应增长以下 2 个 jar 文件:
下面是 dbcp-1.2.2 开发包中的 dbcpconfig.properties文件(实验时,需将该文件 copy 到 src 目录下),其做用同之前咱们本身写的 db.properties 同样,是存放配置 dbcp 链接哪一种数据库、url、用户名、密码等信息的一种配置文件。以下:
#链接设置 driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/jdbc username=root password= #<!-- 初始化链接 --> initialSize=10 #最大链接数量 maxActive=50 #<!-- 最大空闲链接 --> maxIdle=20 #<!-- 最小空闲链接 --> minIdle=5 #<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 --> maxWait=60000 #JDBC驱动创建链接时附带的链接属性属性的格式必须为这样:[属性名=property;] #注意:"user" 与 "password" 两个属性会被明确地传递,所以这里不须要包含他们。 connectionProperties=useUnicode=true;characterEncoding=utf8 #指定由链接池所建立的链接的自动提交(auto-commit)状态。 defaultAutoCommit=true #driver default 指定由链接池所建立的链接的只读(read-only)状态。 #若是没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix) defaultReadOnly= #driver default 指定由链接池所建立的链接的事务级别(TransactionIsolation)。 #可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE defaultTransactionIsolation=READ_COMMITTED
从新设置 JdbcUtils.java,用链接池的方式:
package cn.wk.utils; import java.io.InputStream; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import javax.sql.DataSource; import org.apache.commons.dbcp.BasicDataSourceFactory; public class JdbcUtils_DBCP { private static DataSource ds = null; static { try { // 读配置文件 dbcpconfig.properties InputStream in = JdbcUtils_DBCP.class.getClassLoader() .getResourceAsStream("dbcpconfig.properties"); Properties prop = new Properties(); prop.load(in); BasicDataSourceFactory factory = new BasicDataSourceFactory(); ds = factory.createDataSource(prop); } catch (Exception e) { throw new ExceptionInInitializerError(e); // 异常转换成错误 } } public static Connection getConnection() throws SQLException { return ds.getConnection(); // dbcp conn.close() commit() } public static void release(Connection conn, Statement st, ResultSet rs) { // 模板代码 if (rs != null) { try { rs.close(); } catch (Exception e) { e.printStackTrace(); } rs = null; } if (st != null) { try { st.close(); } catch (Exception e) { e.printStackTrace(); } st = null; } if (conn != null) { try { conn.close(); } catch (Exception e) { e.printStackTrace(); } } } }
C3P0 的jar包在c3p0-0.9.2-pre1
中,导入以下2个jar包:
C3P0数据源配置文件名为c3p0-config.xml
,可放在src目录下,C3P0本身会找到它。
c3p0-config.xml
例子以下:
<c3p0-config> <default-config> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/day16</property> <property name="user">root</property> <property name="password">root</property> <property name="initialPoolSize">10</property> <property name="maxIdleTime">30</property> <property name="maxPoolSize">20</property> <property name="minPoolSize">5</property> <property name="maxStatements">200</property> </default-config> <named-config name="mysql"> <property name="acquireIncrement">50</property> <property name="initialPoolSize">100</property> <property name="minPoolSize">50</property> <property name="maxPoolSize">1000</property><!-- intergalactoApp adopts a different approach to configuring statement caching --> <property name="maxStatements">0</property> <property name="maxStatementsPerConnection">5</property> </named-config> <named-config name="oracle"> <property name="acquireIncrement">50</property> <property name="initialPoolSize">100</property> <property name="minPoolSize">50</property> <property name="maxPoolSize">1000</property><!-- intergalactoApp adopts a different approach to configuring statement caching --> <property name="maxStatements">0</property> <property name="maxStatementsPerConnection">5</property> </named-config> </c3p0-config>
最上面的<default-config>
是默认配置,使用方法以下:
ComboPooledDataSource ds = new ComboPooledDataSource();
若想用<named-config name="oracle">
的配置,使用方法以下:
ComboPooledDataSource ds = new ComboPooledDataSource("oracle");
看起来很是方便。
完整的 C3P0 链接创建代码JdbcUtils_C3P0
以下:
package cn.wk.utils; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import com.mchange.v2.c3p0.ComboPooledDataSource; public class JdbcUtils_C3P0 { private static ComboPooledDataSource ds = null; static { try { ds = new ComboPooledDataSource(); } catch (Exception e) { throw new ExceptionInInitializerError(e); // 异常转换成错误 } } public static Connection getConnection() throws SQLException { return ds.getConnection(); } public static void release(Connection conn, Statement st, ResultSet rs) { // 模板代码 if (rs != null) { try { rs.close(); } catch (Exception e) { e.printStackTrace(); } rs = null; } if (st != null) { try { st.close(); } catch (Exception e) { e.printStackTrace(); } st = null; } if (conn != null) { try { conn.close(); } catch (Exception e) { e.printStackTrace(); } } } }
测试代码:
public class Demo4 { public static void main(String[] args) throws SQLException, InterruptedException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils_C3P0.getConnection(); System.out.println(conn.getClass().getName()); } finally { JdbcUtils_C3P0.release(conn, st, rs); } } }
元数据:数据库、表、列的定义信息。
Connection.getDatabaseMetaData()
DataBaseMetaData对象
ParameterMetaData对象,获取 sql 语句参数的元数据。
以上2个元数据对象例子以下:
public class Demo5 { public static void main(String[] args) throws SQLException { Connection conn = JdbcUtils_C3P0.getConnection(); // 获取数据库的元数据 DatabaseMetaData meta = conn.getMetaData(); System.out.println(meta.getDatabaseProductName()); // 获取参数元数据 String sql = "insert into user(id,name) values(?,?)"; PreparedStatement st = conn.prepareStatement(sql); ParameterMetaData para_meta = st.getParameterMetaData(); System.out.println(para_meta.getParameterCount()); System.out.println(para_meta.getParameterType(1)); // mysql不支持得到类型,抛异常 } }
ResultSetMetaData对象(重要,后面案例用到),结果集元数据:
准备:
模拟环境,先弄一个cn.wk.domain.Account
的javabean:
package cn.wk.domain; public class Account { private int id; private String name; private double money; public int getId() {return id;} public void setId(int id) {this.id = id;} public String getName() {return name;} public void setName(String name) {this.name = name;} public double getMoney() {return money;} public void setMoney(double money) {this.money = money;} }
dao 层方法大体代码:
注意到:crud 变化的是 sql 和 st.set 其他代码均相同
public void add(Account a) throws SQLException{ Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils_DBCP.getConnection(); String sql = "(?,?,?)"; st.setInt(1, a.getId()); st.setString(2, a.getName()); st.setDouble(3, a.getMoney()); st.executeUpdate(); } finally { JdbcUtils_DBCP.release(conn, st, rs); } } public void delete(int id) throws SQLException{ Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils_DBCP.getConnection(); String sql = "delete from where id=?"; st.setInt(1, id); st.executeUpdate(); } finally { JdbcUtils_DBCP.release(conn, st, rs); } }
如今要作优化,抽出相同的部分。
重写了cn.wk.utils.JdbcUtils
,重点在该工具类的release方法的后面, 涉及到如下知识点:
本身 = 框架编写者
package cn.wk.utils; import java.io.InputStream; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import javax.sql.DataSource; import org.apache.commons.dbcp.BasicDataSourceFactory; public class JdbcUtils { private static DataSource ds = null; static { try { // 读配置文件 dbcpconfig.properties InputStream in = JdbcUtils.class.getClassLoader() .getResourceAsStream("dbcpconfig.properties"); Properties prop = new Properties(); prop.load(in); BasicDataSourceFactory factory = new BasicDataSourceFactory(); ds = factory.createDataSource(prop); } catch (Exception e) { throw new ExceptionInInitializerError(e); // 异常转换成错误 } } public static Connection getConnection() throws SQLException { return ds.getConnection(); // dbcp conn.close() commit() } public static void release(Connection conn, Statement st, ResultSet rs) { // 模板代码 if (rs != null) { try { rs.close(); } catch (Exception e) { e.printStackTrace(); } rs = null; } if (st != null) { try { st.close(); } catch (Exception e) { e.printStackTrace(); } st = null; } if (conn != null) { try { conn.close(); } catch (Exception e) { e.printStackTrace(); } } } /* 抽取 增删改 的公共代码 */ // add delete update 都调用下面方法,变化的部分 sql , params // String sql="insert into account(id,name,money) values(?,?,?)"; // object[]{1,"aaa","1000"} public static void update(String sql, Object params[]) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = getConnection(); st = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) st.setObject(i + 1, params[i]); st.executeUpdate(); } finally { release(conn, st, rs); } } // 想替换掉全部 查询 public static Object query(String sql, Object params[], ResultSetHandler handler) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = getConnection(); st = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) st.setObject(i + 1, params[i]); rs = st.executeQuery(); // 接下来, 框架制做者不知道该怎样处理 rs // 方法: 对外暴露个接口,让调用者实现那个接口(handler),咱们用客户所实现的接口处理 rs // 调用用户传来的 handler return handler.handler(rs); } finally { release(conn, st, rs); } } } // 设计一个接口,对外暴露 interface ResultSetHandler { public Object handler(ResultSet rs); // 让用户实现这个方法 } // 框架做者根据现实状况,提早写好一些处理器 class BeanHandler implements ResultSetHandler { // 不知道 bean 是啥, 就定义一个变量接收,且用构造函数提供对外访问方式 private Class clazz; public BeanHandler(Class clazz) { this.clazz = clazz; } @Override public Object handler(ResultSet rs) { try { if (!rs.next()) return null; // 建立出要封装结果集的 bean Object bean = this.clazz.newInstance(); // 经过元数据技术获知 rs 里有啥 ResultSetMetaData meta = rs.getMetaData(); int colNum = meta.getColumnCount(); for (int i = 0; i < colNum; i++) { String name = meta.getColumnName(i + 1); // 结果集每列列名 id Object value = rs.getObject(name); // 1 // 经过 name,反射出 bean 上与 name对应的属性 Field f = bean.getClass().getDeclaredField(name); f.setAccessible(true); // 强制访问私有元素 f.set(bean, value); } return bean; } catch (Exception e) { throw new RuntimeException(e); } } } // 返回包含 bean 的 list 集合 class BeanListHandler implements ResultSetHandler { private Class clazz; public BeanListHandler(Class clazz) { this.clazz = clazz; } @Override public Object handler(ResultSet rs) { List list = new ArrayList(); try { ResultSetMetaData meta = rs.getMetaData(); int count = meta.getColumnCount(); while (rs.next()) { Object bean = this.clazz.newInstance(); for (int i = 0; i < count; i++) { String name = meta.getColumnName(i + 1); Object value = rs.getObject(name); Field f = bean.getClass().getDeclaredField(name); // 反射获取域 f.setAccessible(true); f.set(bean, value); } list.add(bean); } } catch (Exception e) { throw new RuntimeException(e); } return list; } }
模拟使用该框架的 dao 代码:
package cn.wk.utils; import java.sql.SQLException; import org.junit.Test; import cn.wk.domain.Account; // 假设这是 Dao // 注意到:crud 变化的是 sql 和 st.set 其他代码均相同 public class Demo7 { @Test public void test() throws SQLException { List<?> list = getAll(); System.out.println(list.size()); } public void add(Account a) throws SQLException { String sql = "insert into account(name,money) values(?,?)"; Object params[] = { a.getName(), a.getMoney() }; JdbcUtils.update(sql, params); } public void delete(int id) throws SQLException { String sql = "delete from account where id=?"; Object params[] = { id }; JdbcUtils.update(sql, params); } public void update(Account a) throws SQLException { String sql = "update account set name=?, money=? where id=?"; Object params[] = { a.getName(), a.getMoney(), a.getId() }; JdbcUtils.update(sql, params); } public Account find(int id) throws SQLException { String sql = "select * from account where id=?"; Object params[] = { id }; return (Account) JdbcUtils.query(sql, params, new BeanHandler( Account.class)); } public List getAll() throws SQLException { String sql = "select * from account"; Object params[] = {}; return (List) JdbcUtils.query(sql, params, new BeanListHandler( Account.class)); } }