Redis之秒杀活动

目录

全局唯一ID:

为什么 count 不可能为 null?

为什么返回值是 timestamp << COUNT_BITS | count?

整体的逻辑 

(1) 生成时间戳

(2) 生成序列号

(3) 拼接时间戳和序列号

超卖问题:

基于版本号的乐观锁

CAS思想

一人一单:

不建议在方法上直接加锁

更合理的加锁方式

问题:toString 无法保证锁唯一性

解决方法:使用 String.intern()

将锁移动到外部

注意事务失效问题

分布式锁: 

什么是分布式锁?

分布式锁的关键特性

分布式锁的选择

ILOCK接口:

 锁类的代码:

为什么需要面向接口编程?

解耦,提高代码灵活性

为什么不能仅仅使用 Thread.currentThread().getId() 作为锁的唯一标识存储在 Redis 中?

1. 存在冲突和混淆的风险

2. 不同 JVM 的线程 ID 可能重复

解决方案

问题描述

 解决Redis误删锁的问题:

用 Lua 解决锁判断和释放的原子性问题

背景问题

Lua 脚本解决

Redisson

异步秒杀


全局唯一ID:

 为什么采用64-bit long 类型,时间戳为 32-bit,序列号为 31-bit?

采用 64-bit long 类型

  • Java 的 long 类型是 64-bit,能够表示的整数范围非常大,适合生成全局唯一的 ID。
  • 使用一个整数类型作为唯一 ID,比字符串等其他类型更加高效,节省存储空间和计算资源。

时间戳 32-bit

  • 时间戳占据 32 位,足够表示未来很长一段时间的秒数。以 BEGIN_TIMESTAMP = 1736121600 为起点(约为 2024 年 12 月 1 日),加上 2³² 秒(约 136 年),可以覆盖到 2160 年。

序列号 31-bit

  • 序列号占据 31 位,支持在同一秒内生成 2³² - 1个唯一的 ID(约 21 亿个),足够应对大多数高并发场景。

保留 1-bit 未使用

  • 通常,最高位(第 64 位)保留不用,以防止负数的情况(符号位)。如果需要,也可以将它用于其他目的。一般第一位为0保证ID值不为负数

使用Redis实现全局唯一ID:

package com.hmdp.utils;import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;@Component
@RequiredArgsConstructor
public class RedisIdWorker {final StringRedisTemplate stringRedisTemplate;/*** 开始时时间戳**/private  static final long BEGIN_TIMESTAMP = 1736121600L;private static  final  int COUNT_BITS = 32; // 表示时间戳需要往左移动几位public long nextId(String keyPrefix){// 生成时间戳LocalDateTime now = LocalDateTime.now();long nowEpochSecond = now.toEpochSecond(ZoneOffset.UTC);// 表示和初始时间相差多少long timestamp = nowEpochSecond - BEGIN_TIMESTAMP;// 生成序列号// 获取生成ID号的时间String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + data);// 为什么count不可能是null呢?// 如果不加上时间作为key的话,Redis的自增值会达到上限// 拼接并且返回return timestamp << COUNT_BITS | count;}public static void main(String[] args) {LocalDateTime time = LocalDateTime.of(2025, 1, 6, 0, 0, 0);long epochSecond = time.toEpochSecond(ZoneOffset.UTC);System.out.println(epochSecond);}}

为什么 count 不可能为 null

count 的值来源于 stringRedisTemplate.opsForValue().increment() 方法:

increment() 是 Redis 的自增操作:

  • 如果指定的键不存在,Redis 会自动初始化键值为 0,然后执行自增操作,结果是 1。
  • 因此,increment() 的返回值不可能为 null

为什么返回值是 timestamp << COUNT_BITS | count

timestamp << COUNT_BITS

  • 将时间戳左移 32 位(COUNT_BITS),腾出低 32 位给序列号。时间戳占高位,保证生成的 ID 随时间递增。

| count

  • 使用按位或操作,将序列号填入低 32 位。序列号是每秒递增的,用于区分同一秒内的多个 ID。
  • 按位或操作(|)在二进制中逐位比较两个数的每一位,只要有一个为 1,结果就为 1。这个特性使得它可以将某些位的值“合并”到一个数字中,而不改变原来其他位的值。
  • 这两个部分结合起来形成一个唯一的 64-bit 整数 ID。

整体的逻辑 

(1) 生成时间戳

long nowEpochSecond = now.toEpochSecond(ZoneOffset.UTC);

long timestamp = nowEpochSecond - BEGIN_TIMESTAMP;

  • 当前时间 now 转换为秒级时间戳。
  • 减去初始时间戳 BEGIN_TIMESTAMP,得到相对时间戳 timestamp
(2) 生成序列号

long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + data);

  • 基于 Redis 的 increment() 操作生成序列号。
  • 每秒一个 Redis 键("icr:<keyPrefix>:<data>"),保证同一秒内序列号从 1 开始递增。
  • 如果 Redis 键不存在,increment() 会初始化为 0,然后返回 1。
(3) 拼接时间戳和序列号

return timestamp << COUNT_BITS | count;

  • 将时间戳左移 32 位(COUNT_BITS),腾出低 32 位。
  • 通过按位或操作将序列号填入低 32 位。
  • 得到一个 64-bit 的唯一整数 ID。

UUID:

雪花算法(snowflake) :

超卖问题:

基于版本号的乐观锁

原理:在数据表中新增一个版本号字段(如 version),每次更新数据时,要求 version 字段值与当前数据库中的版本号匹配,只有匹配成功时才允许更新。更新后,version 自动加一。

流程

  1. 读取数据时,获取当前版本号。
  2. 更新数据时,带上该版本号作为条件。
  3. 数据库执行更新时会检查版本号是否一致,若一致,则更新成功;否则更新失败。
-- 查询数据和版本号
SELECT id, name, version FROM user WHERE id = 1;-- 更新数据时,使用版本号作为条件
UPDATE user 
SET name = 'new_name', version = version + 1 
WHERE id = 1 AND version = 1;

优点:简单直观,可防止并发修改。

缺点:需要在表中增加版本号字段。

CAS思想

CAS 是一种基于比较和交换的机制,用于确保数据的原子性更新。

基于版本号的乐观锁虽然是一种常见实现方式,但其缺点是需要在数据库表中额外新增一个字段(如 version 字段)。然而,我们可以利用数据库中已有的字段(例如价格、库存数量、更新时间等),通过在执行 SQL 语句时将这些字段的值与执行前查询到的值进行比较,实现类似的乐观锁功能,从而避免新增字段的开销。

CAS 操作包含以下三个核心要素:

执行前查询到的值

  • 这是在操作开始之前,从数据库中读取的字段值(例如,某个商品的库存数量 stock = 100)。
  • 它反映了我们未被修改之前数据的状态,在后续更新时用于与数据库中的当前值进行比较。

当前值(Current Value)

  • 这是在执行 SQL 更新时,数据库中该字段的实际值。
  • 如果数据库中的当前值与预期值一致,说明数据在操作期间未被其他事务修改,可以安全地执行更新操作。

判断之前是否已经被修改了

  • 如果预期值与当前值相等,则执行更新操作,将字段值更新为目标值。
  • 如果不相等,说明数据在操作期间已经被其他事务修改,放弃修改数据库,返回失败。

使用乐观锁存在一个缺陷,就是只要发现别人修改了就放弃执行sql语句,会导致请求的大量失败。

所以还有一个更简单的办法,就是在更新的时候,查看库存是否大于0即可

​
@Service
@RequiredArgsConstructor
@Transactional
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;final RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 判断秒杀是否开始LocalDateTime beginTime = voucher.getBeginTime();if(LocalDateTime.now().isBefore(beginTime)){return Result.fail("秒杀还未开始");}// 判断秒杀是否已经结束LocalDateTime endTime = voucher.getEndTime();if(LocalDateTime.now().isAfter(endTime)){return Result.fail("秒杀已经结束");}// 判断库存是否充足if (voucher.getStock() < 1) {return Result.fail("库存不足!");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).
//                eq("stock",voucher.getStock()). // 加个乐观锁,如果现在的库存和我之前查询的库存不相同,说明在我之前就有线程修改了数据库gt("stock", 0).update();if (!success) {return Result.fail("库存不足!");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单idlong orderID = redisIdWorker.nextId("order");voucherOrder.setId(orderID);// 用户idvoucherOrder.setUserId(UserHolder.getUser().getId());// 代金券idvoucherOrder.setVoucherId(voucherId);// 写入数据库save(voucherOrder);// 返回订单idreturn Result.ok(orderID);}
}​

一人一单:

也就是需要在在判断库存充足之后还需要根据优惠券的id和用户的id来查询订单。如果订单存在就返回异常(说明该用户之前已经购买过了) 如果订单不存在我们就可以扣减库存并且可以创建订单。

// 前面代码一致。。。。 
// 实现一人一单,我们需要先判断该用户是否已经抢过了// 根据优惠券id和用户id查询订单Long userId = UserHolder.getUser().getId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("已经购买过,不可重复购买!");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).
//                eq("stock",voucher.getStock()). // 加个乐观锁,如果现在的库存和我之前查询的库存不相同,说明在我之前就有线程修改了数据库gt("stock", 0).update();if (!success) {return Result.fail("库存不足!");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单idlong orderID = redisIdWorker.nextId("order");voucherOrder.setId(orderID);// 用户idvoucherOrder.setUserId(UserHolder.getUser().getId());// 代金券idvoucherOrder.setVoucherId(voucherId);// 写入数据库save(voucherOrder);// 返回订单idreturn Result.ok(orderID);

 在以上我们在查询用户是否已经下单在并发的情况下存在问题:

当线程 A 查询到 count 的值为 0(表示用户之前没有下过单),但还未完成插入订单数据的操作时,线程 B 可能在此时抢到了 CPU 的执行权,并且同样查询到 count 的值为 0。由于这两个线程的操作是并发的,线程 B 也会插入订单数据,导致用户可以多次下单。这种情况违反了“一人一单”的业务需求。

为了避免这种问题的发生,我们需要在 查询用户是否已经下单插入订单数据 这整个过程中使用一把锁进行保护,确保同一时间只有一个线程能够执行这一逻辑。这样,即使多个线程同时进入方法,也只有第一个线程能够完成操作,其余线程会被阻塞或直接返回,达到线程安全的目的。

可以将这段逻辑提取成一个方法,使用事务管理和锁机制,保证查询和插入操作的原子性。具体代码实现如下:

    @Transactionalpublic synchronized Result createVoucherOrder(Long voucherId) {// 实现一人一单,我们需要先判断该用户是否已经抢过了// 根据优惠券id和用户id查询订单Long userId = UserHolder.getUser().getId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("已经购买过,不可重复购买!");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).
//                eq("stock",voucher.getStock()). // 加个乐观锁,如果现在的库存和我之前查询的库存不相同,说明在我之前就有线程修改了数据库gt("stock", 0).update();if (!success) {return Result.fail("库存不足!");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单idlong orderID = redisIdWorker.nextId("order");voucherOrder.setId(orderID);// 用户idvoucherOrder.setUserId(UserHolder.getUser().getId());// 代金券idvoucherOrder.setVoucherId(voucherId);// 写入数据库save(voucherOrder);// 返回订单idreturn Result.ok(orderID);}

不建议在方法上直接加锁

将锁对象加在方法上会导致以下问题:

  1. 锁的范围过大:锁定整个方法,降低并发性能。
  2. 锁对象为 this:无论哪个用户访问,都需要获取同一把锁,导致线程串行执行,性能较差。

更合理的加锁方式

加锁的对象应该基于业务需求选择更细粒度的对象,例如用户ID(userId),以减小锁的范围。

    @Transactionalpublic  Result createVoucherOrder(Long voucherId) {// 实现一人一单,我们需要先判断该用户是否已经抢过了// 根据优惠券id和用户id查询订单Long userId = UserHolder.getUser().getId();synchronized (userId.toString()) {//具体的业务逻辑}}
问题:toString 无法保证锁唯一性

userId.toString() 每次都会生成一个新的字符串对象,因此不能保证锁定的是同一个对象,这会导致 synchronized 失效,从而无法有效控制并发。

解决方法:使用 String.intern()

可以通过 userId.toString().intern() 来保证锁的唯一性。intern() 方法会将字符串存储到 JVM 的字符串常量池中,相同的字符串值会返回相同的引用,确保锁对象唯一。

synchronized (userId.toString().intern()) { //具体的业务逻辑}

调用 intern() 方法后,userId.toString().intern() 能够保证唯一性,是因为 String.intern() 方法将字符串存储到 JVM 的字符串常量池(String Pool)中。对于相同的字符串值,intern() 方法确保返回的引用是同一个对象,从而实现锁对象的唯一性。

在方法内部加锁还存在一个问题:

将锁移动到外部

在方法内部加锁存在另一个问题:
方法结束时锁会释放,但事务提交是延后的。如果在事务提交前,其他线程查询到数据库发现没有订单,仍可能导致“一人多单”的问题。

因此,锁应放在方法外部:

// seckillVoucher方法
public Result seckillVoucher(Long voucherId) {// 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 判断秒杀是否开始LocalDateTime beginTime = voucher.getBeginTime();if(LocalDateTime.now().isBefore(beginTime)){return Result.fail("秒杀还未开始");}// 判断秒杀是否已经结束LocalDateTime endTime = voucher.getEndTime();if(LocalDateTime.now().isAfter(endTime)){return Result.fail("秒杀已经结束");}// 判断库存是否充足if (voucher.getStock() < 1) {return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()){return createVoucherOrder(voucherId);}
}

注意事务失效问题

在上述代码中,直接调用 createVoucherOrder(voucherId) 可能导致事务失效问题。这是因为 Spring 的事务管理基于动态代理,直接调用会绕过代理,导致事务功能失效。

解决方法是通过 AopContext 获取当前类的代理对象来调用方法:

  IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);

如果有多个JVM的存在,每个JVM都会有自己的锁导致每一个锁,因此对于集群来说,还是会导致一人多单的。(还是锁对象不唯一导致的)

在分布式系统中,每个 JVM 都有自己的内存空间,因此即使在单机环境中通过 synchronizedString.intern() 保证了锁对象的唯一性,到了集群环境中,不同的 JVM 实例仍然可能生成不同的锁对象,从而导致 分布式环境下并发控制失效

分布式锁: 

基于Redis的分布式锁:

什么是分布式锁?

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

分布式锁是一种跨多个进程或服务器实例的机制,用于在分布式系统中对共享资源实现同步控制。它是为了解决多个进程或服务访问同一资源时的并发问题而设计的,确保在同一时间只有一个进程能够访问某个共享资源。

在单机环境中,synchronizedReentrantLock 等 JVM 内部的锁机制可以很好地解决并发问题,但在分布式系统中,多个实例运行在不同的 JVM 上,内存不共享,传统的锁机制就无法满足需求。这时,就需要 分布式锁

分布式锁的关键特性

  1. 互斥性:同一时间只有一个客户端能够获得锁,确保对共享资源的独占访问。
  2. 容错性:即使一个客户端因故障未释放锁,系统也能通过机制(如超时)保证锁最终释放。
  3. 高可用性:分布式锁的获取和释放需要快速响应,且系统中的单点故障不应导致锁机制不可用

分布式锁的选择

实现方式适用场景优点缺点
Redis 锁高性能、高并发场景简单高效,性能优越锁过期或释放需要谨慎处理
ZooKeeper 锁高可靠性和一致性要求场景可靠性高,支持自动释放性能较低,维护成本高
数据库锁并发量小的场景简单易用,无需额外基础设施性能受限,可能出现锁争用

Redis 储存的实际上是锁的唯一值。这个唯一值的主要作用是区分哪个客户端线程持有了锁

ILOCK接口:

package com.hmdp.utils;public interface ILock {/*** 尝试获取锁* @param timeoutSec 锁持有的超时时间,过期后自动释放* @return true代表获取锁成功;false代表获取锁失败*/boolean tryLock(long timeoutSec);/*** 释放锁*/void unlock();
}

 锁类的代码:

package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements  ILock{public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static  final String KEY_PREFIX  = "lock";private String name; // 锁的名称private StringRedisTemplate stringRedisTemplate;@Overridepublic boolean tryLock(long timeoutSec) {Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, Thread.currentThread().getId()+"",timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success) ; // 自动拆箱可能会存在空指针}@Overridepublic void unlock() {stringRedisTemplate.delete(KEY_PREFIX + name);}
}
 public Result seckillVoucher(Long voucherId) {// 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 判断秒杀是否开始LocalDateTime beginTime = voucher.getBeginTime();if(LocalDateTime.now().isBefore(beginTime)){return Result.fail("秒杀还未开始");}// 判断秒杀是否已经结束LocalDateTime endTime = voucher.getEndTime();if(LocalDateTime.now().isAfter(endTime)){return Result.fail("秒杀已经结束");}// 判断库存是否充足if (voucher.getStock() < 1) {return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();// 创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);// 获取锁boolean isLock = lock.tryLock(1200);if(! isLock){// 说明没有获取到锁return Result.fail("不可重复下单!");}// 我们需要获取代理(事务)对象才行//try  finally  事务可以生效,因为没有捕获异常。// 如果catch捕获了异常,需要抛出RuntimeException类型异常,不然事务失效。// 这里加了catch事务也能生效。因为事务失效的场景是在事务方法内部try catch消化掉异常,而这里try catch是在事务方法外部(可以自己抛异常测试一下)try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {lock.unlock();}}

为什么需要面向接口编程?

解耦,提高代码灵活性
  • 问题
    如果直接依赖具体类(例如 SimpleRedisLock),系统会被绑定到 Redis 实现上。如果未来需求变化(如改用 ZooKeeper 或数据库实现分布式锁),就需要修改所有依赖锁的代码,增加维护成本。

  • 解决
    使用接口(例如 ILock)定义分布式锁的行为。业务代码只依赖 ILock 接口,与具体实现无关。这样,当需要更换锁的实现时,只需新增实现类(如 ZookeeperLock),无需修改调用代码,极大提升了灵活性和可扩展性。


为什么不能仅仅使用 Thread.currentThread().getId() 作为锁的唯一标识存储在 Redis 中?

1. 存在冲突和混淆的风险
  • 线程 ID 是数字,直接使用 Thread.currentThread().getId() 可能导致锁值与其他业务逻辑的 Redis 键值冲突或混淆,特别是在调试和监控时,难以区分锁的来源。
2. 不同 JVM 的线程 ID 可能重复
  • 在分布式系统中,不同 JVM 实例可能生成相同的线程 ID。例如,一个 JVM 的线程 ID 为 1,另一个 JVM 的线程也可能是 1,这会导致锁的唯一性失效,从而引发并发问题。

解决方案

使用 UUID + 线程 ID 的组合作为锁的唯一值:

  • UUID 保证跨 JVM 的唯一性。
  • 线程 ID 提供额外的信息,用于定位锁的具体来源。
    String threadId = UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
    

以上代码还存在一个问题,让我们来看以下的场景

问题描述

  1. 线程 A 获取锁:线程 A 成功获取锁并设置了过期时间。
  2. 线程 A 阻塞:由于业务逻辑执行时间较长,线程 A 被阻塞,锁的过期时间已到,Redis 自动释放了锁。
  3. 线程 B 获取锁:此时线程 B 成功获取到锁并开始执行逻辑。
  4. 线程 A 释放锁:线程 A 在执行完逻辑后尝试释放锁,但此时锁已经被线程 B 重新获取,A 无法确认锁是否属于自己,直接删除了锁。
  5. 线程 C 获取锁:因为锁被 A 误删,线程 C 也获得锁,导致锁的控制完全失效,进而出现并发问题。

为了解决锁误删问题,我们需要在释放锁时确认锁的持有者,确保只有锁的持有者才能释放锁。也就是需要在释放锁之前还需要判断是否是自己的锁。这可以通过以下方法实现: 

 解决Redis误删锁的问题:

添加线程标识,并在删除之前判断线程标识

package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements  ILock{public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static  final String KEY_PREFIX  = "lock";private static  final String ID_PREFIX  = UUID.randomUUID().toString(true) + "-";private String name; // 锁的名称private StringRedisTemplate stringRedisTemplate;@Overridepublic boolean tryLock(long timeoutSec) {// 不能使用Thread.currentThread().getId()+""作为唯一的key的标识,因为不同的JVM可能会创建出相同的idString threadId = ID_PREFIX + Thread.currentThread().getId(); // 使用当前线程的唯一标识(线程ID)作为锁的归属标识。Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name,threadId,timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success) ; // 自动拆箱可能会存在空指针}
// 这里还有问题:get和delete不是原子性的。
// 假如时间到期了,另一个线程获取到了锁,这时候你又给删除了@Overridepublic void unlock() {// 判断锁是不是自己的String threadId = ID_PREFIX + Thread.currentThread().getId();if ( stringRedisTemplate.opsForValue().get(KEY_PREFIX+name).equals(threadId)) {// 是自己的锁stringRedisTemplate.delete(KEY_PREFIX + name);}}
}

用 Lua 解决锁判断和释放的原子性问题

背景问题

在 Redis 分布式锁中,可能会出现以下场景:

  1. 线程 A 持有锁并因阻塞导致锁超时。
  2. 线程 B 获得了锁。
  3. 线程 A 恢复运行,并认为锁仍是它的(因为判断逻辑和释放逻辑不是原子性的)。
  4. 线程 A 错误地释放了线程 B 的锁。
Lua 脚本解决

解决方案是针对Java jvm 的阻塞问题的解决。以下是一个示例脚本:

-- Lua 脚本用于判断锁是否属于当前线程并释放
local lockKey = KEYS[1]  -- 锁的 key
local lockValue = ARGV[1]  -- 当前线程的唯一标识-- 判断锁是否属于当前线程
if redis.call("GET", lockKey) == lockValue then-- 如果是,释放锁return redis.call("DEL", lockKey)
else-- 如果不是,返回 0,不执行删除return 0
end

 

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get',KEYS[1])==ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}    
public void unlock() {/**
// 判断锁是不是自己的String threadId = ID_PREFIX + Thread.currentThread().getId();if ( stringRedisTemplate.opsForValue().get(KEY_PREFIX+name).equals(threadId)) {// 是自己的锁stringRedisTemplate.delete(KEY_PREFIX + name);}*/// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());}

DefaultRedisScript 是 Spring Data Redis 提供的类,用于封装 Lua 脚本。

setLocation 指定 Lua 脚本文件的位置,这里脚本文件名为 unlock.lua,存放在类路径(classpath)下。

setResultType 设置 Lua 脚本的返回值类型,此处为 Long,表示 Lua 脚本会返回一个整数(如 01)。

unlock.lua 脚本文件的作用应该是:

  • 判断当前线程是否持有锁。
  • 如果是,则释放锁。
  • 如果不是,则不进行任何操作。

通过 Lua 脚本,将锁的判断和释放操作合并为一个原子性操作,能有效解决线程 A 错误释放线程 B 的锁的问题。Redis 的单线程特性保证了脚本的执行顺序和一致性,是解决此类问题的最佳选择之一。 

Redisson

详细的Redisson的介绍请看

分布式锁Redisson详解,Redisson如何解决不可重入,不可重试,超时释放,主从一致问题的分析解决(包括源码简单分析)-CSDN博客

异步秒杀

我们还可以使用异步秒杀来优化秒杀。详细在以下博客

Redis 优化秒杀(异步秒杀)-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/67713.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VSCode 在Windows下开发时使用Cmake Tools时输出Log乱码以及CPP文件乱码的终极解决方案

在Windows11上使用VSCode开发C程序的时候&#xff0c;由于使用到了Cmake Tools插件&#xff0c;在编译运行的时候&#xff0c;会出现输出日志乱码的情况&#xff0c;那么如何解决呢&#xff1f; 这里提供了解决方案&#xff1a; 当Settings里的Cmake: Output Log Encoding里设…

【C++经典例题】求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a; 期待您的关注 题目描述&#xff1a; 原题链接&#xff1a; 求123...n_牛客题霸_牛客网 (nowcoder.com) 解题思路&#xff1a; …

STM32 单片机 练习项目 LED灯闪烁LED流水灯蜂鸣器 未完待续

个人学习笔记 文件路径&#xff1a;程序源码\STM32Project-DAP&DAPmini\1-1 接线图 3-1LED闪烁图片 新建项目 新建项目文件 选择F103C8芯片 关闭弹出窗口 拷贝资料 在项目内新建3个文件夹 Start、Library、User Start文件拷贝 从资料中拷贝文件 文件路径&#xff1a;固…

DAY15 神经网络的参数和变量

DAY15 神经网络的参数和变量 一、参数和变量 在神经网络中&#xff0c;参数和变量是两个关键概念&#xff0c;它们分别指代不同类型的数据和设置。 参数&#xff08;Parameters&#xff09; 定义&#xff1a;参数是指在训练过程中学习到的模型内部变量&#xff0c;这些变量…

VS Code 可视化查看 C 调用链插件 C Relation

简介 一直想用 SourceInsight 一样的可以查看函数调用链一样的功能&#xff0c;但是又不想用 SourceInsight&#xff0c;找了一圈没有找到 VS Code 有类似功能的插件&#xff0c;索性自己开发了一个。 这是一个可以可视化显示 C 函数调用关系的 VS Code 插件&#xff0c;功能纯…

(k8s)kubectl不断重启问题解决!

1.问题描述&#xff1a; 在服务器上安装完k8s之后&#xff0c;会出现kubectl有时候连得上&#xff0c;等之后再去连接的时候又断开&#xff0c;同时节点出现了NotReady的情况&#xff0c; 出现了这两种双重症状 2.解决问题 自己的思路&#xff1a;查看日志&#xff0c;检查报…

什么是数据湖?大数据架构的未来趋势

&#x1f496; 欢迎来到我的博客&#xff01; 非常高兴能在这里与您相遇。在这里&#xff0c;您不仅能获得有趣的技术分享&#xff0c;还能感受到轻松愉快的氛围。无论您是编程新手&#xff0c;还是资深开发者&#xff0c;都能在这里找到属于您的知识宝藏&#xff0c;学习和成长…

【Leetcode·中等·数组】59. 螺旋矩阵 II(spiral matrix ii)

题目描述 英文版描述 Given a positive integer n, generate an n x n matrix filled with elements from 1 to n(2) in spiral order. Example 1: Input: n 3 Output: [[1,2,3],[8,9,4],[7,6,5]] 提示&#xff1a; 1 < n < 20 英文版地址 https://leetcode.com…

Open WebUI 与 AnythingLLM 安装部署

在前文 Ollama私有化部署大语言模型LLM&#xff08;上&#xff09;-CSDN博客 中通过Ollama来搭建运行私有化大语言模型&#xff0c;但缺少用户交互的界面&#xff0c;特别是Web可视化界面。 对此&#xff0c;本文以Open WebUI和AnythingLLM为例分别作为Ollama的前端Web可视化界…

论文导读 | 数据库系统中基于机器学习的基数估计方法

背景 基数估计任务是在一个查询执行之前预测其基数&#xff0c;基于代价的查询优化器&#xff08;Cost Based Optimizer&#xff09;将枚举所有可能的执行计划&#xff0c;并利用估计的基数选出期望执行代价最小的计划&#xff0c;从而完成查询优化的任务。 然而&#xff0c;…

3D扫描建模有哪些优势和劣势?

3D扫描建模作为一种先进的数字化手段&#xff0c;在多个领域展现出了巨大的潜力和价值&#xff0c;但同时也存在一些劣势。以下是对3D扫描建模优势和劣势的详细分析&#xff1a; 3D扫描建模的优势 高精度数据采集&#xff1a; 三维扫描技术能够以极高的精度获取物体的三维数…

网络安全 信息收集入门

1.信息收集定义 信息收集是指收集有关目标应用程序和系统的相关信息。这些信息可以帮助攻击者了解目标系统的架构、技术实现细节、运行环境、网络拓扑结构、安全措施等方面的信息&#xff0c;以便我们在后续的渗透过程更好的进行。 2.收集方式-主动和被动收集 ①收集方式不同…

MBM指尖六维力触觉传感器:高灵敏度、低漂移,精准掌控力学世界

MBM指尖六维力触觉传感器是一种专为机器人设计的高性能传感器。它通过集成三轴力和三轴力矩的感知能力&#xff0c;能够精准捕捉复杂的力学信息。传感器采用MEMS与应变体复合测量技术&#xff0c;具备数字输出功能&#xff0c;显著降低漂移并减少安装偏移的影响。其紧凑轻便的设…

用 Python 绘制可爱的招财猫

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​​​​​ ​​​​​​​​​ ​​​​ 招财猫&#xff0c;也被称为“幸运猫”&#xff0c;是一种象征财富和好运的吉祥物&#xff0c;经常…

docker常用命令及dockerfile编写

docker常用命令及dockerfile编写 1.docker常用命令1.1镜像相关1.2容器相关1.3数据卷1.4网络模式 2.Dockerfile3.Dockerfile示例 1.docker常用命令 1.1镜像相关 镜像相当于是一个模板&#xff0c;可以实例化出很多个容器&#xff1b; #查看docker版本 docker -v#查看docker默…

2025新年源码免费送

2025很开门很开门的源码免费传递。不需要馒头就能获取4套大开门源码。 听泉偷宝&#xff0c;又进来偷我源码啦&#x1f44a;&#x1f44a;&#x1f44a;。欢迎偷源码 &#x1f525;&#x1f525;&#x1f525; 获取免费源码以及更多源码&#xff0c;可以私信联系我 我们常常…

springboot + vue+elementUI图片上传流程

1.实现背景 前端上传一张图片&#xff0c;存到后端数据库&#xff0c;并将图片回显到页面上。上传组件使用现成的elementUI的el-upload。、 2.前端页面 <el-uploadclass"upload-demo"action"http://xxxx.xxx.xxx:9090/file/upload" :show-file-list&q…

如何用 ESP32-CAM 做一个实时视频流服务器

文章目录 ESP32-CAM 概述ESP32-S 处理器内存Camera 模块MicroSD 卡槽天线板载 LED 和闪光灯其他数据手册和原理图ESP32-CAM 功耗 ESP32-CAM 引脚参考引脚排列GPIO 引脚哪些 GPIO 可以安全使用&#xff1f;GPIO 0 引脚MicroSD 卡引脚 ESP32-CAM 的烧录方式使用 ESP32-CAM-MB 编程…

Virgo:增强慢思考推理能力的多模态大语言模型

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

照片做成图书小程序开发制作介绍

照片做成图书小程序系统&#xff0c;主要是让用户直接通过小程序选择需要做成书的类型和照片排版布局模板&#xff0c;以及上传照片的数量。照片上传完成后&#xff0c;生成模板图片样式进行预览或编辑修改。修改完成全部保存。保存后生成完整的照片书进行预览没问题&#xff0…