有些spring相关的知识点之前一直没有仔细研究:比如spring的事务,并不是没有使用,也曾经简单的在某些需要事务处理的方法上通过增加事务注解来实现事务功能,仅仅是跟随使用(甚至并未测试过事务的正确性),至于如何在项目中配置事务,如何才能将事务写正确,事务的其它的一些原理性的东西从未花时间研究。最近同事正好抛出了一个问题,借此机会学习了一遍。
问题一:增加了readOnly=true的事务中包含写操作,为什么线上运行这段代码是正常的呢?
@Transactional(readOnly = true) public Integer getUID(String key, String type) { keyGeneratorDao.insert(key, type); keyGeneratorDao.update(key, type); return keyGeneratorDao.select(key, type); }
我为什么对这个问题感兴趣?
不懂这个readOnly参数的含义,之前写@Transactional的注解,那都是使用的默认值,不带显示参数。提出配置了readOnly参数后,理论上应该程序报错而实际上没有报错,想搞清楚为什么。
开始写单元测试:
在单元测试类中写事务函数以及测试方法
@Autowired private IkeyGeneratorDao keyGeneratorDao; @Transactional(readOnly=true) public Integer getId(String key, String type){ return keyGeneratorDao.select(key, type); } @Transactional public Integer getUID(String key, String type) { keyGeneratorDao.insert(key, type); keyGeneratorDao.update(key, type); return this.getId(key,type); } @Test public void testCreateGuid(){ int guid=this.getUID("12345", "jim"); System.out.println(guid); }
测试结果显示正常,与上面提到的不允许进行写操作的观点相反,于是想起典型的事务生效问题。
挖的第一个坑:如果事务采用的是cglib动态代理,调用的方法与事务方法处在同一个类中事务不生效。
将两个事务事务转移到单独的类中,然后测试,类代码省略,只是将上面两个标记了@Transactional的方法封装在一个单独的类中。
@Autowired private KeyGeneratorService keyService; @Test public void testCreateGuid2(){ int guid=this.keyService.getUID("12345", "jim"); System.out.println(guid); }
测试结果显示也是正常,于是想确认下事务到底是否生效,加入异常以测试数据是否回滚,修改代码如下:
@Transactional public Integer getUID3(String key, String type) { keyGeneratorDao.insert(key, type); Integer.parseInt("aaa");//throw exception keyGeneratorDao.update(key, type); }
测试结果显示事务回滚正常,可以排除事务环境配置问题。
挖的第二个坑:做测试一定要与原问题代码尽量保持一致,否则会产生其它的不明原因影响判断。通过对比原问题的代码发现我写的测试代码与问题代码有区别,readOnly是加在包含有写操作的方法上,而我的是两个方法,只有在读的方法上增加了readOnly,于时再次修改代码:
@Transactional(readOnly = true) public Integer getUID4(String key, String type) { keyGeneratorDao.insert(key, type); keyGeneratorDao.update(key, type); return keyGeneratorDao.select(key, type); }
测试结果显示运行不正常,提示如下错误:Cause: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed,到这的确说明在在加了readOnly=true的事务内是不允许写入操作的。为什么这段代码在线上运行是成功的呢,于时查看前端的调用,发现前端调用的并不是直接标识了Transactional的方法,而是根据不同的具体业务重新包装的方法,比如我们需要生成订单的编号,前端只调用genOrderCode而不调用getUID。
非事务方法在内部调用了本类事务方法,然后非事务方法被外部调用ServicegenOrderCode,是一个非事务方法,内部调用了getUIDgetUID,是一个事务方法
@Autowired private IkeyGeneratorDao keyGeneratorDao; @Transactional(readOnly = true) public Integer getUID(String key, String type) { keyGeneratorDao.insert(key, type); keyGeneratorDao.update(key, type); return keyGeneratorDao.select(key, type); } public String genOrderCode(Date orderDate) { SimpleDateFormat df = new SimpleDateFormat("yyMMddHH"); String ticketDate = df.format(orderDate); Integer uid = getUID(ticketDate, ORDER_CODE); return ticketDate + genRandom2(uid.toString(), 3, 3) ; }
Test,外部类调用非事务方法
@Test public void testCreateGuid3(){ String guid=this.keyService.genOrderCode(new Date()); System.out.println(guid); }
测试结果居然是正常的,这与线上运行的结果相同,后面经同事提醒,这又是一个不正确使用事务的案例。
挖的第三个坑:当调用一个类的非事务方法且这个非事务方法内部调用了本类自身的事务方法,那么事务也不会生效。
问题二:下面的代码可以实现事务回滚吗?
Service
- genOrderCode方法调用
- getUID2两个方法都是具备相同的事务参数
- getUID2抛出异常
- genOrderCode捕获这个异常
@Transactional public Integer getUID2(String key, String type) { keyGeneratorDao.insert(key, type); Integer.parseInt("aaa");//throw exception keyGeneratorDao.update(key, type); return keyGeneratorDao.select(key, type); } @Transactional public String genOrderCode(Date orderDate) { try{ SimpleDateFormat df = new SimpleDateFormat("yyMMddHH"); String ticketDate = df.format(orderDate); Integer uid = getUID2(ticketDate, ORDER_CODE); return ticketDate + genRandom2(uid.toString(), 3, 3) ; }catch(Exception ex){ System.out.println(ex); } return null; }
Test
@Test public void testCreateGuid3(){ String guid=this.keyService.genOrderCode(new Date()); System.out.println(guid); }
执行测试代码,发现可以成功提交,但数据是不完整的,因为更新操作没有完成。为什么会是这样的呢?因为默认的Propagation.REQUIRED指明多个操作处于一个事务中,由于genOrderCode有异常处理,所以即使getUID2中发生异常,系统也会认定提交是合法的,因此会出现插入操作正常更新不正常但事务正常提交并不回滚的情况。
如果显示指定Propagation.REQUIRES_NEW呢?
@Transactional(propagation=Propagation.REQUIRES_NEW) public Integer getUID2(String key, String type) { keyGeneratorDao.insert(key, type); Integer.parseInt("aaa");//throw exception keyGeneratorDao.update(key, type); return keyGeneratorDao.select(key, type); }
再执行相同的测试,数据正常回滚,这里提供两张图,可以看的清楚些(因为常用的就这两种,其它的有兴趣可以多多研究)
REQUIRED
REQUIRES_NEW
通过事务的两个小问题,总结出解决问题的一些小技巧或者叫经验:发现问题之后,不要局限于某个点,最好根据上下文来结合分析,比如问题一的readonly可写入,单看那段代码很难找出合理的解释,只有结合前后端调用才能找出根本原因。写单元测试尽量写相同的代码,否则有可能会出现一些干扰项影响判断。学习呢,有时间尽量学的全点,比如@Transactional这个注解,除了readOnly还有Propagation等等。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持易盾网络。