百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

redis分布式锁各种坑,一文全部搞懂

mhr18 2024-10-22 12:36 23 浏览 0 评论

目录

  • 分布式锁概念
  • 分布式锁4种雷区
  • 分布式锁特性
  • 错误案例集
  • 正确实现及实现原理
  • 锁超时并发执行 解决方案
  • 集群容错 解决方案

学习目标

  • 分布式锁概念
  • 分布式锁4种雷区
  • 分布式锁特性

分布式锁概念

在分布式系统中,同一时间只允许一个线程/进程对共享资源进行操作。例如:秒杀、积分扣减、抢红包、定时任务执行等等。

分布式锁4种雷区

  • 死锁:加锁成功后,不知什么原因导致服务器出现宕机,未能成功释放,出现死锁。正确做法:设置超时时间
  • 锁误删:只有持有当前锁的线程,才能删除锁,即:解铃还需系铃人。正确做法:唯一id标识当前线程
  • 锁超时并发执行:加锁成功后,由于代码执行非常耗时、下游服务执行慢、调用链太长或GC耗时等原因导致锁超时,其他线程获得锁出现并发执行,后面详细分析
  • 集群容错:成功在master加锁,未能及时同步到slave节点,此时出现脑裂存在多个master节点,其他节点也可以加锁成功,后面详细分析

分布式锁特性

  • 互斥性:当一个线程/进程加锁成功后,其他线程/进程无法加锁,具有排他性。
  • 锁失效机制:加锁成功后,服务器宕机导致锁未能释放,服务恢复后一直获取不到锁。应设置超时时间,防止出现类似死锁情况。
  • 阻塞锁(可选):当前资源已被加锁,其他线程/进程来加锁是否阻塞等待,还是立即返回。
  • 可重入性(可选):当前锁的持有者是否能再次进入。
  • 公平性(可选):加锁顺序和请求加锁顺序是否一致,还是随机抢锁。

基于redis的分布式锁

错误案例集

加锁-错误案例1

public void lock_error1(String lockKey, String requestId, int expireTime) {
    RedisCache cache = redisFactory.getRedisCacheInstance(name);
    Long result = cache.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
    cache.expireSeconds(lockKey, expireTime);
    }
}

setnx和expire两个操作非原子性,expire操作之前程序崩溃,会发生死锁。

加锁-错误案例2(严格意义属于错误案例)

public String lock_error2(String lockKey, int expireTime) {
        RedisCache cache = redisFactory.getRedisCacheInstance(name);
        long lockExpireTime = System.currentTimeMillis() + expireTime * 1000 + 1;//锁超时时间
        String stringOfLockExpireTime = String.valueOf(lockExpireTime);

        if (cache.setnx(lockKey, stringOfLockExpireTime) == 1) { // 获取到锁
            //成功获取到锁, 设置相关标识
            return stringOfLockExpireTime;
        }

        String value = cache.get(lockKey);
        if (value != null && isTimeExpired(value)) { // lock is expired
            // 假设多个线程(非单jvm)同时走到这里
            String oldValue = cache.getSet(lockKey, stringOfLockExpireTime); // getset is atomic
            // 但是走到这里时每个线程拿到的oldValue肯定不可能一样(因为getset是原子性的)
            // 假如拿到的oldValue依然是expired的,那么就说明拿到锁了
            if (oldValue != null && isTimeExpired(oldValue)) {
                //成功获取到锁, 设置相关标识
                return stringOfLockExpireTime;
            }
        }
        return null;
}

这种加锁方式解决的问题是程序崩溃/超时,未能释放导致死锁,使用该方案的前提是:各个服务器时间必须同步,在cache.getSet(lockKey, stringOfLockExpireTime)时会出现时间覆盖问题,只要各个服务器时间同步,时间覆盖也不影响加锁效果,不应该属于错误案例,因为出现时间覆盖了,严格来说就是错误的,主要看怎么定义了。我们原来一直用的是这种方式

解锁-错误案例1

public void unLock_error1(String lockKey) {
    RedisCache cache = redisFactory.getRedisCacheInstance(name);
    cache.del(lockKey);
}

不分是不是自己持有的锁,上来就删除,导致锁误删除

解锁-错误案例2

public void unLock_error2(String lockKey, String requestId) {
    RedisCache cache = redisFactory.getRedisCacheInstance(name);
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(cache.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        cache.del(lockKey);
    }
}

如代码注释,问题在于如果调用cache.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行cache.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了,这也是锁误删除的例子。

正确实现

实现原理

加锁:使用set扩展命令,key:锁标识,value:持有当前锁线程标识,PX:超时时间(毫秒)。

解锁:只有当前锁的持有者才可以执行删除操作,通过lua脚本保证了get和del命令执行的原子性操作。

# 加锁命令
set key value NX PX milliseconds

# 解锁命令
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

实现代码

// 加锁
public boolean lockByLua(String lockKey, String requestId, int expireTime) {

    RedisCache cache = redisFactory.getRedisCacheInstance(name);
    String result = cache.set(lockKey,requestId, "NX", "PX", expireTime);
    if ("OK".equals(result)) {
        return true;
    }

    return false;
}
// 解锁
public boolean unLockByLua(String lockKey, String requestId) {
    Long success = 1L;
    RedisCache cache = redisFactory.getRedisCacheInstance(name);
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = cache.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

    if (success.equals(result)) {
        return true;
    }
    return false;
}

不足之处

  • 未实现可阻塞,可重入性,公平性。
  • 未能解决锁超时并发执行集群容错

锁超时并发执行 解决方案

问题现象

A成功获取锁后并设置超时时间5秒,但是A业务执行超过了5秒,A持有锁过期自动释放,B获取到锁,A和B并发执行。

A和B并发执行显然是不允许的,一般两种解决方式:

  • 设置足够长的时间来保证业务执行完成。
  • 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。

实现方案

守护线程自动续期代码实现:

@Slf4j
public class ExpireDelayThread implements Runnable {

    /**
     * 锁
     */
    private String lockKey;
    /**
     * 持锁线程标识id
     */
    private String requestId;
    /**
     * 过期时间(单位:毫秒)
     */
    private Integer expireTime;

    private RedisClient redisClient;

    private volatile boolean isRun = true;

    public ExpireDelayThread(String lockKey, String requestId, Integer expireTime, RedisClient redisClient){
        this.lockKey = lockKey;
        this.requestId = requestId;
        this.expireTime = expireTime;
        this.redisClient = redisClient;
    }

    public void stop(){
        isRun = false;
    }

    @Override
    public void run() {

        int waitTime = Math.max(1, expireTime * 2 /3);

        while (isRun) {
            try {
                Thread.sleep(waitTime);
                if (redisClient.exprieDelayByLua(lockKey,requestId)){
                    log.info("lock key:{}, thread requestId:{}, waitTime:{}, exprie delay time:{}",lockKey,requestId,waitTime,expireTime);
                } else {
                    log.info("lock key:{}, thread requestId:{}, waitTime:{}, exprie delay time failed!",lockKey,requestId,waitTime);
                    this.stop();
                }
            } catch (InterruptedException e) {
                log.error("lock key:{}, thread requestId:{}, waitTime:{}, InterruptedException!",lockKey,requestId,waitTime);
            } catch (Exception ex) {
                log.error("lock key:"+ lockKey +", thread requestId:"+ expireTime +", waitTime:"+ waitTime +", error!",ex);
            }
        }
    }
}

集群容错 解决方案

问题现象

  • 主从切换:在sentinel集群部署中,当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端 A 成功加锁,指令还未同步,此时主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端 B 加锁时就会成功。
  • 集群脑裂:集群脑裂指因为网络问题,导致 Redis master 节点跟 slave 节点和 sentinel 集群处于不同的网络分区,因为 sentinel 集群无法感知到 master 的存在,所以将 slave 节点提升为 master 节点,此时存在两个不同的 master 节点。Redis Cluster 集群部署方式同理。

当不同的客户端连接不同的 master 节点时,两个客户端可以同时拥有同一把锁。如下:

实现方案

参考RedLock算法



相关推荐

Java培训机构,你选对了吗?(java培训机构官网)

如今IT行业发展迅速,不仅是大学生,甚至有些在职的员工都想学习java开发,需求量的扩大,薪资必定增长,这也是更多人选择java开发的主要原因。不过对于没有基础的学员来说,java技术不是一两天就能...

产品经理MacBook软件清单-20个实用软件

三年前开始使用MacBookPro,从此再也不想用Windows电脑了,作为生产工具,MacBook可以说是非常胜任。作为产品经理,值得拥有一台MacBook。MacBook是工作平台,要发挥更大作...

RAD Studio(Delphi) 本月隆重推出新的版本12.3

#在头条记录我的2025#自2024年9月,推出Delphi12.2版本后,本月隆重推出新的版本12.3,RADStudio12.3,包含了Delphi12.3和C++builder12.3最...

图解Java垃圾回收机制,写得非常好

什么是自动垃圾回收?自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用...

Centos7 初始化硬盘分区、挂载(针对2T以上)添加磁盘到卷

1、通过命令fdisk-l查看硬盘信息:#fdisk-l,发现硬盘为/dev/sdb大小4T。2、如果此硬盘以前有过分区,则先对磁盘格式化。命令:mkfs.文件系统格式-f/dev/sdb...

半虚拟化如何提高服务器性能(虚拟化 半虚拟化)

半虚拟化是一种重新编译客户机操作系统(OS)将其安装在虚拟机(VM)上的一种虚拟化类型,并在主机操作系统(OS)运行的管理程序上运行。与传统的完全虚拟化相比,半虚拟化可以减少开销,并提高系统性能。虚...

HashMap底层实现原理以及线程安全实现

HashMap底层实现原理数据结构:HashMap的底层实现原理主要依赖于数组+链表+红黑树的结构。1、数组:HashMap最底层是一个数组,称为table,它存放着键值对。2、链...

long和double类型操作的非原子性探究

前言“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。其实这里的某些jvm是指...

数据库DELETE 语句,还保存原有的磁盘空间

MySQL和Oracle的DELETE语句与数据存储MySQL的DELETE操作当你在MySQL中执行DELETE语句时:逻辑删除:数据从表中标记为删除,不再可见于查询结果物理...

线程池—ThreadPoolExecutor详解(线程池实战)

一、ThreadPoolExecutor简介在juc-executors框架概述的章节中,我们已经简要介绍过ThreadPoolExecutor了,通过Executors工厂,用户可以创建自己需要的执...

navicat如何使用orcale(详细步骤)

前言:看过我昨天文章的同鞋都知道最近接手另一个国企项目,数据库用的是orcale。实话实说,也有快三年没用过orcale数据库了。这期间问题不断,因为orcale日渐消沉,网上资料也是真真假假,难辨虚...

你的程序是不是慢吞吞?GraalVM来帮你飞起来性能提升秘籍大公开

各位IT圈内外的朋友们,大家好!我是你们的老朋友,头条上的IT技术博主。不知道你们有没有这样的经历:打开一个软件,半天没反应;点开一个网站,图片刷不出来;或者玩个游戏,卡顿得想砸电脑?是不是特别上火?...

大数据正当时,理解这几个术语很重要

目前,大数据的流行程度远超于我们的想象,无论是在云计算、物联网还是在人工智能领域都离不开大数据的支撑。那么大数据领域里有哪些基本概念或技术术语呢?今天我们就来聊聊那些避不开的大数据技术术语,梳理并...

秒懂列式数据库和行式数据库(列式数据库的特点)

行式数据库(Row-Based)数据按行存储,常见的行式数据库有Mysql,DB2,Oracle,Sql-server等;列数据库(Column-Based)数据存储方式按列存储,常见的列数据库有Hb...

AMD发布ROCm 6.4更新:带来了多项底层改进,但仍不支持RDNA 4

AMD宣布,对ROCm软件栈进行了更新,推出了新的迭代版本ROCm6.4。这一新版本里,AMD带来了多项底层改进,包括更新改进了ROCm的用户空间库和AMDKFD内核驱动程序之间的兼容性,使其更容易...

取消回复欢迎 发表评论: