解放生產力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的判斷,但是在併發環境下這麼更新是有問題的,而且這邊我們僅更新了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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章