首页 > 其他分享 >解放生产力orm并发更新下应该这么处理求求你别再用UpdateById了

解放生产力orm并发更新下应该这么处理求求你别再用UpdateById了

时间:2023-08-25 19:12:57浏览次数:49  
标签:status description 你别 更新 name String orm id UpdateById


合集 - easy-query(7)   1.献给转java的c#和java程序员的数据库orm框架05-222.javaer你还在手写分表分库?来看看这个框架怎么做的 干货满满05-263.你没见过的分库分表原理解析和解决方案(一)06-074.你没见过的分库分表原理解析和解决方案(二)06-305.我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱07-266.数据安全之数据库字段加解密检索和前端返回脱敏?看看我这个最强解决方案08-14 7.解放生产力orm并发更新下应该这么处理求求你别再用UpdateById了08-22 收起  

解放生产力orm并发更新下应该这么处理求求你别再用UpdateById了

背景

很多时候为了方便我们都采用实体对象进行前后端的数据交互,然后为了便捷开发我们都会采用DTO对象进行转换为数据库对象,然后调用UpdateById将变更后的数据存入到数据库内,这样的一个做法有什么问题呢,如果你的系统并发量特别少甚至没有并发量那么这么做是没什么关系的无可厚非,但是如果你的系统有并发量那么在某些情况下会有严重的问题.

案例1

现在我们有一条待审核记录,其中status 0表示待提交, 1表示待审核

idnamestatusdescription
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覆盖也就是提交失败,那么如果提交审核会触发一些事件那么就就会有严重的问题产生,操作将会变得不是幂等。

解决方案

乐观锁

首先我们修改表结构添加版本号字段

idnamestatusdescriptionversion
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-query

文档地址 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

 

 

转 https://www.cnblogs.com/xuejiaming/p/17629974.html

标签:status,description,你别,更新,name,String,orm,id,UpdateById
From: https://www.cnblogs.com/wl-blog/p/17657760.html

相关文章

  • element-ui(Form 表单)
    在Form组件中,每一个表单域由一个Form-Item组件构成,表单域中可以放置各种类型的表单控件,包括Input、Select、Checkbox、Radio、Switch、DatePicker、TimePicker<el-formref="form":model="form"label-width="80px"><el-form-itemlabel="活动名称">......
  • mormot2 笔记(三) 实体转JSON
    TOL=class(TObject)publicprocedureW(W:TJsonWriter;Instance:TObject;Options:TTextWriterWriteObjectOptions);end;TPerson=classprivateFName:string;FID:integer;FSex:Byte;publishedpropertyID:integerread......
  • Extract Abends with OGG-01028 Non-Standard Redo Detected in 10g Compatible Forma
    ogg报错ExtractAbendswithOGG-01028Non-StandardRedoDetectedin10gCompatibleFormat抽取进程意外Abend手动重启恢复ExtractAbendswithOGG-01028Non-StandardRedoDetectedin10gCompatibleFormat(DocID1313864.1)根据文档修改添加这个参数'tranlo......
  • The POM for xxxx is missing, no dependency information available解决方案
    骑士李四记录:maveninstall报错ThePOMforcom.xxxxismissing…nodependencyinformationavailable解决方案:现在父工程上先执行maveninstall,这时候会自动下载很多依赖包,等到父工程buildsuccess的时候,在执行子工程。......
  • 基于java极速WEB+ORM 框架:jfinal2.0开发的通用后台管理系统及源码
    final2-common-admin1、基于java极速web开发框架:jfinal2.0开发的通用后台管理系统,包括完整的登录、注册、菜谱管理、厨师管理、餐厅管理等功能2、开发时是基于jdk1.8、tomcat7.0,utf8编码3、运行时请修改配置文件:a_little_config.txt及相应的运行环境:JavaBuildPath、TargetedR......
  • 【Angular】如何将自定义组件绑定为FormControl?
    参考资料:简单Demo:AngularFormcontrolenameCustomComponent关键实现说明:ControlValueAccessor:CustomFormComponentsinAngularAngular自定义表单控件(中文)关于muti:true的说明......
  • 关于C#里IFormatProvider与IFormattable的一些思考
    一,从时间(DateTime)出发先上一段处理时间格式化的代码。该代码在Net6.0框架下运行。vartime=newDateTime(2023,8,24);Console.WriteLine(time);Console.WriteLine(string.Format("{0:yyyyMMdd}",time));//写法1......
  • WinForm微信扫码登录
    源码还需优化,不喜勿喷。微信官方文档: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html前期准备:1.微信开放平台开发者资质认证  https://open.weixin.qq.com/  费用300大概1-2天时间 2.创建网站应用,需要已备案域名、......
  • 一个超经典 WinForm 卡死问题的最后一次反思
    一:背景1.讲故事在我分析的200+dump中,同样会遵循着28原则,总有那些经典问题总是反复的出现,有很多的朋友就是看了这篇一个超经典WinForm卡死问题的再反思找到我,说WinDbg拦截System_Windows_Forms_niSystem.Windows.Forms.Application+MarshalingControl..ctor总会有......
  • 关于前端接口的formData传参
    工作中遇到个简单的问题,后端提供接口需要前端用formdata传文件和普通对象参数拼接的参数;本来是个简单的问题,记录一下做个简单的总结顺便梳理下相关基础性知识点:1.formdata将数据转换成键值对进行传参,key是唯一,一个key可以对应多个value,如果是使用表单初始化,每个表单字段对......