其实缓存早期用来实现应用的分布式session,用来解决应用实例间会话的复制(这么做是可以解决不同服务器之间session的共享,但是如果这台缓存服务器挂了怎么办?用户的"session"信息就没有了嘛?假设单点登录SSO可以依托于这种形式构建,那么这种单点登录的用户信息怎么样响应回去?如果放入cookie中被劫持怎么办?。。。),后来发展为将缓存用于业务去重判断、交易快照、图片索引等等场景,最后才是替换了数据库在业务处理中的作用。
一个架构如何能平稳的支持这样的大促秒杀活动?不会因为大促活动出现秒杀商品的超卖、秒杀界面无法访问、甚至造成整个平台不可用的情况出现。需要在传统的架构基础上进一步优化和调整才能满足这些要求。涉及:缓存技术实现商品数据的高性能读取,以满足秒杀活动中对于商品数据访问的同时不会出现商品超卖等致命业务问题。
说道这里,小伙伴们一定会想到互联网常见的并发请求、秒杀......但是秒杀又会分为不同的情况,商品的库存比较小时候的秒杀和商品库存比较大时候的秒杀又是两种不同的处理方式,不墨迹了,进入正文:
1.小库存商品秒杀
比如库存为10个,秒杀价格为xxx元的手机则是典型的小库存商品秒杀活动。在这种活动中,因为商品会在极短的瞬间库存会降到0,所以只要处理好商品的库存减扣(别超卖了!)就可以平稳的度过这次秒杀活动。
①首先一定要让负责秒杀场景的商品中心与普通商品的商品中心进行隔离部署,通过这种服务分组的方式,保持两个运行环境的隔离,避免因为秒杀产生的过大访问流量造成整个商品服务中心的服务实例都受到影响,以免产生过大范围的影响。
②在秒杀开始前,一定会有大量的用户停留在商品的详情页等待着秒杀活动的开始,同时伴随有大量的页面刷新访问,这个时候,如果每次刷新都要从后端的商品数据库获取商品的相关信息,一定会给数据库带来很大的压力(DB又不是铁打的),如果不进行页面缓存,服务器就会秒杀还未开始就进入不可访问状态。。。所以一定要通过缓存服务器将商品的详细信息(包括库存信息)保存在缓存服务器上,这样商品详情页和购买页的所有需要回显的商品信息均可以通过缓存服务器获取,这样就无需每次都访问DB了。
③当用户进入到付款页面时进行成功的付款操作时,则对商品数据库进行实际的商品库存的修改(下单时是预减,相当于锁定商品库存,支付成功之后才是真正的库存修改),当商品的库存被修改后,会同时修改缓存中的对应商品的库存信息,接下来用户在商品详情页和下单页面看到的就是更新之后的库存信息。
④商品需要定时上架,因为商品秒杀通常都是某一时刻开始,所以就要求商品在那个规定好的时间准时上架,如果提前上架就会有活动还未开始商品就被提前下单买完的分险(这样势必有点丢人。。。),如果上架时间晚了的话会给用户造成体验性差,所以在秒杀开始前,商品界面上的必须要有定时器的实现,为了减少服务端的访问压力,定时器倒计时控制并不能每次都通过获取服务端的时间实现,而是在页面每次刷新时才请求一次服务器,当页面没有进行刷新时,界面上的时间倒计时是在客户端进行自行计算的。这时候稍稍懂一点JAVA的小伙伴就会发现一个问题,当知道访问路径和具体参数代表的意义时,完全可以跳过定时器控制直接去访问商品下单的URL,达到秒杀活动前对提前对商品进行下单,所以在服务端接收到用户提交的商品下单请求后,势必要检查一下当前服务器的时间是否晚于秒杀活动的开始时间,否则就要拒绝该下单请求。
⑤商品库存的乐观锁实现:避免商品出现超卖(即成功下单的订单中的商品的库存数量大于商品现有的库存量)的问题,核心还是要利用数据库的事务锁机制,即最终达到同一件商品的库存记录在同一时间不可以被两个数据库事务修改。提到数据库的事务锁,小伙伴们估计想到了乐观锁和悲观锁,结合业务场景分析一下吧。用户在进行商品下单操作时,会进行一系列的业务逻辑判断和操作,对于商品库存信息这一条访问热点数据,如果采用了悲观锁(比如select.......for update,数据库会开启悲观锁来执行),则会给订单处理带来很大的性能阻塞,具体差异我也没测过,但是这种类似于加了Synchronized或者lock的形式,毫无疑问,使用最简单的方式(请求阻塞)来回避并发处理问题,势必性能会急剧下降,所以得采用乐观锁的方式实现商品库存的操作,实现方式也蛮简单的,在最后执行库存扣减操作时,将事务开始前获取的库存数量代入到SQL语句中与目前数据库记录中的库存数量进行判断,如果数量相同,则更新库存的操作执行成功,如果不想等,说明该商品的库存信息在当前事务执行过程中已经被其他的事务修改了,则会放弃这条update的执行,可以采用乐观锁+重新尝试的方法执行该事务,避免商品超卖。SQL:
UPDATE auction_auctions
SET quantity = #inQuantity#
WHERE auction_id = #itemId# and quantity = #dbQuantity#
其中dbQuantity是事务中执行update语句执行前,通过select语句获取到的商品库存数量(先selectupdate)
总结:小库存商品的秒杀场景中,缓存平台提供了对商品相关信息的缓存服务,使得用户只有在最终的下单环节才需要对数据库中的数据进行访问操作,大大降低了数据库的访问压力,并且因为商品库存少,秒杀活动瞬间就结束了,所以采用这种做法基本可以满足小库存秒杀业务。
2.大库存商品大促
这种商品也有很多形式,比如3000个x元的洗衣液。。。同理,因为洗衣液的库存记录只有一条记录,则在大量客户同时下单的过程中,按照上述的订单创建的逻辑,只有当前事务中碰巧在修改商品库存时,该商品的库存信息相比事务开始时没有变化(秒杀情况下这种几率有点小呐),才可以进行库存的更新,否则就会一直采用重试的机制,这样给用户的体验就是下单扣减库存时处于一直等待中,还可能会出现更奇葩的操作:后点击创建订单的客户反而会比前面已经提交订单的客户提前成功下单,甚至是先下单的客户可能因为商品售完因而没有下单成功。
这样就需要将仅仅作为商品信息浏览的缓存,增加一个新的功能:为库存操作提供事务支持。
跟小库存商品秒杀类似,当用户访问商品详情页的时候,显示从还从中获取商品的库存信息,在缓存中获取到商品库存数量大于0时,用户进入购买页面并且点击下单按钮时,后端处理跟小库存秒杀有了不同的处理。
用户在进行了下单操作后,程序首先会给该订单创建一个订单详细记录,只不过在库存扣减成功前,该订单状态是用户不可见的,保存该订单信息记录的意义是当缓存服务器突然就挂了的情况下,可以通过商品数据库中的初始化缓存的信息(初始化商品库存)和订单详细信息(下单的行为记录)就可以还原出订单最新的处理状态,不至于出现致命性数据丢失了。
在数据库中成功创建订单详单后,会发送一个库存修改的请求到消息服务器(MQ),可以利用消息服务让库存修改的异步MQ调用,这里就牵涉到分布式事务了,分布式事务解决如果采用了TCC(两阶段提交)就太LOW了,TCC处理时间比较长,可以采用基于消息的分布式事务,RocketMQ之前可以支持这种形式的分布式事务,只需要保证事务的弱一致性,即最终一致性,
1.MQ发送方给MQ服务器发送一条预备消息
2.MQ服务器响应MQ发送方:你的消息发送成功了
3.MQ发送方执行自己的本地事务,例如订单的创建
4.根据本地事务的执行可分为commit和rollback,将本地事务处理结果发送给MQ服务器
5.MQ服务器判断:
如果是commit:发送消息。如果是rollback:删除消息不投递(相当于消息回滚了。哈哈)
6.如果这时候MQ服务器既没有收到成功,也没有收到失败,则是处于一种pending状态,MQ服务器可以支持保存在事务消 息堆栈中的事务消息进行定时扫描,如果一段事务消息在该堆栈上的保存时间超过了一段时间,MQ则会进行检测本地事 务的最终执行状态,如果成功则commit,失败则rollback。
7.MQ的订阅方根据MQ服务器中的消息进行拉取(pull)/推送(push)得到消息后,执行MQ订阅方自己的本地事务,消费 消息。
-----------------不扯这么多了,回到大库存商品大促上来吧。。。。。。
使用这种分布式事务解决一致性问题(分布式事务没有最好的处理方案!),当订单创建成功以后,发送消息到MQ,当消息的订阅者获取消息后,对缓存中的库存数据进行更新处理,因为缓存数据的更新时间在纳秒级,所以整体的库存处理能力相比传统的数据库差别数百倍。当缓存中的库存数据更新成功之后,则将之前创建的订单详情状态修改为下单成功状态,这个过程才算完成。
这个方案中,将订单交易创建环节中对于原本商品数据库的库存信息操作替换为在缓存服务器中运行,体现了缓存服务相比于传统关系型数据库的优势,这个方案中我坚持自己的看法:订单服务异步使用MQ去调用库存服务,记得那会儿,阿里的技术总监对我进行技术终面的时候,问我为什么不直接使用同步调用RPC的方式在订单创建完成后去修改商品库存的数量,那会儿也真是跟总监杠上了,leader认为没有必要以异步MQ的方式去处理这个问题,但是我还是坚持了自己的想法。。。我的观点是异步调用MQ速度上相比较同步调用RPC的方式肯定是要快的很多,因为只需要关注本地事务执行成功后消息时候发出去了,另外的一半就不用管了,但是分布式事务较难解决,存在风险性,只要保证弱一致性就可以了,就不用等待RPC的调用返回了,因为RPC调用时传输时间我觉得也是个大头啊。哈哈
写了这么多。。。能加个鸡腿吗