Java+Redis实现撤销重做功能

1.背景

​        在一个编辑页面中,存在多个图表,对图表的配置操作允许撤销和重做;撤销和重做只是针对页面中图表属性变化进行,例如颜色修改、位置移动、字体修改等,对图表的删除、新增操作不在撤销范围内。

​        撤销是把图表的配置更新为上一个状态的值,允许进行连续撤销,直到没有可撤销的记录为止,出于性能考虑一般会设置一个撤销的最大步数。重做是把图表的配置还原为撤销前的值,调用过撤销,才能调用重做,例如图表当前的状态为A,调用一次撤销后变为B,此时调用重做则变为A;允许进行连续重做,前提是之前进行过连续撤销,例如图表的当前状态为A,第一次撤销后变为B,第二次撤销后变为C,此时第一次重做变为B,第二次重做变为A;调用撤销后,紧接着图表进行新的变更,中间穿插着一次变更后,则不能再进行重做,例如图表的当前状态为A,第一次撤销后变为B,接着由B变更为C,此时是不能再进行重做变为A的。

​         编辑页面截图示例:
在这里插入图片描述

2.需求分析

(1)最大撤销步数为20步;

(2)允许连续撤销;

(3)允许连续重做;

(4)撤销之后穿插着其他操作,不能再重做还原回去;

(5)刷新页面后不能撤销到刷新之前的状态,相当于新建一个会话;

(6)编辑过程中有图表item删除,需要删除它的撤销步骤;

(7)第一次加载图表时,需要把此数据作为撤销的初始值,当图表第一次变更后,调用撤销还原为初始值;

(8)通过定时器定时去加载图表时,不再重复添加撤销的初始值;

(9)存储撤销数据的入口为图表变更之后调用updateItem接口,而updateItem接口传递过来的是图表的最新状态,调用撤销是还原为变更之前的状态;

(10)撤销操作使用Redis实现,需要保证同一个会话的缓存数据有相同的过期时间。

3.实现逻辑分析

​        项目使用Java开发,所以此处使用Java+Redis实现撤销重做功能;需要考虑撤销的最大步数,撤销之后穿插着其他操作则不能再重做,所以引入分布式锁Redisson进行加锁处理,防止对图表的操作有并发请求导致处理撤销逻辑混乱。具体引入过程可以参考之前的博客:redisson实现原理

(1)Redis的key与数据类型

​        由前端生成一个不重复的会话sessionId,当页面刷新时重新生成sessionId,调用图表查询、删除图表、撤销、重做接口时都带上这个sessionId,此sessionId作为redis的缓存前缀key。

​        使用Redis的List数据类型来存放数据,因为List类型支持左边进leftPush,左边出leftPop,右边进rightPush,右边出rightPop,可以把List当栈或者队列使用。撤销操作要撤回的是上一个状态的值,越早发生的变更,越晚才撤销到,这正好是栈的特性,可以使用leftPush添加元素,leftPop弹出撤销元素;当栈的数量大于指定数量20时,使用rightPop从栈底出栈。

​        定义一个key为sessionId+undo的撤销List,用于存放所有的撤销记录;定义一个key为sessionId+redo的重做List,用于存放所有的重做记录;有多少个图表,定义多少个图表List,用于存放图表的所有变更过程,之所以每个图表定义一个list的原因:①图表需要撤销为初始状态,一堆图表初次加载数据时不分先后顺序;②有定时去加载图表数据的场景,不能让图表数据初始化重复;③撤销后可以重做,而整个页面是一个整体,需要记录每个图表撤销前的状态,撤销后的状态。key为sessionId+图表id,栈底为此次会话图表的初始状态,栈顶与页面中图表的状态一致。创建的list示例:
在这里插入图片描述
(2)初始化图表栈

​        每次查询图表记录时,都根据sessionId+图表id判断是否已经存在此缓存list,若是不存在,则新建一个list,把查询到的记录作为list的初始值,若是存在则不进行添加。第一次加载完图表信息后的情况:
在这里插入图片描述
(3)图表第一次变更

​        图表有状态变更,则把变化图表对应图表栈的栈顶元素取出来放到undo中,并把最新的记录存放到图表栈的栈顶中。第一次有图表状态变化后的情况:
在这里插入图片描述
(4)图表多次变更

​        页面图表状态一直与缓存图表的栈顶元素保持一致,undo中存放的都是变更之前的状态值。经过多次变更后的图表状态:
在这里插入图片描述
(5)撤销操作

​        当调用撤销接口时,从undo栈中弹出栈顶元素返回给前端,使页面中与弹出元素对应图表的状态变更为上一个状态;根据弹出元素的id找到对应的图表栈,弹出图表栈顶元素,此栈顶元素与调用撤销之前的图表状态一致,把此元素放到redo栈中,当调用完撤销之后,可以调用重做接口把图表的状态还原回去。请求撤销的流程:
在这里插入图片描述
       调用一次撤销后的图表状态:
在这里插入图片描述
(6)重做操作

​        调用重做接口时,从redo栈中取出栈顶元素返回给前端,此处先不弹出元素,因为需要支持连续重做,而前端拿到撤销或者重做返回的图表最新状态后,立马调用updateItem接口更新图表的最新状态,我们记录图表状态变化的入口点也是updateItem接口,所以当有一次变化要保存,需要判断此次变化的状态是否是通过撤销、重做接口获取的,若是通过撤销操作获取的,则不进行undo的入栈;若是通过重做接口获取的,则让redo栈顶出栈,一开始调用redo重做接口不出栈就是为了对比新状态是否通过重做获取的。请求重做的流程:
在这里插入图片描述
​        调用重做接口,更新图表后的状态:
在这里插入图片描述
(7)撤销后重做

​        支持连续撤销,连续重做,连续撤销两次后的图表状态:
在这里插入图片描述
​        两次撤销后,进行一次重做后的图表状态:
在这里插入图片描述
(8)撤销后其他变更

​        当撤销后,没有调用重做(说明撤销前的状态是无用的),中间穿插着其他操作,则清空redo重做栈,看下当前图表状态:
在这里插入图片描述
(9)存储变更逻辑

​        当调用撤销后,前端拿到图表的上一个状态,然后调用updateItem保存图表的最新状态,此时的状态值不再往redis栈中入栈,判断是否通过撤销操作提交的依据为:找到变更图表对应图表栈,拿出栈顶元素,若是栈顶元素等于这次提交过来的新状态,则判断是通过撤销后提交过来的记录,此种情况不进行入栈。若不是通过撤销操作提交过来,则判断是否通过调用重做接口提交过来的,判断的依据为:拿出redo栈的栈顶元素,判断新提交过来的元素是否等于栈顶元素,等于则说明是通过调用重做后提交过来的记录。看下调用updateItem接口操作redis栈的流程图:
在这里插入图片描述
(10)删除图表

​        在编辑过程中,当某个图表被删除了,则需要删除此图表对应的撤销记录,删除某个图表的示意图:
在这里插入图片描述

4.统一过期时间设置

​        undo、redo和图表栈都是基于一个会话进行存储,它们不是同时创建的,编辑过程中也会加入新图表,但是一个会话里面的栈需要有统一的过期时间,出于业务的考虑,一个页面的编辑时间基本不会跨越一天,所以给栈的过期时间设置为当前时间到第二天凌晨12点的秒数+1天的秒数,这样这次会话的失效时间为明天晚上12点。获取统一过期时间的代码实现:

    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.初始图表栈

​        第一次查询图表数据的时候,结果值需要作为图表栈的初始数据,定时器再去加载的时候,不重复添加到栈中,只用判断某个图表在这次会话中是否已经添加redis记录。代码实现:

//图表若是没有初始化过,则进行初始化
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) {
    
    
                //判断需要保存记录是否已经有对应的栈,有的话,则不重复添加
                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.记录图表变化

​        图表有状态变化,调用updateItem接口更新最新状态,在此接口上添加redis的处理。判断需要保存的记录是否已经有对应的栈,若是没有,则新建一个list,把当前状态作为list的初始值,并清空redo栈。若是存在变更图表对应的栈,拿出图表栈的栈顶元素,栈顶元素若是等于这次变更值,则说明此次变更是由撤销操作触发的,不进行入栈;若是不相等,说明是新的状态,则需要入栈,在入栈之前,先判断undo栈的元素是否等于20,等于20则让undo栈底出栈,出栈元素对应的图表栈栈底也出栈,把变化的item对应图表栈顶元素添加到undo栈中,再把变化的item添加到对应图表的栈顶中,这样图表栈顶元素和页面图表的状态保持一致,undo中存的是图表的上一个状态值。判断redo栈是否存在,存在redo栈,则判断redo栈的栈顶元素是否等于变化的item,等于则说明是通过调用重做提交过来的,此时让redo栈顶出栈,不清空redo栈,这样可以支持连续调用重做;若是不等于redo栈顶元素,则说明此次提交过来的数据不是通过重做实现的,穿插着其他操作,需要清空redo栈。代码实现:

    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) {
    
    
                //判断需要保存记录是否已经有对应的栈
                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注解实现,若是实体类有继承关系,则使用@EqualsAndHashCode(callSuper = true)注解标识连带父类字段一块参与hash计算
                    if(!itemUndo.equals(peekItem)){
    
    //相等说明是通过撤销操作再次提交的,不进行入栈到撤销栈中
                        //把图表栈顶元素放到撤销栈中,并判断数量是否等于20
                        addItemToUndoList(peekObject,sessionId);
                        //变化的item放到图表对应栈顶中,经过上面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方法已经出栈;不相等说明上一步不是重做,清空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();
            }
        }
    }

//把图表栈顶元素放到撤销栈中,并判断数量是否等于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);
            //判断数量是否大于等于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注解实现,若是实体类有继承关系,则使用@EqualsAndHashCode(callSuper = true)注解标识连带父类字段一块参与hash计算。

7.撤销操作

​        从undo栈中弹出栈顶元素返回给前端,根据此出栈元素获取到它对应的图表栈顶元素,图表栈顶元素状态与撤销之前页面图表的状态一致,把图表栈顶元素出栈放到redo栈中,当调用重做时能让页面的图表状态还原回去。代码实现:

 public JSONObject undo(String json) {
    
    
        if(StringUtils.isBlank(json)){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "参数为空,请确保参数的准确性");
        }
        JSONObject jsonObject= JSONObject.parseObject(json);
        if(!jsonObject.containsKey("sessionId")){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "缺少参数sessionId,请确保参数的准确性");
        }
        String sessionId = jsonObject.getString("sessionId");
        if(StringUtils.isEmpty(sessionId)){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "参数sessionId为空,请确保参数的准确性");
        }
        //添加分布式锁
        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);
                //转成对象
                JSONObject undoJsonObject = JSONObject.parseObject(undoObject);
                String redoObject = null;
                //根据栈顶元素获取到它对应的图表栈
                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)) {
    
    //已经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栈中获取栈顶元素返回给前端,不出栈,因为数据有变动保存时,需要比对是否由重做触发的,若是重做触发的则弹出redo栈顶元素,不是重做触发的则清空redo栈,这样可以支持连续调用重做。代码实现:

   public JSONObject redo(String json) {
    
    
        if(StringUtils.isBlank(json)){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "参数为空,请确保参数的准确性");
        }
        JSONObject jsonObject= JSONObject.parseObject(json);
        if(!jsonObject.containsKey("sessionId")){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "缺少参数sessionId,请确保参数的准确性");
        }
        String sessionId = jsonObject.getString("sessionId");
        if(StringUtils.isEmpty(sessionId)){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "参数sessionId为空,请确保参数的准确性");
        }

        //添加分布式锁
        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;
                }
                //拿出栈顶元素,不出栈,返回给前端,当调用更新数据变动时,判断新提交过来的数据是否等于重做栈的栈顶,等于则说明是通过重做提交过来的,
                //此时不清空重做栈,因为需要支持多步重做;若是不等于重做栈顶元素,则清空重做栈,说明上一步不是重做
                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、图表栈都需要删除,否则会撤销到一个不存在的记录。代码实现:

    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) {
    
    
                //根据删除图表的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();
                            }
                        }
                        //删除撤销栈数据
                        redisTemplate.delete(redoKey);
                        //经过删除后,撤销栈里面还有数据,则重新添加到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();
                            }
           
                        }
                        //删除重做栈数据
                        redisTemplate.delete(undoKey);
                        //经过删除后,重做栈里面还有数据,则重新添加到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();
            }
        }
    }

猜你喜欢

转载自blog.csdn.net/ZHANGLIZENG/article/details/130772144