摘要
在同一个Spring的事务中,查询一行数据,修改后写回。在并发执行该事务时,如果不加特殊处理,会出现覆盖更新的问题。下面是问题的场景:
直播打赏中,数据库记录主播当前的收入income。观众打赏主播 m 元时,后台开启事务先查询当前主播的income,然后更新income <- income + m,再写回新的income到数据库中。此时有两个人同时打赏主播,如果不做特殊处理,那么将会导致有一个人的打赏金额丢失。
特殊处理包括以下方法:
- 悲观锁
- 例如数据库行锁
- 乐观锁
- 例如Mybatis-Plus 的乐观锁插件
- 并发无锁设计
- 例如记录持久化,变化量内存化
并发查询后更新下的数据库事务
在Service层有一个复杂的逻辑,需要先查询数据库中的一个数据行,然后根据查询结果进行业务逻辑,最后更新该数据行。即在事务中完成对同一个数据行的读改写操作。
数据库使用Mysql,引擎使用InnoDB,隔离级别为可重复读。
举例
例如旋涡鸣人(Naruto)打工赚钱,他找到一份[1,5]天的工作,每天赚1$,为了加速赚钱,他决定用分身术造多个分身去并行打不同的工。
- 首先数据库表格式为
mysql> select * from t_user t where t.name = 'Naruto';
+----+--------+------+--------------------+--------+
| id | name | age | email | income |
+----+--------+------+--------------------+--------+
| 6 | Naruto | 18 | test6@baomidou.com | 0 |
+----+--------+------+--------------------+--------+
- 对应的
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);
}
}
- 现在鸣人用分身成两个人同时打两份不同的工,用单元测试表示为
@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());
}
}
- 下面是一种运行结果
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和分身2分别赚了5$和1$,最终收入应该为6$,但是最终查询结果为5$。这说明分身2的收入被覆盖掉了。
- 进一步分析原因,分身1后更新数据,但是它查询的收入是分身2更新前的结果。
- 在并发事务下,分身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$ |
- 可以看到,当分身2(事务2)获得查询结果时,它将对该数据行加锁,分身1(事务1)对该数据行的查询被阻塞到分身2(事务2)结束。
- 因此加行锁可以解决并发查询后更新的修改覆盖问题。其本质是加悲观锁,这会导致并发程度降低,对该数据行更新的事务串行执行。
使用乐观锁解决并发查询后更新的问题
原理:更新时判断当前数据行版本有无变化,如果当前版本变化了,这更新失败,重试或者回滚。
动作:
- 数据表加入一列 version,取出记录时,获取当前version.
- 更新时,带上这个version
- 执行更新时, set version = newVersion where version = oldVersion
- 如果version不对,就更新失败
修改:
- 使用Mybatis-Plus的乐观锁插件
- 数据库增加一个BIGINT(20)字段
version,表示乐观锁版本 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$,成功 |
- 使用乐观锁可以避免查询后更新的更新覆盖问题。
- 乐观锁适合并发程度不高的场景。可以减少悲观锁的开销。
- 当并发程度高的时候,使用乐观锁会导致大量的更新失败,此时性能不如悲观锁。
查询和更新分开解决并发查询后更新的问题
出现并发查询后更新,数据被覆盖更新问题的根本原因是,working函数希望把读-改-写变成一个原子操作。并且更新的对象,income 是一个会变化的量。
重新思考下设计方案,其实收入并不需要记录到数据库中(或者说不需要实时反应在数据库上)。数据库应该保存记录。也就是说
记录持久化,收入内存化。
收入是赚钱累积的结果,在不断变化。但赚钱是记录,一旦发生,不会再变,分身x赚了10$,这条记录产生了,就永远不会变。这类记录持久化,放在DB里。
有了记录就可以随时重建收入。收入可以持久化到数据库中,也可以不持久化。并且将<收入, 时间戳>一起持久化到数据库中,可以减少重建收入的时间。持久化也不是更新原有的收入,而是插入一条新数据行,这样可以方便后期对账各个时间段收入和赚钱是否匹配。
收入存储在内存中,用并发类或者原子类来保护。
总结
事务中有读改写操作,需要注意数据是否会被覆盖更新。
处理方案有包括以下方法:
- 悲观锁
- 例如数据库行锁
- 乐观锁
- 例如Mybatis-Plus 的乐观锁插件
- 并发无锁设计
- 例如记录持久化,变化量内存化