Spring-Boot中的数据库事务

SpringBoot中使用了aop方式,通过注解可以非常方便的实现数据库的事务,本文将简单介绍如何在SpringBoot中开启事务,以及使用SpringBoot事务时的一些特殊的用法和注意事项。

基本配置

本文中采用是数据库为Mysql 8.0,数据库连接池为druid,持久层采用mybatis plus

pom.xml

<!-- mysql支持 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- jdbc支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- druid数据库连接池,spring-boot版本 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.21</version>
</dependency>

<!-- mybatis -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1.tmp</version>
</dependency>

1. 开启事务

在SpringBoot中开启事务十分简单,只需要在方法或类上添加Transactional注解就可以了,需要注意这个方法所在的类需要被Spring的ioc容器初始化。

PersonServiceImp.java

@Override
@Transactional(rollbackFor = Exception.class)
public Person get(Long id) {
    Person person = personMapper.selectById(id);
    return person;
}
  • 上面的代码中,在get方法上添加了Transactional注解,当其他模块调用这个方法时,Spring将会自动开启事务,并将该方法织入事务切面中,自动调用commit或者rollback。
  • 注解中的rollbackFor属性指定了当程序遇到什么类型的错误时执行回滚,如果不指定默认为runtimeException

2. 隔离级别

上面介绍的只是最基本的事务使用方法,对于SpringBoot中的事务还有一些特殊的配置,这里介绍一种称为隔离级别的配置。
隔离级别实际上是数据库事务的一种设置,数据库事务拥有四个基本属性,分别为原子性,一致性,隔离性和持久性。其中隔离性反映的就是数据库事务的隔离级别。(对于其他几个属性,这里就不详细展开了)
隔离性主要是针对多个事务线程同时访问同一数据的情况,有可能会造成的数据更新丢失(更新没有生效)的情况,为了针对不同的使用场景,提出了4种隔离级别。

隔离级别 描述
未提交读 最低的隔离级别,可以读取其他事务还未提交数据,会导致脏读
读写提交 第二级隔离级别,可以读取其他事务已经提交的数据,但是其他事务可能导致的数据变化无法获知
可重复读 第三级隔离级别,当某一线程事务读取某数据时,其他事务如果要读取这条数据需要等待,但是对于数据条数由于不属于数据本身,依然可能产生幻读(例如插入新数据)
串行化 最高隔离级别,所有sql按照顺序串行执行,效率最低

在SpringBoot中启用隔离级别只需要在Transaction的注解中指定isolation属性即可。

PersonServiceImp.java

@Override
//这里指定了事务的隔离级别为读写提交
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
public int add(Person person) {
    return personMapper.insert(person);
}

也可以全局设置默认隔离级别

application.properties

#数据源默认隔离级别
spring.datasource.tomcat.default-transaction-isolation=3
#数据库连接池默认隔离级别
spring.datasource.druid.default-transaction-isolation=3

2.传播行为

当SpringBoot中一个方法调用另一个方法时,事务是如何执行的?传播行为就是用来配置方法调用过程中事务的策略。
一般我们认为一个方法调用了事务,则在方法中的子方法应该也是处于同一个事务之中,当子方法中有错误产生,则需要回滚整个事务。但是考虑一种情况,如果我们在子方法中执行了100条sql,但是只有一条失败了,我们需要让其他99条提交,而只回滚最后一条,这就需要我们对事务的传播行为进行设置。

Spring的事务机制中提供了7中传播行为,使用枚举类Propagation定义:

枚举值 描述
REQUIRED 需要事务,默认传播行为,如果当前存在事务则沿用当前事务,否则新建一个事务
SUPPORTS 支持事务,如果存在事务则沿用,否则不采用事务
MANDATORY 必须使用事务,没有事务抛出异常,否则沿用当前事务
REQUIRES_NEW 无论是否存在当前事务,都会创建新事务
NOT_SUPPORTED 不支持事务,当前存在事务则挂起事务
NEVER 不支持事务,当前存在事务抛出异常,否则无事务运行
NESTED 当前方法调用子方法时,如果子方法异常,只回滚子方法sql

这里比较常用的就是上面表格中加粗的三类传播行为,之前提出的情况通过上面表格中的NESTED传播行为就可以解决。

使用传播行为也比较方便,只需要在子方法的Transaction注解中设置propagation的属性就可以了。

PersonServiceImp.java

@Service
public class PersonServiceImp implements PersonService, ApplicationContextAware {

    @Autowired
    PersonMapper personMapper;

    //子方法
    @Override
//    @Transactional(
//            rollbackFor = Exception.class, 
//            isolation = Isolation.READ_COMMITTED, 
//            propagation = Propagation.REQUIRES_NEW
//    )//使用新事务
    @Transactional(
            rollbackFor = Exception.class, 
            isolation = Isolation.READ_COMMITTED, 
            propagation = Propagation.NESTED
    )//只回滚当前事务
    public int add(Person person) {
        return personMapper.insert(person);
    }

PersonBatchServiceImp.java

@Service
public class PersonBatchServiceImp implements PersonBatchService {

    @Autowired
    PersonService personService;

    //主方法
    @Override
    @Transactional(
            rollbackFor = Exception.class, 
            isolation = Isolation.READ_COMMITTED, 
            propagation = Propagation.REQUIRED
    )//沿用当前事务
    public int batchAdd(List<Person> personList) {
        int count = 0;
        for(Person person : personList) {
            System.out.println("开始调用子方法...................................");
            count += personService.add(person);
            System.out.println("结束调用子方法...................................");
        }
        return count;
    }
}

上面的例子中,PersonBatchServiceImp的batchAdd方法调用了PersonServiceImp中的add方法,其中add方法指定了事务的传播行为为NESTED,因此,当某一次add失败,只会回滚本次add的sql,不会回滚整个事务。

3.事务失效

在上面的例子中,我们看到当一个方法调用另一个子方法时,我们是通过不同的Service之间进行调用,即使用PersonBatchService.batchAdd调用了PersonService.add。那我们是否可以使用同一个Service调用子方法实现子方法上的事务配置?

我们可以考虑下面的代码是否可行:

PersonServiceImp.java


@Transactional(
        rollbackFor = Exception.class,
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.NESTED
)//只回滚当前事务
public int add(Person person) {
    return personMapper.insert(person);
}

@Transactional(rollbackFor = Exception.class)
public int addBatchSelf(List<Person> personList) {
    int count = 0;

    for(Person person : personList) {
        //这里调用自身的接口子方法
        count += add(person);
    }

    return count;
}

实际上,这里并不会调用子接口的事务,原因在于SpringBoot的事务是基于AOP的,因此需要使用代理对象来执行子方法,才能将方法织入到对应的事务切面,然而这里调用自身的子方法不会使用代理对象,而是自生调用,因此不会触发AOP的过程,因此子方法的事务就无法生效了。

那子调用的方法有没有办法让他的事务生效呢?答案是有的

->我们只需要手动获取当前Service的代理对象就可以了

观察下面的代码:

PersonServiceImp.java

@Service
//这里实现了ApplicationContextAware接口,可以获取ioc容器实例
public class PersonServiceImp implements PersonService, ApplicationContextAware {

    @Autowired
    PersonMapper personMapper;

    //ico容器
    ApplicationContext context;

    @Override
//    @Transactional(
//            rollbackFor = Exception.class,
//            isolation = Isolation.READ_COMMITTED,
//            propagation = Propagation.REQUIRES_NEW
//    )//使用新事务
    @Transactional(
            rollbackFor = Exception.class,
            isolation = Isolation.READ_COMMITTED,
            propagation = Propagation.NESTED
    )//只回滚当前事务
    public int add(Person person) {
        return personMapper.insert(person);
    }

    @Override

    public int addBatchSelf(List<Person> personList) {
        int count = 0;

        //通过IOC容器获取personService的代理对象
        PersonService personService = context.getBean(PersonService.class);
        for(Person person : personList) {
            //这里调用自身的接口子方法,不会触发代理,因此无法走子方法的事务切面
            //count += add(person);

            //手动调用personService的代理,使其触发子方法的事务切面
            count += personService.add(person);

        }

        return count;
    }

    //获取ioc容器
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}

上面的代码里,我们通过手动获取ioc容器,并且从ioc容器中获取了personService的对象,这个对象是一个代理对象,因此,通过这个对象调用add子方法就可以触发子方法的事务。至于为什么ioc容器中获取的对象是一个代理对象,这是因为ioc容器在初始化bean时,会根据类中的方法是否有需要动态代理的方法来判断时直接通过反射实例化还是通过代理创建,如果一个类中的方法被Transactional注解,则这个类会被判断为需要动态代理的类,被实例化为一个代理对象。

总结

Spring-Boot中的事务使用非常方便,同时还可以通过设置隔离级别和传播行为实现各种业务上的需求,但同时也需要注意Spring-Boot的事务是基于Spring的AOP实现的,因此在某些情况下会导致事务失效,需要我们对Spring的AOP原理,以及Sping的Bean在启动时如何初始化有一定的了解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容