RedisTemplate解决高并发下秒杀系统库存超卖方案 — Redis实现分布式锁机制

95 篇文章 3 订阅
12 篇文章 1 订阅
😊 @ 作者: 一恍过去
🎊 @ 社区: Java技术栈交流
🎉 @ 主题: RedisTemplate解决高并发下秒杀系统库存超卖方案 — Redis实现分布式锁机制
⏱️ @ 创作时间: 2022年05月18日

在这里插入图片描述

前言

分布式锁对的实现,redis提供的SETNX命令,简单的基本使用流程如下:

  • 创建锁:
    • 在Redis中使用SETNX命令尝试设置一个键值对,其中键表示锁的名称,值可以是任意唯一标识符(例如客户端ID或随机生成的唯一字符串)。
    • 如果SETNX命令返回1,表示成功设置了键值对,即锁创建成功。此时,该客户端获得了该锁。
    • 如果SETNX命令返回0,表示该锁已经被其他客户端占用,锁创建失败。
  • 释放锁:客户端在完成任务后,使用DEL命令删除对应的锁键,释放锁资源。
  • 锁的过期时间:为了防止某个客户端在获取锁后发生故障或崩溃,导致锁无法被释放,可以设置锁的过期时间。可以在创建锁时使用SETNX命令同时设置一个过期时间。

1、场景

秒杀系统存在高并发的场景,在对商品进行秒杀时,由于并发过高可能会导致库存超卖的情况,那么可以通过Redis提供的事务机制超卖问题;通过Redis提供的SetExNx机制实现上锁一致性,利用lua脚本语句,实现解锁一致性,而从解决超卖问题;

加锁原子性:通过redis自身的setnxex命令即可,setIfAbsent(“lockKey”, value, timeOut, TimeUnit);

解锁原子性:通过redis+lua脚本实现;

2、复现超卖场景

2.1 初始化库存接口

@RestController
@RequestMapping("/redis")
@Slf4j
public class RedisController {
	@Resource
    private RedisTemplate redisTemplate;
	
	//记录实际卖出的商品数量
    private AtomicInteger successNum = new AtomicInteger(0);

	@GetMapping(value = "/init")
    public String init() {
        // 初始化库存数量,模拟库存只要5个商品,写入到redis中
        redisTemplate.opsForValue().set("stock", 5);
		successNum.set(0);
        log.info("===>>>库存初始化成功,库存数为" + 5);
        return "初始化库存成功";
    }
}

2.2 库存扣减接口

@RestController
@RequestMapping("/redis")
@Slf4j
public class RedisController {
	@Resource
    private RedisTemplate redisTemplate;
	
	//记录实际卖出的商品数量
    private AtomicInteger successNum = new AtomicInteger(0);
    
	@GetMapping(value = "/reduce")
    public String reduce() {
        int stock = (Integer) redisTemplate.opsForValue().get("stock");
        log.info("===>>>当前数量" + stock);
        // 模拟只减少一个库存
        stock = stock - 1;
        if (stock < 0) {
            log.info("===>>>库存不足");
            return "库存不足";
        }
        // 将剩余数量回写到redis
        redisTemplate.opsForValue().set("stock", stock);
        // 记录实际卖出的商品数量(线程安全每个请求都会记录)
        log.info("===>>>减少库存成功,共出售" + successNum.incrementAndGet());
        return "减少库存成功";
    }
}

2.3 测试

使用工具JMeter模拟并发请求,此处模拟每秒200次JMeter工具使用参考博客:https://blog.csdn.net/tianqingmuyu/article/details/108401543

注意:测试前先执行初始化库存接口,保证库存写入到Redis中
使用JMeter请求接口,结果如下图:
在这里插入图片描述

3、解决超卖实现

3.1 初始化库存接口

@RestController
@RequestMapping("/redis")
@Slf4j
public class RedisController {
	@Resource
    private RedisTemplate redisTemplate;
	
	//记录实际卖出的商品数量
    private AtomicInteger successNum = new AtomicInteger(0);
    
	@GetMapping(value = "/init")
    public String init() {
        // 初始化库存数量,模拟库存只要5个商品,写入到redis中
        redisTemplate.opsForValue().set("stock", 5);
		successNum.set(0);
        log.info("===>>>库存初始化成功,库存数为" + 5);
        return "初始化库存成功";
    }
}

3.2 库存扣减接口

@RestController
@RequestMapping("/redis")
@Slf4j
public class RedisController {
	@Resource
    private RedisTemplate redisTemplate;
	
	//记录实际卖出的商品数量
    private AtomicInteger successNum = new AtomicInteger(0);
    
	@GetMapping(value = "/reduce")
    public String reduce() {
        int stock = (Integer) redisTemplate.opsForValue().get("stock");
        if (stock <= 0) {
            log.info("===>>>库存不足");
            return "库存不足";
        }
        String LOCK_KEY = "lockKey";
        String value = UUID.randomUUID().toString();
        // value值任意即可,秒杀设置锁的时间为1秒(根据实际情况更多)
        boolean absent = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, value, 1, TimeUnit.SECONDS);
        if (absent) {
            // 当前key没有锁,加锁成功
            log.info("===>>>加锁成功,获取并扣减库存");

            Integer sku = (Integer) redisTemplate.opsForValue().get("stock");
            //模拟只减少一个库存
            sku = sku - 1;
            if (sku < 0) {
                log.info("===>>>库存不足");
                // 执行脚本 删除锁
                redisLockServer.deleteLock(LOCK_KEY, value);

                return "库存不足";
            }
            // 将扣减后的数量写入redis
            redisTemplate.opsForValue().set("stock", sku);

            log.info("===>>>减少库存成功,共出售" + successNum.incrementAndGet());

            // 执行脚本 删除锁
            List<String> lockKeys = Collections.singletonList(LOCK_KEY);
            String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) return 1 else return 0 end";
            RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
            // 删除锁
            Object execute = redisTemplate.execute(luaScript, lockKeys, value);

            log.info("===>>>抢购成功");
            return "抢购成功";
        } else {
            return "抢购失败";
        }
    }
}

3.3 测试

使用工具JMeter模拟并发请求,此处模拟每秒200次JMeter工具使用参考博客:https://blog.csdn.net/tianqingmuyu/article/details/108401543

注意:测试前先执行初始化库存接口,保证库存写入到Redis中
使用JMeter请求接口,结果如下图,没有出现超卖情况:
在这里插入图片描述

结论
通过Redis分布式锁机制,能够有效的解决秒杀系统的超卖问题;

其他实现方式:《RedisTemplate解决高并发下秒杀系统库存超卖方案 — Redis事务+乐观锁》

分布式锁解锁:《Redis实现分布式锁》

声明:JMeter工具使用参考博客:https://blog.csdn.net/tianqingmuyu/article/details/108401543@前端SkyRain

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一恍过去

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值