【Redis实现基础的分布式锁及Lua脚本说明】

发布于:2025-08-03 ⋅ 阅读:(25) ⋅ 点赞:(0)

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


网站公告

今日签到

点亮在社区的每一天
去签到