服务端开发中的事务(未完成版)

81 阅读7分钟

事务的定义和基本概念

事务就是具有原子性的一系列操作,要么成功要么失败。

  • 事务的传播特性:
    参考文章:juejin.cn/post/684490…
    如果系统中存在两个事务方法(方法A和方法B),如果方法B在方法A中被调用,那么阁下将如何应对?B事务可以是事务A中的嵌套的一个事务,或者事务B不使用事务,或者使用和事务A同样的事务。这些规则都得靠事务传播特性来实现。
/**
* required
* 如果不存在外层事务,就主动创建事务;否则就使用外层事务
*/
@Transactional(propagation = Propagation.REQUIRED)

/**
* supports
* 如果不存在外层事务,就不开启事务;否则就使用外层事务
*/
@Transactional(propagation = Propagation.SUPPORTS)

/**
* mandatory
* 如果不存在外层事务,就抛出异常;否则就使用外层事务
*/
@Transactional(propagation = Propagation.MANDATORY)

-   **PROPAGATION_REQUIRES_NEW**:总是主动开启事务;如果存在外层事务,就将外层事务挂起

-   **PROPAGATION_NOT_SUPPORTED**:总是不开启事务;如果存在外层事务,就将外层事务挂起

-   **PROPAGATION_NEVER**:总是不开启事务;如果存在外层事务,则抛出异常

为啥要使用事务的传播特性? 现在有两张表t_user\t_note,save()方法是新增t_user表记录,insertNote()是新增t_note()表的记录。

// 功能代码1
@Transactional(rollbackFor = Exception.class)
public void insertUser(String name) {
    User user = new User(name);
    userRepository.save(user);
    // 插入用户之后,我们插入一条用户笔记
    noteService.insertNote(name + "'s note");
}

// 功能代码2
@Transactional(rollbackFor = Exception.class)
public void insertNote(String content) {
    Note note = new Note(content);
    noteRepository.save(note);
}

// 测试方法,并且将事务自动回滚关闭
@Test
@Rollback(value = false)
public void test() {
    // v1:此时t_user\t_note表中都新增了一条记录
    userService.insertUser("wu ke fan");
}

加上事务传播级别,事务传播级别一般都是加在内层方法中。然后关闭外层方法的事务

// 功能代码1:关闭外层方法的事务
// @Transactional(rollbackFor = Exception.class)
public void insertUser(String name) {
    User user = new User(name);
    userRepository.save(user);
    // 插入用户之后,我们插入一条用户笔记
    noteService.insertNote(name + "'s note");
}

// 功能代码2:内层方法加上默认的事务传播特性
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertNote(String content) {
    Note note = new Note(content);
    noteRepository.save(note);
    // 这里手动抛出异常
    throw new RuntimeException();
}

然后再执行测试代码,这时候数据库表现是t_user新增了一条数据(总共两条),t_note没有新增(总共1条);因为外层方法没有开启事务,内层方法就主动开启一个事务,回滚后就没创建新的t_note记录,而外层没创建事务,就依然新增了一个数据。
那么如果是外层方法抛出异常呢?

@Transactional(rollbackFor = Exception.class)
public void insertUser(String name) {
    User user = new User(name);
    userRepository.save(user);
    noteService.insertNote(name + "'s note");
    throw new RuntimeException();   // ←
}

@Transactional(rollbackFor = Exception.class,  propagation = Propagation.REQUIRED)
public void insertNote(String content) {
    Note note = new Note(content);
    noteRepository.save(note);
}

执行测试代码后,两张表都没新增数据。因为外层有事务,按照required的事务传播特性,内层方法就使用外层事务,所以两张表都没有新增数据,进行了回滚。

  • 事务的隔离级别
    解决的是脏读(无效的数据读出)的问题。
    Read uncommited:一个事务可以读取另一个未提交的事务
    事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
    实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是==脏读==。

    Read committed:一个事务要等待另一个事务提交后才能读取数据。
    事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(==第二次检测金额当然要等待妻子转出金额事务提交完==)。程序员就会很郁闷,明明卡里是有钱的…
    这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。

    Repeatable read:在开始读取数据(事务开启)时,不再允许修改操作 事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(==事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
    重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。
    什么时候出现幻读?
    事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。 Serializable:序列化,事务串行化顺序执行,可以避免脏读、不可重重复读、幻读,但是效率比价低下,一般不使用;

    Mysql的默认隔离级别是Repeatable read

  • 事务的过期时间:

  • 事务的读写特性:

ps:尽量避免一个事务方法去调用另一个事务方法,避免不必要的麻烦。

Mysql事务

Mysql的事务隔离级别:
读提交
串行化

如何使用事务

Spring中怎么处理的?

线程池中怎么处理事务的?

如下代码会出现两个偶现的bug:
1 insert()方法在saveMsg()方法之后执行
2 saveMsg()的参数msg错误

public void insertXxx() {
    T t = insert(x);
    
    // 拼装msg
    Msg msg = doSomething(t);
    
    saveMsg(msg);
    
}

public void saveMsg(Msg msg) {
    // 该方法是在在线程池中执行的,所以是异步方法
    A(msg);
    ...
}

分析:
insertXxx()中的insert(x)方法是一个事务,saveMsg()中的方法A()是一个事务,所以 insert()和saveMsg()不在一个事务上。
bugfix v1:

public void saveMsg(Msg msg) {
    // 这样保证了A方法在前面的事务提交完成之后再执行
    TransationSychronizationManger.registerSynchronization(
        new TranscationSynchronizationsAdapter() {
            @Override
            public void afterCommit() {
                // 该方法是在在线程池中执行的,所以是异步方法
                A(msg); 
            }
        }
    );
}

现在有一个需求,需要外部方法调用saveMsg()方法,但是直接写可能报错:当前没有事务的情况下执行了TransationSychronizationManger.registerSynchronization(...)
所以需要判断当前事务是否存活。
bugfix v2:

public void saveMsg(Msg msg) {
    Boolean isActive = TransactionSynchronizationManager.isSynchronizationActive();
    if () {
        // 这样保证了A方法在前面的事务提交完成之后再执行
        TransationSychronizationManger.registerSynchronization(
            new TranscationSynchronizationsAdapter() {
                @Override
                public void afterCommit() {
                    // 该方法是在在线程池中执行的,所以是异步方法
                    A(msg); 
                }
            }
        );
    } else {
        A();
    }
}

请问为啥不在一个事务上?
上述案例的场景是事务的传播特性是Required,原则上insert()和saveMsg()实在同一个事务上。事务传播特性Required表示如果不存在外层事务,就主动创建事务;否则就使用外层事务。
怎么判断两个方法在不同的事务上?
看事务的传播特性!!
上述案例参考文章:blog.51cto.com/u_12393361/…

扩展:Go语言是怎么支持事务的

事务下的回滚方案

aop切面编程如何使用

Spring相关的注解

@Async注解