Spring Boot With Transaction

Spring Boot中,配置一套使用MySQL的环境非常简单,而且当我们使用了spring-boot-starter-jdbc的时候,Spring Boot会自动注入DataSourceTransactionManager启用帮助配置数据库事务相关的类。

Maven配置

pom.xml中添加

<dependencies>

        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
            <version>${spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--/ Spring Boot -->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <version>${spring-boot.version}</version>
        </dependency>

</dependencies>

然后构建一个主类Application.java:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        new SpringApplicationBuilder().sources(Application.class).run(args);
    }
}

application.properties中添加mysql相关的配置

spring.datasource.url=jdbc:mysql://localhost:3306/spring_demo
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

这个时候,Spring Boot已经默认添加好了事务相关的配置,@Transactional注解标记的方法或类就会被加上事务。

事务名词

在解Spring中事务的概念主要是4个隔离级别7个传播行为

隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。

我们开发时候主要相关的场景包括:

  • 脏读取:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
  • 重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
  • 幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。

所以在Spring中提供了这四种隔离级别:

public enum Isolation {
    // 底层数据库默认的隔离等级
    DEFAULT(-1),
    // 一个事务可以读取另一个事务修改但还没有提交的数据。
    READ_UNCOMMITTED(1),
    // 该隔离级别表示一个事务只能读取另一个事务已经提交的数据。
    READ_COMMITTED(2),
    // 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。
    REPEATABLE_READ(4),
    // 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

传播行为

传播行为指的是如果在开始当前事务之前,一个事务上下文(Context)已经存在,此时有若干选项可以指定一个事务性方法的执行行为。

这里打一个比方:

@Transactional(propagation = Propagation[A])
class ServiceA{
    public void insertOne(){
        // code statement
    } 
}

@Transactional(propagation = Propagation[B])
class ServiceB{
    public void insertMany(list){
        
        // 循环调用
        for(int i;i<list.size();i++){
            serviceA.insertOne()
        }
        
    }
}

ServiceBinsertMany()方法中已经存在Propagation[B]事务的上下文,ServiceAinsertOne()方法在insertMany()中被执行,那么它需要如何处理自己的方法(也就是insertOne()方法)中的事务问题。

在Spring中,提供了以下几种事务传播行为:

public enum Propagation {

    // 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    // @Transactional 注解默认采用这个方案
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

    // 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

    // 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

     // 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

    // 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

    // 以非事务方式运行,如果当前存在事务,则抛出异常。
    NEVER(TransactionDefinition.PROPAGATION_NEVER),

    // 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED
    // 并非所有的TransactionManager都能支持这个传播级别
    NESTED(TransactionDefinition.PROPAGATION_NESTED);

    private final int value;

    Propagation(int value) { this.value = value; }

    public int value() { return this.value; }

}

一般而言,被调用方法上定义的传播行为是高于调用方法的传播行为的,也就是上面例子中,insertOne()在这个例子中的传播行为高于insertMany()

构建测试方法

通过构建一个简单的应用,可以比较清晰地了解以上所说的是4个隔离级别7个传播行为

MySQL中定义一张用户表:

CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

java中对应构建一个实体类(UserEntity):

public class UserEntity {

    private Integer id;

    private String name;

    private Integer age;

    public UserEntity(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

定义一个UserService,然后在insertMany()insertOne()方法上分别定义事务的级别:

@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 添加多条数据
     * 
     * @param entities
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void insertMany(List<UserEntity> entities) {

        String sql = "insert into user (name, age) values(?, ?)";

        final List<Object[]> paramList = entities.stream().map(userEntity -> {

            Object arg1 = userEntity.getName();
            Object arg2 = userEntity.getAge();

            Object objects[] = { arg1, arg2 };

            return objects;

        }).collect(Collectors.toList());

        jdbcTemplate.batchUpdate(sql, paramList);

    }
    
    /**
     * 模拟方法的的反复调用
     * 
     * @param entities
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void insertManyOneByOne(List<UserEntity> entities) {

        for (int i = 0; i < entities.size(); i++) {

            // 假设到第三个用户插入的时候发生异常
            insertOne(entities.get(i),i == 3);
        }

    }

    /**
     * 添加单个数据
     * 
     * @param userEntity
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void insertOne(UserEntity userEntity, boolean isException) {

        if (isException){
            throw new RuntimeException();
        }

        String sql = "insert into user (name, age) values(?, ?)";

        jdbcTemplate.update(sql, userEntity.getName(), userEntity.getAge());

    }

}

测试代码:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserServiceTest extends ApplicationTest {

    @Autowired
    private UserService userService;

    @Test
    public void insertTest() {

        List<UserEntity> userEntityList = new ArrayList<>(10);

        userEntityList.add(new UserEntity("AAA",20));
        userEntityList.add(new UserEntity("BBB",21));
        userEntityList.add(new UserEntity("CCC",22));
        userEntityList.add(new UserEntity("DDD",23));
        userEntityList.add(new UserEntity("EEE",24));
        userEntityList.add(new UserEntity("FFF",25));

        userService.insertManyOneByOne(userEntityList);

    }

}

通过更个UserService中方法的事务级别,不断地换跑测试方法,就能测试Spring中的事务配置了。

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

推荐阅读更多精彩内容