Redis解决超卖Demo(分布式锁)

引言

多并发引起的超卖问题一直都是秒杀业务的核心,在极短的时间内没有中间件进行过渡,同时向底层DB发起请求,DB很容易崩溃,引入缓存中间件又引起了两者双写一致的问题,真令人头大,紧接着又引入一个异步处理的消息中间件。解决超卖的问题大致流程是,并发下单,先在缓存中对库存进行预减,然后向消息中间件推送成功的消息,然后更新DB数据的接口从消息队列中消费消息更新库存(这是性能较优的一种方案)。本文章是专注于探讨分布式锁的案例。

引入依赖以及yml配置文件

  • pom文件
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • yml文件(根据自身环境修改)
server:
  port: 9000
spring:
  application:
    name: redis
  redis:
    host: 192.168.136.130
    port: 6379
    password: 123456
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s

 配置Redis序列化

package redis.chaomai.test.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
/*
* redis序列化配置
* */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory)
    {
        //缓存序列化配置避免存储乱码

        RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return  redisTemplate;
    }
}

 数据准备

  • 在Redis中添加一条Key为Apple,Value为30000的数据。

        set Apple 30000

不加锁的接口

  • 代码
@RequestMapping("test")
 void cherkAndReduceStock(){
     String stock = redisTemplate.opsForValue().get("Apple").toString();

     if (stock!=null)
     {
         Integer value = Integer.valueOf(stock);
         if (value>0){
             redisTemplate.opsForValue().set("Apple",String.valueOf(--value));
         }
     }

 }
  •  使用Jmeter压测

  • 压测结果 ,库存不为0

 添加分布式锁

  • redis命令setnx,同时为了避免死锁,同时设置锁的过期时间,redis命令set key value ex 2 nx
@RequestMapping("testAddsetxAddFinally")
void cherkAndReduceStockAddSetnxAddFinally()
{
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000",2, TimeUnit.SECONDS);
    //获取锁失败,停止50ms,递归调用
    if (!lock){
        try {
            Thread.sleep(50);
            this.cherkAndReduceStockAddSetnxAddFinally();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }else {
        try {
            String stock = redisTemplate.opsForValue().get("Apple").toString();
            if(stock!=null&&stock.length()!=0)
            {
                Integer valueOf = Integer.valueOf(stock);
                if (valueOf>0)
                {
                    redisTemplate.opsForValue().set("Apple",String.valueOf(--valueOf));
                }else {
                    System.out.println("商品售罄!!!");
                }
            }
        }finally {
            redisTemplate.delete("lock-stock");
        }

    }
}
  • 使用Jmeter压测 ,查看结果

库存为0,问题解决

  • 锁过期被删除,代码还没结束

等同于在半路锁过期被删除了,如下模拟场景

  • 使用Jmeter压测后依旧存在超卖
  • 解决方案:1.使用Redisson提供的自动延期机制(看门狗机制)。            

                         2.配合使用Lua脚本原子性指令操作。

总结

本人对redis分布式锁的一些理解,欢迎大家指正!

<think> 首先,用户的问题是关于在同时使用分布式锁和分布式事务的场景(如优惠券领取)中避免死锁的方法。参考提供的引用信息,我需要整合这些信息来给出一个清晰的回答。 关键引用点: - 引用[1]:提到分布式锁用于处理高并发下的资源竞争,如扣减库存、秒杀抢购等。 - 引用[2]:详细说明了分布式锁需要注意的问题,如互斥性、防死锁、锁的续期、可重入性等。 - 引用[3]:提到设置时时间来避免锁长时间无法释放。 - 引用[4]:给出了一个具体的代码示例,其中使用了分布式锁(SimpleRedisLock)在秒杀场景中,包括获取锁、释放锁的流程。 用户的问题焦点是:在同时使用分布式锁和分布式事务的场景中避免死锁的方法。分布式事务通常涉及多个操作的原子性,而分布式锁用于互斥访问共享资源。 在优惠券领取场景中,可能涉及: - 分布式锁:确保只有一个进程可以访问共享资源(如优惠券库存)。 - 分布式事务:确保整个领取操作的原子性(如检查库存、扣减库存、记录领取记录等)。 死锁风险: 死锁可能发生在多个进程互相等待资源时。例如: - 进程A持有锁X,等待事务完成。 - 进程B持有锁Y,等待锁X,同时进程A在等待进程B释放锁Y。 避免死锁的方法: 基于引用和一般知识: 1. **设置时时间**:给锁设置时时间(如引用[3]所示),防止锁被无限期持有。即使进程崩溃,锁也会自动释放。 2. **锁的续期**:如果操作需要更长时间,可以实现锁的续期机制(如引用[2]提到)。 3. **顺序获取锁**:确保所有进程按固定顺序获取锁,避免循环等待。 4. **避免嵌套锁**:尽量避免在事务中持有多个锁;如果必须,确保所有锁按相同顺序获取。 5. **使用分布式事务协调器**:结合分布式事务管理器(如Seata、XA事务)来协调事务和锁。 6. **死锁检测和恢复**:实现死锁检测机制,当检测到死锁时,进行回滚或资源释放。 7. **可重入锁**:在引用[2]中提到可重入性是分布式锁的一个条件;允许同一个进程多次获取同一把锁,避免自己阻塞自己。 8. **最小化锁持有时间**:只在必要时刻获取锁,并尽快释放(如引用[4]代码示例中,在try-finally块中释放锁)。 在优惠券领取场景中: - 使用分布式锁来保护共享资源(如库存)。 - 使用分布式事务确保操作的原子性。 - 避免死锁:通过在事务边界内正确管理锁,设置时,并使用顺序。 回答结构: - 先解释问题。 - 列出避免死锁的方法。 - 引用相关引用部分。 - 最后生成相关问题。 回答格式: - 使用中文。 - 数学表达式按指令处理(尽管这个问题可能不涉及,但要准备好)。 - 引用的段落末尾添加引用标识。 - 结构清晰。 - 生成相关问题。 生成相关问题: - 基于主题,如: 1. 在分布式系统中处理死锁的其他策略? 2. 如何优化分布式锁的性能? 3. 分布式事务在微服务架构中的挑战? 最终回答大纲: 1. 引言:解释在优惠券领取等场景中同时使用分布式锁和分布式事务的必要性及死锁风险。 2. 避免死锁的方法: - 设置锁时时间。 - 锁的续期机制。 - 顺序获取资源。 - 最小化锁持有时间。 - 使用可重入锁。 - 结合死锁检测。 3. 引用具体引用点。 4. 总结。 5. 相关问题。 </think> ### 同时使用分布式锁和分布式事务避免死锁的方法 在优惠券领取等高并发场景中,同时使用分布式锁(用于互斥访问共享资源,如库存)和分布式事务(用于保证操作的原子性,如扣减库存和记录领取记录)时,死锁风险主要源于多个进程互相等待资源释放。例如: - 进程A持有锁X并等待事务提交,但事务需要进程B持有的锁Y。 - 进程B持有锁Y并等待进程A释放锁X,形成循环等待。 基于引用资料和最佳实践,以下是避免死锁的核心方法: #### 1. **设置锁时时间并实现自动释放** - 为分布式锁添加时机制(TTL),确保即使进程崩溃或事务阻塞,锁也能自动释放,防止死锁僵持。例如,引用[3]展示了Redis中通过`SETNX`和`EXPIRE`命令设置时: ```bash 127.0.0.1:6379> SETNX demo_task 100 (integer) 1 127.0.0.1:6379> EXPIRE demo_task 3600 # 设置3600秒(integer) 1 ``` 在代码中,结合引用[4],可使用`lock.tryLock(1200)`指定时时间(单位秒),并在finally块中释放锁,避免资源永久占用[^3][^4]。 #### 2. **实现锁续期机制(Lease Renewal)** - 对于长事务操作,在锁过期前自动续期,防止事务未完成时锁意外释放导致数据不一致。引用[2]强调“锁的续期问题”是防死锁的关键:如果事务执行时间可能过锁时,需周期性调用续期接口(如Redis的`EXPIRE`)。例如: - 在优惠券领取代码中,启动后台线程定期检查并延长锁有效期。 - 同时,确保“解铃还须系铃人”:加锁和解锁必须是同一客户端,避免误删他人锁[^2]。 #### 3. **严格定义资源获取顺序** - 所有进程按全局固定顺序申请锁(如先锁用户ID再锁优惠券ID),打破循环等待条件。引用[2]指出分布式锁需满足“互斥性”和“可重入性”:顺序获取可防止嵌套锁死锁。例如: - 在领取场景,统一要求:先获取用户级锁(防重复领取),再获取库存级锁(防)[^1][^2]。 #### 4. **最小化锁持有时间,隔离事务边界** - 仅在关键操作前获取锁,并在操作后立即释放,减少锁竞争窗口。引用[4]的代码示例中,锁仅在“扣减库存”前获取,事务提交后马上释放: ```java boolean isLock = lock.tryLock(1200); // 获取锁 try { // 事务操作:扣减库存、记录领取 return proxy.createVoucherOrder(voucherId); } finally { lock.unlock(); // 确保释放 } ``` - 将分布式事务(如Seata或XA协议)与锁分离:事务内不嵌套跨资源锁,避免事务回滚导致锁未释放[^4]。 #### 5. **集成死锁检测和重试机制** - 监控锁等待图:当多个进程等待时(如引用[3]的时设置),触发报警或自动回滚。 - 结合引用[2]的“防死锁”要求,实现指数退避重试:获取锁失败时等待随机时间再试,而非阻塞。例如: - 在优惠券代码中,若`lock.tryLock()`失败,返回错误并提示用户重试[^4]。 #### 6. **选择支持高可用的分布式锁实现** - 优先用Redis或ZooKeeper等具备容错性的系统(引用[2]),避免单点故障引发死锁。例如,Redis集群通过多数节点确认锁状态,确保“高可用”[^2]。 ### 总结 在优惠券领取场景中,核心是**时机制 + 顺序加锁 + 最小持有时间**。例如: 1. 设置锁TTL(如1200秒)。 2. 统一顺序:先锁用户 → 再锁库存。 3. 事务完成后立即释放锁。 4. 添加重试逻辑防阻塞。 这些方法能有效降低死锁概率,确保系统高并发下的稳定性[^1][^2][^3][^4]。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值