【Redis实现基础的分布式锁及Lua脚本说明】
使用Redis实现基础的分布式锁及Lua脚本说明
- 1. 概念
- 1.1 什么是分布式锁
- 1.2 什么是Lua脚本
- 2. 为什么要使用Lua脚本
- 3.实现分布式锁
1. 概念
1.1 什么是分布式锁
分布式锁是指,在多个服务或节点中的锁机制,用于协调对共享资源的访问。
说白了就是分布式系统中使用的资源锁,防止系统业务发生并发冲突。
举个栗子
比如,去上卫生间(只有一个公共坑位),如果我先进去了上大了,没有锁门,这时你闹肚子也来了,然后一起进来和我抢坑位。。。。。。
实际上分布式系统中的高并发场景中,可能不只是两个三个人去抢一个坑位,最常见的就是淘宝京东的618,双11活动期间的秒杀、减库存场景。
所以如果有一把锁,先得到锁的人入坑,上锁(加锁),其他人就要排队,等在坑里的人大完后打开锁(释放锁)后,后面排队的人才能拿到锁再进坑。
分布式锁主要包括【互斥性】、【防死锁】、【可重入性】、【高性能】、【高可用】等特性。下面一一解释一下各个特性的概念。
【互斥性】:同一时间点,只能有一个客户端可以持有锁,其他客户端得排队等待锁被释放掉后再去争取资源。(同一时间只能有一个人带锁入坑开大!)
【防死锁】:如果,如果说很不巧,持有锁的客户端发生了崩溃,锁是能够自动释放的,不会陷入死锁情况从而导致整个系统也跟着崩了。(你正在坑里开大,上了锁,偷摸吸食然后被自己臭晕了,卫生间的管理员看你半小时还没动静替你找来了120把你拉走,顺便还开了锁让排队的人继续使用)
【可重入性】:同一个客户端可以多次的获取到同一把锁。(你先拿到了锁,进坑开大了,发现没带纸,可以随时在进去)
【高性能】:锁的响应速度要足够快,加解锁操作低延迟。(五秒真男人,开大足够快)
【高可用】:使用集群保证锁服务不会因单点故障不可用。(怕一个坑被你们拉的堵住了,多修了几间卫生间,一个卫生间一把锁)
1.2 什么是Lua脚本
一个轻量级的脚本语言,专门设计用来嵌入到其他程序里,帮你快速扩展功能。比如,游戏里NPC的行为、奶茶店的自动点单系统,甚至Redis的原子操作,都可以用Lua搞定!
举个栗子
你开了一家奶茶店,想让店里的机器人自动做奶茶。
没有Lua的情况下,你得组建一个开发团队,没日没夜的去搞机器人的行为开发,就是嵌入式开发。
有了Lua的情况下,你直接给机器人安装上【Lua软件】,打开app的对话框用几行简单的脚本就能教它:“先加珍珠,再加牛奶,最后摇一摇!”
Lua的核心特点是【轻量级】、【可嵌入性】、【动态类型】、【高效】
【轻量级】:Lua的代码几乎全部是标准C写的,体积小到只有200KB左右。
【可嵌入性】:Lua能直接嵌入C/C++等程序中,甚至Redis和其他数据库中,像插件一样调用。
【动态类型】:Lua的变量类型在运行时自动确定,不用提前声明类型,想换什么内容都行。
【高效】:LuaJIT(即时编译器)能进一步加速执行,速度堪比编译型语言(比如C)。
2. 为什么要使用Lua脚本
在 Redis 中,分布式锁的核心问题是:
加锁操作必须是原子的(即多个命令不能被中断)。
解锁操作也必须是原子的(避免误删其他客户端的锁)。
如果直接使用多个 Redis 命令(如 SETNX + EXPIRE),可能会出现以下问题:
网络延迟:客户端在 SETNX 成功后,还没来得及设置 EXPIRE 就崩溃,导致锁永远不会过期(死锁)。
并发竞争:其他客户端可能在加锁时读取到错误的状态(如未设置超时的锁)。
Lua 脚本的作用:
原子性:Redis 会将整个 Lua 脚本作为一个整体执行,期间不会被其他命令打断。
逻辑封装:在脚本中可以完成复杂的逻辑(如加锁、设置超时、验证标识),避免多条命令之间的竞态条件。
3.实现分布式锁
加锁:
使用Lua脚本实现 SET key NX PX 原子加锁,防止多个客户端同时处理库存(如果键不存在则设置,同时设置过期时间)。
解锁:
使用 Lua 脚本原子性地检查值(客户端唯一标识)并删除锁,防止误删其他客户端的锁,确保原子性。
实例代码:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;import java.util.Collections;
import java.util.UUID;/*** @author: gaokelai* @date: 2025/7/29*/
@Service
public class StockService {private final RedisTemplate<String, Object> redisTemplate;public StockService(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate;}// Lua 脚本常量private static final String LOCK_SCRIPT ="if redis.call('get', KEYS[1]) == nil then " +" redis.call('set', KEYS[1], ARGV[1], 'nx', 'px', ARGV[2]); " +" return 1; " +"else " +" return 0; " +"end";private static final String UNLOCK_SCRIPT ="if redis.call('get', KEYS[1]) == ARGV[1] then " +" return redis.call('del', KEYS[1]); " +"else " +" return 0; " +"end";// 扣减库存逻辑public boolean deductStock(String productId, int count) {String lockKey = "lock:product:" + productId;String clientId = UUID.randomUUID().toString(); // 客户端唯一标识// 加锁(使用 Lua 脚本)Boolean isLocked = redisTemplate.execute(DefaultRedisScript.of(LOCK_SCRIPT, Boolean.class),Collections.singletonList(lockKey),clientId, 30000 // 锁的过期时间(毫秒));if (Boolean.TRUE.equals(isLocked)) {try {// 扣减库存(原子操作)String stockKey = "stock:" + productId;Long currentStock = redisTemplate.opsForValue().decrement(stockKey, count);if (currentStock == null || currentStock < 0) {// 库存不足,回滚redisTemplate.opsForValue().increment(stockKey, count);return false;}return true;} finally {// 解锁(使用 Lua 脚本)redisTemplate.execute(DefaultRedisScript.of(UNLOCK_SCRIPT, Long.class),Collections.singletonList(lockKey),clientId);}} else {return false; // 获取锁失败}}
}
调用实例
// 初始库存设置
redisTemplate.opsForValue().set("stock:product_1001", "10");// 模拟扣减 product_1001 的库存,每次扣 1 件
stockService.deductStock("product_1001", 1); // 多线程模拟并发扣减
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {executor.submit(() -> {boolean result = stockService.deductStock("product_1001", 1);System.out.println("扣减结果: " + result);});
}
executor.shutdown();