高并发环境下,如何正确使用分布式锁

本期视频地址,(高并发环境下,如何正确使用分布式锁?_哔哩哔哩_bilibili) ,欢迎点赞关注,一键三连。

首先看一段代码。具体如下:

scss 复制代码
    public  void doSomething(User user){
        User existUser=getUser(user.getUserId());
        if(existUser==null){
            insert(user);
            return;
        }
        updateByUserId(user);
    }

上面的代码很简单,查询db中有没有对应的user数据,如果有的话,执行更新操作,如果没有则插入。

我们知道,上面的代码是线程不安全的,在多线程的环境中,就会出现问题。为了能够保证数据的正确性,在单机环境下,我们可以使用synchronized的方法,来保证线程安全,具体修改:

scss 复制代码
    public synchronized void doSomething(User user){
        User existUser=getUser(user.getUserId());
        if(existUser==null){
            insert(user);
            return;
        }
        updateByUserId(user);
    }

在单机器的环境下,能够解决线程安全的问题,那在分布式环境下呢? 这个时候需要用到分布式锁.

分布式锁需要借助其他组件来实现,常用的有rediszookeeper(他们的区别是 redis是基于AP架构,性能更好,zk基于cp架构,一致性更好)。

下面我们就用redis的实现,来说明下问题,分布式锁具体的实现方法如(使用redis中的setnx方法)。

scss 复制代码
    public  void doSomething(User user){
        try {
                String userId=user.getUserId();
                boolean lock=RedisUtils.setnx("xxxx"+userId,userId,1000);//锁定1s
                if(!lock){//说明当前userId已经被锁定
                    return;
                }
                User existUser=getUser(userId);
                if(existUser==null){
                    insert(user);
                    return;
                }
                updateByUserId(user);
        }
        catch(Exception ex){

        }
        finally{
            RedisUtils.delete("xxxx"+userId);
        }
    }

上面的代码好像没有什么问题了,但也存在很大的隐患。

我们分析下,假设第一个请求过来,执行锁定成功,程序开始运行。第二个请求过来,这个时候线程2 会加锁失败,会进入finally,然后是否放锁。第一个请求的锁就被第二个线程释放了,第三次的请求就会造成线程不安全问题。具体如下图:

怎么再去优化呢?问题主要是出现在第一次请求误删锁的问题,所以我们在移除锁的时候要判断能否移除。

思路:我们在锁定的时候,value使用当前的时间戳,删除时判断是否过期如果不过期就不要删除,具体代码如下:

scss 复制代码
  public  void doSomething(User user){
        String  lockTime =LocalDateTime.now().plusSeconds(1).toString();
        try {
            String userId=user.getUserId();
            boolean lock=RedisUtils.setnx("xxxx"+userId, lockTime,1000);//锁定1s
            if(!lock){//说明当前userId已经被锁定
                return;
            }
            User user=getUser(userId);
            if(existUser==null){
                insert(user);
                return;
            }
            updateByUserId(user);
        }
        catch(Exception ex){

        }
        finally{
            String exsitLocaltime=	RedisUtils.get("xxxx"+userId);
            if(StringUtils.isEmpty(esxitLocaltime)){
                return ;
            }
            if(lockTime.equals(exsitLocaltime)||exsitLocaltime.compare(LocalDateTime.now())<0){
                //同一个线程锁 或者锁 已经过期,可以删除key
                RedisUtils.delete("xxxx"+userId);
            }
        }
    }
相关推荐
用户402692448190821 分钟前
CRMEB Pro 新增后台接口全链路:路由、权限、验证器、返回格式一次讲清
前端·后端
泉城老铁42 分钟前
springboot+vue+ ffmpeg 实现视频的拉流播放
前端
PedroQue991 小时前
uni-router v1.8.0新增冷启动守卫补执行
前端·uni-app
leeyi1 小时前
Checkpoint 机制:Agent 怎么在断电后接着跑
redis·aigc·agent
xiaok1 小时前
部署之后,本地浏览器还在读取旧缓存导致页面一直显示loading中
前端
用户059540174461 小时前
Redis缓存一致性踩坑实录:线上故障排查6小时,我用pytest+内存快照把它永久关进了笼子
前端·css
星栈1 小时前
我用 Rust + Dioxus 做了个全栈跨平台笔记应用:第一版先把列表和详情跑通
前端·rust·前端框架
用户1733598075372 小时前
Vue 3 SPA 首屏优化:从 3s 到 1.2s 的 5 个实践
前端·vue.js
咖啡无伴侣2 小时前
基础骨架:30 分钟搭好 pnpm workspace,完成双项目 Monorepo 迁入
前端
谷无姜2 小时前
Webpack5 进阶思考:那些官方文档没讲清楚的事
前端·webpack