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

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


一、分布式锁实现原理

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

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

🎄 分布式锁的核心是实现多进程之间锁的互斥,而满足这一点的方式有很多,常见的有三种:

三、Redis 的 setnx 实现互斥锁



🎄锁获取了,还没有来得及设置过期时间服务器就宕机了

🎄保证 setnx(获取锁)和 expire 设置过期时间两个操作是原子性的

四、基于 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);
    }
}

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


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

  • 在获取锁时存入线程标识(可以用 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 命令原子性导致)

解决方案:Lua 脚本

(1) Lua 脚本

📖 Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性

📖 Lua 是一种编程语言 https://www.runoob.com/lua/lua-tutorial.html


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

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

📖 如果脚本中的 key(gender)、value(handsomeBoy)不想写死,可以作为参数传递

📖 key 类型参数会放入 KEYS 数组

📖 其它参数会放入 ARGV 数组,在脚本中可以从 KEYS 和 ARGV 数组获取这些参数

Lua 语言中下标从 1 开始

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

📖 获取锁(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

(4) RedisTemplate 执行 Lua 脚本

Lua 脚本可写在 Java 的类路径下的资源文件夹中


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());
    }
}

相关文章
|
8月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
530 2
|
8月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
485 6
|
9月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
7月前
|
存储 缓存 NoSQL
Redis基础命令与数据结构概览
Redis是一个功能强大的键值存储系统,提供了丰富的数据结构以及相应的操作命令来满足现代应用程序对于高速读写和灵活数据处理的需求。通过掌握这些基础命令,开发者能够高效地对Redis进行操作,实现数据存储和管理的高性能方案。
212 12
|
7月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
592 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
7月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
7月前
|
存储 消息中间件 NoSQL
【Redis】常用数据结构之List篇:从常用命令到典型使用场景
本文将系统探讨 Redis List 的核心特性、完整命令体系、底层存储实现以及典型实践场景,为读者构建从理论到应用的完整认知框架,助力开发者在实际业务中高效运用这一数据结构解决问题。
|
9月前
|
NoSQL Redis
Lua脚本协助Redis分布式锁实现命令的原子性
利用Lua脚本确保Redis操作的原子性是分布式锁安全性的关键所在,可以大幅减少由于网络分区、客户端故障等导致的锁无法正确释放的情况,从而在分布式系统中保证数据操作的安全性和一致性。在将这些概念应用于生产环境前,建议深入理解Redis事务与Lua脚本的工作原理以及分布式锁的可能问题和解决方案。
323 8
|
8月前
|
存储 缓存 人工智能
Redis六大常见命令详解:从set/get到过期策略的全方位解析
本文将通过结构化学习路径,帮助读者实现从命令语法掌握到工程化实践落地的能力跃迁,系统性提升 Redis 技术栈的应用水平。