事务控制
事务4个特性:ACID
⑴ 原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
⑵ 一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
⑶ 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
⑷ 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。
事务控制一般分为两种方式:
1、编程式事务控制
2、注解式事务控制
首先看一个例子,不做事务控制会怎样?
private int saveUser(int i){ User user1 = new User(); user1.setUserName("哈哈龙"+i); user1.setPassWord("hahalong001"); user1.setAddress("长安"); user1.setEmail("hahalong@163.com"); user1.setGender("1"); user1.setIdentity("321736165504567129"); return userMapper.insertUser(user1); } private int updateUser(int i){ User user1 = new User(); user1.setUserName("哈哈龙"+i); user1.setPassWord("up-hahalong001"); user1.setAddress("长安"); user1.setEmail("up-hahalong@163.com"); user1.setGender("1"); user1.setIdentity("321736165504567129"); return userMapper.updateUserByUserName(user1); } /** * 不做事务控制 */ public void withoutTransControl(){ saveUser(1); int i = 6/0; updateUser(2); }
结果,新增成功了,更新没有执行。违反事务原子性。
1、编程式事务控制:
@Autowired private TransactionTemplate transactionTemplate; /** * 编程式事务控制 */ public boolean programTransControl(){ Boolean isSuccess = transactionTemplate.execute(new TransactionCallback<Boolean>() { public Boolean doInTransaction(TransactionStatus status) { Boolean result = true; try { saveUser(2); updateUser(3); int i = 6/0; } catch (Exception e) { status.setRollbackOnly(); result = false; } return result; } }); return isSuccess; }
运行结果,数据没有插入,也没有更新。
2、注解事务控制
@Transactional public void anotationTransControl(){ saveUser(2); int i = 6/0; updateUser(3); }
运行后,数据没有插入,也没有更新。但是如果捕获异常,这种情况下会怎样?
@Transactional public void anotationTransControl2(){ try{ saveUser(2); int i = 6/0; updateUser(3); }catch (Exception e){ e.printStackTrace(); } }
运行后:插入成功了!!!
此时两种方案
(1)再捕获异常的地方,再次抛异常。注意:可以指定哪些异常可以回滚
//@Transactional(rollbackFor = {Exception.class,RuntimeException.class}) @Transactional public void anotationTransControl3(){ try{ saveUser(3); int i = 6/0; updateUser(4); }catch (Exception e){ e.printStackTrace(); throw new RuntimeException(); } }
(2)在捕获异常的地方代码回滚
@Transactional public void anotationTransControl4(){ try{ saveUser(3); int i = 6/0; updateUser(4); }catch (Exception e){ e.printStackTrace(); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }
两种方法,运行后,数据没有插入,也没有更新。总结:在做注解事务控制,一定要注意异常捕获时的事务处理。
事务隔离级别:
首先了解下不考虑事务隔离性,发生的几种问题:
1、脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
例如:用户A向用户B转账100元。
线程1:A 转100->B,此时还未提交事务;
线程2:B 账户余额记录,发现账户多了100,立马通知B有人给你100,B好开心。。。
线程1:方法有异常,回滚事务;B余额并未多了100.。。
2、不可重复读:不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如:用户A向用户B转账100元。
线程2:B 查询账户余额记录,为X;
线程1:A 转100->B,提交事务;
线程2:B 再次查询账户余额记录,为X+100,嗯?与上次不一样,几个意思,再查下。。。;
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
3、虚读(幻读)
幻读是事务非独立执行时发生的一种现象。
eg:怪兽工厂有一批小怪物,凛冬将至,要苏醒祸及人间了!!
线程1:夜王,小的们别睡了,跟哥去打人,睡眠状态weak_up = 1 改为 weak_up =0;
线程2:老天爷,嗯,怪物种类有点单一,数量稀少,我再给你造几个,插入几条怪物数据,weak_up = 1
线程1:夜王高举大旗,威风凛凛,咦,怎么还有小的在睡觉?怀疑人生。。。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:
- TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
- TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据【推荐】。该级别可以防止脏读,依然防止不了不可重复度和幻读;
- TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
- TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别越高,带来的安全级别越高,但性能也越差。此事古难全,权衡把握。。。。
下面给一个常在代码中犯的一个错误,即加入在业务上username是不重复的,我们会经常先查询是否存在,存在更新,不存在就插入。
请看:
提供一个存储过程:
create procedure saveOrUpdateUsers_produce() BEGIN DECLARE v_count int; set v_count = 0; select count(1) into v_count from c_tbl_users where username='王泽中'; if v_count > 0 THEN update c_tbl_users set email='wangzezhong1@163.com',mobile='18646542313',address='江苏省南京市鼓楼区' where username='王泽中'; else insert into c_tbl_users (username,pwd,gender,email,mobile,identity,address) VALUES('王泽中','wang123','1','wangzezhong@163.com','18646542312','321123198804211819','江苏省南京市雨花区'); end if; END;
代码中调用:
<insert id="callProduce"> call saveOrUpdateUsers_produce(); </insert>
@Transactional
public void callProduce(){
userMapper.callProduce();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
开启两个线程执行
@Test
public void test6(){
new Thread(()->{userService.callProduce();}).start();
new Thread(()->{userService.callProduce();}).start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
当前数据库中无王泽中数据,执行后,请看:
这种寄希望sql控制来实现唯一性是不可取的,上面的例子是一个幻读问题,是不是加了隔离级别就可以了呢?
删掉数据,加上隔离级别,再执行>>
@Transactional(isolation = Isolation.SERIALIZABLE) //@Transactional public void callProduce(){ userMapper.callProduce(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }
结果:
哎呀成功了,手舞足蹈,高手寂寞。。。额等下,单机你可以控制,分布式的多台机器,你怎么办?
所以,总结一点:对于业务上唯一性的数据,要在表上设置唯一键,不要通过sql去控制。
表设计上,也许需要保留数据历史记录,如果保留了,就没法设置唯一键了嘛。对于这点,对于非业务的数据,优先考虑创建历史备份表,而不是让无用的数据充斥在我们的业务表中。
事务传播行为:
Spring中七种事务传播行为
- TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
- TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
解析:
1、PROPAGATION_REQUIRED:
样例演示:
@Transactional(propagation = Propagation.REQUIRED) public void addRequired(User user){ userMapper.insertUser(user); } @Transactional(propagation = Propagation.REQUIRED) public void addRequiredException(User user){ userMapper.insertUser(user); throw new RuntimeException(); }
场景一:外围不加事务
public void test7(){ User user1 = new User(); user1.setUserName("三狗子"); user1.setPassWord("xiaoribiao001"); user1.setAddress("长安"); user1.setEmail("xiaoribiao@163.com"); user1.setGender("1"); user1.setIdentity("321736165502567129"); userService.addRequired(user1); User user2 = new User(); user2.setUserName("雅少"); user2.setPassWord("xiaojiandun001"); user2.setAddress("长安"); user2.setEmail("xiaojiandun@163.com"); user2.setGender("1"); user2.setIdentity("321716165502567129"); userService.addRequired(user2); throw new RuntimeException(); }
场景二:外围不加事务
public void test7(){ User user1 = new User(); user1.setUserName("三狗子"); user1.setPassWord("xiaoribiao001"); user1.setAddress("长安"); user1.setEmail("xiaoribiao@163.com"); user1.setGender("1"); user1.setIdentity("321736165502567129"); userService.addRequired(user1); User user2 = new User(); user2.setUserName("雅少"); user2.setPassWord("xiaojiandun001"); user2.setAddress("长安"); user2.setEmail("xiaojiandun@163.com"); user2.setGender("1"); user2.setIdentity("321716165502567129"); userService.addRequiredException(user2); }
场景三:外围加事务
@Transactional(propagation = Propagation.REQUIRED) public void test7(){ User user1 = new User(); user1.setUserName("三狗子"); //.... userService.addRequired(user1); User user2 = new User(); user2.setUserName("雅少"); //..... userService.addRequired(user2); throw new RuntimeException(); }
场景四:外围加事务
@Transactional(propagation = Propagation.REQUIRED) public void test7(){ User user1 = new User(); user1.setUserName("三狗子"); userService.addRequired(user1); User user2 = new User(); user2.setUserName("雅少"); userService.addRequiredException(user2); }
场景五:外围加事务
@Transactional(propagation = Propagation.REQUIRED) public void test7(){ User user1 = new User(); user1.setUserName("三狗子"); userService.addRequired(user1); User user2 = new User(); user2.setUserName("雅少"); try{ userService.addRequiredException(user2); }catch (Exception e){ e.printStackTrace(); } }
Propagation.REQUIRED
修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚
2、PROPAGATION_REQUIRES_NEW
样例演示:
UserService.java,新增方法
@Transactional(propagation = Propagation.REQUIRES_NEW) public void addRequiresNew(User user){ userMapper.insertUser(user); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void addRequiresNewException(User user){ userMapper.insertUser(user); throw new RuntimeException(); }
新建UserService2.java,原因稍后会列举一个异常不回滚的例子。
在UserService2中编写如下:
private User createUser(String userName){ User user1 = new User(); user1.setUserName(userName); user1.setPassWord("xiaoribiao001"); user1.setAddress("长安"); user1.setEmail("xiaoribiao@163.com"); user1.setGender("1"); user1.setIdentity("321736165502567129"); return user1; }
场景一:
public void doHandleNoTrans_1(){ User user1 = createUser("三狗子"); userService.addRequiresNew(user1); User user2 = createUser("雅少"); userService.addRequiresNew(user2); throw new RuntimeException(); }
场景二:
public void doHandleNoTrans_2(){ User user1 = createUser("三狗子"); userService.addRequiresNew(user1); User user2 = createUser("雅少"); userService.addRequiresNewException(user2); }
场景三:
@Transactional(propagation = Propagation.REQUIRED) public void doHandleTrans_3(){ User user1 = createUser("三狗子"); userService.addRequired(user1); User user2 = createUser("雅少"); userService.addRequiresNew(user2); throw new RuntimeException(); }
场景四
@Transactional(propagation = Propagation.REQUIRED) public void doHandleTrans_4(){ User user1 = createUser("三狗子"); userService.addRequired(user1); User user2 = createUser("雅少"); userService.addRequiresNewException(user2); }
场景五:
@Transactional(propagation = Propagation.REQUIRED) public void doHandleTrans_5(){ User user1 = createUser("三狗子"); userService.addRequired(user1); User user2 = createUser("雅少"); try{ userService.addRequiresNewException(user2); }catch (RuntimeException e){ e.printStackTrace(); } }
结果:
这里发现一个现象,就是加事务的方法如果在同一个类中,事务传播性是最外围的方法的传播性值。
下面分析下例子,在UserService.java中添加如下方法:
@Transactional(propagation = Propagation.REQUIRED) public void doHandleTrans(){ User user1 = createUser("三狗子"); addRequired(user1); User user2 = createUser("雅少"); try{ addRequiresNewException(user2); }catch (RuntimeException e){ e.printStackTrace(); } }
本意是想,三狗子成功,雅少事务独立,异常回滚,然后外围方法捕获异常,不回滚三狗子。在运行结束后,啊。。。。。与预想的完全不一样!!!
分析:代码等同与下面3个方法,注意这里调用与PROPAGATION_REQUIRED场景5不同,其实是addRequired和addRequiredException方法是不会新建事务的,尽管在方法上面加了注解。为什么呢?因为Spring事务是基于动态代理AOP对bean管理和切片,它为每一个Class生成一个代理对象,只有代理对象之间进行调用,才会触发切面逻辑。
同一个Class方法之间调用是原对象方法,不通过代理对象,所以此时是无法通过注解保证事务性的。所以当事务传播性变化时,最好在不同的类中实现。
@Transactional(propagation = Propagation.REQUIRED) public void doHandleTrans_bak1(){ User user1 = createUser("三狗子"); addRequired(user1);//不会有新事务开启,Spring拦截器不会拦截。。。 User user2 = createUser("雅少"); try{ addRequiredException(user2); }catch (RuntimeException e){ e.printStackTrace(); } } @Transactional(propagation = Propagation.REQUIRED) public void doHandleTrans_bak2(){ User user1 = createUser("三狗子"); addRequired(user1); User user2 = createUser("雅少"); try{ addRequired(user2); throw new RuntimeException(); }catch (RuntimeException e){ e.printStackTrace(); } } @Transactional(propagation = Propagation.REQUIRED) public void doHandleTrans_bak3(){ User user1 = createUser("三狗子"); userMapper.insertUser(user1); User user2 = createUser("雅少"); try{ userMapper.insertUser(user2); throw new RuntimeException(); }catch (RuntimeException e){ e.printStackTrace(); } }
哇,真相大白,额,小伙子,你的理论基础呢,叨叨个没完,源代码在哪里?TransactionAspectSupport.class,有兴趣可以看看。。。
3、PROPAGATION_NESTED
UserService.java
@Transactional(propagation = Propagation.NESTED) public void addNested(User user){ userMapper.insertUser(user); } @Transactional(propagation = Propagation.NESTED) public void addNestedException(User user){ userMapper.insertUser(user); throw new RuntimeException(); }
外部无事务,UserService2.java
场景一:
public void doHandleNoTransNested1(){ User user1 = createUser("三狗子"); userService.addNested(user1); User user2 = createUser("雅少"); userService.addNested(user2); throw new RuntimeException(); }
场景二:
public void doHandleNoTransNested2(){ User user1 = createUser("三狗子"); userService.addNested(user1); User user2 = createUser("雅少"); userService.addNestedException(user2); }
结论:外围方法未开启事务的情况下Propagation.NESTED
和Propagation.REQUIRED
作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
外部有事务:
场景三:
@Transactional public void doHandleTransNested3(){ User user1 = createUser("三狗子"); userService.addNested(user1); User user2 = createUser("雅少"); userService.addNested(user2); throw new RuntimeException(); }
场景四:
@Transactional public void doHandleTransNested4(){ User user1 = createUser("三狗子"); userService.addNested(user1); User user2 = createUser("雅少"); userService.addNestedException(user2); }
场景五:
@Transactional public void doHandleTransNested5(){ User user1 = createUser("三狗子"); userService.addNested(user1); User user2 = createUser("雅少"); try{ userService.addNestedException(user2); }catch (Exception e){ e.printStackTrace(); } }
结论:外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务。
区别:
1、NESTED和REQUIRED修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是REQUIRED是加入外围方法事务,所以和外围事务同属于一个事务,一旦REQUIRED事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED是外围方法的子事务,有单独的保存点,所以NESTED方法抛出异常被回滚,不会影响到外围方法的事务。
2、NESTED和REQUIRES_NEW都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。
参考:https://segmentfault.com/a/1190000013341344