Redis高并发防止秒杀超卖实战源码解决方案


Posted in Redis onNovember 01, 2021

1:解决思路

将活动写入 redis 中,通过 redis 自减指令扣除库存。

2:添加 redis 常量

commons/constant/RedisKeyConstant.java

seckill_vouchers("seckill_vouchers:","秒杀券的 key"),

3:添加 redis 配置类

Redis高并发防止秒杀超卖实战源码解决方案

4:修改业务层

废话不多说,直接上源码

1:秒杀业务逻辑层

@Service
public class SeckillService {
@Resource
private SeckillVouchersMapper seckillVouchersMapper;
@Resource
2private VoucherOrdersMapper voucherOrdersMapper;
@Value("${service.name.ms-oauth-server}")
private String oauthServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private RedisTemplate redisTemplate;

2:添加需要抢购的代金券

@Transactional(rollbackFor = Exception.class)
public void addSeckillVouchers(SeckillVouchers seckillVouchers) {
// 非空校验
AssertUtil.isTrue(seckillVouchers.getFkVoucherId()== null,"请选择需要抢购的代金券");
AssertUtil.isTrue(seckillVouchers.getAmount()== 0,"请输入抢购总数量");
Date now = new Date();
AssertUtil.isNotNull(seckillVouchers.getStartTime(),"请输入开始时间");
 
// 生产环境下面一行代码需放行,这里注释方便测试
// AssertUtil.isTrue(now.after(seckillVouchers.getStartTime()),"开始时间不能早于当前时间");
AssertUtil.isNotNull(seckillVouchers.getEndTime(),"请输入结束时间");
AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()),"结束时间不能早于当前时间");
AssertUtil.isTrue(seckillVouchers.getStartTime().after(seckillVouchers.getEndTime()),"开始时间不能晚于结束时间");
 
// 采用 Redis 实现
String key= RedisKeyConstant.seckill_vouchers.getKey() +seckillVouchers.getFkVoucherId();
// 验证 Redis 中是否已经存在该券的秒杀活动,hash 不会做序列化和反序列化,
有利于性能的提高。entries(key),取到 key
Map<String, Object> map= redisTemplate.opsForHash().entries(key);
//如果不为空或 amount 库存>0,该券已经拥有了抢购活动,就不要再创建。
AssertUtil.isTrue(!map.isEmpty() && (int) map.get("amount") > 0,"该券已经拥有了抢购活动");
 
// 抢购活动数据插入 Redis
seckillVouchers.setIsValid(1);
seckillVouchers.setCreateDate(now);
seckillVouchers.setUpdateDate(now);
//key 对应的是 map,使用工具集将 seckillVouchers 转成 map
redisTemplate.opsForHash().putAll(key,BeanUtil.beanToMap(seckillVouchers));
}

3:抢购代金券

@Transactional(rollbackFor = Exception.class)
public ResultInfo doSeckill(Integer voucherId, String accessToken, String path)
{
// 基本参数校验
AssertUtil.isTrue(voucherId == null || voucherId < 0,"请选择需要抢购的代金券");
AssertUtil.isNotEmpty(accessToken,"请登录");
 
// 采用 Redis
String key= RedisKeyConstant.seckill_vouchers.getKey() + voucherId;//根据 key 获取 map
Map<String, Object> map= redisTemplate.opsForHash().entries(key);
//map 转对象
SeckillVouchers seckillVouchers = BeanUtil.mapToBean(map,SeckillVouchers.class, true, null);
 
// 判断是否开始、结束
Date now = new Date();
AssertUtil.isTrue(now.before(seckillVouchers.getStartTime()),"该抢购还未开始");
AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()),"该抢购已结束");
 
// 判断是否卖完
AssertUtil.isTrue(seckillVouchers.getAmount() < 1,"该券已经卖完了");
 
// 获取登录用户信息
String url = oauthServerName +"user/me?access_token={accessToken}";
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class,accessToken);
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
resultInfo.setPath(path);
return resultInfo;
}
 
// 这里的 data 是一个 LinkedHashMap,SignInDinerInfo
SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap)resultInfo.getData(), new SignInDinerInfo(), false);
 
// 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
VoucherOrders order =voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),seckillVouchers.getFkVoucherId());
AssertUtil.isTrue(order != null,"该用户已抢到该代金券,无需再抢");
 
//扣库存,采用 redis,redis 没有设置自减,所以要自减,将步长设置为-1
long count = redisTemplate.opsForHash().increment(key,"amount",-1);
AssertUtil.isTrue(count < 0,"该券已经卖完了");
 
// 下单存储到数据库
VoucherOrders voucherOrders = new VoucherOrders();
voucherOrders.setFkDinerId(dinerInfo.getId());
// Redis 中不需要维护外键信息
//voucherOrders.setFkSeckillId(seckillVouchers.getId());
voucherOrders.setFkVoucherId(seckillVouchers.getFkVoucherId());
String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();
voucherOrders.setOrderNo(orderNo);
voucherOrders.setOrderType(1);
voucherOrders.setStatus(0);
count = voucherOrdersMapper.save(voucherOrders);
AssertUtil.isTrue(count == 0,"用户抢购失败");
return ResultInfoUtil.buildSuccess(path,"抢购成功");
}
}

5:postman 测试

http://localhost:8083/add

{
"fkVoucherId":1,
"amount":100,
"startTime":"2020-02-04 11:12:00",
"endTime":"2021-02-06 11:12:00"
}

Redis高并发防止秒杀超卖实战源码解决方案

查看 redis

Redis高并发防止秒杀超卖实战源码解决方案

再次运行 http://localhost:8083/add

Redis高并发防止秒杀超卖实战源码解决方案

6:压力测试

Redis高并发防止秒杀超卖实战源码解决方案

查看 redis 中的库存出现负值

Redis高并发防止秒杀超卖实战源码解决方案

在 redis 中修改库存要分两部进行,先要获取库存的值,再扣减库存。所以在高并 发情况下,会导致 redis 扣减库存出问题。可以使用 redis 的弱事务或 lua 脚本解决。 7:安装Lua resources/stock.lua

if (redis.call('hexists', KEYS[1], KEYS[2])== 1) then
  local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));
  if (stock > 0) then
    redis.call('hincrby', KEYS[1], KEYS[2],-1);
    return stock;
  end;
    return 0;
end;

hexists', KEYS[1], KEYS[2]) == 1
hexists 是判断 redis 中 key 是否存在。
KEYS[1] 是 seckill_vouchers:1 KEYS[2] 是 amount
hget 是获取 amount 赋给 stock
hincrby 是自增,当为-1 是为自减。
因为在 redis 中没有自减指令,所以当步长为 -1 表示自减。
现在使用 lua 脚本,将 redis 中查询库存和扣减库存当成原子性操作在一个线程内.

8:配置Lua

config/RedisTemplateConfiguration.java

@Bean
public DefaultRedisScript<Long> stockScript() {
  DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
  //放在和 application.yml 同层目录下
  redisScript.setLocation(new ClassPathResource("stock.lua"));
  redisScript.setResultType(Long.class);
  return redisScript;
}

9:修改业务层

ms-seckill/service/SeckilService.java

1:抢购代金券

@Transactional(rollbackFor = Exception.class)
public ResultInfo doSeckill(Integer voucherId, String accessToken, String path)
{
// 基本参数校验
AssertUtil.isTrue(voucherId == null || voucherId < 0,"请选择需要抢购的代金券");
AssertUtil.isNotEmpty(accessToken,"请登录");
// 采用 Redis
String key= RedisKeyConstant.seckill_vouchers.getKey() + voucherId;
//根据 key 获取 map
Map<String, Object> map= redisTemplate.opsForHash().entries(key);
//map 转对象
SeckillVouchers seckillVouchers = BeanUtil.mapToBean(map,SeckillVouchers.class, true, null);
// 判断是否开始、结束
Date now = new Date();AssertUtil.isTrue(now.before(seckillVouchers.getStartTime()),"该抢购还未开始");
AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()),"该抢购已结束");
// 判断是否卖完
AssertUtil.isTrue(seckillVouchers.getAmount() < 1,"该券已经卖完了");
// 获取登录用户信息
String url = oauthServerName +"user/me?access_token={accessToken}";
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class,
accessToken);
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
resultInfo.setPath(path);
return resultInfo;
}
// 这里的 data 是一个 LinkedHashMap,SignInDinerInfo
SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap)
resultInfo.getData(), new SignInDinerInfo(), false);
// 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
VoucherOrders order =voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),
seckillVouchers.getFkVoucherId());
AssertUtil.isTrue(order != null,"该用户已抢到该代金券,无需再抢");
 
//扣库存,采用 redis,redis 没有设置自减,所以要自减,将步长设置为-1
// long count = redisTemplate.opsForHash().increment(key,"amount",-1);
// AssertUtil.isTrue(count < 0,"该券已经卖完了");
// 下单存储到数据库
VoucherOrders voucherOrders = new VoucherOrders();
voucherOrders.setFkDinerId(dinerInfo.getId());
// Redis 中不需要维护外键信息
//voucherOrders.setFkSeckillId(seckillVouchers.getId());
voucherOrders.setFkVoucherId(seckillVouchers.getFkVoucherId());
String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();
voucherOrders.setOrderNo(orderNo);
voucherOrders.setOrderType(1);
voucherOrders.setStatus(0);
long count = voucherOrdersMapper.save(voucherOrders);
AssertUtil.isTrue(count == 0,"用户抢购失败");
// 采用 Redis + Lua 解决问题
// 扣库存
List<String> keys = new ArrayList<>();
//将 redis 的 key 放进去keys.add(key);
keys.add("amount");
Long amount =(Long) redisTemplate.execute(defaultRedisScript, keys);
AssertUtil.isTrue(amount == null || amount < 1,"该券已经卖完了");
return ResultInfoUtil.buildSuccess(path,"抢购成功");
}

10:压力测试

将 redis 中库存改回 100

Redis高并发防止秒杀超卖实战源码解决方案

压力测试

Redis高并发防止秒杀超卖实战源码解决方案

查看 redis 中 amount=0 ,不会变成负值 查看数据库下单表 t_voucher_orders ,共计下 100 个订单。

Redis高并发防止秒杀超卖实战源码解决方案

到此这篇关于Redis高并发防止秒杀超卖实战源码解决方案的文章就介绍到这了,更多相关Redis高并发防止秒杀超卖 内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
基于Redis位图实现用户签到功能
May 08 Redis
redis实现排行榜功能
May 24 Redis
详解Redis瘦身指南
May 26 Redis
SpringBoot 集成Redis 过程
Jun 02 Redis
redis cluster支持pipeline的实现思路
Jun 23 Redis
使用Redis实现实时排行榜功能
Jul 02 Redis
Redis字典实现、Hash键冲突及渐进式rehash详解
Sep 04 Redis
redis缓存存储Session原理机制
Nov 20 Redis
基于Redis zSet实现滑动窗口对短信进行防刷限流的问题
Feb 12 Redis
sentinel支持的redis高可用集群配置详解
Apr 01 Redis
Redis如何实现验证码发送 以及限制每日发送次数
Apr 18 Redis
Redis实现主从复制方式(Master&Slave)
Jun 21 Redis
Redis的字符串是如何实现的
SpringBoot集成Redis的思路详解
详解redis在微服务领域的贡献
详解Redis在SpringBoot工程中的综合应用
Oct 16 #Redis
Redis三种集群模式详解
浅谈Redis的keys命令到底有多慢
基于Redis结合SpringBoot的秒杀案例详解
You might like
PHP在不同页面间传递Json数据示例代码
2013/06/08 PHP
PHP会话处理的10个函数
2015/08/11 PHP
Laravel学习教程之View模块详解
2017/09/18 PHP
Laravel框架路由设置与使用示例
2018/06/12 PHP
php layui实现前端多图上传实例
2019/07/30 PHP
JSON 编辑器实现代码
2009/12/06 Javascript
Tinymce+jQuery.Validation使用产生的BUG
2010/03/29 Javascript
jQuery.ajax 用户登录验证代码
2010/10/29 Javascript
jquery 关于event.target使用的几点说明介绍
2013/04/26 Javascript
jquery实现树形二级菜单实例代码
2013/11/20 Javascript
AngularJs实现分页功能不带省略号的代码
2016/05/30 Javascript
JS工作中的小贴士之”闭包“与事件委托的”阻止冒泡“
2016/06/16 Javascript
基于Bootstrap的UI扩展 StyleBootstrap
2016/06/17 Javascript
Bootstrap Table的使用总结
2016/10/08 Javascript
JQuery学习总结【二】
2016/12/01 Javascript
详解JavaScript中this的指向问题
2017/01/20 Javascript
js中获取键盘按下键值event.keyCode、event.charCode和event.which的兼容性详解
2017/03/15 Javascript
JavaWeb表单及时验证功能在输入后立即验证(含用户类型,性别,爱好...的验证)
2017/06/09 Javascript
详解nodejs通过代理(proxy)发送http请求(request)
2017/09/22 NodeJs
vue fetch中的.then()的正确使用方法
2020/04/17 Javascript
python daemon守护进程实现
2016/08/27 Python
python Matplotlib底图中鼠标滑过显示隐藏内容的实例代码
2019/07/31 Python
python requests库爬取豆瓣电视剧数据并保存到本地详解
2019/08/10 Python
numpy 声明空数组详解
2019/12/05 Python
python对批量WAV音频进行等长分割的方法实现
2020/09/25 Python
Expedia丹麦:全球领先的旅游网站
2018/03/18 全球购物
Booking.com亚太地区:Booking.com APAC
2020/02/07 全球购物
管理学专业个人求职信范文
2013/09/21 职场文书
项目副经理岗位职责
2013/12/30 职场文书
机关出纳岗位职责
2014/04/03 职场文书
租赁协议书范本
2014/04/22 职场文书
终止或解除劳动合同及劳动关系的证明书
2014/10/06 职场文书
企业安全生产规章制度
2015/08/06 职场文书
2016暑期校本培训心得体会
2016/01/08 职场文书
python3美化表格数据输出结果的实现代码
2021/04/14 Python
TS 类型兼容教程示例详解
2022/09/23 Javascript