事务问题(一) | wsztrush
写在前面
现在开发的系统对准确性要求非常高,而数据的一致性和准确性几乎完全依赖数据库的事务,如果事务跪了,结局是要多惨有多惨(经历过的都懂的)。
当然MySQL本身也有一些BUG但并不多,大部分的出错都是业务代码写残了,下面来看遇到过的一些例子!
常遇问题
下面的场景都是Java操作MySQL/InnoDB、事务隔离级别为RC时遇到的:
不生效之一:代码问题
不生效模板代码如下:
1 2 3 4 5 6 7 8 9 10 | public class Manager { public void func1() { func2(); } public void func2() { // 数据库操作 } } |
调用manager.func2事务是生效的,而调用manager.func1时不生效!开始分析原因:
- Spring对@Transactional修饰的方法进行AOP拦截处理;
- Spring中的AOP是通过代理实现的;
那么,先来看个代理的例子(CGLIB):
1 2 3 4 5 6 | public Object intercept(Object targe, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("BEFORE"); Object result = methodProxy.invokeSuper(targe, args); System.out.println("AFTER"); return result; } |
因此,调用fun2时顺序为:

而调用func1时顺序为:

代理对象中发现并不需要代理func1方法(因为它上面没有注解),因此不会开启事务,到了target以后自己调用自己的func2,虽然方法上有@Transactional,但是target并不管它(只有代理对象才管),因此事务并不生效。
解决办法:在需要事务的入口方法上面都加注解(略暴力)。
不生效之二:配置问题
和事务相关的配置常用的有以下三个:
- datasource
- transactionManager:需要datasource
- annotation-driven:需要transactionManager
当transactionManager或者annotation-driven设置的datasource与访问数据库所使用的不一致时,会出现事务不生效!下面来从Spring对事务的管理方式上找答案:
在开启事务,会调用TransactionSynchronizationManager.bindResource将链接绑定到ThreadLocal上,其中key为datasource,value为ConnectionHolder:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public static void bindResource(Object key, Object value) throws IllegalStateException { // 根据datasource生成真正的key Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Assert.notNull(value, "Value must not be null"); Map<Object, Object> map = resources.get(); // 初始化map if (map == null) { map = new HashMap<Object, Object>(); resources.set(map); } Object oldValue = map.put(actualKey, value); // .... } |
执行SQL前会通过TransactionSynchronizationManager.getResource来获取链接:
1 2 3 4 5 6 7 8 9 | public static Object getResource(Object key) { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Object value = doGetResource(actualKey); if (value != null && logger.isTraceEnabled()) { logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } return value; } |
于是,当transactionManager配置的datasource与写数据的datasource不一样时,开启事务和执行SQL会分别用两个链接,这样就导致:
- 执行数据库操作使用的链接A
- 回滚事务用的链接B
当然也就回滚不掉了,这种情况都是在切换数据源的时候遇到的,干这种活的时候小心点就行了!
在Commit成功之后没有数据
常遇到是事务不生效,前端时间遇到事务明明生效,但COMMIT后却没有数据,这时候就直接懵逼了!
现象是:
A调用B服务,B写数据库成功并将数据库ID返回给A,但B对应的数据库中并没有数据。
是不是感觉见鬼了。首先,我们来对B服务的执行过程描述一下:
- 数据库链接是绑定到线程上的;
- 用线程池来处理服务请求,也就是说同一个线程可能会依次处理多个请求;
也就是说:两次不同的请求可能会互相影响,而恰巧在B中使用的手动事务,那么该线程执行的流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public void test() throws Exception { /********** 第一次HSF调用 **********/ // 开启事务 DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setIsolationLevel(DefaultTransactionDefinition.ISOLATION_READ_COMMITTED); def.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransaction(def); // 由于各种原因没有终结 /********** 第二次HSF调用 **********/ // 调用被@Transactional拦截的方法。 // 返回成功,但是数据没有落库。 } |
在手动事务没有完成(回滚或提交)时,对应的线程又调用了@Transactional拦截的方法:
- 在方法开始前:认为已经在事务中了,所以不会开启新事务
- 在方法完成后:认为事务还没有处理完,所以并不会完结它
简单来说:谁开启了事务,谁负责把它结束掉。手动事务在用的时候一定要慎重,能不用就别用了。
总结
事务是应用保证的最后一道防线,必须引起足够的重视,而且,感觉用的越简单越可靠越好!