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 Cluster 字段模糊匹配及删除
May 27 Redis
Redis 哨兵集群的实现
Jun 18 Redis
k8s部署redis cluster集群的实现
Jun 24 Redis
Redis如何实现分布式锁
Aug 23 Redis
Redis高并发防止秒杀超卖实战源码解决方案
Nov 01 Redis
Redis+Lua脚本实现计数器接口防刷功能(升级版)
Feb 12 Redis
高并发下Redis如何保持数据一致性(避免读后写)
Mar 18 Redis
Redis实战高并发之扣减库存项目
Apr 14 Redis
解决 redis 无法远程连接
May 15 Redis
Redis的字符串是如何实现的
SpringBoot集成Redis的思路详解
详解redis在微服务领域的贡献
详解Redis在SpringBoot工程中的综合应用
Oct 16 #Redis
Redis三种集群模式详解
浅谈Redis的keys命令到底有多慢
基于Redis结合SpringBoot的秒杀案例详解
You might like
一个显示某段时间内每个月的方法 返回由这些月份组成的数组
2012/05/16 PHP
简单实现php上传文件功能
2017/09/21 PHP
TP5框架model常见操作示例小结【增删改查、聚合、时间戳、软删除等】
2020/04/05 PHP
硬盘浏览程序,保存成网页格式便可使用
2006/12/03 Javascript
求得div 下 img的src地址的js代码
2007/02/28 Javascript
把JS与CSS写在同一个文件里的书写方法
2007/06/02 Javascript
javascript获得CheckBoxList选中的数量
2009/10/27 Javascript
JavaScript中SQL语句的应用实现
2010/05/04 Javascript
javascript instanceof 内部机制探析
2010/10/15 Javascript
当jQuery1.7遇上focus方法的问题
2014/01/26 Javascript
jquery easyui 结合jsp简单展现table数据示例
2014/04/18 Javascript
js实现网页自动刷新可制作节日倒计时效果
2014/05/27 Javascript
jQuery实现分隔条左右拖动功能
2015/11/21 Javascript
jQuery插件EasyUI校验规则 validatebox验证框
2015/11/29 Javascript
Backbone中View之间传值的学习心得
2016/08/09 Javascript
bootstrap自定义样式之bootstrap实现侧边导航栏功能
2018/09/10 Javascript
JavaScript 对引擎、运行时、调用堆栈的概述理解
2018/10/22 Javascript
从零开始在NPM上发布一个Vue组件的方法步骤
2018/12/20 Javascript
vue动态绑定class的几种常用方式小结
2019/05/21 Javascript
nuxt+axios实现打包后动态修改请求地址的方法
2020/04/22 Javascript
Windows和Linux下使用Python访问SqlServer的方法介绍
2015/03/10 Python
Python实现的数据结构与算法之基本搜索详解
2015/04/22 Python
基于Python的XSS测试工具XSStrike使用方法
2017/07/29 Python
Python内置模块hashlib、hmac与uuid用法分析
2018/02/12 Python
Python采集代理ip并判断是否可用和定时更新的方法
2018/05/07 Python
wxPython绘图模块wxPyPlot实现数据可视化
2019/11/19 Python
python GUI框架pyqt5 对图片进行流式布局的方法(瀑布流flowlayout)
2020/03/12 Python
python 实现表情识别
2020/11/21 Python
HTML5 canvas基本绘图之文字渲染
2016/06/27 HTML / CSS
在html页面中取得session中的值的方法
2020/08/11 HTML / CSS
Expedia加拿大官方网站:加拿大最大的在线旅游提供商
2017/12/31 全球购物
Pandora西班牙官方商店:PandoraShop.es
2020/10/05 全球购物
个人委托书范本
2014/09/13 职场文书
出纳试用期自我评价
2015/03/10 职场文书
Java实现经典游戏泡泡堂的示例代码
2022/04/04 Java/Android
在windows server 2012 r2中安装mysql的详细步骤
2022/07/23 Servers