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

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

时间:2023-08-22 09:03:46浏览次数:35  
标签:status description 你别 name String orm version id 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的判断,但是在并发环境下这么更新是有问题的,而且这边我们仅更新了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

标签:status,description,你别,name,String,orm,version,id,UpdateById
From: https://www.cnblogs.com/xuejiaming/p/17629974.html

相关文章

  • jmeter详解-线程组详解(9)-bzm - Free-Form Arrivals Thread Group
    bzm-Free-FormArrivalsThreadGroup介绍: 顾名思义,相当于自由形式的ArrivalsThreadGroup,它只是提供了自由形式的时间表的能力。相当于我们可以更灵活的控制 每分钟/每秒钟的请求数。页面说明:ThreadsSchedule(线程场景):Startvalue:开始时的用户数Endvalue:结束时......
  • 使用 Terraform 与事件驱动的 Amazon CodeBuild 提升云上数据应用运维效率
    背景信息企业客户在云上部署的一系列数据应用的过程中,数据开发团队往往负责脚本内容,而其背后一系列云上资源的管理通常由一支云运维职能团队通过IaC(InfrastructreasCode)实现。然而,当数据开发团队开发及部署相应脚本内容时,不可避免会涉及到云上资源的变动,如Glue、Lambda的资......
  • Webstorm 2023.2 最新安装教程(附激活码,亲测有效)
    前言WebStorm 是JetBrains公司旗下一款JavaScript开发工具。已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScriptIDE”等。它提供了许多功能,例如代码自动完成、错误突出显示、重构、调试、版本控制等,可以帮助开发人员提高效率和代码......
  • transformer模型的历史
    Transformer模型在深度学习领域,尤其是自然语言处理(NLP)中,起到了革命性的作用。以下是其发展历程的简要概述:背景:在Transformer出现之前,循环神经网络(RNN)及其更先进的版本,如长短时记忆网络(LSTM)和门控循环单元(GRU)是处理序列任务的主流架构,例如机器翻译和文本生成。这些模型逐......
  • 前端项目实战壹佰柒拾贰react-admin+material ui-react-admin之Create之transform
    我是歌谣微信公众号关注前端小歌谣import*asReactfrom'react';import{Button,Typography,Box}from'@mui/material';import{Create,useNotify,SimpleForm,TextInput,DateInput,required,TopToolbar}from'react-admin';constPostCreateActio......
  • 前端项目实战壹佰柒拾叁react-admin+material ui-react-admin之Create之transform
    我是歌谣微信公众号关注前端小歌谣exportconstUserCreate=(props)=>{consttransform=(data)=>{constsanitizedData={};for(constkeyindata){if(typeofdata[key]==="string"&&data[key].length===0)......
  • QOJ # 6508. This is not an Abnormal Team!
    题面传送门感觉网络流学艺不精,被薄纱了/kk原题意是最少一个点的链,在此基础上最少三个点的链,比较难去用网络流考虑。换个思路:先最大匹配出两点链,然后让最多两点链合并上一个单点变成三点链。这样显然单点最少,并且保证了不会有\(3\)个两点链合并成两个三点链,所以这样是符合题目......
  • transformer小白入门
    transformer库是huggingface发布的1个框架,非常好用,很多外行看起来高大上的问题,用它都可以轻松解决,先来看1个小例子: 一、情感分析fromtransformersimportpipelineclassifier=pipeline('sentiment-analysis')classifier('youarebeautiful')这简单的三行代码,就能分析......
  • 论文解读(CTDA)《Contrastive transformer based domain adaptation for multi-source c
    Note:[wechat:Y466551|可加勿骚扰,付费咨询]论文信息论文标题:Contrastivetransformerbaseddomainadaptationformulti-source cross-domainsentimentclassification论文作者:YanpingFu,YunLiu论文来源:2021aRxiv论文地址:download论文代码:download视屏讲解:click1......
  • 使用-Terraform-创建一个新的Azure-资源组-虚拟网络-子网
    本文主要讲解如何使用Terraform-在AzureCloud中创建一个新的资源组然后在这个资源组中,创建一个虚拟网络-192.168.10.0/24,以及8个/27位子网环境说明:一般都是资源与资源组在一个Location位置中,写法也相对简单一些因此本文,使用了,资源组,与虚拟网络资源不在同一个Location位置的......