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次;
注意:测试前先执行初始化库存接口,保证库存写入到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次;
注意:测试前先执行初始化库存接口,保证库存写入到Redis中
使用JMeter请求接口,结果如下图,没有出现超卖情况:
发表评论 取消回复