Java+Redis实现撤销重做功能

news2024/9/30 2:56:43

文章目录

      • 1.背景
      • 2.需求分析
      • 3.实现逻辑分析
      • 4.统一过期时间设置
      • 5.初始图表栈
      • 6.记录图表变化
      • 7.撤销操作
      • 8.重做操作
      • 9.删除图表处理

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进行加锁处理,防止对图表的操作有并发请求导致处理撤销逻辑混乱。具体引入过程可以参考之前的博客:

(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();
            }
        }
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/545323.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

马斯克宣布将卸任推特 CEO:已找到接班人,自己要去当 CTO

作者 | 李冬梅 来源 | AI前线 ID | ai-front 当地时间 5 月 11 日&#xff0c;马斯克在特推上发文宣布&#xff0c;他将在 6 个星期后正式卸任推特 CEO 一职&#xff0c;并且已经找到了一位女性接班人&#xff0c;自己将转到技术岗位。 马斯克在推文中写道&#xff1a;“很…

Yolov5轻量级:EfficientViT, better speed and accuracy

EfficientViT: Memory Efficient Vision Transformer with Cascaded Group Attention 论文:https://arxiv.org/abs/2305.07027 代码:Cream/EfficientViT at main microsoft/Cream GitHub 🏆🏆🏆🏆🏆🏆Yolo轻量化模型🏆🏆🏆🏆🏆🏆 近些年对视觉Tra…

Java【网络原理3】TCP 协议的确认应答、超时重传机制

文章目录 前言一、确认应答1, 什么是确认应答2, 序列号和确认应答号 二、超时重传1, 什么是超时重传 总结 前言 各位读者好, 我是小陈, 这是我的个人主页, 希望我的专栏能够帮助到你: &#x1f4d5; JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系…

【redis】redis为什么这么快?高性能设计之epoll和I/O多路复用深度解析

系列文章目录 文章目录 系列文章目录前言一、before 学习I/O多路复用之前多路复用 需要解决的问题 一对一性能差结论 需要让一个进程同时处理多个连接 二、I/O多路复用模型1、是什么&#xff1f;一句话 2、redis单线程如何处理那么多并发客户端连接&#xff0c;为什么单线程&am…

Edge插件之WeTab,画面优美,可以免费使用chatgpt,很难不爱

目录 一、普通的edge新标签页 二、安装WeTab插件 1.WeTab插件的安装非常简单&#xff0c;只需在百度搜索wetab&#xff0c;进入官网&#xff1a; 2.进入官网&#xff0c;点击edge图标&#xff0c;进入插件下载页面&#xff1a; 3.这里由于我是已经安装成功&#xff0c;显示…

无法上网问题解决过程

下班&#xff0c;收到一同事在群里说&#xff0c;环境里有冒充网关的mac的&#xff0c;现在无法上网&#xff0c;让arp -s ip mac地址&#xff0c;先绑定正确的网关mac地址&#xff0c;先临时使用&#xff0c;等第二天上班再查找原因。 不能上网原因&#xff1a; 1、环境…

Cloud Studio 内核升级之触手可及

前言 Cloud Studio是基于浏览器的集成式开发环境&#xff08;IDE&#xff09;&#xff0c;为开发者提供了一个永不间断的云端工作站。用户在使用 Cloud Studio 时无需安装&#xff0c;随时随地打开浏览器就能使用。云端开发体验与本地几乎一样&#xff0c;上手门槛更低&#x…

IMU和GPS融合定位(ESKF)

说明 1.本文理论部分参考文章https://zhuanlan.zhihu.com/p/152662055和https://blog.csdn.net/brightming/article/details/118057262 ROS下的实践参考https://blog.csdn.net/qinqinxiansheng/article/details/107108475和https://zhuanlan.zhihu.com/p/163038275 理论 坐标…

三年测试,月薪才12k,想跳槽又不太敢.....

在我们的身边&#xff0c;存在一个普遍现象&#xff1a;很多人从事软件测试岗&#xff0c;不计其数&#xff0c;经历的心酸难与外人道也。可是技术确难以提升、止步不前&#xff0c;薪资也只能看着别人水涨船高&#xff0c;自己却没有什么起色。 虽然在公司里属于不可缺少的一…

java学习笔记

java学习笔记 直接写出来的人可以理解的数据&#xff0c;在java中叫做字面量。 字面量分类&#xff1a; 数据类型分类&#xff1a; 不同的数据类型分配了不同的内存空间&#xff0c;不同的内存空间&#xff0c;所存储的数据大小是不一样的。 数据类型内存占用和取值范围…

JavaSE入门必读篇——详解数组

文章目录 数组的概念1.什么是数组呢&#xff1f;2.如何创建数组3.遍历数组4.扩展&#xff1a;快速批量初始化 数组原理内存图1. 内存概述2.Java虚拟机的内存划分3.其存储方式图4.认识null 二维数组二维数组初始化遍历二维数组 数组常见异常1. 数组越界异常2. 数组空指针异常 Ja…

Windows下编译安装gRPC

gRPC是Google基于HTTP/2协议开发的一套开源、跨平台的高性能RPC框架&#xff0c;可用于连接微服务架构内的各种服务&#xff0c;亦可以连接客户端与后端服务。 Ref. from gRPC gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can…

代码随想录算法训练营第二十四天|理论基础 77. 组合

文章目录 理论基础77.组合思路代码总结 理论基础 回溯算法&#xff1a;一种暴力搜索方式 回溯是递归的副产品&#xff0c;只要有递归就会有回溯。 回溯法&#xff0c;一般可以解决如下几种问题&#xff1a; 组合问题&#xff1a;N个数里面按一定规则找出k个数的集合切割问题…

数据安全技术工作部成员动态 | 鸿翼联合天空卫士打造“基于内容的敏感信息处理”解决方案

据2022年统计数据表明&#xff0c;因IT故障、人为错误、供应链攻击、破坏性攻击和勒索软件攻击等原因导致的数据泄露事件频繁发生&#xff0c;信息安全问题比以往任何一个时代都更为突出。信息泄漏造成的危害体现在多方面&#xff0c;不法分子通过各种途径收集的公司的某些重要…

大数据法律监督模型优势特色及应用场景

大数据法律监督平台是基于监督数据整合管理平台、监督模型构建平台、内置模型库以及法律监督线索管理平台打造的一套服务于检察机关法律监督工作的专业化系统。通过数据采集、融合、挖掘、建模、展现等一系列能力&#xff0c;辅助检察官从纷繁复杂的数据中&#xff0c;开展多维…

基于可靠姿态图初始化和历史重加权的鲁棒多视图点云配准

论文作者 | Haiping Wang, Haiping Wang,Haiping Wang,etal 论文来源 | CVPR2023 文章解读 | William 1 摘要 之前的多视图配准方法依赖于穷举成对配准构造密集连接的位姿图&#xff0c;并在位姿图上应用迭代重加权最小二乘(IRLS)计算扫描位姿。但是&#xff0c;构造一个密集…

产品经理必读丨如何找准产品定位?

我们都知道&#xff0c;当一款新产品开始立项之前&#xff0c;势必需要经过谨慎的市场调研才能整合资源启动新的项目&#xff0c;但除此之外&#xff0c;作为产品经理还需要做好一件关键的事情——找准产品在市场中的定位。 什么是产品定位 百度百科对产品定位的解释是非常准确…

重磅新闻!!! ChatGPT手机 app上线苹果app store

就在刚才&#xff0c;打开Tor访问openAI官网准备看看有什么新闻&#xff0c;结果吓我一跳啊&#xff01; &#x1f447;&#x1f447;&#x1f447; ChatGPT app上线APP store了&#xff01; 我马上拿出手机&#xff0c;一搜索&#xff0c;哎~出现了&#xff1a; chatgpt ios …

想端起“铁饭碗”,你最好先学会这个!

正文共 886 字&#xff0c;阅读大约需要 3 分钟 公务员必备技巧&#xff0c;您将在3分钟后获得以下超能力&#xff1a; 快速生成推荐材料 Beezy评级 &#xff1a;B级 *经过简单的寻找&#xff0c; 大部分人能立刻掌握。主要节省时间。 ●图片由Lexica 生成&#xff0c;输入&a…

Linux---cd命令、pwd命令、mkdir命令

1. cd命令 当Linux终端&#xff08;命令行&#xff09;打开的时候&#xff0c;会默认以用户的HOME目录作为当前的工作目录 我们可以通过cd命令&#xff0c;更改当前所在的工作目录。 cd命令来自英文&#xff1a;Change Directory 语法&#xff1a;cd [ linux路径] cd命令…