欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java+Redis撤銷重做功能實現(xiàn)

 更新時間:2023年05月22日 10:40:15   作者:清云青云  
這篇文章主要介紹了Java+Redis實現(xiàn)撤銷重做功能,需要考慮撤銷的最大步數(shù),撤銷之后穿插著其他操作則不能再重做,所以引入分布式鎖Redisson進(jìn)行加鎖處理,防止對圖表的操作有并發(fā)請求導(dǎo)致處理撤銷邏輯混亂,感興趣的朋友跟隨小編一起看看吧

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)文章

  • 詳解SpringMVC中攔截器的概念及入門案例

    詳解SpringMVC中攔截器的概念及入門案例

    攔截器(Interceptor)是一種動態(tài)攔截方法調(diào)用的機(jī)制,在SpringMVC中動態(tài)攔截控制器方法的執(zhí)行。本文將詳細(xì)講講SpringMVC中攔截器的概念及入門案例,感興趣的可以嘗試一下
    2022-07-07
  • 利用微信小程序+JAVA實現(xiàn)微信支付的全過程

    利用微信小程序+JAVA實現(xiàn)微信支付的全過程

    微信支付是一種在線支付解決方案,允許用戶通過微信內(nèi)的支付功能進(jìn)行付款,下面這篇文章主要給大家介紹了關(guān)于利用微信小程序+JAVA實現(xiàn)微信支付的相關(guān)資料,需要的朋友可以參考下
    2024-08-08
  • JAVA項目如何打包部署到Linux服務(wù)器上

    JAVA項目如何打包部署到Linux服務(wù)器上

    本文詳細(xì)介紹了在服務(wù)器上部署環(huán)境包括JDK、MySQL、Tomcat的設(shè)置,以及使用Idea-Maven-SpringBoot進(jìn)行jar包打包部署的流程,內(nèi)容涵蓋了MySQL配置注意事項、pom.xml配置、打包命令等關(guān)鍵步驟,同時,也提供了如何將jar包上傳到Linux服務(wù)器并運行的具體方法
    2024-10-10
  • SpringBoot integration實現(xià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ì)學(xué)習(xí)Java Cookie技術(shù)(用戶登錄、瀏覽、訪問權(quán)限)

    這篇文章主要為大家詳細(xì)介紹了Java Cookie技術(shù),顯示用戶上次登錄的時間、顯示用戶最近瀏覽的若干個圖片(按比例縮放)等,感興趣的小伙伴們可以參考一下
    2016-08-08
  • Spring MVC攔截器的基本使用方法

    Spring MVC攔截器的基本使用方法

    這篇文章主要給大家介紹了關(guān)于Spring MVC攔截器的基本使用方法,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Spring MVC具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-07-07
  • SpringBoot中對SpringMVC的自動配置詳解

    SpringBoot中對SpringMVC的自動配置詳解

    這篇文章主要介紹了SpringBoot中的SpringMVC自動配置詳解,Spring MVC自動配置是Spring Boot提供的一種特性,它可以自動配置Spring MVC的相關(guān)組件,簡化了開發(fā)人員的配置工作,需要的朋友可以參考下
    2023-10-10
  • java web監(jiān)聽器統(tǒng)計在線用戶及人數(shù)

    java web監(jiān)聽器統(tǒng)計在線用戶及人數(shù)

    本文主要介紹了java web監(jiān)聽器統(tǒng)計在線用戶及人數(shù)的方法解析。具有很好的參考價值。下面跟著小編一起來看下吧
    2017-04-04
  • Java多線程之線程通信生產(chǎn)者消費者模式及等待喚醒機(jī)制代碼詳解

    Java多線程之線程通信生產(chǎn)者消費者模式及等待喚醒機(jī)制代碼詳解

    這篇文章主要介紹了Java多線程之線程通信生產(chǎn)者消費者模式及等待喚醒機(jī)制代碼詳解,具有一定參考價值,需要的朋友可以了解下。
    2017-10-10
  • springmvc流程圖以及配置解析

    springmvc流程圖以及配置解析

    這篇文章主要介紹了springmvc流程圖以及配置解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-09-09

最新評論