java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > spring声明式事务

spring声明式事务@Transactional开发常犯的几个错误及最新解决方案

作者:梦在旅途

使用声明式事务@Transactional进行事务一致性的管理,在开发过程中,发现很多开发同学都用错了spring声明式事务@Transactional或使用不规范,导致出现各种事务问题,这篇文章主要介绍了spring声明式事务@Transactional开发常犯的几个错误及解决办法,需要的朋友可以参考下

目前JAVA的微服务项目基本都是SSM结构(即:springCloud +springMVC+Mybatis),而其中Mybatis事务的管理也是交由spring来管理,大部份都是使用声明式事务(@Transactional)来进行事务一致性的管理,然后在实际日常开发过程中,发现很多开发同学都用错了spring声明式事务(@Transactional)或者说使用非常不规范,导致出现各种事务问题。我(梦在旅途)今天周日休息,花了几个小时把目前我已知的开发常犯的几个错误都列举出来并逐一分析根本原因同时针对原因给出解决方案及示例,希望能帮助到广大JAVA开发者。

1. 事务不生效

问题现象:明明有事务注解,在事务方法内部有抛错,但事务却没有回滚,该执行的SQL都执行了

示例代码如下:(doInsert方法是有事务注解的)

/**
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
@Service
public class DemoUserService {
    //... ...
   public DemoUser doGet() {
        try {
            doInsert(1);
        } catch (Exception ex) {
            System.out.println("insert error: " + ex.toString());
        }
        return demoUserMapper.get(1);
    }
        @Transactional
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        throw new RuntimeException("mock insert ex");  //模拟抛错
 		return result;
    }
}
//演示调用,最终打印出了ID为1的那条记录,事务并没有回滚
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");

解决方案:不论是在BEAN外部或BEAN方法内部,要确保一定是调用代理BEAN的公开事务方法,确保调用事务方法有被SPRING事务拦截处理,示例代码如下:【在BEAN内部则需要先注入BEAN本身的代理BEAN实例(有很多中获取当前BEAN的代理BEAN方案,在此不细说),然后通过代理BEAN调事务方法即可。】

/**
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
@Service
public class DemoUserService {
    @Autowired
    @Lazy //加上这个,是防止循环自依赖
    private DemoUserService selfService; //注入自己的代理BEAN实例
        //... ...
    public DemoUser doGet() {
        try {
           selfService.doInsert(1); //这里改为使用代理BEAN调doInsert的事务方法,确保走切面
        } catch (Exception ex) {
            System.out.println("insert error: " + ex.toString());
        }
        return demoUserMapper.get(1);
    }
    @Transactional
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        throw new RuntimeException("mock insert ex");
//        return result;
    }
}
//演示调用,最终打印出了none,说明事务有回滚,无法查出ID为1的那个记录
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");

2. 事务提交报错

问题现象:事务方法内有catch住错误,但却无法正常提交事务,报错:Transaction rolled back because it has been marked as rollback-only,示例代码如下:

/**
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
@Service
public class DemoUserService {
    @Autowired
    @Lazy //加上这个,是防止循环自依赖
    private DemoUserService selfService; //注入自己的代理BEAN实例
        //... ...
    @Transactional
    public DemoUser doGet() {
        try {
           selfService.doInsert(1);
        } catch (Exception ex) { //有catch错误,但当doGet返回时却报错了
            System.out.println("insert error: " + ex.toString());
        }
        return demoUserMapper.get(1);
    }
    @Transactional
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        if (id==1) {
            throw new RuntimeException("mock insert ex");
        }
        return result;
    }
}
//演示调用,最终有报错:Transaction rolled back because it has been marked as rollback-only
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");

解决方案:避免事务继承 或 确保事务方法内部不再调用其他事务方法(即:事务方法变成普通方法,小技巧参照我之前文章:任何Bean通过实现ProxyableBeanAccessor接口即可获得动态灵活的获取代理对象或原生对象的能力 - 梦在旅途 - 博客园 (cnblogs.com)),示例代码如下:

/**
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
@Service
public class DemoUserService {
    @Autowired
    @Lazy //加上这个,是防止循环自依赖
    private DemoUserService selfService; //注入自己的代理BEAN实例
        //... ...
    @Transactional
    public DemoUser doGet() {
        try {
           selfService.doInsert(1);
           // doInsert(1);  方案二:内部直接doInsert方法,此时是原生方法调用,不走事务切面,也就不会触发事务记录的情况
        } catch (Exception ex) { //有catch错误
            System.out.println("insert error: " + ex.toString());
        }
        selfService.doInsert(2);
        return demoUserMapper.get(2);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:这里加上REQUIRES_NEW、或NOT_SUPPORTED,确保不继承外部事务即可
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        if (id==1) {
            throw new RuntimeException("mock insert ex");
        }
        return result;
    }
}
//演示调用,最终正确打印了ID为2的记录,说明虽然插入ID=1的记录失败了,但插入2的记录是正确的,入口事务有正确的提交
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");

3. 事务不回滚

问题现象:事务方法内部有报错,但事务却仍提交了,示例代码如下:

  /**代码片段
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
//第一种情况:错误被catch住了
@Transactional
    public DemoUser doGet1() {
        try {
            doInsert(1); //doInsert原生调用,代码看似有事务,实际此时无事务,也就不存在事务回滚的情况
        } catch (Exception ex) { //catch错误,doGet事务正常提交
            System.out.println("insert error: " + ex.toString());
        }
        selfService.doInsert(2);
        return demoUserMapper.get(2);
    }
//第二种情况:外层报错,内层事务正常提交
	@Transactional
    public DemoUser doGet2() {
        selfService.doInsert(2); //doInsert切面调用,有事务且单独事务,执行完即提交
        throw new RuntimeException("mock doGet ex");//这里抛错不影响doInsert的提交
        return demoUserMapper.get(2);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        if (id==1) {
            throw new RuntimeException("mock insert ex");
        }
        return result;
    }
//演示调用,第1种情况
DemoUser result = demoUserService.doGet1();
System.out.println(result != null ? result.toString() : "none");
//演示调用,第2种情况
DemoUser result = demoUserService.doGet2();
System.out.println(result != null ? result.toString() : "none");

解决方案:若需保证事务的完整性,需确保若有异常一定要抛错而非catch错误,另外需确保一定有事务,当事务方法内部有嵌套调用其他事务方法时,若希望被调用的事务方法与当前事务保持一致,那么就应确保是事务继承,否则就说明可以允许局部事务不一致,示例代码如下:

 /**代码片段
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
@Transactional
    public DemoUser doGet() {
        doInsert(1);//不要catch,若catch后记录日志后再抛出,总之一定要抛错
        selfService.doInsert(1);//这种也可以,当doInsert报错,则doInsert与doGet方法均回滚(本质是同一个事务)
        selfService.doInsert(2);
        return demoUserMapper.get(2);
    }
    @Transactional(propagation = Propagation.REQUIRED) //若需与外层事务这一致,这里建议采用REQUIRED的传播特性
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        if (id==1) {
            throw new RuntimeException("mock insert ex");
        }
        return result;
    }

4. 死锁

问题现象:执行SQL有报死锁,示例代码如下:

  /**代码片段
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
@Transactional
    public DemoUser doGetX() {
        selfService.doInsert(1);
        DemoUser user=selfService.get(1);
        user.setName("xxx");
        update(user); //这里是原生方法调用,等同于在doGetX同一个事务方法内部执行
        user.setName("xxx2");
        selfService.update(user); //这里新开事务调用,由于doGetX中已经有调用update(id=1)且事务还未提交,故这里需要等待doGetX事务提交以便释放锁,而doGetX事务则因为这里等待无法往下执行,形成事务循环自依赖了
        return demoUserMapper.get(1);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW) //这里新开事务
    public int update(DemoUser demoUser) {
        return demoUserMapper.update(demoUser);
    }
//演示调用,执行报错,不同DB的报错提示可能有所不同
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none");    

解决方案:避免事务被循环自依赖,示列代码如下:

 /**代码片段
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
//优化一
    @Transactional
    public DemoUser doGetX() {
        selfService.doInsert(1);
        DemoUser user=selfService.get(1);
        user.setName("xxx");
        update(user); //这里是原生方法调用,等同于在doGetX同一个事务方法内部执行
        user.setName("xxx2");
        update(user); //这里也改为原生方法调用,等同于在doGetX同一个事务方法内部执行
        return demoUserMapper.get(1);
    }
     //优化二
    @Transactional
    public DemoUser doGetX() {
        selfService.doInsert(1);
        DemoUser user=selfService.get(1);
        user.setName("xxx");
        selfService.update(user); //这里是代理BEAN方法调用,新开事务,直接执行并提交,与doGetX事务互不影响
        user.setName("xxx2");
        selfService.update(user); //这里是代理BEAN方法调用,新开事务,直接执行并提交,与doGetX事务互不影响
        return demoUserMapper.get(1);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW) //这里新开事务
    public int update(DemoUser demoUser) {
        return demoUserMapper.update(demoUser);
    }
//演示调用,执行报错,不同DB的报错提示可能有所不同
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none");    

5. 在事务提交后回调事件方法中开事务不生效

问题现象:在事务提交后回调事件方法中【即:afterCommit】开启事务不生效(即:添加了@Transactional,也执行了代理方法的调用,但就像没有事务一样,出现报错事务不回滚,也无法在事务方法中再次注册事务提交后回调事务件方法),示例代码如下:

 /**代码片段
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
@Transactional
    public DemoUser doGetX() {
        doInsert(1);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                selfService.doInsert(2);//走切面调用,确保执行代理的事务方法,但实际还是无事务,报错也不会回滚
            }
        });
        return demoUserMapper.get(1);
    }
    @Transactional(propagation = Propagation.REQUIRED)
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        if (id==2) {
            throw new RuntimeException("mock insert ex");
        }
        return result;
    }
    //演示调用:虽然doGetX有报错,但最终doInsert方法均有执行,且都能查出ID=1 与2的记录
        try {
            DemoUser result = demoUserService.doGetX();
            System.out.println(result != null ? result.toString() : "none");
        }catch (Exception e){
            System.out.println("error " + e.toString());
        }
        DemoUser result1 =demoUserService.get(1);
        System.out.println(result1 != null ? result1.toString() : "none");
        DemoUser result2 =demoUserService.get(2);
        System.out.println(result2 != null ? result2.toString() : "none");

解决方案:在事务提交后回调事件方法中【即:afterCommit】开启新事务(即:传播特性为:REQUIRES_NEW) 或者 执行前强制清除事务状态【需要编写事务状态清除工具类】,示例代码如下:

 /**代码片段
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
@Transactional
    public DemoUser doGetX() {
	    TxManagerUtils.clearTxStatus();//方案二:通过事务状态清除工具类注册事务回调后首先清除事务状态,二选其一即可
        doInsert(1);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                selfService.doInsert(2);//走切面调用,确保执行代理的事务方法
            }
        });
        return demoUserMapper.get(1);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:这里强制开启新事务,二选其一即可
    public int doInsert(int id) {
        DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
                "shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
        int result = demoUserMapper.insert(user);
        if (id==2) {
            throw new RuntimeException("mock insert ex");
        }
        return result;
    }
    //演示调用:虽然doGetX有报错,但只能查出ID=1的记录,ID=2由于报错事务回滚了,说明afterCommit中再开启事务是OK的
        try {
            DemoUser result = demoUserService.doGetX();
            System.out.println(result != null ? result.toString() : "none");
        }catch (Exception e){
            System.out.println("error " + e.toString());
        }
        DemoUser result1 =demoUserService.get(1);
        System.out.println(result1 != null ? result1.toString() : "none");
        DemoUser result2 =demoUserService.get(2);
        System.out.println(result2 != null ? result2.toString() : "none");

事务状态清除工具类如下:

package org.springframework.jdbc.datasource; //必需放在这个包目录下,因为connectionHolder.setTransactionActive 是protected方法
import com.example.springwebapp.utils.SpringUtils;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.sql.DataSource;
/**
 * @author zuowenjun
 * @see wwww.zuowenjun.cn
 */
public class TxManagerUtils {
    //建议在每个事务方法的第一行调用,避免事务方法内部中途若有其他方法需要注册事务提交后回调方法
    public static void clearTxStatus() {
        DataSource dataSource = SpringUtils.getBean(DataSource.class);
        ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public int getOrder() {
                return Integer.MIN_VALUE; //确保最先执行
            }
            @Override
            public void afterCommit() {
                doClearTxStatus(); //第一个回调事件中先清除事务状态
            }
            @Override
            public void afterCompletion(int status) {
                TransactionSynchronizationManager.bindResource(dataSource, connectionHolder); //恢复DB连接绑定,避免执行事务清理时报错
            }
        });
    }
    private static void doClearTxStatus() {
        DataSource dataSource = SpringUtils.getBean(DataSource.class);
        TransactionSynchronizationManager.setActualTransactionActive(false); //设置事务状态为非激活
        ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        connectionHolder.setTransactionActive(false);//设置事务状态为非激活
        TransactionSynchronizationManager.unbindResource(dataSource); //暂时解绑DB连接
    }
}

注:后面我预计还会针对spring事务这块进行其他方面的分享(比如:spring事务在多数据源中切换数据源不生效、事务隔离级别下的并发处理等),敬请期待,原创不易,若有不足欢迎指出,谢谢!

到此这篇关于spring声明式事务@Transactional开发常犯的几个错误及解决办法的文章就介绍到这了,更多相关spring声明式事务内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文