解放生产力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的判断,但是在并发环境下这么更新是有问题的,而且这边我们仅更新了descriptionstatus字段缺把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了的更多相关文章

  1. 解放生产力「GitHub 热点速览 v.21.51」

    作者:HelloGitHub-小鱼干 解放生产力一直都是我们共同追求的目标,能在摸鱼的空闲把赚了.而大部分好用的工具便能很好地解放我们的生产力,比如本周特推 RedisJSON 不用对 JSON 做哈 ...

  2. 如何在高并发环境下设计出无锁的数据库操作(Java版本)

    一个在线2k的游戏,每秒钟并发都吓死人.传统的hibernate直接插库基本上是不可行的.我就一步步推导出一个无锁的数据库操作. 1. 并发中如何无锁. 一个很简单的思路,把并发转化成为单线程.Jav ...

  3. 解决并发情况下库存减为负数问题--update2016.04.24

    场景: 一个商品有库存,下单时先检查库存,如果>0,把库存-1然后下单,如果<=0,则不能下单,事务包含两条sql语句: ; update products ) WHERE id=; 在并 ...

  4. php高并发状态下文件的读写

    php高并发状态下文件的读写   背景 1.对于PV不高或者说并发数不是很大的应用,不用考虑这些,一般的文件操作方法完全没有问题 2.如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件 ...

  5. [转]高并发访问下避免对象缓存失效引发Dogpile效应

    避免Redis/Memcached缓存失效引发Dogpile效应 Redis/Memcached高并发访问下的缓存失效时可能产生Dogpile效应(Cache Stampede效应). 推荐阅读:高并 ...

  6. Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

    精彩理解:  https://www.jianshu.com/p/21be831e851e ;  https://blog.csdn.net/heyutao007/article/details/19 ...

  7. mongodb三种存储引擎高并发更新性能专题测试

    背景说明 近期北京理财频道反馈用来存放股市实时数据的MongoDB数据库写响应请求很慢,难以跟上业务写入速度水平.我们分析了线上现场的情况,发现去年升级到SSD磁盘后,数据持久化的磁盘IO开销已经不是 ...

  8. Linux的虚拟内存管理-如何分配和释放内存,以提高服务器在高并发情况下的性能,从而降低了系统的负载

    Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和free是如何分配和释放内存?如何查看堆内内存的碎片情况?既然堆内内存brk和sbrk不能直接释放,为什么不全 ...

  9. Django 2.0 学习(16):Django ORM 数据库操作(下)

    Django ORM数据库操作(下) 一.增加表记录 对于表单有两种方式: # 方式一:实例化对象就是一条表记录france_obj = models.Student(name="海地&qu ...

  10. mysql并发更新问题

    问题背景: 假设MySQL数据库有一张会员表vip_member(InnoDB表),结构如下:   当一个会员想续买会员(只能续买1个月.3个月或6个月)时,必须满足以下业务要求: •如果end_at ...

随机推荐

  1. docker安装rabbitmq:management

    1.拉取镜像 docker pull rabbitmq:management 2.安装 docker run -dit --name rabitmq -e RABBITMQ_DEFAULT_USER= ...

  2. 浅谈如何使用 github.com/yuin/gopher-lua

    最近熟悉 go 项目时,发现项目中有用到 github.com/yuin/gopher-lua这个包,之前并没有接触过,特意去看了官方文档和找了些网上的资料,特此记录下. 本次介绍计划分为两篇文章,这 ...

  3. status能否设置为布尔值类型,前端采用复选框形式

    是的,可以将status设置为布尔类型,这样可以在前端使用复选框形式展示.在模型中的定义可以如下: class Acceptance(models.Model): # ... status = mod ...

  4. 【数据库】时区及JDBC的时区设置

    JDBC连接时有个TimeZone配置,这玩意到底有用吗?我是使用Postgresql和Mysql两个数据库验证的.结果如下: 数据库 部署方式 版本 JDBC连接TimeZone参数 JDBC连接s ...

  5. flutter填坑之旅(环境搭建篇)

    自从Google 在 2018 世界移动大会上发布 Flutter 的 Beta 版本,看看官方的介绍Flutter widgets are built using a modern framewor ...

  6. OSI7层模型和TCP/IP模型

    前言 在计算机网络领域中,OSI7层模型和TCP/IP模型是两个重要的概念.本文将对这两个模型进行介绍和比较,让大家了解它们的区别和联系. 目录 前言 OSI7层模型 TCP/IP模型 OSI7层模型 ...

  7. GTX.Zip:一款可以替代 gzip 的基因大数据压缩软件

    今天给大家推荐一款基因大数据压缩的大杀器:GTX.Zip. GTX.Zip 这款软件是由曾在 2016 年 GCTA 风云挑战赛中的那匹黑马--人和未来生物科技有限公司开发的,而当时他们也是打破了基因 ...

  8. 预测 motif 的计算原理

    本文章来源于简书,作者小潤澤,已获原作者授权:部分内容有调整. 前言 蛋白质中功能的基本单元是 domain,是一种特殊的三维结构,不同结构的 domain 与其他分子特异性结合从而发挥功能.与此类似 ...

  9. Anaconda3安装(Win_x64)

    一.获取Anaconda3 链接:https://pan.baidu.com/s/14Imqk1KBsB84Mwzebpv2BA?pwd=no2x 提取码:no2x --来自百度网盘超级会员V4的分享 ...

  10. 如何使用Stable Diffusion生成艺术二维码?

    硬件准备 物理内存:至少16G(8G直接安装阶段就卡死) N卡:此处我使用GTX 1660 6G (2019年双12购买) 操作系统 windows 11 软件准备 网络要通畅 git: https: ...