基于Redis+Lua实现分布式锁模拟秒杀扣减库存业务(非常详细,良心解析)

最近和几个小伙伴聊了聊基于Redis的分布式锁实现秒杀扣减库存业务的一些技术细节,刚好最近钻研了一段时间,本篇内容通过1个详细的案例,把这个实现方案作个记录,当做自己对知识的总结积累,同时也欢迎广大开发者朋友一起交流,学习,大家可以留言讨论,原创写作不易,请勿喷,如果觉得有用,不要忘了关注点赞哦。

本案例我们通过以下6个部分来讲解基于Redis+Lua实现分布式锁的详细过程,案例背景是模拟秒杀扣减库存的经典业务:

1、什么是分布式锁 ?

要说起分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。

线程锁:主要用来给方法、代码块加锁。当某个方法或代码块使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是不同线程之间对于同一JVM共享其内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:当多个进程(多个应用程序)不在同一个系统中,比如微服务部署多节点,就要用分布式锁控制多个进程对资源的访问。

2、什么场景下需要分布式锁 ?

随着互联网的高速发展,一些电商系统承载的业务体量也在大大增加,那么企业为了应对巨大的业务体量,很多时候会用到微服务,单个服务会部署多个节点,以此来处理瞬间激增的用户请求,比如秒杀促销的时候,某个界面一下子有10000人同时抢购,那么这10000个人的请求会按照特定的负载均衡策略,把10000个用户请求路由到不同的多个节点去处理,大大减轻了单台应用处理业务的能力,提升了效率,提升了用户的体验度。那么这个时候就要用到分布式锁去满足技术方案。

3、用了分布式锁会带来什么好处 ?

接着上面的举例,如果没用到分布式锁,那么很可能出出现,用户1下单的代码过程中,还没等用户1处理完订单数据,用户2的请求进入到用户1下单的线程中了,这2个线程在同1个应用中执行,是共享的同1个JVM内存,那么可能会出现订单数据错乱,最终导致订单业务完全发生重大严重错误。但是如果采用了分布式锁,并且使用得当的情况下完全可以避免这个问题。这时就凸显分布式锁的重要作用了。

4、实现分布式锁可以采用哪些方案实现 ?

目前行业中分布式锁实现众所周知的主要有3种方案:
1.采用数据库的事务锁

2.采用Zookeeper框架

3.基于redis实现

目前主流的比较成熟并且比较大众化的方案就是基于redis实现,当然再组合上Lua,实现分布式锁实现过程会更简单,功能更强大。本案例笔者就以springboot+Jedis+redis+Lua通过具体的例子详细讲解分布式锁的实现过程和思路。

5、分布式锁需要满足的4个的必要条件

  1. 互斥性。在任意时刻,只有一个客户端线程能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也要保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

6、代码实现

(1)、引入依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

(2)、正确加锁方式

/** 
 * @author guobh 
 * @date 2020年8月27日 上午11:10:50 
 * @version 1.0 
 * @since jdk1.8
 * @description <>
 */

@Slf4j
@Component
public class RedisPool {

	private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    
    private static final Long RELEASE_SUCCESS = 1L;
    
    // 锁的过期时间
    private static int EXPIRE_TIME = 500;
    
	private static JedisPool pool;//jedis连接池对象
	 
    private stati
### Java Redis Lua 扣减库存 实现方案 示例代码 #### 方案概述 为了实现高效的库存扣减操作并解决高并发场景下的竞争问题,可以利用 Redis 的原子性和 Lua 脚本来完成这一功能。Lua 脚本可以在服务器端执行,从而避免多次网络往返带来的延迟,并确保操作的原子性。 以下是基于 Java 和 Redis 结合 Lua 脚本实现库存扣减的具体方法: --- #### 1. **引入依赖** 在项目中引入 Redis 客户端库 `Jedis` 或者 `Lettuce` 来连接 Redis 并执行命令。这里以 Jedis 为例: ```xml <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.0.0</version> </dependency> ``` --- #### 2. **Lua 脚本设计** 编写一个简单的 Lua 脚本用于库存扣减逻辑。该脚本会在 Redis 中运行,判断当前库存是否充足,并进行相应的扣减操作。 ```lua -- Lua script for stock decrement with lock local key = KEYS[1] -- Key name, e.g., "stock" local value = tonumber(ARGV[1]) -- Decrement amount if redis.call('GET', key) and tonumber(redis.call('GET', key)) >= value then local current_stock = redis.call('DECRBY', key, value) return current_stock else return -1 -- Insufficient stock or no stock available end ``` 此脚本的功能如下: - 判断指定键是否存在以及其值是否大于等于要扣减的数量。 - 如果满足条件,则调用 `DECRBY` 减少库存。 - 否则返回 `-1` 表示库存不足[^3]。 --- #### 3. **Java 实现代码** 下面是一个完整的 Java 方法,展示如何加载 Lua 脚本并通过 Redis 进行库存扣减。 ```java import redis.clients.jedis.Jedis; import redis.clients.jedis.Scripting; public class StockService { private static final String STOCK_KEY = "stock"; private static final int INITIAL_STOCK = 10; // Initial stock quantity public static void main(String[] args) { try (Jedis jedis = new Jedis("localhost", 6379)) { // Initialize the stock in Redis jedis.set(STOCK_KEY, String.valueOf(INITIAL_STOCK)); // Define the Lua script as a string String luaScript = "local key = KEYS[1]\n" + "local value = tonumber(ARGV[1])\n" + "\n" + "if redis.call('GET', key) and tonumber(redis.call('GET', key)) >= value then\n" + " local current_stock = redis.call('DECRBY', key, value)\n" + " return current_stock\n" + "else\n" + " return -1\n" + "end"; // Simulate multiple threads to perform concurrent decrements simulateConcurrentDecrement(jedis, luaScript); } } private static void simulateConcurrentDecrement(Jedis jedis, String luaScript) { long successCount = 0; int threadCount = 6; CountDownLatch latch = new CountDownLatch(threadCount); Runnable task = () -> { boolean isSuccessful = false; while (!isSuccessful && jedis.exists(STOCK_KEY)) { Long result = (Long) jedis.eval(luaScript, 1, STOCK_KEY, "1"); if (result != null && result != -1) { isSuccessful = true; successCount++; } } latch.countDown(); }; List<Thread> threads = new ArrayList<>(); for (int i = 0; i < threadCount; i++) { Thread t = new Thread(task); threads.add(t); t.start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("成功扣减库存的操作数: " + successCount); System.out.println("最终剩余库存数量: " + jedis.get(STOCK_KEY)); } } ``` --- #### 4. **解释与注意事项** - 上述代码模拟了多个线程同时尝试扣减库存的情况,验证了 Lua 脚本的原子性。 - 使用 `jedis.eval()` 将 Lua 脚本发送至 Redis 服务端执行,确保整个过程不会被其他客户端中断[^4]。 - 当前库存不足以支持扣减时,脚本会返回 `-1`,表示失败。 --- #### 5. **扩展优化建议** 如果需要进一步增强系统的可靠性和性能,可考虑以下改进措施: - **设置过期时间**:为库存键设置合理的 TTL(Time To Live),防止数据永久驻留。 - **分布式锁机制**:虽然 Lua 脚本本身具有原子性,但在更复杂的业务场景下仍需配合分布式锁工具(如 Redlock 算法)[^1]。 - **监控与报警**:实时监测 Redis 数据状态,及时发现异常情况。 ---
评论 8
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值