解放生产力orm并发更新下应该这么处理求求你别再用UpdateById了
解放生产力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 = new TestUpdateEntity();
testUpdateEntity.setId("1");
testUpdateEntity.setName("测试1");
testUpdateEntity.setStatus(0);
testUpdateEntity.setDescription("描述信息");
easyQuery.insertable(testUpdateEntity).executeRows();
return "ok";
审核普通更新
一般而言我们会先选择查询对象,然后判断状态然后将dto请求赋值给对象,之后更新对象
@Mapping(value = "/testUpdate2",method = MethodType.POST)
public String testUpdate2(@Validated TestUpdate2Rquest request){
TestUpdateEntity testUpdateEntity = easyQuery.queryable(TestUpdateEntity.class)
.whereById(request.getId()).firstNotNull("未找到对应的记录");
if(!testUpdateEntity.getStatus().equals(0)){
return "当前状态不是0";
}
BeanUtil.copyProperties(request,testUpdateEntity);
testUpdateEntity.setStatus(1);
easyQuery.updatable(testUpdateEntity).executeRows();
return "ok";
}

==> Preparing: SELECT `id`,`name`,`status`,`description` FROM `test_update` WHERE `id` = ? LIMIT 1
==> Parameters: 1(String)
<== Time Elapsed: 22(ms)
<== Total: 1
==> Preparing: UPDATE `test_update` SET `name` = ?,`status` = ?,`description` = ? WHERE `id` = ?
==> Parameters: 测试1(String),1(Integer),123(String),1(String)
<== Total: 1
我们看到这边更新将status由0改成了1,虽然我们中间做了一次是否为0的判断,但是在并发环境下这么更新是有问题的,而且这边我们仅更新了description和status字段缺把name字段也更新了
审核并发更新
首先我们改造一下代码,在请求方法上添加了对应的注解@EasyQueryTrack又因为我们配置了默认开启追踪所以仅需要查询数据库对象既可以追踪数据
//自动追踪差异更新 需要开启default-track: true如果没开启那么就使用`asTracking`启用追踪
@EasyQueryTrack
@Mapping(value = "/testUpdate3",method = MethodType.POST)
public String testUpdate3(@Validated TestUpdate2Rquest request){
TestUpdateEntity testUpdateEntity = easyQuery.queryable(TestUpdateEntity.class)
//.asTracking() //如果配置文件默认选择追踪那么只需要添加 @EasyQueryTrack 注解
.whereById(request.getId())
.firstNotNull("未找到对应的记录");
if(!testUpdateEntity.getStatus().equals(0)){
return "当前状态不是0";
}
BeanUtil.copyProperties(request,testUpdateEntity);
testUpdateEntity.setStatus(1);
easyQuery.updatable(testUpdateEntity)
//指定更新条件为主键和status字段
.whereColumns(o->o.columnKeys().column(TestUpdateEntity::getStatus))
.executeRows(1,"当前状态不是0");//如果更新返回的受影响函数不是1,那么就抛出错误,当然你也可以获取返回结果自行处理
return "ok";
}

==> Preparing: SELECT `id`,`name`,`status`,`description` FROM `test_update` WHERE `id` = ? LIMIT 1
==> Parameters: 1(String)
<== Time Elapsed: 23(ms)
<== Total: 1
==> Preparing: UPDATE `test_update` SET `status` = ?,`description` = ? WHERE `id` = ? AND `status` = ?
==> Parameters: 1(Integer),123(String),1(String),0(Integer)
<== Total: 1
更新条件自动感知需要更新的列,不会无脑全更新,并且支持简单的配置支持当前status并发更新,会自动在where上带上原来的值,并且在set处更新为新值,整个更新条件对于并发情况下的处理变得非常简单
乐观锁
@Data
@Table("test_update_version")
public class TestUpdateVersionEntity {
@Column(primaryKey = true)
private String id;
private String name;
private Integer status;
private String description;
@Version(strategy = VersionUUIDStrategy.class)
private String version;
}
//初始化数据
TestUpdateVersionEntity testUpdateVersionEntity = new TestUpdateVersionEntity();
testUpdateVersionEntity.setId("1");
testUpdateVersionEntity.setName("测试1");
testUpdateVersionEntity.setStatus(0);
testUpdateVersionEntity.setDescription("描述信息");
testUpdateVersionEntity.setVersion(UUID.randomUUID().toString().replaceAll("-",""));
easyQuery.insertable(testUpdateVersionEntity).executeRows();
==> Preparing: INSERT INTO `test_update_version` (`id`,`name`,`status`,`description`,`version`) VALUES (?,?,?,?,?)
==> Parameters: 1(String),测试1(String),0(Integer),描述信息(String),0603b2e00a1d4b869d13cf974a5cc885(String)
<== Total: 1
审核乐观锁
@Mapping(value = "/testUpdate2",method = MethodType.POST)
public String testUpdate2(@Validated TestUpdate2Rquest request){
TestUpdateVersionEntity testUpdateVersionEntity = easyQuery.queryable(TestUpdateVersionEntity.class)
.whereById(request.getId()).firstNotNull("未找到对应的记录");
if(!testUpdateVersionEntity.getStatus().equals(0)){
return "当前状态不是0";
}
BeanUtil.copyProperties(request,testUpdateVersionEntity);
testUpdateVersionEntity.setStatus(1);
easyQuery.updatable(testUpdateVersionEntity).executeRows();
return "ok";
}

==> Preparing: SELECT `id`,`name`,`status`,`description`,`version` FROM `test_update_version` WHERE `id` = ? LIMIT 1
==> Parameters: 1(String)
<== Time Elapsed: 16(ms)
<== Total: 1
==> Preparing: UPDATE `test_update_version` SET `name` = ?,`status` = ?,`description` = ?,`version` = ? WHERE `version` = ? AND `id` = ?
==> Parameters: 测试1(String),1(Integer),123(String),cf6c2f3106b24aba965bb4cc54235076(String),0603b2e00a1d4b869d13cf974a5cc885(String),1(String)
<== Total: 1
虽然我们采用了乐观锁但是还是会出现全字段更新的情况,所以这边再次使用差异更新来实现
@EasyQueryTrack
@Mapping(value = "/testUpdate3",method = MethodType.POST)
public String testUpdate3(@Validated TestUpdate2Rquest request){
TestUpdateVersionEntity testUpdateVersionEntity = easyQuery.queryable(TestUpdateVersionEntity.class)
.whereById(request.getId()).firstNotNull("未找到对应的记录");
if(!testUpdateVersionEntity.getStatus().equals(0)){
return "当前状态不是0";
}
BeanUtil.copyProperties(request,testUpdateVersionEntity);
testUpdateVersionEntity.setStatus(1);
easyQuery.updatable(testUpdateVersionEntity).executeRows();
return "ok";
}

==> Preparing: UPDATE `test_update_version` SET `status` = ?,`description` = ?,`version` = ? WHERE `version` = ? AND `id` = ?
==> Parameters: 1(Integer),1234(String),7e96f217bc13451c9d10a8fba50780a6(String),cf6c2f3106b24aba965bb4cc54235076(String),1(String)
<== Total: 1
使用追踪查询仅更新我们需要更新的字段easy-query一款为开发者而生的orm框架,拥有非常完善的功能且支持非常易用的功能,让你在编写业务时可以非常轻松的实现并发操作,哪怕没有乐观锁。
最后
看到这边您应该已经知道了solon国产框架的简洁和easy-query的便捷,如果本篇文章对您有帮助或者您觉得还行请给我一个星星表示支持谢谢
当前项目地址demo https://gitee.com/xuejm/solon-encrypt
easy-qeury
文档地址 https://xuejm.gitee.io/easy-query-doc/
GITHUB地址 https://github.com/xuejmnet/easy-query
GITEE地址 https://gitee.com/xuejm/easy-query
solon
文档地址 https://xuejm.gitee.io/easy-query-doc/
GITHUB地址 https://github.com/noear/solon
GITEE地址 https://gitee.com/noear/solon
解放生产力orm并发更新下应该这么处理求求你别再用UpdateById了的更多相关文章
- 解放生产力「GitHub 热点速览 v.21.51」
作者:HelloGitHub-小鱼干 解放生产力一直都是我们共同追求的目标,能在摸鱼的空闲把赚了.而大部分好用的工具便能很好地解放我们的生产力,比如本周特推 RedisJSON 不用对 JSON 做哈 ...
- 如何在高并发环境下设计出无锁的数据库操作(Java版本)
一个在线2k的游戏,每秒钟并发都吓死人.传统的hibernate直接插库基本上是不可行的.我就一步步推导出一个无锁的数据库操作. 1. 并发中如何无锁. 一个很简单的思路,把并发转化成为单线程.Jav ...
- 解决并发情况下库存减为负数问题--update2016.04.24
场景: 一个商品有库存,下单时先检查库存,如果>0,把库存-1然后下单,如果<=0,则不能下单,事务包含两条sql语句: ; update products ) WHERE id=; 在并 ...
- php高并发状态下文件的读写
php高并发状态下文件的读写 背景 1.对于PV不高或者说并发数不是很大的应用,不用考虑这些,一般的文件操作方法完全没有问题 2.如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件 ...
- [转]高并发访问下避免对象缓存失效引发Dogpile效应
避免Redis/Memcached缓存失效引发Dogpile效应 Redis/Memcached高并发访问下的缓存失效时可能产生Dogpile效应(Cache Stampede效应). 推荐阅读:高并 ...
- Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。
精彩理解: https://www.jianshu.com/p/21be831e851e ; https://blog.csdn.net/heyutao007/article/details/19 ...
- mongodb三种存储引擎高并发更新性能专题测试
背景说明 近期北京理财频道反馈用来存放股市实时数据的MongoDB数据库写响应请求很慢,难以跟上业务写入速度水平.我们分析了线上现场的情况,发现去年升级到SSD磁盘后,数据持久化的磁盘IO开销已经不是 ...
- Linux的虚拟内存管理-如何分配和释放内存,以提高服务器在高并发情况下的性能,从而降低了系统的负载
Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和free是如何分配和释放内存?如何查看堆内内存的碎片情况?既然堆内内存brk和sbrk不能直接释放,为什么不全 ...
- Django 2.0 学习(16):Django ORM 数据库操作(下)
Django ORM数据库操作(下) 一.增加表记录 对于表单有两种方式: # 方式一:实例化对象就是一条表记录france_obj = models.Student(name="海地&qu ...
- mysql并发更新问题
问题背景: 假设MySQL数据库有一张会员表vip_member(InnoDB表),结构如下: 当一个会员想续买会员(只能续买1个月.3个月或6个月)时,必须满足以下业务要求: •如果end_at ...
随机推荐
- Cobalt Strike 连接启动教程(1)
第一步:把cobaltstrike4(解压后)拷贝到虚拟机Kali系统的root目录下 第二步:进入cobalstrike4文件夹中 第三步:选寻kali系统 IP地址 第四步: 启动服务端:(t ...
- 单例bean与类加载过程
构造单例bean的方式有很多种,我们来看一下其中一种,饿汉式 public class Singleton1 implements Serializable { //1.构造函数私有 private ...
- 2023-06-23:redis中什么是缓存击穿?该如何解决?
2023-06-23:redis中什么是缓存击穿?该如何解决? 答案2023-06-23: 缓存击穿是指一个缓存中的热点数据非常频繁地被大量并发请求访问,当该热点数据失效的瞬间,持续的大并发请求无法通 ...
- maven从远程仓库下载依赖包失败(因权限问题导致)
背景 在学习rocketMq时,编译官方提供的可视化项目:rocketmq-dashboard,频频失败,报以下错误 Could not transfer artifact org.apache.ro ...
- 「学习笔记」vector
本文并不是 vector 的入门教程. 定义 std::vector 是封装动态数组的顺序容器. vector 通常占用多于静态数组的空间,因为要分配更多内存以管理将来的增长.如果元素数量已知,可以使 ...
- 关于SQL SERVER 字段类型char(n) , nchar(n) , varchar(n) , nvarchar(n)
对于很多新手来说,经常被字段类型搞得晕头转向,今天我用通俗易懂的解释帮大家理解这些类型. 在数据库字段类型定义中,可以分为两大类,一类为Unicode类型,另一种就是非Unicode. Unicode ...
- 从钢铁行业数字化管控与超自动化融合,看华为WeAutomate能力进化
文/王吉伟 钢铁行业的数字化转型,历来都是值得探讨的热点话题. 2022年,我国粗钢产量10.13亿吨,占据了全球钢铁供给市场的半壁江山. 这组数据可谓非常抢眼,但仍旧难掩诸多企业的各种经营问题. 钢 ...
- CF1794C Scoring Subsequences题解
文中 \(a\) 为题目中给的 \(a\). 如果我们要求 \(a_1, a_2, a_3, \dots, a_m\) 的结果, 那么我们可以把 \(a\) 数组从后往前依次除以 \(i\),\(i\ ...
- 【Vue】Echart图表
vue-echart-ui vue 集成 echart 图表的小 demo. 基础 series.type 包括:line(折线图).bar(条形图).pie(饼图).scatter(散点图).gra ...
- 2023年郑州轻工业大学校赛邀请赛clk
需要总结的地方挺多的,首先是题目一次通过率有待提高,对于一些特别的样例还要加以分析,算法熟练的不高,不能清晰的看出在哪道题考什么算法,就比如兔子爱吃萝卜那道题,就是一个背包问题,比较基础,但是我们团队 ...