1,場景再現
場景:總公司可以給分公司下發今年的規划任務(可能只是寫了個規划大綱),分公司收到后,進行詳細的規划補充,然后提交。
比如規划表:
CREATE TABLE `sys_plan` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`branch_offince_id` int(11) DEFAULT NULL COMMENT '分公司id',
`head_office_plan` varchar(255) DEFAULT NULL COMMENT '總公司規划',
`branch_office_plan` varchar(255) DEFAULT NULL COMMENT '分公司規划',
`create_time` datetime DEFAULT NULL COMMENT '創建時間',
`update_time` datetime DEFAULT NULL COMMENT '修改時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
為了簡化業務場景,這里用兩個字段:總公司規划、分公司規划模擬。
比如總公司給分公司A新建的規划,填寫在總公司規划字段(head_office_plan),分公司收到消息后進行補充,填寫在分公司規划(branch_office_plan)字段。
可能出現的問題的場景:
1,總公司用戶A,給某分公司B新建了一條規划: 1,銷售額1000萬;2,生產產品2萬件
此時數據庫數據是這樣的:
2,分公司收到消息提醒,登錄了系統,查看到總公司派發的任務,頁面是這樣的:
然后陷入沉思,思考該怎么填寫自己的規划.
3,此時總公司想再補充一條規划,就登錄系統,打開頁面,編輯head_office_plan字段:3,員工規模擴充到100人,然后提交了。此時頁面是這樣的。
數據庫是這樣的:
4,分公司想好了怎么填寫規划,此時總公司補充的規划,開始填寫:1,提高生產效率,2,...
由於分公司在總公司提交補充規划3之前就打開了頁面,所以規划3這里是不顯示的。
然后,問題就出現了,分公司把總公司的規划 3,員工規模擴充到100人 這條規划給覆蓋成空的了。數據庫中現在是這樣的:
PS:
其他的如政務系統,用戶體系有國家級別、地方級別,像這種用戶體系有上下級關系的管理系統,上下級更可能操作同一條數據,更可能出現這種情況,其他對於C端用戶的系統,我們編輯的一般都是編輯自己的資源,不會出現這種場景。
2,要達到的目標
如果某條數據正在被編輯,另一個人也要編輯該數據,就給出友好提示“某某某正在編輯該數據,請稍后重試”,或者是直接就不能查看。
3,解決方案
網上的方案:
方案1
在操作的表里添加一個version字段數值類型的默認0,只要對數據進行了操作就對version加1,每一次頁面操作(刪除、修改)都先判斷version是否和打開時的version值一樣,如果不一樣請先刷新,在進行操作
方案2:
在數據表里添加一個UUID字段,其值為32位的隨機數。
1.記錄新建時,在數據提交后台,插入DB之前,生成UUID,保存之。
2.記錄編輯時,在編輯頁面將UUID隱藏,提交時Check該隱藏值是否與DB一致。
不一致則返回前台畫面,報對應的Message;
一致則提交后台,生成新UUID,與業務數據一起保存到表中
這兩種方案弊端,只能是讓第二個想編輯的人刷新頁面,重新填寫。
牛總公司的方案:
數據庫加字段,比如加一列,is_edit,當有人編輯的時候,設置is_edit=1,編輯完成后設置is_edit=0,其他人再查詢該條數據,查看is_edit是否=1,如果是就給出提示;但是,如果第一個人打開頁面進行編輯,設置了is_edit=1,然后他把瀏覽器關了,is_edit就=1了,此時誰也編輯不了了,所以這種方案不可取。不知道他們怎么處理這種關閉瀏覽器的。
終極redis方案
所以,我們討論的方案是,用redis做。采用類似用redis做分布式鎖的思路,來解決並發編輯問題。
用redis的SETNX 命令: 設置成功,返回 1 , 設置失敗,返回 0 。
原理:
以 lock_plan_{planId} 為redis的key,userId為value,某個用戶在獲取plan的時候,先用 lock_plan_{planId}往redis設置值,如果返回false,說明這個資源已經有人加了鎖了,返回失敗。
定義一個公共資源鎖的服務類:
提供3個方法:
1,獲得鎖:當獲取某條數據的同時,先去獲得鎖(鎖設定一個有效期,這個有效期根據業務定,頁面內容多就多設置一些,內容少就設置短一點,設置有效期保證長時間不操作,不會死鎖),如果獲取鎖成功,就查詢那條數據,否則返回提示。
2,釋放鎖:當成功獲取了某條數據時,進行編輯后,update操作之后,釋放鎖。讓等待的人可以正常獲取鎖。
3,延續鎖的時長:當用戶操作某條數據持續時間較長,前端設置一個心跳,定時調用此接口延續鎖的有效期,類似與redission的自動續期鎖時長。這個心跳時間間隔,根據業務定,小於鎖的有效期,比如設置為1/3 鎖的時長,鎖的延期間的時長,自己定,比如1/3有效期(redission好似也是1/3有效期)。
/**
* 公共資源鎖服務
* create by lihaoyang on 2020/8/17
*/
public interface CommonLockResourceService {
/**
* 獲得鎖
* @param resourceKeyPrefix 鎖的redis前綴
* @param resourceId 資源id
* @param userId 用戶id
* @return
*/
boolean getLock(String resourceKeyPrefix,int resourceId,int userId);
//釋放鎖
boolean unLock(String resourceKeyPrefix,int resourceId,int userId);
//鎖延期
boolean resetLock(String resourceKeyPrefix,int resourceId, int userId);
}
實現類: 主要要確保鎖的可重入性,同一個用戶多次加鎖,要獲得同一把鎖。
@Service
@Transactional
public class CommonLockResourceServiceImpl implements CommonLockResourceService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean getLock(String resourceKeyPrefix, int resourceId, int userId) {
String lock = resourceKeyPrefix + resourceId;
//如果該userId已經有該項目的鎖,鎖續期
if(StringUtils.equals(""+userId,stringRedisTemplate.opsForValue().get(lock.intern()))){
//鎖的可重入
long ttl = stringRedisTemplate.getExpire(lock);
//續期時間,自己定
stringRedisTemplate.expire(lock.intern(),ttl+60L, TimeUnit.SECONDS);
return true;
}
//枷鎖
boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(),userId+"",60L,TimeUnit.SECONDS);
return isLock;
}
@Override
public boolean unLock(String resourceKeyPrefix, int resourceId, int userId) {
String lock = resourceKeyPrefix + resourceId;
if((userId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
stringRedisTemplate.delete(lock.intern());
return true;
}
return false;
}
@Override
public boolean resetLock(String resourceKeyPrefix, int resourceId, int userId) {
String lock = resourceKeyPrefix + resourceId;
//如果該userId已經有該項目的鎖,鎖續期
if(StringUtils.equals(""+userId,stringRedisTemplate.opsForValue().get(lock.intern()))){
long ttl = stringRedisTemplate.getExpire(lock);
stringRedisTemplate.expire(lock.intern(),ttl+60L,TimeUnit.SECONDS);
return true;
}
return false;
}
}
Controller:
@RestController
@RequestMapping("/sysPlan")
public class SysPlanController {
static final String lockKeyPrefix = "lock_plan_";
@Autowired
private SysPlanService planService;
@Autowired
private CommonLockResourceService commonLockResourceService;
//~============= redis鎖 ================
@GetMapping("/getByIdLock")
public Result getByIdLock(@RequestParam int planId, @RequestParam int userId){
//TODO:userId應該從session獲取而不是傳過來
boolean isLock = commonLockResourceService.getLock(lockKeyPrefix,planId,userId);
if(isLock){
SysPlan plan = planService.getById(planId);
return Result.ok(plan);
}
//還可以獲取到誰在編輯,如果需要的話
return Result.error("當前規划正在編輯中,請稍后重試");
}
@GetMapping("/update")
public Result update(@RequestParam int planId,@RequestParam int userId){
//這里應該放在service層
//update By Id
//planService.updateById();
boolean isRelease = commonLockResourceService.unLock(lockKeyPrefix,planId,userId);
return isRelease?Result.ok():Result.error("釋放鎖失敗");
}
@GetMapping("/resetLock")
public Result resetLock(@RequestParam int planId,@RequestParam int userId){
boolean success = commonLockResourceService.resetLock(lockKeyPrefix,planId,userId);
return success?Result.ok():Result.error("釋放鎖失敗");
}
}
4,實驗
數據庫數據:
1,用戶一(userId=101),前端通過plan_id查詢某條規划:
localhost:8888/sysPlan/getByIdLock?planId=1&userId=101
返回成功:
{
"message": "成功",
"code": 200,
"result": {
"id": 1,
"branchOffinceId": 1,
"headOfficePlan": "xxasdaaaaaaa",
"branchOfficePlan": "1,提高生產效率",
"createTime": null,
"updateTime": "2020-08-17T08:27:36.000+0000"
},
"timestamp": 1597658245474
}
2,用戶二(userId=102),嘗試獲取該資源。(這里直接傳入不同userId代表不同用戶)
localhost:8888/sysPlan/getByIdLock?planId=1&userId=102
返回:
{
"message": "當前項目正在編輯中,請稍后重試",
"code": 500,
"result": null,
"timestamp": 1597659012412
}
3,如果用戶一(userId=101)編輯這條數據持續的時間較長(可能是一個文本域,輸入很多文本),前端做一個定時器,定時調用延續鎖時長接口,在操作期內,使自己一直拿到當前的鎖,防止操作沒完成,鎖被釋放了,別人拿到了鎖。
localhost:8888/sysPlan/resetLock?planId=1&userId=101
返回:
{
"message": "成功",
"code": 200,
"result": null,
"timestamp": 1597659371463
}
4,用戶一(userId=101)編輯完成,提交編輯,主動釋放鎖。
localhost:8888/sysPlan/update?planId=1&userId=101,此時redis中的鎖被清除。
5,用戶二(userId=102)再次嘗試獲得數據
localhost:8888/sysPlan/getByIdLock?planId=1&userId=102
返回:
{
"message": "成功",
"code": 200,
"result": {
"id": 1,
"branchOffinceId": 1,
"headOfficePlan": "xxasdaaaaaaa",
"branchOfficePlan": "1,提高生產效率",
"createTime": null,
"updateTime": "2020-08-17T08:27:36.000+0000"
},
"timestamp": 1597659558272
}
5,總結
用數據庫字段方案,有點“重”,需要不斷地維護這個字段,而且還有限制,用redis方案,友好又能解決需求,比較輕量級。
補充:項目業務還有其他場景,比如要提示當前誰正在編輯該數據,如沒有此需求,用數據庫version字段比較合適。
如有問題,歡迎交流
歡迎關注個人公眾號交流學習: