Redis实现秒杀
前期准备
缓存选择考虑
Redis和Redis Cluster(分布式版本),是一个分布式缓存系统。其支持多种数据结构,也支持MQ。Redis在性能上做了大量优化。因此使用Redis或者Redis Cluster就可以轻松实现一个强大的秒杀系统。
用Redis的这些命令就可以了。
RPUSH key value
插入秒杀请求
当插入的秒杀请求数达到上限时,停止所有后续插入。
后台启动多个工作线程,使用
LPOP key
读取秒杀成功者的用户id,进行后续处理。
或者使用LRANGE key start end命令读取秒杀成功者的用户id,进行后续处理。
每完成一条秒杀记录的处理,就执行INCR key_num。一旦所有库存处理完毕,就结束该商品的本次秒杀,关闭工作线程,也不再接收秒杀请求。
秒杀思路(缓存redis,定时器:spring整合quartz)
1、秒杀商品由商家后台添加,秒杀商品数据保存在tb_seckilll_goods表中,关键字段包括:
id,status(审核状态),start_time(开始时间),end_time(结束时间),stock_count(库存量);
2、写一个定时器,定时从秒杀商品表中扫描数据,将符合条件的商品加载到缓存(redis)中;条件:审核状态="1",start_time < 当前时间 < end_time,库存量大于0;
3、前端展示,此处略
4、点击抢购,拿着秒杀商品的id去缓存中查询,如果缓存中商品不存在或者为空,提示“已售罄”,否则生成订单(原子操作),保存到缓存中,防止用户重复秒杀,订单表tb_seckill_order;
5、库存-1,判断减完之后缓存中商品的库存是否大于0,大于0则更新缓存,否则删除该秒杀商品的缓存,并更新到数据库
6、异步处理,所有秒杀订单和库存在内存(redis),后边异步同步到mysql
方案一:使用商品ID
作为分布式锁,加锁后扣减库存
实现流程
为:
- 用户发起秒杀请求到
Redis
,Redis
先使用商品ID
作为key
尝试加锁,保证只有一个用户进入之后流程,保证原子性
; - 如果加锁
成功
,则查询库存。如果库存充足,则扣减库存,记录订单,代表秒杀成功;若库存不足,直接返回秒杀失败;
/**
* 使用分布式锁秒杀,加锁后再查询redis库存,最后扣减库存
* @param lockId 锁ID
* @param userId 用户ID
* @param goodKey 商品ID
* @return 秒杀成功返回 true,否则返回 false
*/
private boolean subStock(String lockId, String userId, String goodKey) {
// 尝试先加锁,如果加锁成功再进行查询库存量,和扣减库存操作,此时只能有一个线程进入代码块
if (redisLock.lock(lockId, userId, 4000)) {
try {
// 查询库存
Integer stock = (Integer) redisTemplate.opsForValue().get(goodKey);
if (stock == null) {
System.out.println("商品不在缓存中");
}
// 如果剩余库存量大于零,则扣减库存
if (stock > 0) {
redisTemplate.opsForValue().decrement(goodKey);
return true;
} else {
return false;
}
} finally {
// 释放锁
redisLock.unlock(lockId, userId);
}
}
return false;
}
该方案存在一些缺点:
用户进来后都要抢锁,即便是库存量已经为零,仍然需要抢锁,这无疑带来了很多无用争抢;
锁的是商品ID,锁粒度太大,并发性能可以进一步优化;
解决方案:
抢锁前先查询库存,如果库存已经为零,则直接返回false,不必参与抢锁过程;
使用商品ID+库存量作为锁,降低锁粒度,进一步提升并发性能;
方案二:先查询,再使用商品ID
作为分布式锁,加锁后扣减库存
实现流程
为:
- 用户发起秒杀请求到
Redis
,Redis
先查询库存量,然后根据商品ID+库存量
作为key
尝试加锁,保证只有一个用户进入之后流程,保证原子性
; - 如果加锁
成功
,则查询库存(解决超卖问题,第一步骤并发查有库存,但是另外一个thread已经先行扣除成功,需要原子操作再加一步查询)。如果库存充足,则扣减库存,代表秒杀成功;若库存不足,直接返回秒杀失败;
//查询库存是否>0
Integer curStock = (Integer) redisTemplate.opsForValue().get(goodKey);
if (curStock <= 0) {
return false;
}
//尝试加锁实现秒杀下单过程
if (redisLock.lock(lockId, userId, 4000)) {
try {
// 查询库存
Integer stock = (Integer) redisTemplate.opsForValue().get(goodKey);
if (stock == null) {
System.out.println("商品不在缓存中");
}
// 如果剩余库存量大于零,则扣减库存
if (stock > 0) {
redisTemplate.opsForValue().decrement(goodKey);
return true;
} else {
return false;
}
} finally {
// 释放锁
redisLock.unlock(lockId, userId);
}
}
return false;
Java+Redis系统实现
步骤参考:Redis实现商品秒杀-CSDN博客
1、将数据库中的商品库存数量存入Redis中。
// 商品列表名称
String redisKey = "goods:" + seckillGoods.getId();
// 添加所有库存商品
for (int i = 0; i < seckillGoods.getGoodsCount(); i++) {
redisTemplate.opsForList().rightPush(redisKey, String.valueOf(seckillGoods.getId()));
}
2、判断用户是否已经秒杀成功过。
// 查询用户是否已经秒杀过该商品
Object orderObj = redisTemplate.opsForHash().get("seckill_orders", seckillUser.getId() + ":" + seckillGoods.getId());
if (orderObj != null) {
throw new SEckillException(ErrorCodeEnum.REPEAT_SEC_KILL_ERROR);
}
// 查询用户是否在排队中
Object userInQueueObj = redisTemplate.opsForSet().isMember("seckill_queues:" + seckillGoods.getId(), seckillUser.getId());
if (userInQueueObj != null) {
throw new SEckillException(ErrorCodeEnum.WAITING_IN_QUEUE_ERROR);
}
3、利用Redis的事务实现处理抢购成功的逻辑。
// 开启事务
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
// 从商品列表中弹出一个商品
redisTemplate.opsForList().leftPop(redisKey);
// 利用setValueAt等方法,获取用户信息和商品信息,此处略过
// 判断是否获取到商品信息
if (seckillGoods == null) {
redisTemplate.discard();
throw new SEckillException(ErrorCodeEnum.SEC_KILL_FINISH_ERROR);
}
// 秒杀成功,生成秒杀订单
redisTemplate.opsForHash().put("seckill_orders", seckillUser.getId() + ":" + seckillGoods.getId(), seckillOrder);
// 秒杀成功的商品写入Set中
redisTemplate.opsForSet().add("seckill_success:" + seckillGoods.getId(), String.valueOf(seckillGoods.getId()));
// 提交事务
redisTemplate.exec();