秒杀优化
目前存在的问题
查/写数据库过多,数据库本身并发能力差
打个比方来说就是,在一家餐厅中,只有一个工人,同时进行着收银和做饭的工作,这里的查询优惠券、判断秒杀库存、查询订单、校验一人一单就相当于收银,而减库存、创建订单这种实质性工作就相当于做饭,一个人做两件事,必定性能低,因此采取异步的方法,即分成两部分,由两个人异步来做,异步即不知道两人的工作顺序是怎么样的
如上,在新增优惠券时将一些消息先存入redis,比如库存,然后将一部分判断、校验等工作划分出来进行,这些工作做完后会将相关信息存入阻塞队列(相当于收银员与厨师之间有一个单子,单子记录了菜品,等着厨师来做,并且因为单子是有先后顺序的,所以用阻塞队列相对合适),另一方就依次取阻塞队列中的信息,进行减库存、创建订单等实质性工作
lua脚本保证原子性
像判断库存、校验一人一单这类工作需要原子性,因此使用lua脚本来保证原子性
使用阻塞队列实现异步秒杀
第一步,在新增优惠券时将优惠券库存的信息存入redis,便于异步地进行判断是否还有库存,其中key为一个业务名拼接优惠券的种类id,value为优惠券的库存
编写lua脚本,由于要判断库存是否充足、校验是否一人一单,因此需要库存、用户id这两个参数,需要在调用时传入,由于要查库存,需要知道对应的库存的key,这里使用两个点进行拼接(lua语法),然后就是判断逻辑了,校验一人一单也是相同的意思,最后要补上return 0
只是redis先减库存,数据库的库存还未改变
调用lua脚本,同前几个章节,提前读出可以避免io流问题,同时声明一个阻塞队列,当然也可以用线程池,因为线程池默认有阻塞队列
修改秒杀方法,执行lua脚本判断完成后,根据返回的结果将相关信息存入阻塞队列(订单id),之后通过依次取阻塞队列,来进行减库存秒杀的工作
实现秒杀减库存,开启独立线程异步地执行下单减库存的任务,这里使用线程池,但指定只是用一个线程,并且由于下单减库存的工作在项目启动后就随时需要执行,因此需要在类初始化的时候就执行这个任务,因此使用PostConstruct来实现初始化就执行任务
接着由于要一直从阻塞队列里取相关信息,所以使用while循环,就算阻塞队列里没信息了,他也会卡在这里直到又有新的信息,从阻塞队列里取到相关信息后,就执行减库存的实质性工作
数据库减库存,首先由于是开启了一个子线程异步地执行减库存的工作,所以无法从父线程中获取到用户id(记得好像是用到了Userholder.getUser.getId),也就是说子线程是一个全新的线程,父线程中已有的变量什么的不能被子线程访问到,因此需要从阻塞队列得到的信息中再次获取用户id,进而像之前一样获取锁等操作
由于是子线程,而代理对象proxy底层是通过currentThread来获取的,因此同样会出现上面用户id的问题,所以还是只能在父进程中获取代理对象,并且把代理对象提取成一个全局变量,这样子线程也可以调用这个变量,如下图所示,在秒杀之前声明成全局变量,在秒杀方法中对代理对象初始化,同时也不需要return了,因为下面的修改不再需要返回值了
同时由于在原先的createVoucherOrder这个创建订单的方法中,它是有以下的业务逻辑
这个创建订单逻辑在redis是已经实现了(redis实现下单,子线程异步减库存),所以是不需要的,可以直接修改这个方法,于是改成传入参数voucherOrder,同样的,子线程无法获取用户id,所以需要从voucherOrder中取,而由于redis已经做了判断工作,这个方法中的判断实际上不需要,保留只是为了兜底,最后直接save订单信息即可
存在的问题:
由于使用的jdk里的阻塞队列,会受到jvm的内存限制,并且给他设置了大小1024*1024,当订单量足够大时,存在内存溢出的风险
另外,若是服务宕机,存在阻塞队列的信息全部丢失,同样有风险
再者,如果从阻塞队列中取出了信息,但在执行任务的时候发生异常导致终止,那么这一份订单的数据同样会丢失
整个完整实现流程
判断重复下单的逻辑在前面已经做了,所以这部分判断其实可以不要
使用阻塞队列实现异步秒杀存在的问题:
由于使用的jdk里的阻塞队列,会受到jvm的内存限制,并且给他设置了大小1024*1024,当订单量足够大时,存在内存溢出的风险
另外,若是服务宕机,存在阻塞队列的信息全部丢失,同样有风险
再者,如果从阻塞队列中取出了信息,但在执行任务的时候发生异常导致终止,那么这一份订单的数据同样会丢失
使用消息队列实现异步秒杀
消息队列是阻塞队列的升级版,首先它独立于jvm,是jvm之外的一种队列,不受jvm内存限制,不必担心内存溢出,同时消息队列做了持久化操作(PubSub不支持持久化),避免由于服务宕机造成数据丢失,并且每当消费者从队列取出一个信息执行任务,必须回应一个确定,否则信息会一直存在与消息队列中,直到消费者回应了确定才消失
redis实现消息队列的三种方式
使用redis的List结构实现消息队列
缺陷:基于List仍然无法避免取出后在执行过程中发生异常导致数据丢失的情况且无法支持多消费者消费同一个消息的功能
使用Pubsub实现消息队列
缺陷:PubSub不支持持久化,而之前的List实现本质上是因为List就是用来做数据存储的,因此有持久化功能(不是说服务宕机会缺失吗?),而PubSub如果没有被订阅,那么Publish出去的消息就没人接收,于是就丢失了,并且,由于消费者(客户端)消费消息的时候其实是先将消息缓存到本地,然后再去消费,如果消费时间过长,那么缓存迟早会爆满,超出时数据丢失(那缓存满的时候不去接收消息不就可以吗???
使用Strem实现消息队列
使用Stream读取消息队列中消息:
阻塞式读取消息:
缺点:使用$读取最新消息,当读取到一条消息并执行相关业务时,如果在执行过程中一下来了多条消息,那么等业务执行完后,只会去读最新一条,中间的就被忽略掉了
比较
消费者组
为了解决因为使用$导致的消息漏读,使用消费者组可以给消息加标识,记录最后一个被处理的消息,既可以防止宕机导致数据丢失,也可以从这条最后处理消息继续往后读取信息,预防信息漏读
同时消费者组还可以消息分流,相当于多个厨师,每个厨师处理各自的单子
消费者组还提供消息确认,pending其实也是消息标识,可以防止宕机造成数据丢失,即重启后根据这个标识再去执行中断的消息,处理完后返回确认信息,保证每一个消息至少被消费一次
创建消费者组
从消费者组读取消息:
注意ID参数,如果是>,则从消息队列中,每次获取未消费的消息(正常情况)
如果是其他,则适用于服务宕机、出现异常导致中断的情况,pending-list中滞留未处理的消息,因此通过参数可以指定去完成这些未处理好的消息(异常情况)