欢迎光临
我们一直在努力

一篇文章搞懂分布式锁(上)

一.分布式锁的介绍

定义:

分布式锁是满足在分布式系统或集群模式下,实现多进程可见并且互斥的锁机制。

核心特性(基于图示):

  • 多进程可见:不同服务器节点上的进程都能感知到锁的状态。
  • 互斥:同一时刻只允许一个进程持有锁,防止并发冲突。
  • 高可用:锁服务本身需稳定可靠,避免单点故障。
  • 高性能:加锁与解锁操作需高效,低延迟。
  • 安全性:保障锁机制不被恶意篡改或误用。
  • 二.基于Redis实现分布式锁

    <1>原理分析

    在分布式系统中,为了保证多个节点对共享资源的互斥访问,常使用 Redis 实现分布式锁。其核心是实现两个基本方法:获取锁 和 释放锁。

    1、获取锁

    • 互斥性:确保同一时刻只有一个线程能成功获取锁。
    • 非阻塞尝试:尝试一次,成功返回 true,失败返回 false,不等待。

    命令

    SET lock thread1 NX EX 10

    • NX:仅当 key 不存在时才设置 → 保证互斥
    • EX 10:设置过期时间为 10 秒 → 防止死锁

    2、释放锁

    • 手动释放:业务执行完成后主动删除锁。
    • 超时自动释放:通过设置过期时间,即使服务宕机或业务超时,锁也会自动失效,避免死锁。

    命令

    DEL key

    <2>流程图

    <3>代码实现Redis锁—版本1

    获取锁与删除锁代码

    public class SimpleRedisLock implements ILock{
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
    this.name = name;
    this.stringRedisTemplate = stringRedisTemplate;
    }
    private static final String KEY_PREFIX = "lock:";
    @Override
    public boolean tryLock(long timeoutSec) {
    String threadId = String.valueOf(Thread.currentThread().getId());
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(success);
    }

    @Override
    public void unlock() {
    //删除锁
    stringRedisTemplate.delete(KEY_PREFIX + name);
    }
    }

    将锁添加进业务中

    //5.1获取用户id
    Long UserId = UserHolder.getUser().getId();
    //创建锁对象
    SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + UserId, stringRedisTemplate);
    //获取锁
    boolean isLock = simpleRedisLock.tryLock(1200);
    //判断获取锁成功
    if (!isLock) {
    return Result.fail("不允许重复下单");
    }
    //获取代理对象
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId, voucher);

    <3>解决Redis分布锁中的误删问题

    1.产生原因以及解决办法

    误删问题的核心原因是:锁的持有者(线程)与释放者不一致,且缺乏身份校验机制。

    具体产生场景如下:

  • 业务执行超时:线程 A 获取了锁,但业务逻辑执行时间过长,超过了锁设定的过期时间(TTL)。
  • 锁自动失效:Redis 因超时自动删除了该锁,此时锁处于“空闲”状态。
  • 新线程介入:线程 B 趁虚而入,成功获取了新的锁,并开始执行业务。
  • 旧线程“诈尸”:线程 A 终于执行完了(虽然已超时),它 unaware 锁已过期或被别人拿走,直接执行“释放锁”操作。
  • 误删发生:线程 A 把线程 B 正在持有的锁给删掉了。
  • 后果:线程 B 失去保护,此时若有线程 C 进来,就能和线程 B 同时访问临界资源,导致数据竞争和不一致。

    根本症结:释放锁时,没有判断“当前要删的锁是不是我自己加的那把”。

    解决办法

    线程1先成功获取锁并执行业务,但在完成前锁因超时被自动释放;此时它尝试释放锁时发现锁已不属于自己(返回NIL),于是不操作。随后线程2趁机获取到新锁,正常执行业务后校验归属一致,安全释放锁。接着线程3也顺利拿到锁开始执行。

    2.代码实现

    改进Redis的分布式锁

    需求:修改之前的分布式锁实现,满足:

  • 在获取锁时存入线程标示(可以用UUID表示)
  • 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
    • 如果一致则释放锁
    • 如果不一致则不释放锁
  • //获取锁
    @Override
    public boolean tryLock(long timeoutSec) {
    String threadId = ID_PREFIX + String.valueOf(Thread.currentThread().getId());
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(success);
    }
    //删除锁
    @Override
    public void unlock() {
    //获取线程标识
    String threadId = ID_PREFIX + String.valueOf(Thread.currentThread().getId());
    //获取锁中的标识
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    //判断标识是一致
    if (threadId.equals(id)) {
    stringRedisTemplate.delete(KEY_PREFIX + name);
    }
    }

    <4>分布式锁的原子性问题

    1.产生原因

    产生原因

  • 线程A拿到锁,执行业务。
  • 业务执行时间 > 锁超时时间 → Redis自动释放锁。
  • 线程B趁机获取锁,开始执行。
  • 线程A业务结束,尝试删锁 → 此时锁已是线程B的!但A仍会误删!
  • → 结果:线程B的锁被A提前删掉,后续线程C可立即抢锁,导致并发失控!

    2.解决办法—-Redis的Lua脚本

    Lua脚本简介

    Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性。 Lua 是一种编程语言,基本语法可参考:https://www.runoob.com/lua/lua-tutorial.html

    这里重点介绍调用函数语法

    redis.call('命令名称', 'key', '其它参数', …)

    示例一:执行 set name jack

    redis.call('set', 'name', 'jack')

    示例 2:先 set name Rose,再 get name

    redis.call('set', 'name', 'Rose')
    local name = redis.call('get', 'name')
    return name

    写好脚本以后,需要用 Redis 命令来调用脚本,调用脚本的常见命令如下:

    127.0.0.1:6379> help @scripting

    EVAL script numkeys key [key …] arg [arg …]
    summary: Execute a Lua script server side
    since: 2.6.0

    例如,我们要执行 redis.call('set', 'name', 'jack') 这个脚本,语法如下:

    # 调用脚本
    EVAL "return redis.call('set', 'name', 'jack')" 0

    • 脚本内容:"return redis.call('set', 'name', 'jack')"
    • Key 参数个数:0(表示脚本中没有使用 KEYS 数组类型的参数)

    如果脚本中的 key、value 不想写死,可以作为参数传递。

    • key 类型参数会放入 KEYS 数组。
    • 其它参数会放入 ARGV 数组。
    • 在脚本中可以从 KEYS 和 ARGV 数组获取这些参数。

    示例代码

    # 调用脚本
    EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose

    3.代码实现

    释放锁的业务流程是这样的:

  • 获取锁中的线程标示
  • 判断是否与指定的标示(当前线程标示)一致
  • 如果一致则释放锁(删除)
  • 如果不一致则什么都不做
  • 用 Lua 脚本表示如下:

    — 这里的 KEYS[1] 就是锁的 key,这里的 ARGV[1] 就是当前线程标示
    — 获取锁中的标示,判断是否与当前线程标示一致
    if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    — 一致,则删除锁
    return redis.call('DEL', KEYS[1])
    end
    — 不一致,则直接返回
    return 0

    我们要在idea中集成一个EmmyLua插件

    然后创建这个文件

    将lua脚本写入文件中

    — 比较线程标示与锁中的标示是否一致
    if(redis.call('get', KEYS[1]) == ARGV[1]) then
    — 释放锁 del key
    return redis.call('del', KEYS[1])
    end
    return 0

    完整代码

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
    UNLOCK_SCRIPT = new DefaultRedisScript<>();
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    UNLOCK_SCRIPT.setResultType(Long.class);
    }
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
    this.name = name;
    this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public void unlock() {
    stringRedisTemplate.execute(UNLOCK_SCRIPT,
    Collections.singletonList(KEY_PREFIX + name),
    ID_PREFIX + String.valueOf(Thread.currentThread().getId()));
    }

    赞(0)
    未经允许不得转载:171主机测评 » 一篇文章搞懂分布式锁(上)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址