isolation
数据库事务并发问题
假设现在有两个事务,t1和t2并发执行
事务隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
①读未提交:READ UNCOMMITTED
- 允许t1读取t2未提交的修改,这就导致了脏读。
- t2提交,t1再次读取,读取到的数据是修改过的数据,与上一次读取到的数据不一致,这就导致了不可重复读
- t2向表中插入一些新的book信息,t1查询所有,可以查到新增加的数据,这就导致了幻读 ②读已提交:READ COMMITTED
- t1只能读取t2已提交的修改,避免了脏读 ③可重复读:REPEATABLE READ
- 确保t1可以多次从一个字段中读取到相同的值,即t1执行期间禁止其它事务对这个字段进行更新。 ④串行化:SERIALIZABLE
- 确保t1可以多次从一个表中读取到相同的行,在t1执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
mysql查询隔离级别的命令
修改MySQL隔离级别
SET [SESSION(当前会话) | GLOBAL(全局)] TRANSACTION ISOLATION LEVEL
{ READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }
如:设置当前会话的事务隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
查询MySQL的隔离级别
SELECT @@global.tx_isolation; //查询全局隔离级别
SELECT @@session.tx_isolation;//查询当前会话隔离级别
SELECT @@tx_isolation;//同上
事务操作
开启事务 start transaction;
提交事务 commit;
回滚事务 rollback;
读未提交READ_UNCOMMITTED
BookService中增加getPrice方法,设置isolation属性为READ_UNCOMMITTED
@Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) public int getPrice(String isbn){ return bookDao.getPrice(isbn); } 复制代码
BookServiceTest中增加测试方法
// 读未提交测试 @Test public void getPrice() { int price = bookService.getPrice("ISBN-001"); System.out.println("价格:" + price); } 复制代码
首先执行查询图书价格测试,查询到的图书价格为100
然后在命令行中开启一个事务,修改图书价格,不提交
再次执行测试
控制台输出价格为200,读取到了命令行中未提交的脏数据
读已提交READ_COMMITTED
首先恢复price为100,将代码中隔离级别修改为READ_COMMITTED,此时新打开一个命令行窗口,执行修改图书价格为200的SQL,再次执行getPrice方法的测试
在未提交的情况下,READ_COMMITTED隔离级别读取到的数据仍然是修改前的数据,此时在命令行窗口执行commit提交修改命令,再次执行测试
读取到了提交修改后的值,避免了脏读,但是前后两次读取到的数据不一致导致了不可重复读
可重复读REPEATABLE-READ
任何时候读取都是一样的,打开两个命令行窗口,首先在第一个命令行中开启事务并设置隔离级别为REPEATABLE-READ,并查询一次price,结果为100;接着在第二个命令行中更新并提交price,在第一个命令行中再次查询price,结果仍然为100;在第二个命令行中执行删除并提交的操作,在第一个命令行中再次查询price,结果仍然为100;这就是可重复读,在一个会话SESSION中,读取到的数据自始至终都是一样的,避免了脏读和不可重复读。
plus:并发修改数据时会出现排队的现象,只有等待另一个的修改commit之后,才能继续修改。这种现象与隔离级别无关。
有事务的业务逻辑,容器中保存的是这个业务逻辑的代理对象,只有代理对象才可以执行事务
@Test public void getClazz(){ System.out.println(bookService.getClass()); } 复制代码
propagation
事务传播行为,如果有多个事务嵌套运行,子事务是否要和上层事务或者已存在的事务共享同一个事务
- REQUIRED: 支持当前事务,没有则新建
- REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起
- SUPPORTS:如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中
- NOT_SUPPORTS:当前的方法不应该运行在事务中,如果有运行的事务,将它挂起
- MANDATORY:当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常
- NEVER:当前方法不应该运行在事务中,如果运行在事务中就抛出异常
- NESTED:如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则就启动一个新的事务,并在它自己的事务内运行
以上属性,只有REQUIRED和REQUIRES_NEW是最常用的。
BookDao中新增方法,更新图书价格
public void updatePrice(String isbn, int price){ String updatePriceSql = "UPDATE book SET price=? WHERE isbn=?"; jdbcTemplate.update(updatePriceSql,price,isbn); } 复制代码
BookService中调用BookDao的updatePrice方法,并增加事务@Transactional
@Transactional public void updatePrice(String isbn,int price){ bookDao.updatePrice(isbn, price); } 复制代码
新增一个ComplexService,注入BookService,新增一个collaborateTransaction方法,该方法中调用了BookService的checkout方法和updatePrice方法,并增加@Transactional注解,这样就形成了collaborateTransaction一个大的事务里面嵌套了checkout和updatePrice两个小的事务,当其中collaborateTransaction发生异常或者checkout或者updatePrice发生异常,其他事务是否会回滚呢?
@Service public class ComplexService { @Resource private BookService bookService; // 综合事务 @Transactional public void collaborateTransaction(String username,String isbn, int price){ // 事务1,事务2失败事务1是否需要回滚?可设置是否回滚,这就是事务传播行为, // 是否和上层事务共享一个事务 bookService.checkout(username,isbn); // 事务2 bookService.updatePrice(isbn,price); } } 复制代码
REQUIRED
给BookService的checkout方法和updatePrice设置传播行为,即给@Transactional注解增加属性propagation = Propagation.REQUIRED,checkout的@Transactional注解中其他演示过的属性可以删除,REQUIRED的意思是该方法需要事务,如果存在事务就使用已存在的事务,如果没有就新建一个事务
新增一个测试类ComplexServiceTest, 继承BookServiceTest
public class ComplexServiceTest extends BookServiceTest{ @Resource private ComplexService complexService; @Test public void collaborateTransaction() { complexService.collaborateTransaction("Stark","ISBN-002",200); } } 复制代码
恢复默认数据,所有book的price改为100,stock都改为1000,并且在updatePrice方法中增加异常代码
// 异常代码 System.out.println(10/0); 复制代码
执行测试,查看数据库,price、stock、balance数据都没有变化,可以确定发生异常后chekcout和update都进行了回滚
REQUIRES_NEW
修改checkout的事务属性为Propagation.REQUIRES_NEW,即创建一个新事务,不与其他方法共享事务,发生异常时其他事务不会回滚,再次执行测试。
可以发现price价格不变,updatePrice发生了回滚
stock库存数量减少,checkout正常执行并没有进行回滚
将collaborateTransaction方法中的checkout方法和updatePrice方法都改为REQIURES_NEW,并且在collaborateTransaction中增加异常,注释updatePrice方法中的异常,恢复初始数据后执行测试
price发生变化,updatePrice方法没有回滚
stock库存数量减少,checkout方法也没有回滚
这是因为两个事务是新的事务,与上层方法的事务不属于同一个事务,所有上层方法出现异常并不会影响这两个方法
plus:与上层方法共享事务时,该事物本身设置的属性都失效,以上层事务设置的属性为准。
五、基于XML的声明式事务
基于注解的声明式事务配置步骤
- Spring中提供事务管理器(事务切面),配置事务管理器
- 开启基于注解的声明式事务,依赖tx名称空间
- 需要事务的方法上添加注解
基于XML配置的声明式事务,依赖tx和aop名称空间
<!--基于XML配置的声明式事务--> <aop:config> <!--切面--> <aop:pointcut id="txPoint" expression="execution(* com.citi.service.*.*(..))"/> <!--增强 advice-ref:指向事务管理器配置--> <aop:advisor advice-ref="myAdvice" pointcut-ref="txPoint"></aop:advisor> </aop:config> <!--配置事务管理器--> <tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager"> <tx:attributes> <!--指明哪些方法是事务方法--> <!--切入点表达式只是说事务管理器要切入这些方法,哪些方法加事务使用tx:method指定--> <tx:method name="checkout" propagation="REQUIRED" timeout="-1"/> <tx:method name="get*" read-only="true"></tx:method> </tx:attributes> </tx:advice> 复制代码
将所有方法上的@Transactional注解全部注释 执行BookServiceTest中的testCheckoutSuccess方法
@Test public void testCheckoutSuccess() throws FileNotFoundException { bookService.checkout("Stark","ISBN-002"); System.out.println("Checkout Success!"); } 复制代码
执行后库存和余额都能正常扣除
在BookService的checkout方法增加异常代码
// 测试基于XML配置的声明式事务时的异常代码 int i = 10 / 0; 复制代码
再次执行测试。查看数据库可以确定余额和库存发生了回滚,数据不变
最后基于注解的声明式事务配置和基于XML的声明式事务如何选择?
重要的事务使用配置或者当事务非常非常多的时候,不重要的事务使用注解