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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 【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());
    }
}

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
缓存 NoSQL 搜索推荐
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。
152 6
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
|
2月前
|
缓存 NoSQL 测试技术
Redis压测脚本及持久化机制
Redis压测脚本及持久化机制简介: Redis性能压测通过`redis-benchmark`工具进行,可评估读写性能。持久化机制包括无持久化、RDB(定期快照)和AOF(操作日志),以及两者的结合。RDB适合快速备份与恢复,但可能丢失数据;AOF更安全,记录每次写操作,适合高数据安全性需求。两者结合能兼顾性能与安全性,建议同时开启并定期备份RDB文件以确保数据安全。
52 9
|
7月前
|
缓存 NoSQL Redis
Redis 脚本
10月更文挑战第18天
69 3
|
3月前
|
NoSQL Redis 数据库
Redis 功能扩展 Lua 脚本 对Redis扩展 eval redis.call redis.pcall
通过本文的介绍,我们详细讲解了 Lua 脚本在 Redis 中的作用、`eval` 命令的使用方法以及 `redis.call` 和 `redis.pcall` 的区别和用法。通过合理使用 Lua 脚本,可以实现复杂的业务逻辑,确保操作的原子性,并减少网络开销,从而提高系统的性能和可靠性。
121 13
|
5月前
|
存储 SpringCloudAlibaba Java
【SpringCloud Alibaba系列】一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论
一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论。
【SpringCloud Alibaba系列】一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论
|
7月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
131 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
2月前
|
缓存 监控 NoSQL
Redis--缓存击穿、缓存穿透、缓存雪崩
缓存击穿、缓存穿透和缓存雪崩是Redis使用过程中可能遇到的常见问题。理解这些问题的成因并采取相应的解决措施,可以有效提升系统的稳定性和性能。在实际应用中,应根据具体场景,选择合适的解决方案,并持续监控和优化缓存策略,以应对不断变化的业务需求。
111 29
|
2月前
|
缓存 NoSQL Java
Redis应用—8.相关的缓存框架
本文介绍了Ehcache和Guava Cache两个缓存框架及其使用方法,以及如何自定义缓存。主要内容包括:Ehcache缓存框架、Guava Cache缓存框架、自定义缓存。总结:Ehcache适合用作本地缓存或与Redis结合使用,Guava Cache则提供了更灵活的缓存管理和更高的并发性能。自定义缓存可以根据具体需求选择不同的数据结构和引用类型来实现特定的缓存策略。
135 16
Redis应用—8.相关的缓存框架
|
28天前
|
人工智能 缓存 NoSQL
Redis 与 AI:从缓存到智能搜索的融合之路
Redis 已从传统缓存系统发展为强大的 AI 支持平台,其向量数据库功能和 RedisAI 模块为核心,支持高维向量存储、相似性搜索及模型服务。文章探讨了 Redis 在实时数据缓存、语义搜索与会话持久化中的应用场景,并通过代码案例展示了与 Spring Boot 的集成方式。总结来看,Redis 结合 AI 技术,为现代应用提供高效、灵活的解决方案。
|
2月前
|
存储 缓存 NoSQL
Redis缓存设计与性能优化
Redis缓存设计与性能优化涵盖缓存穿透、击穿、雪崩及热点key重建等问题。针对缓存穿透,可采用缓存空对象或布隆过滤器;缓存击穿通过随机设置过期时间避免集中失效;缓存雪崩需确保高可用性并使用限流熔断组件;热点key重建利用互斥锁防止大量线程同时操作。此外,开发规范强调键值设计、命令使用和客户端配置优化,如避免bigkey、合理使用批量操作和连接池管理。系统内核参数如vm.swappiness、vm.overcommit_memory及文件句柄数的优化也至关重要。慢查询日志帮助监控性能瓶颈。
78 9
下一篇
oss创建bucket