「黑马点评」四、分布式锁

2025 年 4 月 23 日 星期三
4

「黑马点评」四、分布式锁

分布式锁

产生背景:各自单机看各自的锁监视器却无法共享锁

image.png|500|500

image.png|500|500

不用 JVM 内部的锁,使用一个,满足分布式系统或集群模式下多进程可见且互斥的锁,即分布式锁

image.png|500|500

image.png|500|500

分布式锁:多进程可见、互斥、高可用、高性能、安全性......

常见实现方案

image.png|500|500

image.png|500|500

基于 Redis 的分布式锁

最基本的两个操作

  • 获取锁
    • 互斥:确保只有一个线程获取到锁
    • SETNX lock thread1
    • EXPIRE lock 10
    • 抢锁和设置过期时间需要有原子性
    • SET lock thread1 EX 10 NX
    • 对于获取锁操作,分为:
      • 阻塞式:复杂且浪费处理机
      • 非阻塞式:尝试一次,立即返回 true OR false
  • 释放锁
    • 手动释放
    • 超时释放
    • DEL lock

业务示意图

image.png|500|500

image.png|500|500

基于 Redis 实现分布式锁初级版本

背景:根据 ILock 接口,利用 Redis 实现分布式锁

public interface ILock {
    public boolean tryLock(long timeoutSec);
    public void unlock();
}

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) {
        long threadId = Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success); // 防止拆箱得到空指针
    }
 
    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

示例

Long userId = UserHolder.getUser().getId();
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
boolean isLock = lock.tryLock(120);
if (!isLock) {
    return Result.error("不允许重复下单");
}
try {
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
} finally {
    lock.unlock();
}

初级分布式锁超时误删问题

极端情况:某一线程业务处理时间过长,锁超时释放,结果等线程处理完毕,将别人获取到的锁删除

image.png|500|500

image.png|500|500

解决方法:检查锁业务标志,即 value,一致才有资格释放

image.png|500|500

image.png|500|500

流程示意图

image.png|500|500

image.png|500|500

但是由于不同 JVM 会有相同递增到 PID 序列,所以考虑用 UUID 表示线程标示

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:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success); // 防止拆箱得到空指针
    }
 
    @Override
    public void unlock() {
        String value = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        if (threadId.equals(value)) { // value 可能为 null,所以用 threadId 来判断
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

初级分布式锁非原子性误删

线程安全问题:检查锁是否是自身的以及删除锁并不是原子性的,所以可能出现检查通过但是突然阻塞,另一个线程抢夺锁成功后被原先的线程删除锁,导致误删锁现象再次出现

image.png|500|500

image.png|500|500

原子性方案考虑用 Redis 配合乐观锁,但是太过于复杂

此处选择 Lua 脚本确保原子性 > Redis 提供了 Lua 脚本功能,可以在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性,Lua 是一种编程语言,基本语法

Redis 在 Lua 中提供的调用函数

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

Redis 为 Lua 脚本提供的调用命令 > 末尾的 0 代表之后 key 类型参数个数

EVAl "return redis.call('set', 'name', 'jack')" 0

key 类型参数会放入 KEYS 数组,其他参数会放入 ARGV 数组,在脚本中可以从 KEYS 和 ARGV 中获取

EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose

当前分布式锁对应的 Lua 脚本

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

利用 Lua 优化原子性后的基于 Redis 的分布式锁

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:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success); // 防止拆箱得到空指针
    }

    @Override
    public void unlock() {
        stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId()
        );
    }
}

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...