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 11 Redis
浅谈Redis的几个过期策略
May 27 Redis
Windows中Redis安装配置流程并实现远程访问功能
Jun 07 Redis
为什么RedisCluster设计成16384个槽
Sep 25 Redis
Jedis操作Redis实现模拟验证码发送功能
Sep 25 Redis
源码分析Redis中 set 和 sorted set 的使用方法
Mar 22 Redis
浅谈Redis 中的过期删除策略和内存淘汰机制
Apr 03 Redis
Redis基本数据类型Zset有序集合常用操作
Jun 01 Redis
使用Redis实现分布式锁的方法
Jun 16 Redis
Redis Lua脚本实现ip限流示例
Jul 15 Redis
Redis过期数据是否会被立马删除
Jul 23 Redis
Redis的字符串是如何实现的
SpringBoot集成Redis的思路详解
详解redis在微服务领域的贡献
详解Redis在SpringBoot工程中的综合应用
Oct 16 #Redis
Redis三种集群模式详解
浅谈Redis的keys命令到底有多慢
基于Redis结合SpringBoot的秒杀案例详解
You might like
实用函数3
2007/11/08 PHP
php中使用ExcelFileParser处理excel获得数据(可作批量导入到数据库使用)
2010/08/21 PHP
PHP 小心urldecode引发的SQL注入漏洞
2011/10/27 PHP
php调用dll的实例操作动画与代码分享
2012/08/14 PHP
php获取本周开始日期和结束日期的方法
2015/03/09 PHP
php通过ksort()函数给关联数组按照键排序的方法
2015/03/18 PHP
Javascript 兼容firefox的一些问题
2009/05/21 Javascript
为JavaScript添加重载函数的辅助方法
2010/07/04 Javascript
node.js中的console.log方法使用说明
2014/12/09 Javascript
js简单获取表单中单选按钮值的方法
2016/08/23 Javascript
原生JS实现图片懒加载(lazyload)实例
2017/06/13 Javascript
vue.js声明式渲染和条件与循环基础知识
2017/07/31 Javascript
微信小程序之分享页面如何返回首页的示例
2018/03/28 Javascript
vue自定义移动端touch事件之点击、滑动、长按事件
2018/07/10 Javascript
vue改变循环遍历后的数据实例
2019/11/07 Javascript
vue实现防抖的实例代码
2021/01/11 Vue.js
Python 装饰器深入理解
2017/03/16 Python
python3写的简单本地文件上传服务器实例
2018/06/04 Python
Python内置random模块生成随机数的方法
2019/05/31 Python
python实现最小二乘法线性拟合
2019/07/19 Python
用Python批量把文件复制到另一个文件夹的实现方法
2019/08/16 Python
python GUI库图形界面开发之PyQt5表格控件QTableView详细使用方法与实例
2020/03/01 Python
PyQt5 文本输入框自动补全QLineEdit的实现示例
2020/05/13 Python
美国婚戒购物网站:Anjays Designs
2017/06/28 全球购物
C#实现对任一张表的数据进行增,删,改,查要求,运用Webservice,体现出三层架构
2014/07/11 面试题
建筑工程管理专业自荐信范文
2013/12/28 职场文书
全国道德模范事迹
2014/02/01 职场文书
教学质量评估实施方案
2014/03/17 职场文书
知识就是力量演讲稿
2014/09/13 职场文书
党员个人对照检查材料
2014/10/01 职场文书
检讨书范文1000字
2015/01/28 职场文书
客服专员岗位职责范本
2015/04/07 职场文书
预备党员转正党小组意见
2015/06/01 职场文书
2016年教代会开幕词
2016/03/04 职场文书
python井字棋游戏实现人机对战
2022/04/28 Python
Java Spring Boot请求方式与请求映射过程分析
2022/06/25 Java/Android