Redis基础 - 基本类型及常用命令
Redis基础 - Java客户端
Redis 基础 - 短信验证码登录
Redis 基础 - 用Redis查询商户信息
Redis 基础 - 优惠券秒杀《非集群》
Redis 基础 - 优惠券秒杀《分布式锁(初级)》
Redis 基础 - 优惠券秒杀《分布式锁(使用Redisson)》

Redis优化秒杀

回顾秒杀业务的流程

前端发起请求,到达Nginx,Nginx会把请求负载均衡到Tomcat,在Tomcat内部的业务流程是,根据优惠券id查询优惠券->判断库存->没问题就去查订单(为了一人一单)->没问题就去减库存并创建订单。这些代码可以看出业务时串行执行的,所以整个业务的耗时就是每一步业务的耗时之和,而这里面中查询优惠券、查询订单、减库存、创建订单都会去操作数据库,而数据库的并发能力本身就是比较差的,更何况减库存、创建订单都是对数据库的写操作,另外为了避免安全问题,这里还加了分布式锁,所以整个业务的性能可想而知,整个业务的耗时也变得比较长,所以并发能力也会变得更低。

用Redis和独立线程做改善

比如可以把这个业务分成两部分,第一部分是对秒杀资格的判断,也就是判断秒杀库存、校验一人一单,这部分的耗时其实比较短;第二部分是减库存、创建订单等,因为他们是对数据库的写操作,所以耗时较久。即原来是一个人都做,而现在是要把这两部分交给两个人去做,即交给两个线程。即请求到来后,主线程要做的是判断用户的秒杀资格,然后如果她有购买资格,可以开启独立的线程来处理耗时较久的减库存、创建订单之类的。这样的话,这个业务的效率就会大大提升。

当然,为了进一步提高业务的性能,除了把他分离成两块儿之外,我们也还要尽可能去提高秒杀资格判断这一部分的效率,因为这一部分要判断,依然要去查数据库,所以他的性能依然会受到数据库的影响,所以相比于数据库的性能Redis的性能更好,因此可以使用Redis,把优惠券信息、订单信息缓存在Redis中。即把秒杀资格的判断用Redis做。

所以请求来后,先判断库存是否充足(可以用简单的string类型),如果不足,就返回错误,如果充足,就去判断用户是否下单(可以用set),如果已经下单,就范湖错误,如果没下单,就扣减库存,并把userid存入到当前优惠券的set集合,然后再返回。但为了保证这些操作的原子性,可以使用lua。

所以业务修改后大概如下,请求到来后,执行Lua脚本,判断是否有资格下单,如果有资格,就把优惠券id、用户id、订单id存入到阻塞队列中,即谁购买了什么东西,还有你的订单编号是什么,保存下来后,方便将来的异步线程去执行她来完成真正的下单。当然,存到队列后,就把订单id返回到前端。

Redis和异步秒杀代码示例

  • 新增秒杀优惠券的同时,将优惠券库存信息保存到Redis中
  • 基于lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
  • 若抢购成功,将优惠券id和用户id封装后存入阻塞队列
  • 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

新增秒杀券

VoucherController.java

@RequestController
@RequestMapping("/voucher")
public class VoucherController {@Resourceprivate IVoucherService voucherService;// 新增秒杀券 Voucher里面还包含了秒杀券核心的那几个字段,比如库存、生效时间、失效时间等。@PostMapping("seckill")public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());}
}

VoucherServiceImpl.java

@Resource
private StringRedisTemplate stringRedisTemplate;@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucher seckillVoucher = new SeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock);seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 保存秒杀库存到Redis中(key是优惠券id,值是库存数)stringRedisTemplate.opsForValue().set("seckill:stock:" + voucher.getId(), voucher.getStock().toString());
}

然后可以用postman去添加,如下:

{"shopId" : 1,"title" : "100元代金券","subTitle" : "周一至周五均可使用","rules" : "全场通用\\n无需预约\\n可无限叠加\\n不兑现、不找零\\n仅限堂食","payValue" : 8000,"actualValue" : 10000,"type" : 1,"stock" : 100,"beginTime" : "2022-01-26T10:09:17";"endTime" : "2022-01-26T24:09:04";
}

这样的话,秒杀券添加了,可以去抢购了。

对秒杀资格的判断

/resouces/seckill.lua

-- 1,参数列表
-- 1.1 优惠券id(因为要从Redis查库存)
local voucherId = ARGV[1] -- 不用KEYS是因为,这个不是key,而是要拼接才能用的
-- 1.2 用户id(因为要在set集合中查询是否存在)
local userId = ARGV[2]-- 2,数据key
-- 2.1 库存key
local stockKey = "seckill:stock:" .. voucherId -- lua中字符串拼接是两个点-- 2.2 订单key(set的key,value中保存购买这个优惠券的所有用户id)
local orderKey = "seckill:order:" .. voucherId-- 3,脚本业务
-- 3.1 判断库存是否充足(tonumber是把字符串转成数字,不然没法和数字比较大小)
if (tonumber(redis.call("get", stockKey)) <= 0) then-- 3.2 库存不足,返回1return 1
end -- 3.2 判断用户是否下单(判断某个元素是不是set集合的成员可以用sismember命令,返回值是1或0)
if (redis.call("sismember", orderKey, userId) == 1) then-- 3.3 存在说明是重复下单,返回2return 2
end-- 能执行到这里,就说明库存够,而且没下过单-- 3.4 扣库存
redis.call("incrby", stockKey, -1)-- 3.5 下单(把用户保存到set)
redis.call("sadd", orderKey, userId)
return 0

修改秒杀业务代码

VoucherOrderServiceImpl.java

@Resource
prviate ISeckillVoucherService iSeckillVoucherService;@Resource
prviate RedisIdWorker redisIdWorker;@Resource
prviate StringRedisTemplate stringRedisTemplate;// 注入RedissonClient
@Resource
private RedissonClient redissonClient;private static final DefultRedisScript<Long> SECKILL_SCRIPT;static {// 随着类的加载而执行,并且只会执行一次,因为这玩意(seckill.lua)加载一次可以,没必要每次都加载SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);// 设置返回值为long
}/* BlockingQueue是阻塞队列,元素是VoucherOrder。实现类很多,用最简单的ArrayBlockingQueue,并指定
队列初始化的大小,这里给1024 * 1024,太大了也不好。*/
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);// 由于要开独立线程,所以需要线程池和任务
private static final ExcutorService SECKILL_ORDER_EXCUTOR = Excutors.newSingleThreadExcutor();// 线程池// 任务
/*
什么时候执行这个任务呢,肯定是用户秒杀抢购之前开始,因为用户一旦开始秒杀,他就会向队列里添加
新的订单,那我们任务就要去队列取出订单信息,所以他必须在队列之前执行。事实上,这个项目一启动,
用户随时可能来抢购,所以应该在这个类初始化之后赶紧的执行这个任务。可以通过spring提供的注解来做:
@PostConstruct,这个注解是在当前类初始化完毕后,去执行init方法。
*/
@PostConstruct
private void init() {// 类初始化完毕后,执行任务SECKILL_ORDER_EXCUTOR.submit(new VoucherOrderHandler());
}private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while(true) {// 1,获取队列中的订单信息// 获取和删除该队列的第一个元素,如果有需要则等待直到有元素可用为止VoucherOrder voucherOrder = orderTasks.take();// 由于没有元素就卡在这里,有元素才执行,所以不用担心因为死循环而对CPU带来负担// 2,创建订单handleVoucherOrder(voucherOrder);}}
}// 异步处理创建订单干入数据库
private void handleVoucherOrder(VoucherOrder voucherOrder) {// 1,获取用户(因为是其他线程异步处理,所以不能从UserHolder取)Long userId = voucherOrder.getUserId();// 2,锁对象/*从理论上讲,这里不加锁也OK,因为已经在Redis里做了并发判断了,这里再加一次锁,其实就是做一个兜底以防万一,万一Redis出了问题没判断成功呢,虽然这种可能性几乎没有,但是该做的判断还是要做一下*/RLock lock = redissonClient.getLock("lock:order:" + userId);// 3,获取锁boolean isLock = lock.tryLock();// 4,判断是否获取锁成功if (!isLock) {// 若不成功,记录日志就行,因为是异步执行,没必要返回给前端return;}try {proxy.createVoucherOrder(voucherOrder);} finally {lock.unlock();}
}IVoucherOrderService proxy;// 这里定义,子线程可以用。@Override
// @Transactional 由于这个方法里只有查的,所以不需要这个。
public Result seckillVoucher(Long voucherId) {// 获取用户Long userId = UserHolder.getUser().getId(); // 用户id// 1,执行lua脚本,结果告诉我们有没有购买的资格Long result = stringRedisTemplate.excute(SECKILL_SCRIPT,Collections.emptyList(),// 传空集合,因为这里的Lua中不需要keysvoucherId.toString(),userId.toString());// 2,判断结果是否为0int r = result.intValue();if (r != 0) {// 2.1 不为0,代表没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}// 2.2 为0,有购买资格,把下单信息保存到阻塞队列,后续可以基于这个队列异步完成下单业务VoucherOrder voucherOrder = new VoucherOrder();// 对应着优惠券订单表tb_voucher_order// 2.3 订单idlong order_id = redisIdWorker.nextId("order");voucherOrder.setId(order_id);// 2.4 用户idvoucherOrder.setUserId(userId);// 2.5 代金券idvoucherOrder.setVoucherId(voucherId);// 2.6 其他的是取默认值,所以不用设置// 2.7 放入阻塞队列/*阻塞队列的特点:当一个线程尝试从这个队列里获取元素的时候,如果没有元素,那么这个线程就会被阻塞,直到队列中有元素,她才会被唤醒,并且获取元素。比如,这个例子中,订单不可能一直会有,只有有人下单才会有,没人下单就没有,所以在这里用阻塞队列刚刚好。*/orderTasks.add(voucherOrder);// 2.8 获取代理对象。拿到当前对象的代理对象(获取跟事务有关的代理对象)// 因为在子线程里是获取不到的,所以在主线程获取这个,在子线程直接使用她proxy = (IVoucherOrderService)AopContext.currentProxy();// 3,返回订单idreturn Result.ok(orderId);
}@Transactional // 更新的都跑到这里了,所以这里加事务
public void createVoucherOrder (VoucherOrder voucherOrder) {// 5,一人一单Long userId = voucherOrder.getUserId();// 5.1 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();// 5.2 判断是否存在if (count > 0) {// 用户已经购买过了,其实不容易出现这个情况,但以防万一记个日志return;}// 6,更新库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", voucher.getStock()) // where id = ? and stock = ?.update();if (!success) {// 库存不足。由于Redis做了判断,所以不太可能出现,但为了以防万一,可以加日志return;}// 7.5 订单信息写入数据库save(voucherOrder);
}

总结

秒杀优化的思路

把同步下单改为异步下单。同步下单是来了以后判断你有没有资格,有资格我就立即去下单,因为下单、扣库存等等这些业务要加各种各样的事务或者是锁,导致了这一系列的业务全部执行完,她的耗时就会非常的久,最低耗时可能达到100到200多毫秒,甚至平均耗时达到了400多毫秒。而现在的思路是,尽可能去简化业务,然后把业务分成两部分,一部分是对于抢购资格的判断,如果发现有资格,就立即结束就告诉你下单成功,至于耗时较久的下单部分,我们可以异步去完成。

  • 先利用Redis完成库存余量、一人一单判断,完成抢单业务。(这样的话响应时间就变短)
  • 再将下单业务放入阻塞队列,利用独立线程异步下单。

基于阻塞队列的异步秒杀有哪些问题

1)内存限制问题

这种模式虽然提升了秒杀业务的性能,但是她也存在一些问题。关键是这里使用的是JDK里面的阻塞队列,而这个阻塞队列她使用了JVM的内存,如果说不加以限制在高并发情况下可能会有无数个订单对象需要去创建并且放到阻塞队列里,可能会导致将来内存溢出。所以在创建阻塞队列时,我们设置了队列的长度,有个上限,那如果这个队列内东西存满了呢,再有新的订单需要往里面塞,就塞不进去了。

2)数据安全问题

现在是基于内存保存了订单信息,如果服务突然宕机了,那内存里的所有订单信息都会丢失,用户已经完成下单了,你告诉人家成功了让人家付款,结果后台没有相关订单数据,这样一来就出现了数据不一致的问题。还有一种情况是,比如我们现在有一个线程从队列里取出了一个下单的任务要去执行,可惜就在此时,发生了严重的事故,比如出现异常之类的,这样一来,这个任务没有被执行,而任务一旦取出来队列里也就没有了,也就是说以后再也不会执行了,导致了任务的丢失,那再一次出现了不一致的情况。

Redis 基础 - 优惠券秒杀《初步优化(异步秒杀)》相关推荐

  1. Redis 基础 - 优惠券秒杀《分布式锁(初级)》

    参考 Redis基础 - 基本类型及常用命令 Redis基础 - Java客户端 Redis 基础 - 短信验证码登录 Redis 基础 - 用Redis查询商户信息 Redis 基础 - 优惠券秒杀 ...

  2. Redis 基础 - 优惠券秒杀《非集群》

    参考 Redis基础 - 基本类型及常用命令 Redis基础 - Java客户端 Redis 基础 - 短信验证码登录 Redis 基础 - 用Redis查询商户信息 摘要 用Redis生成保证唯一性 ...

  3. Redis基础与高可用集群架构进阶详解

    一.NoSQL简介 1.问题引入 每年到了过年期间,大家都会自觉自发的组织一场活动,叫做春运!以前我们买票都是到火车站排队,后来呢,有了 12306,有了它以后就更方便了,我们可以在网上买票,但是带来 ...

  4. Redis基础(八)——集群

    文章目录 集群 1 主从复制 1.1 建立连接阶段 1.2 数据同步阶段 1.3 命令传播阶段 2 哨兵 2.1 监控阶段 2.2 通知阶段 2.3 故障转移阶段 3 集群 集群 1 主从复制 为提高 ...

  5. Redis进阶-高可用:集群

     前言 前面几篇文章中,已经介绍了Redis的几种高可用技术:持久化.主从复制和哨兵,但这些方案仍有不足,其中最主要的问题是存储能力受单机限制,以及无法实现写操作的负载均衡. Redis集群解决了上述 ...

  6. SpringBoot使用Redis 数据访问(单点、集群、哨兵、连接池、Pipline、分布式框架Redisson、解决方案)

    目录 Redis 文献资料 用Redis编程 Redis模块API 教程和常见问题解答 管理 嵌入式和物联网 故障排除 Redis集群 其他基于Redis的分布式系统 在SSD和永久性存储器上进行Re ...

  7. Redis(3)--哨兵模式,集群

    Redis的哨兵模式 什么是哨兵模式 Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行.其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例. ...

  8. NoSQL(3) 之Redis主从复制、哨兵和集群介绍及详细搭建步骤

    文章目录 一.主从复制 1.1 主从复制的概念 1.2 主从复制的作用 1.3 主从复制的流程 1.4 部署Redis 主从复制步骤 1)首先要搭建redis,在之前的博客写过, 具体可参考:NoSQ ...

  9. Redis系列教程(二):详解Redis的存储类型、集群架构、以及应用场景

    高并发架构系列 高并发架构系列:数据库主从同步的3种一致性方案实现,及优劣比较 高并发架构系列:Spring Cloud的核心成员.以及架构实现详细介绍 高并发架构系列:服务注册与发现的实现原理.及实 ...

最新文章

  1. JAVA爬虫三大运营商
  2. Mysql在离线安装时提示:error: Found option without preceding group in config file
  3. HDU - 4856 Tunnels(哈密顿路径+状压dp)
  4. 2018年最好用的20个Bootstrap网站模板
  5. python 命名实体识别_使用Python和Keras的有关命名实体识别(NER)的完整教程
  6. 【渝粤教育】 国家开放大学2020年春季 2528监督学 参考试题
  7. 解决:关于Git无法提交 index.lock File exists的问题
  8. Google Go:初级读本
  9. Java案例:编译器生成桥方法
  10. ListView列排序功能实现
  11. IBM 确认裁员约 1700 人;华为新款操作系统来了!开通 5G 服务不换卡不换号 | 极客头条...
  12. mysql 基于 ssl 的主从复制
  13. 如何给CSDN博客添加个人微信二维码或自定义栏目
  14. ThinkPHP3.2.3 的异常和错误屏蔽处理
  15. python制作gif动图_Python几行代码制作Gif动图
  16. golang 生成定单号
  17. 自动驾驶 11-2: 激光雷达传感器模型和点云 LIDAR Sensor Models and Point Clouds
  18. 多智能体强化学习MAPPO源代码解读
  19. Weblogic部署程序运行不起来的坑
  20. 软件工程~数据字典例子解释

热门文章

  1. 二维离散动力学系统的混沌研究【基于matlab的动力学模型学习笔记_9】
  2. 华为nova8pro鸿蒙系统怎么看,华为nova8的隐藏功能_华为nova8隐藏功能怎么开启
  3. 锂离子电池和燃料电池特性介绍
  4. JAVA泛型通配符T,E,K,V区别,T以及ClassT,ClassT的区别
  5. 嵌入式软件之应用调试
  6. 我的世界自动生成服务器主城指令,我的世界生成主城的指令
  7. 2021年茶艺师(中级)考试报名及茶艺师(中级)考试APP
  8. OTHER:环比与同比
  9. Spark 图计算实战
  10. 第一回 开篇 D3D渲染流程简介