当前位置: 首页 > news >正文

【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();
http://www.dtcms.com/a/303727.html

相关文章:

  • 【CAN总线】STM32 的 CAN 总线通信开发笔记(基于 HAL)
  • Spring Boot 自动配置:从 2.x 到 3.x 的进化之路
  • Python 程序设计讲义(28):字符串的用法——格式化字符串
  • 【C++】第十九节—一文万字详解 | AVL树实现
  • Go进阶:流程控制(if/for/switch)与数组切片
  • adb reboot 与 adb shell svc power reboot 的区别
  • 爬虫自动化:一文掌握 PyAutoGUI 的详细使用
  • 【RH134 问答题】第 9 章 访问网络附加存储
  • 智能制造的空间度量:机器视觉标定技术解析
  • 数据结构【红黑树】
  • 架构实战——互联网架构模板(“用户层”和“业务层”技术)
  • MySql插入中文生僻字/Emoji报错django.db.utils.DataError: (1366, “Incorrect string value
  • 14、distance_object_model_3d算子
  • Web3 网络安全漏洞的预防措施
  • 解决IDEA拉取GitLab项目报错:必须为访问令牌授予作用域[api, read user]
  • 风口还是伪命题?AI + Web3 赛道价值何在?
  • Time drifts can result in unexpected behavior such as time-outs.
  • IDEA中全局搜索快捷键Ctrl+Shift+F为何失灵?探寻原因与修复指南
  • imx6ull-驱动开发篇3——字符设备驱动开发实验
  • 【C++算法】79.BFS解决FloodFill算法_图像渲染
  • 【C#|C++】C#调用C++导出的dll之非托管的方式
  • 数据结构 排序(1)---插入排序
  • 基于mysql云数据库对比PowerBI vs QuickBI vs FineBI更换数据源的可行性
  • Kafka——Kafka控制器
  • 如何选择工业电脑?
  • 【VOS虚拟操作系统】未来之窗打包工具在前端资源优化中的应用与优势分析——仙盟创梦IDE
  • Spring AI集成Elasticsearch向量检索时filter过滤失效问题排查与解决方案
  • ICT模拟零件测试方法--晶体管测试
  • Linux救援模式之应用篇
  • 算法第29天|动态规划dp2:不同路径、不同路径Ⅱ、整数拆分、不同的二叉搜索树