zoukankan      html  css  js  c++  java
  • 用redis解决多用户同时编辑同一条数据问题

    1,场景再现

    场景:总公司可以给分公司下发今年的规划任务(可能只是写了个规划大纲),分公司收到后,进行详细的规划补充,然后提交。

    比如规划表:

    CREATE TABLE `sys_plan` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
      `branch_offince_id` int(11) DEFAULT NULL COMMENT '分公司id',
      `head_office_plan` varchar(255) DEFAULT NULL COMMENT '总公司规划',
      `branch_office_plan` varchar(255) DEFAULT NULL COMMENT '分公司规划',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      `update_time` datetime DEFAULT NULL COMMENT '修改时间',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    

    为了简化业务场景,这里用两个字段:总公司规划、分公司规划模拟。

    比如总公司给分公司A新建的规划,填写在总公司规划字段(head_office_plan),分公司收到消息后进行补充,填写在分公司规划(branch_office_plan)字段。

    可能出现的问题的场景:

    1,总公司用户A,给某分公司B新建了一条规划: 1,销售额1000万;2,生产产品2万件

    此时数据库数据是这样的:

    2,分公司收到消息提醒,登录了系统,查看到总公司派发的任务,页面是这样的:

    然后陷入沉思,思考该怎么填写自己的规划.

    3,此时总公司想再补充一条规划,就登录系统,打开页面,编辑head_office_plan字段:3,员工规模扩充到100人,然后提交了。此时页面是这样的。

    数据库是这样的:

    4,分公司想好了怎么填写规划,此时总公司补充的规划,开始填写:1,提高生产效率,2,...

    由于分公司在总公司提交补充规划3之前就打开了页面,所以规划3这里是不显示的。

    然后,问题就出现了,分公司把总公司的规划 3,员工规模扩充到100人 这条规划给覆盖成空的了。数据库中现在是这样的:

    PS:

    其他的如政务系统,用户体系有国家级别、地方级别,像这种用户体系有上下级关系的管理系统,上下级更可能操作同一条数据,更可能出现这种情况,其他对于C端用户的系统,我们编辑的一般都是编辑自己的资源,不会出现这种场景。

    2,要达到的目标

    如果某条数据正在被编辑,另一个人也要编辑该数据,就给出友好提示“某某某正在编辑该数据,请稍后重试”,或者是直接就不能查看。

    3,解决方案

    网上的方案:

    方案1

    在操作的表里添加一个version字段数值类型的默认0,只要对数据进行了操作就对version加1,每一次页面操作(删除、修改)都先判断version是否和打开时的version值一样,如果不一样请先刷新,在进行操作

    方案2:

    在数据表里添加一个UUID字段,其值为32位的随机数。
    1.记录新建时,在数据提交后台,插入DB之前,生成UUID,保存之。
    2.记录编辑时,在编辑页面将UUID隐藏,提交时Check该隐藏值是否与DB一致。
    不一致则返回前台画面,报对应的Message;
    一致则提交后台,生成新UUID,与业务数据一起保存到表中

    这两种方案弊端,只能是让第二个想编辑的人刷新页面,重新填写。

    牛总公司的方案:

    数据库加字段,比如加一列,is_edit,当有人编辑的时候,设置is_edit=1,编辑完成后设置is_edit=0,其他人再查询该条数据,查看is_edit是否=1,如果是就给出提示;但是,如果第一个人打开页面进行编辑,设置了is_edit=1,然后他把浏览器关了,is_edit就=1了,此时谁也编辑不了了,所以这种方案不可取。不知道他们怎么处理这种关闭浏览器的。

    终极redis方案

    所以,我们讨论的方案是,用redis做。采用类似用redis做分布式锁的思路,来解决并发编辑问题。

    用redis的SETNX 命令: 设置成功,返回 1 , 设置失败,返回 0 。

    原理:

    以 lock_plan_{planId} 为redis的key,userId为value,某个用户在获取plan的时候,先用 lock_plan_{planId}往redis设置值,如果返回false,说明这个资源已经有人加了锁了,返回失败。

    定义一个公共资源锁的服务类:

    提供3个方法:

    1,获得锁:当获取某条数据的同时,先去获得锁(锁设定一个有效期,这个有效期根据业务定,页面内容多就多设置一些,内容少就设置短一点,设置有效期保证长时间不操作,不会死锁),如果获取锁成功,就查询那条数据,否则返回提示。

    2,释放锁:当成功获取了某条数据时,进行编辑后,update操作之后,释放锁。让等待的人可以正常获取锁。

    3,延续锁的时长:当用户操作某条数据持续时间较长,前端设置一个心跳,定时调用此接口延续锁的有效期,类似与redission的自动续期锁时长。这个心跳时间间隔,根据业务定,小于锁的有效期,比如设置为1/3 锁的时长,锁的延期间的时长,自己定,比如1/3有效期(redission好似也是1/3有效期)。

    /**
     * 公共资源锁服务
     * create by lihaoyang on 2020/8/17
     */
    public interface CommonLockResourceService {
    
    
        /**
         * 获得锁
         * @param resourceKeyPrefix 锁的redis前缀
         * @param resourceId 资源id
         * @param userId 用户id
         * @return
         */
        boolean getLock(String resourceKeyPrefix,int resourceId,int userId);
    
    
        //释放锁
        boolean unLock(String resourceKeyPrefix,int resourceId,int userId);
    
        //锁延期
        boolean resetLock(String resourceKeyPrefix,int resourceId, int userId);
    }
    
    

    实现类: 主要要确保锁的可重入性,同一个用户多次加锁,要获得同一把锁。

    @Service
    @Transactional
    public class CommonLockResourceServiceImpl implements CommonLockResourceService {
    
    
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public boolean getLock(String resourceKeyPrefix, int resourceId, int userId) {
    
            String lock = resourceKeyPrefix + resourceId;
    
            //如果该userId已经有该项目的锁,锁续期
            if(StringUtils.equals(""+userId,stringRedisTemplate.opsForValue().get(lock.intern()))){
                //锁的可重入
                long ttl = stringRedisTemplate.getExpire(lock);
                //续期时间,自己定
                stringRedisTemplate.expire(lock.intern(),ttl+60L, TimeUnit.SECONDS);
                return true;
            }
            //枷锁
            boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(),userId+"",60L,TimeUnit.SECONDS);
            return isLock;
    
        }
    
        @Override
        public boolean unLock(String resourceKeyPrefix, int resourceId, int userId) {
            String lock = resourceKeyPrefix + resourceId;
            if((userId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
                stringRedisTemplate.delete(lock.intern());
                return true;
            }
            return false;
        }
    
        @Override
        public boolean resetLock(String resourceKeyPrefix, int resourceId, int userId) {
            String lock = resourceKeyPrefix + resourceId;
            //如果该userId已经有该项目的锁,锁续期
            if(StringUtils.equals(""+userId,stringRedisTemplate.opsForValue().get(lock.intern()))){
                long ttl = stringRedisTemplate.getExpire(lock);
                stringRedisTemplate.expire(lock.intern(),ttl+60L,TimeUnit.SECONDS);
                return true;
            }
            return false;
        }
    }
    

    Controller:

    @RestController
    @RequestMapping("/sysPlan")
    public class SysPlanController {
    
        static final String lockKeyPrefix = "lock_plan_";
    
        @Autowired
        private SysPlanService planService;
    
        @Autowired
        private CommonLockResourceService commonLockResourceService;
    
    
        //~============= redis锁 ================
        @GetMapping("/getByIdLock")
        public Result getByIdLock(@RequestParam int planId, @RequestParam int userId){
            //TODO:userId应该从session获取而不是传过来
            boolean isLock = commonLockResourceService.getLock(lockKeyPrefix,planId,userId);
            if(isLock){
                SysPlan plan = planService.getById(planId);
                return Result.ok(plan);
            }
            //还可以获取到谁在编辑,如果需要的话
            return Result.error("当前规划正在编辑中,请稍后重试");
        }
    
    
        @GetMapping("/update")
        public Result update(@RequestParam int planId,@RequestParam int userId){
            //这里应该放在service层
            //update By Id
            //planService.updateById();
            boolean isRelease = commonLockResourceService.unLock(lockKeyPrefix,planId,userId);
            return isRelease?Result.ok():Result.error("释放锁失败");
        }
    
        @GetMapping("/resetLock")
        public Result resetLock(@RequestParam int planId,@RequestParam int userId){
    
            boolean success = commonLockResourceService.resetLock(lockKeyPrefix,planId,userId);
            return success?Result.ok():Result.error("释放锁失败");
        }
    
    
    }
    
    
    

    4,实验

    数据库数据:

    1,用户一(userId=101),前端通过plan_id查询某条规划:

    localhost:8888/sysPlan/getByIdLock?planId=1&userId=101

    返回成功:

    {
        "message": "成功",
        "code": 200,
        "result": {
            "id": 1,
            "branchOffinceId": 1,
            "headOfficePlan": "xxasdaaaaaaa",
            "branchOfficePlan": "1,提高生产效率",
            "createTime": null,
            "updateTime": "2020-08-17T08:27:36.000+0000"
        },
        "timestamp": 1597658245474
    }
    

    2,用户二(userId=102),尝试获取该资源。(这里直接传入不同userId代表不同用户)

    localhost:8888/sysPlan/getByIdLock?planId=1&userId=102

    返回:

    {
        "message": "当前项目正在编辑中,请稍后重试",
        "code": 500,
        "result": null,
        "timestamp": 1597659012412
    }
    

    3,如果用户一(userId=101)编辑这条数据持续的时间较长(可能是一个文本域,输入很多文本),前端做一个定时器,定时调用延续锁时长接口,在操作期内,使自己一直拿到当前的锁,防止操作没完成,锁被释放了,别人拿到了锁。

    localhost:8888/sysPlan/resetLock?planId=1&userId=101

    返回:

    {
        "message": "成功",
        "code": 200,
        "result": null,
        "timestamp": 1597659371463
    }
    

    4,用户一(userId=101)编辑完成,提交编辑,主动释放锁。

    localhost:8888/sysPlan/update?planId=1&userId=101,此时redis中的锁被清除。

    5,用户二(userId=102)再次尝试获得数据

    localhost:8888/sysPlan/getByIdLock?planId=1&userId=102

    返回:

    {
        "message": "成功",
        "code": 200,
        "result": {
            "id": 1,
            "branchOffinceId": 1,
            "headOfficePlan": "xxasdaaaaaaa",
            "branchOfficePlan": "1,提高生产效率",
            "createTime": null,
            "updateTime": "2020-08-17T08:27:36.000+0000"
        },
        "timestamp": 1597659558272
    }
    

    5,总结

    用数据库字段方案,有点“重”,需要不断地维护这个字段,而且还有限制,用redis方案,友好又能解决需求,比较轻量级。

    如有问题,欢迎交流

  • 相关阅读:
    http://www.reg007.com/
    快速入门:十分钟学会Python(转)
    Python入门教程 超详细1小时学会Python(转)
    值得关注的10个python语言博客(转)
    【.NET特供-第三季】ASP.NET MVC系列:MVC与三层图形对照
    LeetCode——Spiral Matrix
    HTML中Select的使用具体解释
    为什么没有好用的Android游戏引擎?
    Map.EntrySet的使用方法
    jquery 仅仅读
  • 原文地址:https://www.cnblogs.com/lihaoyang/p/13521585.html
Copyright © 2011-2022 走看看