Java+Redis撤銷重做功能實現(xiàn)
1.背景
? 在一個編輯頁面中,存在多個圖表,對圖表的配置操作允許撤銷和重做;撤銷和重做只是針對頁面中圖表屬性變化進行,例如顏色修改、位置移動、字體修改等,對圖表的刪除、新增操作不在撤銷范圍內(nèi)。
? 撤銷是把圖表的配置更新為上一個狀態(tài)的值,允許進行連續(xù)撤銷,直到?jīng)]有可撤銷的記錄為止,出于性能考慮一般會設置一個撤銷的最大步數(shù)。重做是把圖表的配置還原為撤銷前的值,調(diào)用過撤銷,才能調(diào)用重做,例如圖表當前的狀態(tài)為A,調(diào)用一次撤銷后變?yōu)锽,此時調(diào)用重做則變?yōu)锳;允許進行連續(xù)重做,前提是之前進行過連續(xù)撤銷,例如圖表的當前狀態(tài)為A,第一次撤銷后變?yōu)锽,第二次撤銷后變?yōu)镃,此時第一次重做變?yōu)锽,第二次重做變?yōu)锳;調(diào)用撤銷后,緊接著圖表進行新的變更,中間穿插著一次變更后,則不能再進行重做,例如圖表的當前狀態(tài)為A,第一次撤銷后變?yōu)锽,接著由B變更為C,此時是不能再進行重做變?yōu)锳的。
? 編輯頁面截圖示例:

2.需求分析
(1)最大撤銷步數(shù)為20步;
(2)允許連續(xù)撤銷;
(3)允許連續(xù)重做;
(4)撤銷之后穿插著其他操作,不能再重做還原回去;
(5)刷新頁面后不能撤銷到刷新之前的狀態(tài),相當于新建一個會話;
(6)編輯過程中有圖表item刪除,需要刪除它的撤銷步驟;
(7)第一次加載圖表時,需要把此數(shù)據(jù)作為撤銷的初始值,當圖表第一次變更后,調(diào)用撤銷還原為初始值;
(8)通過定時器定時去加載圖表時,不再重復添加撤銷的初始值;
(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進行加鎖處理,防止對圖表的操作有并發(fā)請求導致處理撤銷邏輯混亂。具體引入過程可以參考之前的博客:redisson實現(xiàn)原理
(1)Redis的key與數(shù)據(jù)類型
? 由前端生成一個不重復的會話sessionId,當頁面刷新時重新生成sessionId,調(diào)用圖表查詢、刪除圖表、撤銷、重做接口時都帶上這個sessionId,此sessionId作為redis的緩存前綴key。
? 使用Redis的List數(shù)據(jù)類型來存放數(shù)據(jù),因為List類型支持左邊進leftPush,左邊出leftPop,右邊進rightPush,右邊出rightPop,可以把List當?;蛘哧犃惺褂?。撤銷操作要撤回的是上一個狀態(tài)的值,越早發(fā)生的變更,越晚才撤銷到,這正好是棧的特性,可以使用leftPush添加元素,leftPop彈出撤銷元素;當棧的數(shù)量大于指定數(shù)量20時,使用rightPop從棧底出棧。
? 定義一個key為sessionId+undo的撤銷List,用于存放所有的撤銷記錄;定義一個key為sessionId+redo的重做List,用于存放所有的重做記錄;有多少個圖表,定義多少個圖表List,用于存放圖表的所有變更過程,之所以每個圖表定義一個list的原因:①圖表需要撤銷為初始狀態(tài),一堆圖表初次加載數(shù)據(jù)時不分先后順序;②有定時去加載圖表數(shù)據(jù)的場景,不能讓圖表數(shù)據(jù)初始化重復;③撤銷后可以重做,而整個頁面是一個整體,需要記錄每個圖表撤銷前的狀態(tài),撤銷后的狀態(tài)。key為sessionId+圖表id,棧底為此次會話圖表的初始狀態(tài),棧頂與頁面中圖表的狀態(tài)一致。創(chuàng)建的list示例:

(2)初始化圖表棧
? 每次查詢圖表記錄時,都根據(jù)sessionId+圖表id判斷是否已經(jīng)存在此緩存list,若是不存在,則新建一個list,把查詢到的記錄作為list的初始值,若是存在則不進行添加。第一次加載完圖表信息后的情況:

(3)圖表第一次變更
? 圖表有狀態(tài)變更,則把變化圖表對應圖表棧的棧頂元素取出來放到undo中,并把最新的記錄存放到圖表棧的棧頂中。第一次有圖表狀態(tài)變化后的情況:

(4)圖表多次變更
? 頁面圖表狀態(tài)一直與緩存圖表的棧頂元素保持一致,undo中存放的都是變更之前的狀態(tài)值。經(jīng)過多次變更后的圖表狀態(tài):

(5)撤銷操作
? 當調(diào)用撤銷接口時,從undo棧中彈出棧頂元素返回給前端,使頁面中與彈出元素對應圖表的狀態(tài)變更為上一個狀態(tài);根據(jù)彈出元素的id找到對應的圖表棧,彈出圖表棧頂元素,此棧頂元素與調(diào)用撤銷之前的圖表狀態(tài)一致,把此元素放到redo棧中,當調(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接口,所以當有一次變化要保存,需要判斷此次變化的狀態(tài)是否是通過撤銷、重做接口獲取的,若是通過撤銷操作獲取的,則不進行undo的入棧;若是通過重做接口獲取的,則讓redo棧頂出棧,一開始調(diào)用redo重做接口不出棧就是為了對比新狀態(tài)是否通過重做獲取的。請求重做的流程:

? 調(diào)用重做接口,更新圖表后的狀態(tài):

(7)撤銷后重做
? 支持連續(xù)撤銷,連續(xù)重做,連續(xù)撤銷兩次后的圖表狀態(tài):

? 兩次撤銷后,進行一次重做后的圖表狀態(tài):

(8)撤銷后其他變更
? 當撤銷后,沒有調(diào)用重做(說明撤銷前的狀態(tài)是無用的),中間穿插著其他操作,則清空redo重做棧,看下當前圖表狀態(tài):

(9)存儲變更邏輯
? 當調(diào)用撤銷后,前端拿到圖表的上一個狀態(tài),然后調(diào)用updateItem保存圖表的最新狀態(tài),此時的狀態(tài)值不再往redis棧中入棧,判斷是否通過撤銷操作提交的依據(jù)為:找到變更圖表對應圖表棧,拿出棧頂元素,若是棧頂元素等于這次提交過來的新狀態(tài),則判斷是通過撤銷后提交過來的記錄,此種情況不進行入棧。若不是通過撤銷操作提交過來,則判斷是否通過調(diào)用重做接口提交過來的,判斷的依據(jù)為:拿出redo棧的棧頂元素,判斷新提交過來的元素是否等于棧頂元素,等于則說明是通過調(diào)用重做后提交過來的記錄??聪抡{(diào)用updateItem接口操作redis棧的流程圖:

?(10)刪除圖表
? 在編輯過程中,當某個圖表被刪除了,則需要刪除此圖表對應的撤銷記錄,刪除某個圖表的示意圖:

4.統(tǒng)一過期時間設置
? undo、redo和圖表棧都是基于一個會話進行存儲,它們不是同時創(chuàng)建的,編輯過程中也會加入新圖表,但是一個會話里面的棧需要有統(tǒng)一的過期時間,出于業(yè)務的考慮,一個頁面的編輯時間基本不會跨越一天,所以給棧的過期時間設置為當前時間到第二天凌晨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ù)的時候,結果值需要作為圖表棧的初始數(shù)據(jù),定時器再去加載的時候,不重復添加到棧中,只用判斷某個圖表在這次會話中是否已經(jīng)添加redis記錄。代碼實現(xià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)有對應的棧,有的話,則不重復添加
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, "服務器繁忙,請稍后重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務器繁忙,請稍后重試");
} finally {
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
//不存在item,則新添加
private void notExistItemNewAdd(String itemKey, String sessionId, String jsonValue) {
redisTemplate.opsForList().leftPush(itemKey,jsonValue);
//設置過期時間為當前時間到晚上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)有對應的棧,若是沒有,則新建一個list,把當前狀態(tài)作為list的初始值,并清空redo棧。若是存在變更圖表對應的棧,拿出圖表棧的棧頂元素,棧頂元素若是等于這次變更值,則說明此次變更是由撤銷操作觸發(fā)的,不進行入棧;若是不相等,說明是新的狀態(tài),則需要入棧,在入棧之前,先判斷undo棧的元素是否等于20,等于20則讓undo棧底出棧,出棧元素對應的圖表棧棧底也出棧,把變化的item對應圖表棧頂元素添加到undo棧中,再把變化的item添加到對應圖表的棧頂中,這樣圖表棧頂元素和頁面圖表的狀態(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)有對應的棧
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),若是實體類有繼承關系,則使用@EqualsAndHashCode(callSuper = true)注解標識連帶父類字段一塊參與hash計算
if(!itemUndo.equals(peekItem)){//相等說明是通過撤銷操作再次提交的,不進行入棧到撤銷棧中
//把圖表棧頂元素放到撤銷棧中,并判斷數(shù)量是否等于20
addItemToUndoList(peekObject,sessionId);
//變化的item放到圖表對應棧頂中,經(jīng)過上面20步的限制后,可能會把key為itemKey的list清空,清空則代表著刪除,需要重新設置過期時間
if(redisTemplate.hasKey(itemKey)) {
redisTemplate.opsForList().leftPush(itemKey,JSONObject.toJSONString(itemUndo));
} else {
redisTemplate.opsForList().leftPush(itemKey,JSONObject.toJSONString(itemUndo));
//設置過期時間為當前時間到晚上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, "服務器繁忙,請稍后重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務器繁忙,請稍后重試");
} 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);
//對應圖表的棧底出棧
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);
//設置過期時間為當前時間到晚上12點+1天
redisTemplate.expire(undoKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS);
}
}tip:比較實體是否相等,可以重寫實體的hashCode、equals方法,也可以使用lombok的@Data注解實現(xiàn),若是實體類有繼承關系,則使用@EqualsAndHashCode(callSuper = true)注解標識連帶父類字段一塊參與hash計算。
7.撤銷操作
? 從undo棧中彈出棧頂元素返回給前端,根據(jù)此出棧元素獲取到它對應的圖表棧頂元素,圖表棧頂元素狀態(tài)與撤銷之前頁面圖表的狀態(tài)一致,把圖表棧頂元素出棧放到redo棧中,當調(diào)用重做時能讓頁面的圖表狀態(tài)還原回去。代碼實現(xiàn):
public JSONObject undo(String json) {
if(StringUtils.isBlank(json)){
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "參數(shù)為空,請確保參數(shù)的準確性");
}
JSONObject jsonObject= JSONObject.parseObject(json);
if(!jsonObject.containsKey("sessionId")){
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "缺少參數(shù)sessionId,請確保參數(shù)的準確性");
}
String sessionId = jsonObject.getString("sessionId");
if(StringUtils.isEmpty(sessionId)){
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "參數(shù)sessionId為空,請確保參數(shù)的準確性");
}
//添加分布式鎖
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ù)棧頂元素獲取到它對應的圖表棧
String itemKey = RedisKeyPrefixConstants.PAGE_UNDO_REDO+sessionId+":item:"+undoJsonObject.get("id");
//存在對應的圖表棧
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棧,則添加,并設置過期時間
redisTemplate.opsForList().leftPush(redoKey,redoObject);
//設置過期時間為當前時間到晚上12點+1天
redisTemplate.expire(redoKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS);
}
}
//undo棧頂元素返回給前端
return undoJsonObject;
} else {
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務器繁忙,請稍后重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務器繁忙,請稍后重試");
} 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ù)的準確性");
}
JSONObject jsonObject= JSONObject.parseObject(json);
if(!jsonObject.containsKey("sessionId")){
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "缺少參數(shù)sessionId,請確保參數(shù)的準確性");
}
String sessionId = jsonObject.getString("sessionId");
if(StringUtils.isEmpty(sessionId)){
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "參數(shù)sessionId為空,請確保參數(shù)的準確性");
}
//添加分布式鎖
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;
}
//拿出棧頂元素,不出棧,返回給前端,當調(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, "服務器繁忙,請稍后重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務器繁忙,請稍后重試");
} 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);
//設置過期時間為當前時間到晚上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);
//設置過期時間為當前時間到晚上12點+1天
redisTemplate.expire(undoKey,getSecondsNextEarlyMorningAddOneDay(), TimeUnit.SECONDS);
}
}
}
} else {
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務器繁忙,請稍后重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "服務器繁忙,請稍后重試");
} finally {
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}到此這篇關于Java+Redis實現(xiàn)撤銷重做功能的文章就介紹到這了,更多相關Java+Redis實現(xiàn)撤銷重做功能內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java?swing實現(xiàn)應用程序?qū)?shù)據(jù)庫的訪問問題
這篇文章主要介紹了Java?swing實現(xiàn)應用程序?qū)?shù)據(jù)庫的訪問,本次實驗需要做一個GUI界面和一個連接查詢功能,在論壇上借鑒了其他大佬獲取網(wǎng)站內(nèi)容的部分代碼,然后自己做了一個及其簡陋的swing界面,算是把這個實驗完成了,需要的朋友可以參考下2022-09-09
Java使用Redis實現(xiàn)消息訂閱/發(fā)布的幾種方式
Redis 提供了 Pub/Sub (發(fā)布/訂閱) 模式,允許客戶端訂閱頻道并接收發(fā)布到這些頻道的消息,以下是 Java 中使用 Redis 實現(xiàn)消息訂閱的幾種方式,需要的朋友可以參考下2025-08-08

