【数据库 老中医】并发查询后更新下的数据库事务

479 阅读3分钟

摘要

在同一个Spring的事务中,查询一行数据,修改后写回。在并发执行该事务时,如果不加特殊处理,会出现覆盖更新的问题。下面是问题的场景:

直播打赏中,数据库记录主播当前的收入income。观众打赏主播 m 元时,后台开启事务先查询当前主播的income,然后更新income <- income + m,再写回新的income到数据库中。此时有两个人同时打赏主播,如果不做特殊处理,那么将会导致有一个人的打赏金额丢失。

特殊处理包括以下方法:

  1. 悲观锁
    • 例如数据库行锁
  2. 乐观锁
    • 例如Mybatis-Plus 的乐观锁插件
  3. 并发无锁设计
    • 例如记录持久化,变化量内存化

并发查询后更新下的数据库事务

Service层有一个复杂的逻辑,需要先查询数据库中的一个数据行,然后根据查询结果进行业务逻辑,最后更新该数据行。即在事务中完成对同一个数据行的读改写操作。

数据库使用Mysql,引擎使用InnoDB,隔离级别为可重复读。

举例

例如旋涡鸣人(Naruto)打工赚钱,他找到一份[1,5]天的工作,每天赚1$,为了加速赚钱,他决定用分身术造多个分身去并行打不同的工。

  1. 首先数据库表格式为
mysql> select * from t_user t where t.name = 'Naruto';
+----+--------+------+--------------------+--------+
| id | name   | age  | email              | income |
+----+--------+------+--------------------+--------+
|  6 | Naruto |   18 | test6@baomidou.com |      0 |
+----+--------+------+--------------------+--------+
  1. 对应的service
@Service
@Slf4j
public class UserServiceImpl implements UserSerivce {
    @Resource
    private UserDAO userDAO;
    @Resource
    private Random random;

    @SneakyThrows
    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void working(String name) {
        UserEntity user = userDAO.getByName(name);
        // 打印他当前收入
        log.info("the current income of {} is {}$.", name, user.getIncome());

        // 获得一份工作,工作时间为[1, 5]天,每天赚1$
        int workDays = random.nextInt(5) + 1;
        log.info("{} workes {} days. working...", name, workDays);

        // 工作一天休眠一秒
        Thread.sleep(workDays * 1000);

        // 工作一天赚1$
        int money = workDays * 1;
        long income = user.getIncome() + money;
        log.info("{} made a profit of {}$ and his current income is {}.", name, money, income);
        user.setIncome(income);

        boolean succ = userDAO.updateById(user);
        log.info("update successful? {}", succ);
    }
}
  1. 现在鸣人用分身成两个人同时打两份不同的工,用单元测试表示为
@SpringBootTest(classes = {RunningApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
public class RunningApplicationTest {
    @Resource
    private UserSerivce userSerivce;
    @Resource
    private UserDAO userDAO;

    @SneakyThrows
    @Test
    public void test() {
        String name = "Naruto";
        // 分身1 打工
        Thread workThread1 = new Thread(() -> userSerivce.working(name));
        workThread1.setName("Naruto-shadow-1");
        // 分身2 打工
        Thread workThread2 = new Thread(() -> userSerivce.working(name));
        workThread2.setName("Naruto-shadow-2");
        
        workThread1.start();
        workThread2.start();

        workThread1.join();
        workThread2.join();
        
        // 等分身都打完工后,统计收入
        UserEntity user = userDAO.getByName(name);
        log.info("the current income of {} is {}$.", name, user.getIncome());
    }
}
  1. 下面是一种运行结果
2021-08-15 13:49:19.978 DEBUG 6944 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : ==>  Preparing: SELECT id,name,age,email,income FROM t_user WHERE (name = ?)
2021-08-15 13:49:19.978 DEBUG 6944 --- [Naruto-shadow-1] org.example.mapper.UserMapper.selectOne  : ==>  Preparing: SELECT id,name,age,email,income FROM t_user WHERE (name = ?)
2021-08-15 13:49:20.005 DEBUG 6944 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : ==> Parameters: Naruto(String)
2021-08-15 13:49:20.005 DEBUG 6944 --- [Naruto-shadow-1] org.example.mapper.UserMapper.selectOne  : ==> Parameters: Naruto(String)
2021-08-15 13:49:20.033 DEBUG 6944 --- [Naruto-shadow-1] org.example.mapper.UserMapper.selectOne  : <==      Total: 1
2021-08-15 13:49:20.033 DEBUG 6944 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : <==      Total: 1
2021-08-15 13:49:20.040  INFO 6944 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : the current income of Naruto is 0$.
2021-08-15 13:49:20.040  INFO 6944 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : Naruto workes 1 days. working...
2021-08-15 13:49:20.040  INFO 6944 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : the current income of Naruto is 0$.
2021-08-15 13:49:20.041  INFO 6944 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : Naruto workes 5 days. working...
2021-08-15 13:49:21.043  INFO 6944 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : Naruto made a profit of 1$ and his current income is 1.
2021-08-15 13:49:21.048 DEBUG 6944 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : ==>  Preparing: UPDATE t_user SET name=?, age=?, email=?, income=? WHERE id=?
2021-08-15 13:49:21.048 DEBUG 6944 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : ==> Parameters: Naruto(String), 18(Integer), test6@baomidou.com(String), 1(Long), 6(Long)
2021-08-15 13:49:21.051 DEBUG 6944 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : <==    Updates: 1
2021-08-15 13:49:21.051  INFO 6944 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : update successful? true
2021-08-15 13:49:25.045  INFO 6944 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : Naruto made a profit of 5$ and his current income is 5.
2021-08-15 13:49:25.046 DEBUG 6944 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : ==>  Preparing: UPDATE t_user SET name=?, age=?, email=?, income=? WHERE id=?
2021-08-15 13:49:25.046 DEBUG 6944 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : ==> Parameters: Naruto(String), 18(Integer), test6@baomidou.com(String), 5(Long), 6(Long)
2021-08-15 13:49:25.049 DEBUG 6944 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : <==    Updates: 1
2021-08-15 13:49:25.049  INFO 6944 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : update successful? true
2021-08-15 13:49:25.054 DEBUG 6944 --- [           main] org.example.mapper.UserMapper.selectOne  : ==>  Preparing: SELECT id,name,age,email,income FROM t_user WHERE (name = ?)
2021-08-15 13:49:25.055 DEBUG 6944 --- [           main] org.example.mapper.UserMapper.selectOne  : ==> Parameters: Naruto(String)
2021-08-15 13:49:25.057 DEBUG 6944 --- [           main] org.example.mapper.UserMapper.selectOne  : <==      Total: 1
2021-08-15 13:49:25.058  INFO 6944 --- [           main] org.example.RunningApplicationTest       : the current income of Naruto is 5$.

结果分析:

首先展示分身1和分身2的先后顺序:

时间分身1分身2
1查询收入为0$查询收入为0$
2赚了1$
3收入更新为1$
4赚了5$
5收入更新为5$
6最终两个分身共赚了6$, 但收入为5$
  1. 分身1和分身2分别赚了5$和1$,最终收入应该为6$,但是最终查询结果为5$。这说明分身2的收入被覆盖掉了。
  2. 进一步分析原因,分身1后更新数据,但是它查询的收入是分身2更新前的结果。
  3. 在并发事务下,分身1和分身2同时查询鸣人的当前收入,当分身2更新了鸣人收入时,分身1并不知道,导致分身1更新数据库时覆盖了分身2的更新。

使用行锁解决并发查询后更新的问题

原理:在查询时锁定数据行select for update,防止本事务在结束前该行数据被其他事务读取和修改,其他事务被阻塞。

修改内容:将按名字查询getByName加行锁select * from t_user user where user.name = #{name} for update

@Service
public class UserDAOImpl extends ServiceImpl<UserMapper, UserEntity> implements UserDAO {
    @Override
    public UserEntity getByName(String name) {
        return baseMapper.getByNameForUpdate(name);
    }
}
public interface UserMapper extends BaseMapper<UserEntity> {
    @Select(value = "select * from t_user user where user.name = #{name} for update")
    UserEntity getByNameForUpdate(@Param("name") String name);
}

下面是一种运行结果

2021-08-15 14:17:13.400 DEBUG 89281 --- [Naruto-shadow-1] o.e.m.UserMapper.getByNameForUpdate      : ==>  Preparing: select * from t_user user where user.name = ? for update
2021-08-15 14:17:13.400 DEBUG 89281 --- [Naruto-shadow-2] o.e.m.UserMapper.getByNameForUpdate      : ==>  Preparing: select * from t_user user where user.name = ? for update
2021-08-15 14:17:13.426 DEBUG 89281 --- [Naruto-shadow-1] o.e.m.UserMapper.getByNameForUpdate      : ==> Parameters: Naruto(String)
2021-08-15 14:17:13.426 DEBUG 89281 --- [Naruto-shadow-2] o.e.m.UserMapper.getByNameForUpdate      : ==> Parameters: Naruto(String)
2021-08-15 14:17:13.454 DEBUG 89281 --- [Naruto-shadow-2] o.e.m.UserMapper.getByNameForUpdate      : <==      Total: 1
2021-08-15 14:17:13.460  INFO 89281 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : the current income of Naruto is 0$.
2021-08-15 14:17:13.461  INFO 89281 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : Naruto workes 4 days. working...
2021-08-15 14:17:17.464  INFO 89281 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : Naruto made a profit of 4$ and his current income is 4.
2021-08-15 14:17:17.494 DEBUG 89281 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : ==>  Preparing: UPDATE t_user SET name=?, age=?, email=?, income=? WHERE id=?
2021-08-15 14:17:17.498 DEBUG 89281 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : ==> Parameters: Naruto(String), 18(Integer), test6@baomidou.com(String), 4(Long), 6(Long)
2021-08-15 14:17:17.501 DEBUG 89281 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : <==    Updates: 1
2021-08-15 14:17:17.501  INFO 89281 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : update successful? true
2021-08-15 14:17:17.506 DEBUG 89281 --- [Naruto-shadow-1] o.e.m.UserMapper.getByNameForUpdate      : <==      Total: 1
2021-08-15 14:17:17.506  INFO 89281 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : the current income of Naruto is 4$.
2021-08-15 14:17:17.507  INFO 89281 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : Naruto workes 1 days. working...
2021-08-15 14:17:18.509  INFO 89281 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : Naruto made a profit of 1$ and his current income is 5.
2021-08-15 14:17:18.510 DEBUG 89281 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : ==>  Preparing: UPDATE t_user SET name=?, age=?, email=?, income=? WHERE id=?
2021-08-15 14:17:18.510 DEBUG 89281 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : ==> Parameters: Naruto(String), 18(Integer), test6@baomidou.com(String), 5(Long), 6(Long)
2021-08-15 14:17:18.512 DEBUG 89281 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : <==    Updates: 1
2021-08-15 14:17:18.512  INFO 89281 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : update successful? true
2021-08-15 14:17:18.515 DEBUG 89281 --- [           main] o.e.m.UserMapper.getByNameForUpdate      : ==>  Preparing: select * from t_user user where user.name = ? for update
2021-08-15 14:17:18.516 DEBUG 89281 --- [           main] o.e.m.UserMapper.getByNameForUpdate      : ==> Parameters: Naruto(String)
2021-08-15 14:17:18.517 DEBUG 89281 --- [           main] o.e.m.UserMapper.getByNameForUpdate      : <==      Total: 1
2021-08-15 14:17:18.517  INFO 89281 --- [           main] org.example.RunningApplicationTest       : the current income of Naruto is 5$.

结果分析:

首先展示分身1和分身2的先后顺序:

时间分身1分身2
1发出查询发出查询
2当前收入为0$
3工作4天,赚了4$
4更新收入为4$
5当前收入为4$
6工作1天,赚了1$
7更新收入为5$
8分身共赚了5$,收入5$
  1. 可以看到,当分身2(事务2)获得查询结果时,它将对该数据行加锁,分身1(事务1)对该数据行的查询被阻塞到分身2(事务2)结束。
  2. 因此加行锁可以解决并发查询后更新的修改覆盖问题。其本质是加悲观锁,这会导致并发程度降低,对该数据行更新的事务串行执行。

使用乐观锁解决并发查询后更新的问题

原理:更新时判断当前数据行版本有无变化,如果当前版本变化了,这更新失败,重试或者回滚。

动作:

  1. 数据表加入一列 version,取出记录时,获取当前version.
  2. 更新时,带上这个version
  3. 执行更新时, set version = newVersion where version = oldVersion
  4. 如果version不对,就更新失败

修改:

  1. 使用Mybatis-Plus的乐观锁插件
  2. 数据库增加一个BIGINT(20)字段version,表示乐观锁版本
  3. UserServiceImpl关闭事务,因为在可重复读的隔离级别下,重复读取同一行数据,其结果相同。因此此时乐观锁只能用来判断更新是否成功,不能用于重试更新,达成更新成功目标。
mysql> select * from t_user t where t.name = 'Naruto';
+----+---------+--------+------+--------------------+--------+
| id | version | name   | age  | email              | income |
+----+---------+--------+------+--------------------+--------+
|  6 |       2 | Naruto |   18 | test6@baomidou.com |      0 |
+----+---------+--------+------+--------------------+--------+
@Configuration
public class UtilConfiguration {
    @Bean
    public Random random() {
        return new Random();
    }

    // 使用Mybatis-Plus的乐观锁插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
@Service
@Slf4j
public class UserServiceImpl implements UserSerivce {
    @Resource
    private UserDAO userDAO;
    @Resource
    private Random random;

    @SneakyThrows
    @Override
    public void working(String name) {
        UserEntity user = userDAO.getByName(name);
        // 打印他当前收入
        log.info("the current income of {} is {}$.", name, user.getIncome());

        // 获得一份工作,工作时间为[1, 5]天,每天赚1$
        int workDays = random.nextInt(5) + 1;
        log.info("{} workes {} days. working...", name, workDays);

        // 工作一天休眠一秒
        Thread.sleep(workDays * 1000);

        // 工作一天赚1$
        int money = workDays * 1;
        long income = user.getIncome() + money;
        log.info("{} made a profit of {}$ and his current income is {}.", name, money, income);
        user.setIncome(income);

        // 更新是否成功
        boolean succ = userDAO.updateById(user);
        log.info("update successful? {}", succ);

        // 重试直到更新成功
        while (!succ) {
            user = userDAO.getByName(name);
            log.info("the current income of {} is {}$.", name, user.getIncome());
            income = user.getIncome() + money;
            user.setIncome(income);
            succ = userDAO.updateById(user);
            log.info("update successful? {}", succ);
        }
    }
}

下面是一种运行结果

2021-08-15 15:07:20.091 DEBUG 24117 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : ==>  Preparing: SELECT id,version,name,age,email,income FROM t_user WHERE (name = ?)
2021-08-15 15:07:20.091 DEBUG 24117 --- [Naruto-shadow-1] org.example.mapper.UserMapper.selectOne  : ==>  Preparing: SELECT id,version,name,age,email,income FROM t_user WHERE (name = ?)
2021-08-15 15:07:20.116 DEBUG 24117 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : ==> Parameters: Naruto(String)
2021-08-15 15:07:20.116 DEBUG 24117 --- [Naruto-shadow-1] org.example.mapper.UserMapper.selectOne  : ==> Parameters: Naruto(String)
2021-08-15 15:07:20.142 DEBUG 24117 --- [Naruto-shadow-1] org.example.mapper.UserMapper.selectOne  : <==      Total: 1
2021-08-15 15:07:20.142 DEBUG 24117 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : <==      Total: 1
2021-08-15 15:07:20.148  INFO 24117 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : the current income of Naruto is 0$.
2021-08-15 15:07:20.148  INFO 24117 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : the current income of Naruto is 0$.
2021-08-15 15:07:20.148  INFO 24117 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : Naruto workes 3 days. working...
2021-08-15 15:07:20.148  INFO 24117 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : Naruto workes 4 days. working...
2021-08-15 15:07:23.152  INFO 24117 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : Naruto made a profit of 3$ and his current income is 3.
2021-08-15 15:07:23.158 DEBUG 24117 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : ==>  Preparing: UPDATE t_user SET version=?, name=?, age=?, email=?, income=? WHERE id=? AND version=?
2021-08-15 15:07:23.159 DEBUG 24117 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : ==> Parameters: 1(Long), Naruto(String), 18(Integer), test6@baomidou.com(String), 3(Long), 6(Long), 0(Long)
2021-08-15 15:07:23.161 DEBUG 24117 --- [Naruto-shadow-1] o.example.mapper.UserMapper.updateById   : <==    Updates: 1
2021-08-15 15:07:23.161  INFO 24117 --- [Naruto-shadow-1] o.example.service.impl.UserServiceImpl   : update successful? true
2021-08-15 15:07:24.152  INFO 24117 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : Naruto made a profit of 4$ and his current income is 4.
2021-08-15 15:07:24.154 DEBUG 24117 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : ==>  Preparing: UPDATE t_user SET version=?, name=?, age=?, email=?, income=? WHERE id=? AND version=?
2021-08-15 15:07:24.155 DEBUG 24117 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : ==> Parameters: 1(Long), Naruto(String), 18(Integer), test6@baomidou.com(String), 4(Long), 6(Long), 0(Long)
2021-08-15 15:07:24.156 DEBUG 24117 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : <==    Updates: 0
2021-08-15 15:07:24.156  INFO 24117 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : update successful? false
2021-08-15 15:07:24.157 DEBUG 24117 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : ==>  Preparing: SELECT id,version,name,age,email,income FROM t_user WHERE (name = ?)
2021-08-15 15:07:24.158 DEBUG 24117 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : ==> Parameters: Naruto(String)
2021-08-15 15:07:24.159 DEBUG 24117 --- [Naruto-shadow-2] org.example.mapper.UserMapper.selectOne  : <==      Total: 1
2021-08-15 15:07:24.159  INFO 24117 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : the current income of Naruto is 3$.
2021-08-15 15:07:24.160 DEBUG 24117 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : ==>  Preparing: UPDATE t_user SET version=?, name=?, age=?, email=?, income=? WHERE id=? AND version=?
2021-08-15 15:07:24.161 DEBUG 24117 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : ==> Parameters: 2(Long), Naruto(String), 18(Integer), test6@baomidou.com(String), 7(Long), 6(Long), 1(Long)
2021-08-15 15:07:24.163 DEBUG 24117 --- [Naruto-shadow-2] o.example.mapper.UserMapper.updateById   : <==    Updates: 1
2021-08-15 15:07:24.163  INFO 24117 --- [Naruto-shadow-2] o.example.service.impl.UserServiceImpl   : update successful? true
2021-08-15 15:07:24.165 DEBUG 24117 --- [           main] org.example.mapper.UserMapper.selectOne  : ==>  Preparing: SELECT id,version,name,age,email,income FROM t_user WHERE (name = ?)
2021-08-15 15:07:24.165 DEBUG 24117 --- [           main] org.example.mapper.UserMapper.selectOne  : ==> Parameters: Naruto(String)
2021-08-15 15:07:24.167 DEBUG 24117 --- [           main] org.example.mapper.UserMapper.selectOne  : <==      Total: 1
2021-08-15 15:07:24.167  INFO 24117 --- [           main] org.example.RunningApplicationTest       : the current income of Naruto is 7$.

结果分析

首先展示分身1和分身2的先后顺序

时间分身1分身2
1发出查询,当前收入为0$发出查询,当前收入为0$
2工作3天工作4天
3赚了3$
4尝试更新收入为3$,成功
5赚了4$
6尝试更新收入为4$,失败
7发出查询,当前收入为3$
8尝试更新收入为7$,成功
  1. 使用乐观锁可以避免查询后更新的更新覆盖问题。
  2. 乐观锁适合并发程度不高的场景。可以减少悲观锁的开销。
  3. 当并发程度高的时候,使用乐观锁会导致大量的更新失败,此时性能不如悲观锁。

查询和更新分开解决并发查询后更新的问题

出现并发查询后更新,数据被覆盖更新问题的根本原因是,working函数希望把读-改-写变成一个原子操作。并且更新的对象,income 是一个会变化的量。

重新思考下设计方案,其实收入并不需要记录到数据库中(或者说不需要实时反应在数据库上)。数据库应该保存记录。也就是说

记录持久化,收入内存化。

收入是赚钱累积的结果,在不断变化。但赚钱是记录,一旦发生,不会再变,分身x赚了10$,这条记录产生了,就永远不会变。这类记录持久化,放在DB里。

有了记录就可以随时重建收入。收入可以持久化到数据库中,也可以不持久化。并且将<收入, 时间戳>一起持久化到数据库中,可以减少重建收入的时间。持久化也不是更新原有的收入,而是插入一条新数据行,这样可以方便后期对账各个时间段收入和赚钱是否匹配。

收入存储在内存中,用并发类或者原子类来保护。

总结

事务中有读改写操作,需要注意数据是否会被覆盖更新。

处理方案有包括以下方法:

  1. 悲观锁
    • 例如数据库行锁
  2. 乐观锁
    • 例如Mybatis-Plus 的乐观锁插件
  3. 并发无锁设计
    • 例如记录持久化,变化量内存化