百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

从 0 到 1 掌握 Redis 分布式锁,搞定秒杀场景难题

wptr33 2025-01-29 18:20 19 浏览

上次有篇文章谈到了处理高并发时数据唯一性的问题,随后有网友反馈说:


RedisTemplate的opsForHash()方法好用,但别依赖它解决所有并发问题,分布式锁+数据库乐观锁+消息队列异步处理才是高并发系统的标配。



果然是高手在民间! 虽然说可能有些厉害的程序员可能已经掌握着这种技术,不过我想真正掌握了的可能也并不多,所以就策划着写一下相关技术的介绍和应用,让大家有更全面和深刻的认识。


为了让大家能更好的去理解,更系统化的去学习,我将这段需求文字拆成四个步骤:


1.分布式锁,主要介绍Redis 分布式锁

2. 数据库乐观锁,主要通过MYSQL来实际讲述乐观锁

3. 消息队列,主要通过Kafka实战来讲述消息队列及异步操作

4. 综合技术应用,通过模拟案例来讲述这个技术如何相互配合,轻松应对高并发问题


以这四个主题为主线,中间也可能会穿插讲解一些其他的技术,敬请关注!


咱们今天进行第一步,了解Redis分布式锁。


Redis 分布式锁是什么?



在深入探讨 Redis 分布式锁之前,我们先来理解一下什么是分布式锁。在分布式系统中,多个节点可能同时访问和修改共享资源,这就可能导致数据不一致或其他并发问题。分布式锁就是一种用于控制分布式系统中多个节点对共享资源进行同步访问的机制 ,它确保在同一时刻只有一个节点能够访问被锁定的资源。


对比一下我们熟悉的 Java 本地锁。在单机应用中,Java 的本地锁(如synchronized关键字、ReentrantLock等)能够很好地控制多个线程对共享资源的访问,保证同一时刻只有一个线程能获取到锁并执行临界区代码。但在分布式场景下,不同的服务实例可能部署在不同的机器上,它们拥有各自独立的 JVM,此时 Java 本地锁就无法在多个实例之间实现对共享资源的同步控制了。


而 Redis 分布式锁正是为了解决分布式环境下的这种问题而诞生的。简单来说,Redis 分布式锁是利用 Redis 的特性来实现的一种分布式锁机制。通过在 Redis 中设置一个特定的键值对来表示锁的状态,多个节点通过操作这个键值对来获取和释放锁,从而达到对共享资源的同步访问控制。

Redis 分布式锁的技术特性


了解了 Redis 分布式锁的概念后,我们再深入探讨一下它的几个关键技术特性 。


互斥性


这是分布式锁最基本的特性。在任何时刻,对于同一个锁资源,只能有一个客户端能够获取到锁 。Redis 分布式锁利用 Redis 的单线程特性以及相关命令(如SETNX,即SET if Not eXists,如果不存在则设置)来实现互斥性。当一个客户端使用SETNX命令尝试设置一个锁的键值对时,如果该键不存在,说明没有其他客户端持有锁,此客户端设置成功,即获取到锁;如果该键已存在,说明已有其他客户端持有锁,当前客户端设置失败,无法获取锁。例如:

Jedis jedis = new Jedis("localhost", 6379);
String lockKey = "product:1:lock";
String requestId = UUID.randomUUID().toString();
// 使用SETNX命令尝试获取锁
Boolean lockAcquired = jedis.setnx(lockKey, requestId) == 1;
if (lockAcquired) {
// 获取到锁,执行业务逻辑
try {
// 业务操作
} finally {
// 释放锁
jedis.del(lockKey);
}
} else {
// 未获取到锁,处理相应逻辑
}


高可用性


在分布式场景下,必须保证锁服务的高可用性,即使部分节点出现故障,也不能影响锁的正常获取和释放 。为了实现这一点,通常会采用集群部署的方式来部署 Redis。通过 Redis 集群,当某个节点发生故障时,其他节点可以继续提供服务,确保锁的操作不受影响。比如,使用 Redis Cluster 模式,数据会分布在多个节点上,即使个别节点宕机,只要集群中大部分节点正常运行,就可以正常进行锁的相关操作 。


防止锁超时


在分布式系统中,可能会出现客户端获取锁后,由于某些原因(如网络故障、程序崩溃等)未能及时释放锁的情况,这就会导致其他客户端永远无法获取到该锁,从而产生死锁 。为了避免这种情况,Redis 分布式锁需要设置一个合理的过期时间,当锁的持有时间超过这个过期时间后,Redis 会自动释放锁,这样其他客户端就有机会获取锁。例如,在使用SET命令设置锁时,可以同时指定过期时间:

// 使用SET命令设置锁,同时指定过期时间为10秒
Boolean lockAcquired = jedis.set(lockKey, requestId, "NX", "EX", 10)!= null;


独占性


只有加锁的客户端才能解锁,不能出现一个客户端加锁,另一个客户端却能解锁的情况 。为了保证这一特性,在加锁时,每个客户端会生成一个唯一的标识(如 UUID),并将其作为锁的值存储在 Redis 中。在解锁时,首先会检查当前锁的值是否与自己加锁时的唯一标识一致,如果一致,则说明是自己加的锁,才执行解锁操作。通常会借助 Lua 脚本来保证解锁操作的原子性,避免在检查和删除锁的过程中出现并发问题 。以下是一个使用 Lua 脚本解锁的示例:


String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), requestId);
if ((Long) result == 1) {
// 解锁成功
} else {
// 解锁失败
}



秒杀商品场景中的应用




加锁操作


在秒杀场景中,使用 Redis 的SET命令来实现加锁是非常常见的方式。下面是一个使用 Java 和 Jedis 客户端进行加锁操作的代码示例:

import redis.clients.jedis.Jedis;
import java.util.UUID;
public class Seckill {
private static final String LOCK_KEY = "seckill:product:1:lock";
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final int LOCK_EXPIRE = 10; // 锁的过期时间,单位秒
public boolean tryLock() {
Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);
String requestId = UUID.randomUUID().toString();
// 使用SET key value NX EX seconds命令尝试获取锁
// NX表示只有当键不存在时才设置键值对
// EX seconds表示设置键的过期时间为seconds秒
boolean lockAcquired = "OK".equals(jedis.set(LOCK_KEY, requestId, "NX", "EX", LOCK_EXPIRE));
jedis.close();
return lockAcquired;
}
}


在上述代码中,我们首先创建了一个 Jedis 实例来连接 Redis 服务器。然后生成一个唯一的requestId,这个requestId用于标识当前加锁的客户端。接着使用jedis.set(LOCK_KEY, requestId, "NX", "EX", LOCK_EXPIRE)命令来尝试获取锁。如果该命令执行成功,返回OK,说明锁被成功获取,lockAcquired为true;否则,说明锁已被其他客户端持有,获取锁失败,lockAcquired为false。


业务逻辑处理


当成功获取到锁后,就可以进行商品库存检查、扣减等业务操作了。以下是相关代码示例:

public String seckillProduct() {
if (tryLock()) {
try {
Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);
String productStockKey = "seckill:product:1:stock";
// 获取商品库存
int stock = Integer.parseInt(jedis.get(productStockKey));
if (stock > 0) {
// 扣减库存
jedis.set(productStockKey, String.valueOf(stock - 1));
jedis.close();
return "秒杀成功";
} else {
jedis.close();
return "商品已售罄";
}
} catch (Exception e) {
e.printStackTrace();
return "秒杀失败";
} finally {
// 解锁操作将在后续介绍
}
} else {
return "抢购人数过多,请稍后重试";
}
}

在这段代码中,首先调用tryLock方法尝试获取锁。如果获取到锁,就从 Redis 中获取商品的库存数量。判断库存数量是否大于 0,如果大于 0,则将库存数量减 1,并更新到 Redis 中,返回 “秒杀成功”;如果库存数量不大于 0,则返回 “商品已售罄”。在整个业务逻辑处理过程中,由于加了锁,所以不会出现多个请求同时扣减库存导致超卖的情况 。


解锁操作


解锁操作同样重要,如果不妥善处理,可能会导致锁无法释放,从而影响后续的业务流程。为了确保解锁操作的安全性和原子性,通常会借助 Lua 脚本。下面是使用 Lua 脚本进行解锁的代码示例:

public void unlock() {
Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String requestId = UUID.randomUUID().toString();
Object result = jedis.eval(script, 1, LOCK_KEY, requestId);
if ((Long) result == 1) {
System.out.println("解锁成功");
} else {
System.out.println("解锁失败");
}
jedis.close();
}

在上述代码中,定义了一个 Lua 脚本,该脚本首先通过redis.call('get', KEYS[1])获取锁的当前值,并与加锁时的requestId(即ARGV[1])进行比较。如果两者相等,说明当前客户端是加锁的客户端,执行redis.call('del', KEYS[1])删除锁,返回 1 表示解锁成功;否则,返回 0 表示解锁失败 。通过这种方式,保证了只有加锁的客户端才能解锁,避免了误解锁的情况 。


Redis 分布式锁用法



核心要点回顾


在使用 Redis 分布式锁时,加锁操作要确保原子性,例如使用SET key value NX EX seconds命令,通过NX选项保证只有在锁不存在时才能设置成功,实现互斥性;同时利用EX选项设置合理的过期时间,防止因程序异常导致死锁 。在业务逻辑处理完成后,解锁操作同样关键,使用 Lua 脚本可以保证解锁的原子性,先判断锁的持有者是否为当前客户端,只有在确认的情况下才执行删除锁的操作 。


最佳实践建议


合理设置锁超时时间非常重要,应根据业务处理的实际时间来预估,既要保证业务有足够的时间完成,又不能设置过长导致资源长时间被占用。可以在预估业务处理时间的基础上,适当增加一定的缓冲时间。同时,选择合适的锁实现方式,如使用 Lua 脚本实现原子性操作,或者考虑使用更高级的分布式锁框架(如 Redisson),它提供了更丰富的功能和更便捷的使用方式 。此外,在实际应用中,还需要考虑锁的重试机制,当获取锁失败时,根据业务需求进行合理的重试,以提高系统的可用性 。

相关推荐

Linux文件系统操作常用命令(linux文件内容操作命令)

在Linux系统中,有一些常用的文件系统操作命令,以下是这些命令的介绍和作用:#切换目录,其中./代表当前目录,../代表上一级目录cd#查看当前目录里的文件和文件夹ls#...

别小看tail 命令,它难倒了技术总监

我把自己以往的文章汇总成为了Github,欢迎各位大佬star...

lnav:基于 Linux 的高级控制台日志文件查看器

lnav是一款开源的控制台日志文件查看器,专为Linux和Unix-like系统设计。它通过自动检测日志文件的格式,提取时间戳、日志级别等关键信息,并将多个日志文件的内容按时间顺序合并显示,...

声明式与命令式代码(声明模式和命令模式)

编程范式中的术语和差异信不信由你,你可能已经以开发人员的身份使用了多种编程范例。因为没有什么比用编程理论招待朋友更有趣的了,所以这篇文章可以帮助您认识代码中的流行范例。命令式编程命令式编程是我们从As...

linux中的常用命令(linux常用命令和作用)

linux中的常用命令linux中的命令统称shell命令shell是一个命令行解释器,将用户命令解析为操作系统所能理解的指令,实现用户与操作系统的交互shell终端:我们平时输入命令,执行程序的那个...

提高工作效率的--Linux常用命令,能够决解95%以上的问题

点击上方关注,第一时间接受干货转发,点赞,收藏,不如一次关注评论区第一条注意查看回复:Linux命令获取linux常用命令大全pdf+Linux命令行大全pdf...

如何限制他人操作自己的电脑?(如何控制别人的电脑不让发现)

这段时间,小猪罗志祥正处于风口浪尖,具体是为啥?还不知道的小伙伴赶紧去补一下最近的娱乐圈八卦~简单来说,就是我们的小罗同事,以自己超强的体力,以及超强的时间管理能力,重新定义了「多人运动」的含义,重新...

最通俗易懂的命令模式讲解(命令模式百科)

我们先不讲什么是命令模式,先通过一个场景来引出命令模式,看看命令模式能解决什么样的问题。现在有一个渣男张三,他有还几个女朋友,你现在是不是还是单身狗,你就说你气不气?然后他需要每天分别叫几个女朋友起床...

互联网大厂后端必看!Spring Boot 中Runtime执行与停止命令?

你是否曾在使用SpringBoot开发项目时,遇到需要执行系统命令的场景?比如调用脚本进行文件处理,又或是启动外部程序?很多后端开发人员会使用Processexec=Runtime.get...

Linux 常用命令(linux常用的20个命令面试)

日志排查类操作命令...

Java字节码指令:if_icmpgt(0xA3)(java字节码使用的汇编语言)

if_icmpgt是Java字节码中的一条条件跳转指令,其全称是"IfIntegerCompareGreaterThan"。它用于比较两个整数值的大小。如果栈顶的第一个...

外贸干货|如何增加领英的曝光量和询盘

#跨境电商#...

golang执行linux命令(golang调用shell脚本)

需求需要通过openssl生成rsa秘钥,然后保存该秘钥。代码实例packagemainimport("io/ioutil""bytes"&...

LINUX磁盘挂载(linux磁盘挂载到windows)

1、使用root用户查看磁盘挂载情况:fdisk-l2、使用df查看当前磁盘挂载情况,根据和fdisk-l的结果进行对比,查看还有那些磁盘未使用3、挂载:mount磁盘挂载路径...

Linux命令学习——nl命令(linux ln命令的使用)

nl命令主要功能为每一个文件添加行号,每一个输入的文件添加行号后发送到标准输出。当没有文件或文件为-时,读取标准输入...