Java+Redis撤銷重做功能實現(xiàn)
1.背景
? 在一個編輯頁面中,存在多個圖表,對圖表的配置操作允許撤銷和重做;撤銷和重做只是針對頁面中圖表屬性變化進(jìn)行,例如顏色修改、位置移動、字體修改等,對圖表的刪除、新增操作不在撤銷范圍內(nèi)。
? 撤銷是把圖表的配置更新為上一個狀態(tài)的值,允許進(jìn)行連續(xù)撤銷,直到?jīng)]有可撤銷的記錄為止,出于性能考慮一般會設(shè)置一個撤銷的最大步數(shù)。重做是把圖表的配置還原為撤銷前的值,調(diào)用過撤銷,才能調(diào)用重做,例如圖表當(dāng)前的狀態(tài)為A,調(diào)用一次撤銷后變?yōu)锽,此時調(diào)用重做則變?yōu)锳;允許進(jìn)行連續(xù)重做,前提是之前進(jìn)行過連續(xù)撤銷,例如圖表的當(dāng)前狀態(tài)為A,第一次撤銷后變?yōu)锽,第二次撤銷后變?yōu)镃,此時第一次重做變?yōu)锽,第二次重做變?yōu)锳;調(diào)用撤銷后,緊接著圖表進(jìn)行新的變更,中間穿插著一次變更后,則不能再進(jìn)行重做,例如圖表的當(dāng)前狀態(tài)為A,第一次撤銷后變?yōu)锽,接著由B變更為C,此時是不能再進(jìn)行重做變?yōu)锳的。
? 編輯頁面截圖示例:
2.需求分析
(1)最大撤銷步數(shù)為20步;
(2)允許連續(xù)撤銷;
(3)允許連續(xù)重做;
(4)撤銷之后穿插著其他操作,不能再重做還原回去;
(5)刷新頁面后不能撤銷到刷新之前的狀態(tài),相當(dāng)于新建一個會話;
(6)編輯過程中有圖表item刪除,需要刪除它的撤銷步驟;
(7)第一次加載圖表時,需要把此數(shù)據(jù)作為撤銷的初始值,當(dāng)圖表第一次變更后,調(diào)用撤銷還原為初始值;
(8)通過定時器定時去加載圖表時,不再重復(fù)添加撤銷的初始值;
(9)存儲撤銷數(shù)據(jù)的入口為圖表變更之后調(diào)用updateItem接口,而updateItem接口傳遞過來的是圖表的最新狀態(tài),調(diào)用撤銷是還原為變更之前的狀態(tài);
(10)撤銷操作使用Redis實現(xiàn),需要保證同一個會話的緩存數(shù)據(jù)有相同的過期時間。
3.實現(xiàn)邏輯分析
? 項目使用Java開發(fā),所以此處使用Java+Redis實現(xiàn)撤銷重做功能;需要考慮撤銷的最大步數(shù),撤銷之后穿插著其他操作則不能再重做,所以引入分布式鎖Redisson進(jìn)行加鎖處理,防止對圖表的操作有并發(fā)請求導(dǎo)致處理撤銷邏輯混亂。具體引入過程可以參考之前的博客:redisson實現(xiàn)原理
(1)Redis的key與數(shù)據(jù)類型
? 由前端生成一個不重復(fù)的會話sessionId,當(dāng)頁面刷新時重新生成sessionId,調(diào)用圖表查詢、刪除圖表、撤銷、重做接口時都帶上這個sessionId,此sessionId作為redis的緩存前綴key。
? 使用Redis的List數(shù)據(jù)類型來存放數(shù)據(jù),因為List類型支持左邊進(jìn)leftPush,左邊出leftPop,右邊進(jìn)rightPush,右邊出rightPop,可以把List當(dāng)?;蛘哧犃惺褂谩3蜂N操作要撤回的是上一個狀態(tài)的值,越早發(fā)生的變更,越晚才撤銷到,這正好是棧的特性,可以使用leftPush添加元素,leftPop彈出撤銷元素;當(dāng)棧的數(shù)量大于指定數(shù)量20時,使用rightPop從棧底出棧。
? 定義一個key為sessionId+undo的撤銷List,用于存放所有的撤銷記錄;定義一個key為sessionId+redo的重做List,用于存放所有的重做記錄;有多少個圖表,定義多少個圖表List,用于存放圖表的所有變更過程,之所以每個圖表定義一個list的原因:①圖表需要撤銷為初始狀態(tài),一堆圖表初次加載數(shù)據(jù)時不分先后順序;②有定時去加載圖表數(shù)據(jù)的場景,不能讓圖表數(shù)據(jù)初始化重復(fù);③撤銷后可以重做,而整個頁面是一個整體,需要記錄每個圖表撤銷前的狀態(tài),撤銷后的狀態(tài)。key為sessionId+圖表id,棧底為此次會話圖表的初始狀態(tài),棧頂與頁面中圖表的狀態(tài)一致。創(chuàng)建的list示例:
(2)初始化圖表棧
? 每次查詢圖表記錄時,都根據(jù)sessionId+圖表id判斷是否已經(jīng)存在此緩存list,若是不存在,則新建一個list,把查詢到的記錄作為list的初始值,若是存在則不進(jìn)行添加。第一次加載完圖表信息后的情況:
(3)圖表第一次變更
? 圖表有狀態(tài)變更,則把變化圖表對應(yīng)圖表棧的棧頂元素取出來放到undo中,并把最新的記錄存放到圖表棧的棧頂中。第一次有圖表狀態(tài)變化后的情況:
(4)圖表多次變更
? 頁面圖表狀態(tài)一直與緩存圖表的棧頂元素保持一致,undo中存放的都是變更之前的狀態(tài)值。經(jīng)過多次變更后的圖表狀態(tài):
(5)撤銷操作
? 當(dāng)調(diào)用撤銷接口時,從undo棧中彈出棧頂元素返回給前端,使頁面中與彈出元素對應(yīng)圖表的狀態(tài)變更為上一個狀態(tài);根據(jù)彈出元素的id找到對應(yīng)的圖表棧,彈出圖表棧頂元素,此棧頂元素與調(diào)用撤銷之前的圖表狀態(tài)一致,把此元素放到redo棧中,當(dāng)調(diào)用完撤銷之后,可以調(diào)用重做接口把圖表的狀態(tài)還原回去。請求撤銷的流程:
調(diào)用一次撤銷后的圖表狀態(tài):
(6)重做操作
? 調(diào)用重做接口時,從redo棧中取出棧頂元素返回給前端,此處先不彈出元素,因為需要支持連續(xù)重做,而前端拿到撤銷或者重做返回的圖表最新狀態(tài)后,立馬調(diào)用updateItem接口更新圖表的最新狀態(tài),我們記錄圖表狀態(tài)變化的入口點也是updateItem接口,所以當(dāng)有一次變化要保存,需要判斷此次變化的狀態(tài)是否是通過撤銷、重做接口獲取的,若是通過撤銷操作獲取的,則不進(jìn)行undo的入棧;若是通過重做接口獲取的,則讓redo棧頂出棧,一開始調(diào)用redo重做接口不出棧就是為了對比新狀態(tài)是否通過重做獲取的。請求重做的流程:
? 調(diào)用重做接口,更新圖表后的狀態(tài):
(7)撤銷后重做
? 支持連續(xù)撤銷,連續(xù)重做,連續(xù)撤銷兩次后的圖表狀態(tài):
? 兩次撤銷后,進(jìn)行一次重做后的圖表狀態(tài):
(8)撤銷后其他變更
? 當(dāng)撤銷后,沒有調(diào)用重做(說明撤銷前的狀態(tài)是無用的),中間穿插著其他操作,則清空redo重做棧,看下當(dāng)前圖表狀態(tài):
(9)存儲變更邏輯
? 當(dāng)調(diào)用撤銷后,前端拿到圖表的上一個狀態(tài),然后調(diào)用updateItem保存圖表的最新狀態(tài),此時的狀態(tài)值不再往redis棧中入棧,判斷是否通過撤銷操作提交的依據(jù)為:找到變更圖表對應(yīng)圖表棧,拿出棧頂元素,若是棧頂元素等于這次提交過來的新狀態(tài),則判斷是通過撤銷后提交過來的記錄,此種情況不進(jìn)行入棧。若不是通過撤銷操作提交過來,則判斷是否通過調(diào)用重做接口提交過來的,判斷的依據(jù)為:拿出redo棧的棧頂元素,判斷新提交過來的元素是否等于棧頂元素,等于則說明是通過調(diào)用重做后提交過來的記錄??聪抡{(diào)用updateItem接口操作redis棧的流程圖:
?(10)刪除圖表
? 在編輯過程中,當(dāng)某個圖表被刪除了,則需要刪除此圖表對應(yīng)的撤銷記錄,刪除某個圖表的示意圖:
4.統(tǒng)一過期時間設(shè)置
? undo、redo和圖表棧都是基于一個會話進(jìn)行存儲,它們不是同時創(chuàng)建的,編輯過程中也會加入新圖表,但是一個會話里面的棧需要有統(tǒng)一的過期時間,出于業(yè)務(wù)的考慮,一個頁面的編輯時間基本不會跨越一天,所以給棧的過期時間設(shè)置為當(dāng)前時間到第二天凌晨12點的秒數(shù)+1天的秒數(shù),這樣這次會話的失效時間為明天晚上12點。獲取統(tǒng)一過期時間的代碼實現(xiàn):
public Long getSecondsNextEarlyMorningAddOneDay() { Calendar cal = Calendar.getInstance(); cal.add(Calendar.DAY_OF_YEAR, 2); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.MILLISECOND, 0); return (cal.getTimeInMillis() - System.currentTimeMillis()) / 1000; }
5.初始圖表棧
? 第一次查詢圖表數(shù)據(jù)的時候,結(jié)果值需要作為圖表棧的初始數(shù)據(jù),定時器再去加載的時候,不重復(fù)添加到棧中,只用判斷某個圖表在這次會話中是否已經(jīng)添加redis記錄。代碼實現(xiàn):
//圖表若是沒有初始化過,則進(jìn)行初始化 public void addItemInit(Item itemUndo, String sessionId) { //添加分布式鎖 String lockKey = RedissonKeyPrefixeConstants.LOCK_PAGE_UNDO_REDO+sessionId; RLock lock = redissonClient.getLock(lockKey); try { boolean res = lock.tryLock(30, TimeUnit.SECONDS); if(res) { //判斷需要保存記錄是否已經(jīng)有對應(yīng)的棧,有的話,則不重復(fù)添加 String itemKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":item:"+itemUndo.getId(); if(!redisTemplate.hasKey(itemKey)){//不存在,則添加 notExistItemNewAdd(itemKey,sessionId,JSONObject.toJSONString(itemUndo)); } } else { throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } } catch (InterruptedException e) { e.printStackTrace(); throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } //不存在item,則新添加 private void notExistItemNewAdd(String itemKey, String sessionId, String jsonValue) { redisTemplate.opsForList().leftPush(itemKey,jsonValue); //設(shè)置過期時間為當(dāng)前時間到晚上12點+1天 redisTemplate.expire(itemKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS); //清空重做棧,說明中間穿插著新加入圖表的操作 String redoKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":redo"; redisTemplate.delete(redoKey); }
6.記錄圖表變化
? 圖表有狀態(tài)變化,調(diào)用updateItem接口更新最新狀態(tài),在此接口上添加redis的處理。判斷需要保存的記錄是否已經(jīng)有對應(yīng)的棧,若是沒有,則新建一個list,把當(dāng)前狀態(tài)作為list的初始值,并清空redo棧。若是存在變更圖表對應(yīng)的棧,拿出圖表棧的棧頂元素,棧頂元素若是等于這次變更值,則說明此次變更是由撤銷操作觸發(fā)的,不進(jìn)行入棧;若是不相等,說明是新的狀態(tài),則需要入棧,在入棧之前,先判斷undo棧的元素是否等于20,等于20則讓undo棧底出棧,出棧元素對應(yīng)的圖表棧棧底也出棧,把變化的item對應(yīng)圖表棧頂元素添加到undo棧中,再把變化的item添加到對應(yīng)圖表的棧頂中,這樣圖表棧頂元素和頁面圖表的狀態(tài)保持一致,undo中存的是圖表的上一個狀態(tài)值。判斷redo棧是否存在,存在redo棧,則判斷redo棧的棧頂元素是否等于變化的item,等于則說明是通過調(diào)用重做提交過來的,此時讓redo棧頂出棧,不清空redo棧,這樣可以支持連續(xù)調(diào)用重做;若是不等于redo棧頂元素,則說明此次提交過來的數(shù)據(jù)不是通過重做實現(xiàn)的,穿插著其他操作,需要清空redo棧。代碼實現(xiàn):
public void addItemUndo(Item itemUndo,String sessionId) { //添加分布式鎖 String lockKey = RedissonKeyPrefixeConstants.LOCK_PAGE_UNDO_REDO+sessionId; RLock lock = redissonClient.getLock(lockKey); try { boolean res = lock.tryLock(30, TimeUnit.SECONDS); if(res) { //判斷需要保存記錄是否已經(jīng)有對應(yīng)的棧 String itemKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":item:"+itemUndo.getId(); if(redisTemplate.hasKey(itemKey)){ //拿出圖表棧棧頂元素,不出棧 String peekObject = (String)redisTemplate.opsForList().index(itemKey,0); Item peekItem = JSONObject.parseObject(peekObject,Item.class); //判斷此次變化的item是否等于棧頂元素,比較實體是否相等,可以重寫實體的hashCode、equals方法,也可以使用lombok的@Data注解實現(xiàn),若是實體類有繼承關(guān)系,則使用@EqualsAndHashCode(callSuper = true)注解標(biāo)識連帶父類字段一塊參與hash計算 if(!itemUndo.equals(peekItem)){//相等說明是通過撤銷操作再次提交的,不進(jìn)行入棧到撤銷棧中 //把圖表棧頂元素放到撤銷棧中,并判斷數(shù)量是否等于20 addItemToUndoList(peekObject,sessionId); //變化的item放到圖表對應(yīng)棧頂中,經(jīng)過上面20步的限制后,可能會把key為itemKey的list清空,清空則代表著刪除,需要重新設(shè)置過期時間 if(redisTemplate.hasKey(itemKey)) { redisTemplate.opsForList().leftPush(itemKey,JSONObject.toJSONString(itemUndo)); } else { redisTemplate.opsForList().leftPush(itemKey,JSONObject.toJSONString(itemUndo)); //設(shè)置過期時間為當(dāng)前時間到晚上12點+1天 redisTemplate.expire(itemKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS); } String redoKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":redo"; if(redisTemplate.hasKey(redoKey)) { //出棧 String redoPeekObject = (String)redisTemplate.opsForList().leftPop(redoKey); JSONObject redoPeekJsonObject = JSONObject.parseObject(redoPeekObject); //組件item Item redoPeekItem = JSONObject.parseObject(redoPeekObject,Item.class); if(!itemUndo.equals(redoPeekItem)) {//相等說明是通過重做操作再次提交的,重做棧頂出棧,leftPop方法已經(jīng)出棧;不相等說明上一步不是重做,清空redo redisTemplate.delete(redoKey); } } } } else { //不存在,則添加 notExistItemNewAdd(itemKey,sessionId,JSONObject.toJSONString(itemUndo)); } } else { throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } } catch (InterruptedException e) { e.printStackTrace(); throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } //把圖表棧頂元素放到撤銷棧中,并判斷數(shù)量是否等于20 private void addItemToUndoList(String peekObject,String sessionId) { String undoKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":undo"; if(redisTemplate.hasKey(undoKey)) { Long undoSize = redisTemplate.opsForList().size(undoKey); //判斷數(shù)量是否大于等于20步,大于等于20步則讓棧底出棧 if(undoSize >= 20) { //棧底出棧 String popUndoObject = (String)redisTemplate.opsForList().rightPop(undoKey); JSONObject popUndoJsonObject = JSONObject.parseObject(popUndoObject); //對應(yīng)圖表的棧底出棧 String popItemKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":item:"+popUndoJsonObject.get("id"); if(redisTemplate.hasKey(popItemKey)){ redisTemplate.opsForList().rightPop(popItemKey); } } redisTemplate.opsForList().leftPush(undoKey,peekObject); } else { redisTemplate.opsForList().leftPush(undoKey,peekObject); //設(shè)置過期時間為當(dāng)前時間到晚上12點+1天 redisTemplate.expire(undoKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS); } }
tip:比較實體是否相等,可以重寫實體的hashCode、equals方法,也可以使用lombok的@Data注解實現(xiàn),若是實體類有繼承關(guān)系,則使用@EqualsAndHashCode(callSuper = true)注解標(biāo)識連帶父類字段一塊參與hash計算。
7.撤銷操作
? 從undo棧中彈出棧頂元素返回給前端,根據(jù)此出棧元素獲取到它對應(yīng)的圖表棧頂元素,圖表棧頂元素狀態(tài)與撤銷之前頁面圖表的狀態(tài)一致,把圖表棧頂元素出棧放到redo棧中,當(dāng)調(diào)用重做時能讓頁面的圖表狀態(tài)還原回去。代碼實現(xiàn):
public JSONObject undo(String json) { if(StringUtils.isBlank(json)){ throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "參數(shù)為空,請確保參數(shù)的準(zhǔn)確性"); } JSONObject jsonObject= JSONObject.parseObject(json); if(!jsonObject.containsKey("sessionId")){ throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "缺少參數(shù)sessionId,請確保參數(shù)的準(zhǔn)確性"); } String sessionId = jsonObject.getString("sessionId"); if(StringUtils.isEmpty(sessionId)){ throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "參數(shù)sessionId為空,請確保參數(shù)的準(zhǔn)確性"); } //添加分布式鎖 String lockKey = RedissonKeyPrefixeConstants.LOCK_PAGE_UNDO_REDO+sessionId; RLock lock = redissonClient.getLock(lockKey); try { boolean res = lock.tryLock(30, TimeUnit.SECONDS); if(res) { String undoKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":undo"; //不包含撤銷棧,直接返回空 if(!redisTemplate.hasKey(undoKey)){ return null; } //彈出undo棧頂元素 String undoObject = (String)redisTemplate.opsForList().leftPop(undoKey); //轉(zhuǎn)成對象 JSONObject undoJsonObject = JSONObject.parseObject(undoObject); String redoObject = null; //根據(jù)棧頂元素獲取到它對應(yīng)的圖表棧 String itemKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":item:"+undoJsonObject.get("id"); //存在對應(yīng)的圖表棧 if(redisTemplate.hasKey(itemKey)){ redoObject = (String)redisTemplate.opsForList().leftPop(itemKey); } if(StringUtils.isNotEmpty(redoObject)){ //把圖表棧的棧頂元素添加到redo棧中 String redoKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":redo"; if(redisTemplate.hasKey(redoKey)) {//已經(jīng)redo棧則直接追加 redisTemplate.opsForList().leftPush(redoKey,redoObject); } else {//不包含redo棧,則添加,并設(shè)置過期時間 redisTemplate.opsForList().leftPush(redoKey,redoObject); //設(shè)置過期時間為當(dāng)前時間到晚上12點+1天 redisTemplate.expire(redoKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS); } } //undo棧頂元素返回給前端 return undoJsonObject; } else { throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } } catch (InterruptedException e) { e.printStackTrace(); throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } }
8.重做操作
? 從redo棧中獲取棧頂元素返回給前端,不出棧,因為數(shù)據(jù)有變動保存時,需要比對是否由重做觸發(fā)的,若是重做觸發(fā)的則彈出redo棧頂元素,不是重做觸發(fā)的則清空redo棧,這樣可以支持連續(xù)調(diào)用重做。代碼實現(xiàn):
public JSONObject redo(String json) { if(StringUtils.isBlank(json)){ throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "參數(shù)為空,請確保參數(shù)的準(zhǔn)確性"); } JSONObject jsonObject= JSONObject.parseObject(json); if(!jsonObject.containsKey("sessionId")){ throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "缺少參數(shù)sessionId,請確保參數(shù)的準(zhǔn)確性"); } String sessionId = jsonObject.getString("sessionId"); if(StringUtils.isEmpty(sessionId)){ throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "參數(shù)sessionId為空,請確保參數(shù)的準(zhǔn)確性"); } //添加分布式鎖 String lockKey = RedissonKeyPrefixeConstants.LOCK_PAGE_UNDO_REDO+sessionId; RLock lock = redissonClient.getLock(lockKey); try { boolean res = lock.tryLock(30, TimeUnit.SECONDS); if(res) { String redoKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":redo"; //不包含重做棧,直接返回空 if(!redisTemplate.hasKey(redoKey)){ return null; } //拿出棧頂元素,不出棧,返回給前端,當(dāng)調(diào)用更新數(shù)據(jù)變動時,判斷新提交過來的數(shù)據(jù)是否等于重做棧的棧頂,等于則說明是通過重做提交過來的, //此時不清空重做棧,因為需要支持多步重做;若是不等于重做棧頂元素,則清空重做棧,說明上一步不是重做 String redoObject = (String)redisTemplate.opsForList().index(redoKey,0); JSONObject redoJsonObject = JSONObject.parseObject(redoObject); //拿出棧頂元素返回給前端 return redoJsonObject; } else { throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } } catch (InterruptedException e) { e.printStackTrace(); throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } }
9.刪除圖表處理
? 刪除圖表時需要刪除此圖表的操作記錄,undo、redo、圖表棧都需要刪除,否則會撤銷到一個不存在的記錄。代碼實現(xiàn):
public void deleteItem(List<Integer> idList, String sessionId) { //添加分布式鎖 String lockKey = RedissonKeyPrefixeConstants.LOCK_PAGE_UNDO_REDO+sessionId; RLock lock = redissonClient.getLock(lockKey); try { boolean res = lock.tryLock(30, TimeUnit.SECONDS); if(res) { //根據(jù)刪除圖表的id,刪除圖表棧 for(int i = 0;i < idList.size();i++) { String itemKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":item:"+idList.get(i); redisTemplate.delete(itemKey); } //刪除撤銷棧 String redoKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":redo"; if(redisTemplate.hasKey(redoKey)) { //包含redo棧才處理 //獲取redo棧的所有記錄 List redoList = redisTemplate.opsForList().range(redoKey, 0, -1); if(null != redoList && redoList.size() > 0) { Iterator redoIt = redoList.iterator(); //遍歷redo棧的所有記錄 while(redoIt.hasNext()) { String redoObject = (String)redoIt.next(); JSONObject redoJsonObject = JSONObject.parseObject(redoObject); //redo棧中的元素存在于刪除的圖表集合中,則刪除棧中的元素 if(idList.contains(redoJsonObject.getInteger("id"))){//判斷是否為刪除的id redoIt.remove(); } } //刪除撤銷棧數(shù)據(jù) redisTemplate.delete(redoKey); //經(jīng)過刪除后,撤銷棧里面還有數(shù)據(jù),則重新添加到redis中 if(null != redoList && redoList.size() > 0) { redisTemplate.opsForList().leftPushAll(redoKey,redoList); //設(shè)置過期時間為當(dāng)前時間到晚上12點+1天 redisTemplate.expire(redoKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS); } } } //刪除重做棧 String undoKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":undo"; if(redisTemplate.hasKey(undoKey)) { //獲取undo棧的所有記錄 List undoList = redisTemplate.opsForList().range(undoKey, 0, -1); if(null != undoList && undoList.size() > 0) { Iterator undoIt = undoList.iterator(); while(undoIt.hasNext()) { String undoObject = (String)undoIt.next(); JSONObject undoJsonObject = JSONObject.parseObject(undoObject); //undo棧中的元素存在于刪除的圖表集合中,則刪除棧中的元素 if(idList.contains(undoJsonObject.getInteger("id"))){//判斷是否為刪除的id undoIt.remove(); } } //刪除重做棧數(shù)據(jù) redisTemplate.delete(undoKey); //經(jīng)過刪除后,重做棧里面還有數(shù)據(jù),則重新添加到redis中 if(null != undoList && undoList.size() > 0) { redisTemplate.opsForList().leftPushAll(undoKey,undoList); //設(shè)置過期時間為當(dāng)前時間到晚上12點+1天 redisTemplate.expire(undoKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS); } } } } else { throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } } catch (InterruptedException e) { e.printStackTrace(); throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務(wù)器繁忙,請稍后重試"); } finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } }
到此這篇關(guān)于Java+Redis實現(xiàn)撤銷重做功能的文章就介紹到這了,更多相關(guān)Java+Redis實現(xiàn)撤銷重做功能內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot integration實現(xiàn)分布式鎖的示例詳解
常規(guī)項目都是采用Redission來實現(xiàn)分布式鎖,進(jìn)行分布式系統(tǒng)中資源競爭加鎖操作,偶然發(fā)現(xiàn)SpringBoot中的integration也實現(xiàn)多種載體的分布式鎖控制,下面我們就來看看具體實現(xiàn)方法吧2023-12-12詳細(xì)學(xué)習(xí)Java Cookie技術(shù)(用戶登錄、瀏覽、訪問權(quán)限)
這篇文章主要為大家詳細(xì)介紹了Java Cookie技術(shù),顯示用戶上次登錄的時間、顯示用戶最近瀏覽的若干個圖片(按比例縮放)等,感興趣的小伙伴們可以參考一下2016-08-08java web監(jiān)聽器統(tǒng)計在線用戶及人數(shù)
本文主要介紹了java web監(jiān)聽器統(tǒng)計在線用戶及人數(shù)的方法解析。具有很好的參考價值。下面跟著小編一起來看下吧2017-04-04Java多線程之線程通信生產(chǎn)者消費者模式及等待喚醒機(jī)制代碼詳解
這篇文章主要介紹了Java多線程之線程通信生產(chǎn)者消費者模式及等待喚醒機(jī)制代碼詳解,具有一定參考價值,需要的朋友可以了解下。2017-10-10