十三、JDBC事务管理
1.事务的概念
(1)事务的概念:事务是指逻辑上的一组操作,这组操作要么同时完成要么同时不完成。参考转账操作。 (2)如果你自己不去控制事务,数据库默认一条sql语句就处在自己单独的事务当中。 (3)也可以使用命令去开启一个事务:
start transaction;--开启事务,这条语句之后的sql语句将处在一个事务当中,这些sql语句并不会立即执行。
Commit--提交事务,一旦提交事务,事务中的所有sql语句才会执行。 Rollback -- 回滚事务,将之前所有的sql取消。 conn.setAutoCommit(false); conn.commit(); conn.rollback(); conn.setSavePoint(); conn.rollback(sp); 案例代码: public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; Savepoint sp = null; try{ conn = JDBCUtils.getConn(); 滚
conn.setAutoCommit(false);//相当于start transaction ps = conn.prepareStatement(\ ps.setString(1, \ ps.executeUpdate(); ps = conn.prepareStatement(\ ps.setString(1, \ ps.executeUpdate(); //设置回滚点 sp = conn.setSavepoint(); ps = conn.prepareStatement(\ ps.setString(1, \ ps.executeUpdate(); String str = null; str.toUpperCase(); ps = conn.prepareStatement(\ ps.setString(1, \ ps.executeUpdate(); conn.commit(); }catch (Exception e) { try { if(sp == null){//如果回滚点为null说明没有执行到设置回滚点代码时就抛了异常,应该所有语句进行回
conn.rollback();
}else{//说明sp不为null,可以会滚到回滚点,接着执行其他操作,但是要注意,如果希望之前的语句起作用,仍然需要做提交操作 conn.rollback(sp); conn.commit(); } } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); }finally{ JDBCUtils.close(rs, ps, conn); } }
2.事务的特性(ACID)
? ?
原子性:事务的一组操作是原子的不可再分割的,这组操作要么同时完成要么同时不完成。 一致性: 事务在执行前后数据的完整性保持不变。数据库在某个状态下符合所有的完整性约束的状态叫做数据库具有完整性。在解散一个部门时应该同时处理员工表中的员工保证这个事务结束后,仍然保证所有的员工能找到对应的部门,满足外键约束。
隔离性:当多个事务同时操作一个数据库时,可能存在并发问题,此时应保证各个事务要进行隔离,事务之间不能互相干扰。
持久性:持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,不能再回滚。
? ?
3.隔离性导致的问题
将数据库设计成单线程的数据库,可以防止所有的线程安全问题,自然就保证了隔离性。但是如果数据库设计成这样,那么效率就会极其低下。
? 如果是两个线程并发修改,一定会互相捣乱,这时必须利用锁机制防止多个线程的并发修改。 ? 如果两个线程并发查询,没有线程安全问题。
? 如果两个线程一个修改,一个查询,则会出现以下问题:
(1) 脏读:一个事务读取到了另一个事务未提交的数据。
(2) 不可重复读:在一个事务内读取表中的某一行数据,多次读取结果不同。 (3) 幻读(虚读):一个事务读取到了另一个事务插入的数据(已提交)。
4.四大隔离级别
? Read uncommitted:如果将数据库设定为此隔离级别,数据库将会有脏读、不可重复度、幻读的问题。
? Read committed:如果将数据库设定为此隔离级别,数据库可以防止脏读,但有不可重复度、幻读的问题。 ? Repeatable read: 如果将数据库设定为此隔离级别,数据库可以防止脏读、不可重复度,但是不能防止幻读。 ? Serializable:将数据库串行化,可以避免脏读、不可重复读、幻读。 安全:Serializable>Repeatable read>Read committed>Read uncommitted 效率:Serializable 通常来说,一般的应用都会选择Repeatable read或Read committed作为数据库隔离级别来使用。 mysql默认的数据库隔离级别为:Repeatable read oracle默认的数据库隔离级别为:Read committed 查询当前数据库的隔离级别:select @@tx_isolation; 设置隔离级别:set [global/session] transaction isolation level xxxx;其中如果不写默认是session指的是修改当前客户端和数据库交互时的隔离级别,而如果使用golbal,则修改的是数据库的默认隔离级别。 数据库中的锁机制: 共享锁:在非Serializable隔离级别做查询不加任何锁,而在Serializable隔离级别下做的查询加共享锁。 共享锁的特点:共享锁和共享锁可以共存,但是共享锁和排他锁不能共存。 排他锁:在所有隔离级别下进行增删改的操作都会加排他锁。 排他锁的特点:和任意其他锁都不能共存。 5.问题演示 演示不同隔离级别下的并发问题 set transaction isolation level 设置事务隔离级别 select @@tx_isolation 查询当前事务隔离级别 1.当把事务的隔离级别设置为read uncommitted时,会引发脏读、不可重复读和虚读 A窗口 set transaction isolation level read uncommitted; start transaction; select * from account; -----发现a帐户是1000元,转到b窗口 B窗口 start transaction; update account set money=money+100 where name='aaa'; -----不要提交,转到a窗口查询 select * from account -----发现a多了100元,这时候a读到了b未提交的数据(脏读) 2.当把事务的隔离级别设置为read committed时,会引发不可重复读和虚读,但避免了脏读 A窗口 set transaction isolation level read committed; start transaction; select * from account; -----发现a帐户是1000元,转到b窗口 B窗口 start transaction; update account set money=money+100 where name='aaa'; commit; -----转到a窗口 select * from account; -----发现a帐户多了100,这时候,a读到了别的事务提交的数据,两次读取a帐户读到的是不同的结果(不可重复读) 3.当把事务的隔离级别设置为repeatable read(mysql默认级别)时,会引发虚读,但避免了脏读、不可重复读 A窗口 set transaction isolation level repeatable read; start transaction; select * from account; ----发现表有4个记录,转到b窗口 B窗口 start transaction; insert into account(name,money) values('ggg',1000); commit; -----转到 a窗口 select * from account; ----可能发现表有5条记如,这时候发生了a读取到另外一个事务插入的数据(虚读) 4.当把事务的隔离级别设置为Serializable时,会避免所有问题 A窗口 set transaction isolation level Serializable; start transaction; select * from account; -----转到b窗口 B窗口 start transaction; insert into account(name,money) values('ggg',1000); -----发现不能插入,只能等待a结束事务才能插入 6.更新丢失 如果多个线程操作,基于同一个查询结构对表中的记录进行修改,那么后修改的记录将会覆盖前面修改的记录,前面的修改就丢失掉了,这就叫做更新丢失。 Serializable可以防止更新丢失问题的发生。其他的三个隔离级别都有可能发生更新丢失问题。 Serializable虽然可以防止更新丢失,但是效率太低,通常数据库不会用这个隔离级别,所以我们需要其他的机制来防止更新丢失。 乐观锁和悲观锁不是数据库中真正存在的锁,只是人们在解决更新丢失时的不同的解决方案,体现的是人们看待事务的态度。 ? 悲观锁:修改非常多,查询非常少,使用悲观锁。 隔离级别不设置为Serializable,防止效率过低。 在查询时手动加上排他锁。 如果数据库中的数据查询比较多而更新比较少的话,悲观锁将会导致效率低下。 通过以下语句实现悲观锁: select * from table lock in share mode(读锁、共享锁) select * from table for update (写锁、排它锁) ? 乐观锁:查询非常多,修改非常少,使用乐观锁。 在表中增加一个version字段,在更新数据库记录是将version加一,从而在修改数据时通过检查版本号是否改变判断出当前更新基于的查询是否已经是过时的版本。 如果数据库中数据的修改比较多,更新失败的次数会比较多,程序需要多次重复执行更新操作。 通过时间戳字段实现乐观锁。 十四、数据库连接池 1.原理分析 ? 编写连接池需实现javax.sql.DataSource接口。DataSource接口中定义了两个重载的getConnection方法: Connection getConnection() Connection getConnection(String username, String password) ? 实现DataSource接口,并实现连接池功能的步骤: 在DataSource构造函数中批量创建与数据库的连接,并把创建的连接保存到一个集合对象中。 实现getConnection方法,让getConnection方法每次调用时,从集合对象中取一个Connection返回给用户。 当用户使用完Connection,调用Connection.close()方法时,Collection对象应保证将自己返回到连接池的集合对象中,而不要把conn还给数据库。 2.编写数据库连接池核心 ? 扩展Connection的close方法 在关闭数据库连接时,将connection存回连接池中,而并非真正的关闭 ? 扩展类的三种方式 基于继承--- 方法覆盖 使用装饰模式包装类,增强原有行为 使用动态代理 --- 基于字节码Class在内存中执行过程 使用动态代理技术构建连接池中的connection 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(\ pool.addLast(conn); return null; } return method.invoke(conn, args); } }); 3.开源数据库连接池(DataSource) 现在很多WEB服务器(Weblogic, WebSphere, Tomcat)都提供了DataSoruce的实现,即连接池的实现。通常我们把DataSource的实现,按其英文含义称之为数据源,数据源中都包含了数据库连接池的实现。 也有一些开源组织提供了数据源的独立实现: ? DBCP 数据库连接池 ? C3P0 数据库连接池 ? Apache Tomcat内置的连接池(apache dbcp) 实际应用时不需要编写连接数据库代码,直接从数据源获得数据库的连接。程序员编程时也应尽量使用这些数据源的实现,以提升程序的数据库访问性能。 4.DBCP数据源 DBCP 是 Apache 软件基金组织下的开源连接池实现,使用DBCP数据源,应用程序应在系统中增加如下两个 jar 文件: Commons-dbcp.jar:连接池的实现 Commons-pool.jar:连接池实现的依赖库 Tomcat 的连接池正是采用该连接池来实现的。该数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。 方式1: BasicDataSource source = new BasicDataSource(); source.setDriverClassName(\source.setUrl(\source.setUsername(\source.setPassword(\方式2: Properties prop = new Properties(); prop.load(new FileReader(\BasicDataSourceFactory factory = new BasicDataSourceFactory(); DataSource source = factory.createDataSource(prop); 配置文件中: driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql:///day11 username=root