1 事务
事务是数据库操作最基本单位,要么都成功,要么都失败。
事务四个特性ACID:原子性,一致性,隔离性,持久性。
Spring事务管理有两种方式:编程式事务管理 和 声明式事务管理,一般使用声明式事务管理,底层使用AOP原理。
声明式事务管理有两种方式:基于xml配置方式 和 基于注解方式,一般使用注解方式。
Spring事务管理提供了一个接口,叫做事务管理器,这个接口针对不同的框架提供不同的实现类。
在service类上面或者service类的方法上面添加事务注解@Transactional
如果把@Transactional添加在类上面,这个类里面所有方法都添加事务。
如果只是添加在方法上面,则只为这个方法添加事务。
1 2 3 4
| @Service @Transactional public class UserService { }
|
声明式事务
propagation:事务传播行为,总共有7种。
isolation:事务隔离级别
有三个读问题:脏读,不可重复读,虚读(幻读)。
设置隔离级别,解决读问题:

timeout:超时时间
- 事务需要在一定时间内进行提交,超过时间后回滚。
- 默认值是-1,设置时间以秒为单位。
readOnly:是否只读
- 默认值为false,表示可以查询,也可以增删改。
- 设置为true,只能查询。
rollbackFor:回滚,设置出现哪些异常进行事务回滚。
noRollbackFor:不回滚,设置出现哪些异常不进行事务回滚。
1 2 3
| @Service @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED) public class AccountService {
|
完全注解实现声明式事务管理
配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Configuration @ComponentScan(basePackages = "com.oymn.spring5") @EnableTransactionManagement public class Config {
@Bean public DruidDataSource getDruidDataSource(){ DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setDriverClassName("com.mysql.jdbc.Driver"); druidDataSource.setUrl("jdbc:mysql://localhost:3306/book"); druidDataSource.setUsername("root"); druidDataSource.setPassword("000000"); return druidDataSource; } @Bean public JdbcTemplate getJdbcTemplate(DataSource dataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } @Bean public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){ DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } }
|
service类:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Service public class AccountService {
@Autowired private AccountDao accountDao;
@Transactional public void accountMoney(){ accountDao.add(); accountDao.reduce(); } }
|
事务传播行为
在spring中一共有7中传播行为,REQUIRED、NESTED、REQUIRES_NEW常用
1 2 3 4 5 6 7 8 9 10 11 12 13
| REQUIRED:如果当前没有事务,就新建一个事务。如果当前存在事务,则加入这个事务。 NESTED:如果当前没有事务,就新建一个事务。如果当前事务存在,则执行一个嵌套事务。 REQUIRES_NEW:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。 SUPPORTS:如果当前没有事务,就以非事务方式执行。 如果当前有事务,则使用事务。 NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 MANDATORY:以事务方式执行,如果当前没有事务,就抛出异常。 NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
|
场景示例
有个注册业务,注册时需要记录登录账号、密码、身份证号、姓名、手机号这5个信息,涉及三张表,分别是t_user_account、t_user_idcard、t_user_phone,刚开始的业务要求这些都不能为空,如果其中一个发生异常,那么其余已经插入的全部回滚(这些异常是我用作数据校验时抛出的,正常生产上会先进行数据校验,校验成功了再更改数据库,我这里为了演示@Transactional的回滚效果,先更改数据,如果校验不通过,便会通过spring的事务进行回滚)。
1 2 3 4 5 6 7 8 9
| @Data public class UserVO { private Long id; private String userAccount; private String userPwd; private String phoneNum; private String userName; private String IDCard; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Controller public class UserController { @Autowired private UserService userService;
public void register(UserVO userVO) { userService.insertId(userVO); try { userService.register(userVO); } catch (Exception e) { e.printStackTrace(); } } }
|
1 2 3 4 5 6
| public interface UserService { void insertId(UserVO userVO); void register(UserVO userVO); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Service public class UserServiceImpl implements UserService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private UserDao userDao; @Override public void insertId(UserVO userVO) { String sql1 = "insert into t_user_account (user_id) values (?)"; jdbcTemplate.update(sql1,userVO.getId()); String sql2 = "insert into t_user_idcard (user_id) values (?)"; jdbcTemplate.update(sql2,userVO.getId()); String sql3 = "insert into t_user_phone (user_id) values (?)"; jdbcTemplate.update(sql3,userVO.getId()); } @Transactional(propagation = Propagation.REQUIRED) @Override public void register(UserVO userVO) { userDao.insertAccountAndPwd(userVO); String sql1 = "update t_user_phone set phone_num = ? where user_id = ?"; jdbcTemplate.update(sql1,userVO.getPhoneNum(),userVO.getId()); if (userVO.getPhoneNum() == null) { throw new RuntimeException("手机号码不能为空"); } userDao.insertIDcardAndName(userVO); } }
|
注:这里选择抛出的是RuntimeException,该异常是运行时异常,如果不做异常回滚的配置,默认非运行时异常(比如我刚开始抛出的是Exception),则不会发生回滚。
1 2 3 4 5 6 7 8
| public interface UserDao { void insertAccountAndPwd(UserVO userVO); void insertIDcardAndName(UserVO userVO); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Repository public class UserDaoImpl implements UserDao { @Autowired private JdbcTemplate jdbcTemplate; @Transactional(propagation = Propagation.REQUIRED) @Override public void insertAccountAndPwd(UserVO userVO) { String sql1 = "update t_user_account set user_account = ?, user_pwd = ? where user_id = ?"; jdbcTemplate.update(sql1,userVO.getUserAccount(),userVO.getUserPwd(),userVO.getId()); if (userVO.getUserAccount() == null) { throw new RuntimeException("账号不能为空"); } if (userVO.getUserPwd() == null) { throw new RuntimeException("密码不能为空"); } } @Transactional(propagation = Propagation.REQUIRED) @Override public void insertIDcardAndName(UserVO userVO) { String sql1 = "update t_user_idcard set ID_card = ?, user_name = ? where user_id = ?"; jdbcTemplate.update(sql1,userVO.getIDCard(),userVO.getUserName(),userVO.getId()); if (userVO.getIDCard() == null) { throw new RuntimeException("身份证号不能为空"); } if (userVO.getUserName() == null) { throw new RuntimeException("姓名不能为空"); } } }
|
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @SpringJUnitConfig(locations = "classpath:txSpreadBean.xml") public class TestTxSpread { @Autowired private UserController userController; @Test public void test2() { UserVO userVO1 = new UserVO(1L,"花花","123456",null,"花木兰","511387197807180001"); userController.register(userVO1); UserVO userVO2 = new UserVO(2L,"花花","123456","13612312345","花木兰",null); userController.register(userVO2); } }
|
在该示例中涉及事务传播行为要点的是UserServiceImpl的register方法。
register方法:是外围方法,除了调用了insertAccountAndPwd方法和insertIDcardAndName方法外,直接调用jdbcTemplate进行手机号的插入。
insertAccountAndPwd方法:是内围方法,进行账号密码的插入。
insertIDcardAndName方法:是内围方法,进行身份证号、姓名的插入。
场景1
我们要实现登录账号、密码、手机号、身份证号、姓名,这些都不能为空,如果其中一个发生异常,那么其余已经插入的全部回滚。
根据这个业务及我们当前代码结构,我们需要在这一个外围方法和两个内围方法中都添加@Transactional注解,因为要求有异常,只要有数据库改动的代码全部回滚。
传播行为选择
将@Transactional的事务传播属性propagation值设为Propagation.REQUIRED
这个值是默认的,它的含义是:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。
这正好符合我们业务需求:外围方法已经是一个事务后,被调用的两个内围方法也加入到这个事务中,就实现了只要有任意一个异常,只会插入一个id,其余需要插入的登录账号、密码、手机号、身份证号、姓名都回滚。
测试:




可以看见:只要一个方法内有异常,这三个方法都会回滚,数据库里没有一个表中有新记录。
场景2
业务逻辑改为注册时可以不提交身份证号与姓名(但是身份证号和姓名必须同时添加或不添加),后续确定后再提交即可,账号密码和手机号仍然不可缺少,少一个则全部回滚。
那么这里我们仍保持内围方法insertIDcardAndName方法为事务,外围方法register和内围方法insertAccountAndPwd也为事务,但是要保证insertIDcardAndName方法出现异常时不能导致外围方法回滚。
导致外围回滚有两种途径,一个是内围方法出现异常后会抛出给外围方法,然后外围方法的@Transactional感知到异常回滚,另一个是外围方法与内围方法本事是一个事务,那么内围方法回滚的时候自然会使外围方法也会滚。
传播行为选择
首先要在外围方法中将内围方法insertIDcardAndName的异常捕获,不能再使用REQUIRED为事务传播属性了,因为内围方法事务传播属性为REQUIRED代表加入到外围方法的事务中,会同时回滚。将@Transactional的事务传播属性propagation值设为Propagation.NESTED,它的含义是:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,就新建一个事务。这正好符合我们业务需求:外围方法是一个事务的情况下,我们的内围方法有异常只需要回滚自己就行,外围方法有异常才回滚全部。


测试:




上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现当insertIDcardAndName方法有异常,并不会导致外围方法register和内围方法insertAccountAndPwd回滚。
场景3
注册时可以不提交身份证号与姓名(但是身份证号和姓名必须同时添加或不添加),也可以不提交手机号,后续确定后再提交即可,账号密码和手机号仍然不可缺少,少一个则全部回滚。
传播行为选择
上次业务修改我们只要保证内围方法insertIDcardAndName回滚不带上外围方法回滚,这次我们还要保证外围方法register回滚时不要带上内围方法insertAccountAndPwd回滚。
所以这次我们可以将@Transactional的事务传播属性propagation值设为Propagation.REQUIRES_NEW,它的含义是:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。
这正好符合我们业务需求:外围方法是一个事务的情况下,我们的内围方法开始它自己的业务,外围方法回滚不带上内围方法。
测试




上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现当外围方法register方法有异常,并不会导致内围方法insertAccountAndPwd回滚。
总结
当我们不想让内围回滚带上外围也回滚,内围方法事务就不要使用REQUIRED,可以使用NESTED或REQUIRES_NEW。
当我们不想让外围回滚带上内围也回滚,内围方法事务就不要使用REQUIRED和NESTED,可以使用REQUIRES_NEW。