解放生产力orm并发更新下应该这么处理求求你别再用UpdateById了
背景
很多时候为了方便我们都采用实体对象进行前后端的数据交互,然后为了便捷开发我们都会采用DTO对象进行转换为数据库对象,然后调用UpdateById
将变更后的数据存入到数据库内,这样的一个做法有什么问题呢,如果你的系统并发量特别少甚至没有并发量那么这么做是没什么关系的无可厚非,但是如果你的系统有并发量那么在某些情况下会有严重的问题.
案例1
现在我们有一条待审核记录,其中status
0表示待提交, 1表示待审核
id | name | status | description |
---|---|---|---|
1 | 记录1 | 0 | 我是备注 |
假设有两个用户,A用户想对当前记录的description
字段进行修改,B用户想对当前记录进行提交
用户请求
/api/update
- 用户A:
{"id":1,"name":"记录1","status":0,"description":"修改后的备注"}
- 用户B:
{"id":1,"name":"记录1","status":1,"description":"我是备注 "}
修改接口
A用户伪代码
Entity entity = entityMapper.selectOne(1);//A1
//查询结果{"id":1,"name":"记录1","status":0,"description":"我是备注'"}
if(status.待审核!=entity.status){//A2
throw new BusinessException("当前记录无法修改");
}
BeanUtil.copyProperties(request,entity);//A3
entityMapper.updateById(entity);//A4
-- update table set name='记录1',status=0,description='修改后的备注' where id=1
提交接口
B用户伪代码
Entity entity = entityMapper.selectOne(1);//B1
//查询结果{"id":1,"name":"记录1","status":0,"description":"我是备注'"}
if(status.待审核!=entity.status){//B2
throw new BusinessException("当前记录无法提交");
}
entity.status=status.待审核;//B3
entityMapper.updateById(entity);//B4
-- update table set name='记录1',status=1,description='我是备注', where id=1
提交请求
A1=>A2=>A3=>B1=>B2=>B3=>B4=>A4
加入并发情况下那么针对当前记录我们生成的两个操作因为没有考虑并发问题基于上述执行顺序,最终数据库的记录将会被A4覆盖也就是提交失败,那么如果提交审核会触发一些事件那么就就会有严重的问题产生,操作将会变得不是幂等。
解决方案
乐观锁
首先我们修改表结构添加版本号字段
id | name | status | description | version |
---|---|---|---|---|
1 | 记录1 | 0 | 我是备注 | 1 |
A4和B4的执行sql改为orm支持的乐观锁模式
-- A4
update table set name='记录1',status=0,description='修改后的备注',version=2 where id=1 and version=1
-- B4
update table set name='记录1',status=1,description='我是备注',version=2 where id=1 and version=1
因为A4和B4两条记录只有一条记录可以生效,所以另一条语句肯定返回受影响行数为0.对于返回为0的操作可以告知用户端操作失败请重试。
这种方式看着看着很美好但是也是有一定的缺点的,就是他是乐观锁强串行化,针对一些不必要的字段其实大部分的时候我们完全可以采取后覆盖
模式比如修改name
,修改description
,但是因为乐观锁的存在导致我们的并发粒度变粗所以是否使用乐观锁需要进行一个取舍。
分布式锁
通过在请求外部也就是A1-A4和B1-B4外部进行lock包裹,让两个执行变成串行化,可以用id:1作为分布式锁的key,加入A先执行那么B执行后可以提交,加入B先执行那么A就会报错,缺点也很明显需要将对应记录的任何操作都进行分布式锁进行处理。需要掌握好锁的粒度和管理,如果出现其他业务操作中涉及到当前记录的修改那么分布式锁又会遇到很多问题,在单一环境下分布式锁可以解决,但是大部分情况下并不是用在这个场景下。
以判断条件为乐观锁
既然乐观锁有粒度太粗导致并发度太低,那么可以选择性不要一刀切,我们以状态来作为乐观锁更新数据
-- A4
update table set name='记录1',status=0,description='修改后的备注' where id=1 and status=0//status=0是因为我们查到的是0
-- B4
update table set name='记录1',status=1,description='我是备注' where id=1 and status=0//status=0是因为我们查到的是0
这种方式我们解决了name
或者description
这些无关顺序痛痒的更新粒度,使其更新其余字段并发度大大提高,大家可以多个线程一起更新name或者description都是不会出现乐观锁的错误。
虽然我们解决了普通字段的更新修改但是针对部分关键字段的更新如果是整个对象更新依然会有问题,那么又回到了乐观锁是一个比较好的处理方式,比如stock_num
字段
easy-query
我们来看看如果在easy-query
下我们分别如何实现上述功能,首先我们还是在之前的solon项目中进行代码添加,
@Data
@Table("test_update")
public class TestUpdateEntity {
@Column(primaryKey = true)
private String id;
private String name;
private Integer status;
private String description;
}
//添加测试数据
TestUpdateEntity testUpdateEntity =