【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性

目录

  • 一、分布式锁实现原理
  • 二、不同的分布式锁实现方案
  • 三、Redis 的 setnx 实现互斥锁
  • 四、基于 Redis 实现分布式锁初级版
  • 五、误删锁问题(业务阻塞导致)
  • 六、误删锁(Redis 命令原子性导致)
    • (1) Lua 脚本
    • (2) Redis 编写和执行 Lua 脚本
    • (3) 复杂逻辑的 Lua 脚本(业务相关)
    • (4) RedisTemplate 执行 Lua 脚本

一、分布式锁实现原理

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第1张图片

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第2张图片

二、不同的分布式锁实现方案

分布式锁的核心是实现多进程之间锁的互斥,而满足这一点的方式有很多,常见的有三种:
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第3张图片

三、Redis 的 setnx 实现互斥锁

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第4张图片


【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第5张图片


【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第6张图片

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第7张图片

锁获取了,还没有来得及设置过期时间服务器就宕机了
保证 setnx(获取锁)和 expire 设置过期时间两个操作是原子性的

在这里插入图片描述

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第8张图片

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

需求:定义一个类,实现下面的接口,利用 Redis 实现分布式锁功能

public interface LockInter {
    /**
     * 尝试获取锁
     *
     * @param ttlSecond 锁的过期时间
     * @return true: 成功获取锁; false: 获取锁失败
     */
    boolean tryLock(long ttlSecond);

    /**
     * 释放锁
     */
    void unlock();
}
public class LockImplV1 implements LockInter {
    private String name; // 和业务相关的锁的名字
    private StringRedisTemplate stringRedisTemplate;

    private static final String LOCK_KEY_PREFIX = "lock:";

    public LockImplV1(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long ttlSecond) {
        String key = LOCK_KEY_PREFIX + name;
        // value 里面放当前线程的唯一标识(线程 ID)
        String val = Thread.currentThread().getId() + "";
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key,
                val,
                ttlSecond,
                TimeUnit.SECONDS);

        // Boolean -- boolean 会自动拆箱
        // 当 success 为 null 的时候会抛异常
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(LOCK_KEY_PREFIX + name);
    }
}

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第9张图片

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第10张图片

五、误删锁问题(业务阻塞导致)

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第11张图片

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第12张图片


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

  • 在获取锁时存入线程标识(可以用 UUID 表示)
  • 在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
    ① 如果一致则释放锁
    ② 如果不一致则不释放锁

要用 UUID,避免线程 ID 重复

public class LockImplV2 implements LockInter {
    private String name; // 和业务相关的锁的名字
    private StringRedisTemplate stringRedisTemplate;

    private static final String LOCK_KEY_PREFIX = "lock:";
    private static final String UNIQUE_PREFIX = UUID.randomUUID().toString(true);

    public LockImplV2(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long ttlSecond) {
        String key = LOCK_KEY_PREFIX + name;
        // value 里面放当前线程的唯一标识(线程 ID)
        String val = UNIQUE_PREFIX + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key,
                val,
                ttlSecond,
                TimeUnit.SECONDS);

        // Boolean -- boolean 会自动拆箱
        // 当 success 为 null 的时候会抛异常
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        String k = LOCK_KEY_PREFIX + name;
        String cacheVal = stringRedisTemplate.opsForValue().get(k);

        String curVal = UNIQUE_PREFIX + Thread.currentThread().getId();

        if (curVal.equals(cacheVal)) {
            stringRedisTemplate.delete(k);
        }
    }
}

六、误删锁(Redis 命令原子性导致)

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第13张图片

解决方案:Lua 脚本

(1) Lua 脚本

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


(2) Redis 编写和执行 Lua 脚本

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第14张图片

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第15张图片

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第16张图片

参数有两种:key 类型参数,其他参数

如果脚本中的 key(gender)、value(handsomeBoy)不想写死,可以作为参数传递
key 类型参数会放入 KEYS 数组
其它参数会放入 ARGV 数组,在脚本中可以从 KEYS 和 ARGV 数组获取这些参数

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第17张图片

Lua 语言中下标从 1 开始

(3) 复杂逻辑的 Lua 脚本(业务相关)

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第18张图片

获取锁(Redis 缓存)中的线程标识 cacheVal
判断是否与当前线程标识一致 curVal
如果一致则释放锁(del
如果不一致则什么都不做

上述操作要通过 Lua 脚本执行,保证多条 Redis 命令的原子性(防止误删锁)

--- 当前线程的线程标识
local curVal = ARGV[1] 

--- 要删除的锁的 key
local lockKey = KEYS[1]

if(cacheVal == curVal) 
	then
		return redis.call('DEL', KEYS[1])
	end
return 0

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第19张图片

(4) RedisTemplate 执行 Lua 脚本

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第20张图片

Lua 脚本可写在 Java 的类路径下的资源文件夹中
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第21张图片

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第22张图片


【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第23张图片

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第24张图片

public class LockImplV3 implements LockInter {
    private String name; // 和业务相关的锁的名字
    private StringRedisTemplate stringRedisTemplate;

    private static final String LOCK_KEY_PREFIX = "lock:";
    private static final String UNIQUE_PREFIX = UUID.randomUUID().toString(true);

    private static final DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT;

    static { // 初始化 UNLOCK_LUA_SCRIPT
        UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_LUA_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_LUA_SCRIPT.setResultType(Long.class);
    }

    public LockImplV3(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long ttlSecond) {
        String key = LOCK_KEY_PREFIX + name;
        // value 里面放当前线程的唯一标识(线程 ID)
        String val = UNIQUE_PREFIX + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key,
                val,
                ttlSecond,
                TimeUnit.SECONDS);

        // Boolean -- boolean 会自动拆箱
        // 当 success 为 null 的时候会抛异常
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.execute(
                UNLOCK_LUA_SCRIPT,
                Collections.singletonList(LOCK_KEY_PREFIX + name),
                UNIQUE_PREFIX + Thread.currentThread().getId());
    }
}

【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性_第25张图片

你可能感兴趣的:(Redis,redis,分布式,lua)