解放生产力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. 2021-04-26:整型数组arr长度为n(3 <= n <= 10^4),最初每个数字是<=200的正数且满足如下条件: 1. arr[0] <= arr[1]。2.arr[n-1] <= arr

    2021-04-26:整型数组arr长度为n(3 <= n <= 10^4),最初每个数字是<=200的正数且满足如下条件: 1. arr[0] <= arr[1].2.arr ...

  2. pupstudy的使用

    打开环境 点击管理--打开根目录 把靶场放在www文件夹里 网页打开127.0.0.1/靶场文件名即可

  3. 逍遥自在学C语言 | 宏定义技巧让你的C代码快人一步

    前言 在C语言中,宏定义是一种预处理指令,用于在代码中定义和使用常量.函数或代码片段的替代. 宏定义使用#define关键字来定义,并在代码中进行替换.宏定义具有以下优点: 简化代码:宏定义可以将一些 ...

  4. .Net全网最简RabbitMQ操作【强烈推荐】

    [前言] 本文自1年前的1.0版本推出以来,已被业界大量科技公司采用.同时也得到了.Net圈内多位大佬的关注+推荐,文章也被多家顶级.Net/C#公众号转载. 现在更新到了7.0版本,更好的服务各位. ...

  5. 关于进程、线程、协程的概念以及Java中的应用

    进程.线程.协程 本文将从"操作系统"."Java应用"上两个角度来探究这三者的区别. 一.进程 在我本人的疑惑中,我有以下3个问题. 1.1为什么要引入进程? ...

  6. 【HMS Core】Health Kit查询历史数据查询数据和返回数据不一致

    [问题描述] 查询一个月运动记录,只能查询到最早5月26的数据,但是华为健康app里的数据最早为5月8日,为什么会查询不到? [解决方案] 1.需要检查是否申请了历史数据权限,查询数据时,出于对用户的 ...

  7. 前端Vue自定义导航栏菜单 定制左侧导航菜单按钮 中部logo图标 右侧导航菜单按钮

    前端Vue自定义导航栏菜单 定制左侧导航菜单按钮 中部logo图标 右侧导航菜单按钮, 下载完整代码请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin? ...

  8. Java 设计模式实战系列—工厂模式

    在 Java 开发中,对象的创建是一个常见的场景,如果对象的创建和使用都写在一起,代码的耦合度高,也不利于后期的维护.我们可以使用工厂模式来解决这个问题,工厂模式是一个创建型模式,将对象的创建和使用分 ...

  9. 【Mybatis】动态SQL

    目录 动态SQL if语句 动态SQL if+where语句 动态SQL if+set语句 动态SQL choose(when,otherwise)语句 动态SQL trim语句 动态SQL SQL片 ...

  10. dash构建多页应用

    dash 构建多页面应用一种方案 本方案对dash官网多页面案例使用dash_bootstrap_components案例进行优化与测试,效果如下 项目代码结构如下 │ app.py │ ├─asse ...