文章目录
- 前言
- 一、场景
- 二、商品超卖的场景
- 三、使用分布式锁解决超卖
- 四、使用Redis事务乐观锁解决超卖
**
前言
Redis事务
Redis事务是一种将多个命令打包执行的机制,确保这些命令要么全部执行成功,要么全部执行失败。Redis事务通过MULTI、EXEC、DISCARD和WATCH这四个命令来实现。
MULTI:该命令标志着事务的开始,之后的命令会被缓存起来而不会立即执行。
EXEC:该命令用于执行之前缓存的所有命令,如果执行成功,则事务中的所有命令都会被执行。如果其中有任何一条命令执行失败(例如语法错误),那么事务将中止,但不会影响其他客户端的事务。
DISCARD:该命令用于取消事务,清除之前缓存的所有命令。
WATCH:该命令用于在事务开始之前监视一个或多个键。如果在执行EXEC之前,被监视的键被其他客户端修改,那么这个事务会中止。
使用事务可以确保原子性操作,但是不支持回滚,即使其中某些命令执行失败,其他命令仍然会被执行。因此,在使用事务时,需要确保所有命令都能正确执行,或者在失败时进行适当的处理。
Lua脚本
基本原理为使脚本相当于一个 redis 命令,可以结合 redis 原有命令,自定义脚本逻辑。
相同点
很好的实现了一致性、隔离性和持久性,但没有实现原子性,无论是 redis 事务,还是 lua 脚本,如果执行期间出现运行错误,之前的执行过的命令是不会回滚的。
不同点
(1)redis 事务是基于乐观锁,lua 脚本是基于 redis 的单线程执行命令。
(2)redis 事务的执行原理就是一次命令的批量执行,而 lua 脚本可以加入自定义逻辑。
**
一、场景
秒杀系统存在高并发的场景,在对商品进行秒杀时,由于并发过高可能会导致库存超卖的情况,那么可以通过Redis提供的事务机制超卖问题;Redis事务实际就是将所有命令都按顺序地执行。事务在执行时不会被其他的命令所打断。
二、商品超卖的场景
代码复现
@RestController
@RequestMapping("/second-kill")
@Slf4j
public class SecondKillController {
@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 "初始化库存成功";
}
@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 "减少库存成功";
}
}
执行结果:
这里我们使用JMeter并发进行测试
我们的库存总共只有5,而这里就共出售了12件商品,出现了超卖的情况。
三、使用分布式锁解决超卖
代码实现:
@GetMapping(value = "/distributedLocksReduce")
public String distributedLocksReduce() {
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("===>>>库存不足");
// 执行脚本 删除锁
redisTemplate.delete(LOCK_KEY);
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 {
log.info("===>>>抢购失败");
return "抢购失败";
}
}
执行结果:
这里我们使用JMeter并发进行测试
我们可以看到这里总共只出售了五件商品
四、使用Redis事务乐观锁解决超卖
代码实现:
@GetMapping(value = "/optimisticReduce")
public String optimisticReduce() {
// 开启事务
redisTemplate.setEnableTransactionSupport(true);
List<Object> results = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
// 监视key
operations.watch("stock");
Integer stock = (Integer) operations.opsForValue().get("stock");
operations.multi();
stock = stock - 1;
if (stock < 0) {
log.info("===>>>库存不足");
return null;
}
operations.opsForValue().set("stock", stock);
return operations.exec();
}
});
if (results != null && results.size() > 0) {
log.info("===>>>减少库存成功,共出售" + successNum.incrementAndGet());
return "减少库存成功";
}
log.info("库存不足");
return "库存不足";
}
执行结果: