秒杀的是一个很常见的业务了。就是在某个时刻,让大量用户抢购少量的优惠的商品,从而达到商品曝光和电商网站的曝光,增大用户流量,从而提升整体销售额。
比如今年疫情下,各大电商网站,就针对口罩开展了秒杀业务。
通用秒杀逻辑
1、秒杀页动态获取服务器时间,前端根据时间倒计时
2、倒计时结束,开始秒杀
3、获取后端秒杀接口
4、真正执行秒杀(减库存,下订单insert)
一般减库存是放在redis中顶的,因为秒杀那一刻,会有大量请求过来。
下单秒杀
说说之前的一种秒杀场景,就是下单秒杀。
就是每个人都可以下单,只有你下单成功了,然后减库存成功了,才算秒杀成功。
这种秒杀,相对上面那种,会比较重一些,比较难一点。
这种情况,就必不可少的会用数据库。
做的复杂点,是将所有的操作先在nosql中保存起来,或者发出一个mq消息,然后让数据库慢慢消化,但这样就可能会出现不一致的问题。
所以,如果用户量不是特别大,也可以直接考虑用数据库顶。
如果用数据库顶,也要考虑,比如先下单,再减库存好,还是先减库存,再下单好。
直接说结果,那就是先下单,再减库存比较好。
以 MySQL 作为 DB 为例,下订单就是 insert,在使用索引的情况下,insert 插入是行级锁,支持每秒 4W 的并发。减库存就是 update 操作,命中索引时也是行级锁,但是这是个独占锁,所有的操作都要等待前一个释放锁后才能继续 update。
问题就在这里,根据 MySQL 两段锁协议,我们应该把热点操作放到离 commit 近的位置,这样可以减少行级锁的持有时间!自然处理效率就更好一些。
购买资格秒杀
比如京东的那个卖口罩的秒杀,秒杀成功只是代表你获得了购买的资格,然后你跳到结算页,就可以购买成功了,而没有获取秒杀资格的人,短时间也可以跳到结算页,最终会购买失败。如果库存减完了,页面直接置灰,显示秒杀结束。
那这个实现方式,相比秒杀完还要下单的场景,就简单多了,redis搞一个原子计数器,设置好库存,秒杀成功减库存,同时记录秒杀成功的用户。后面只有这些记录的用户才可以购买。
mysql
如果你是一个小的商城服务,成本有限,没有时间和钱去维护那么多nosql,mq等高可用服务,实在没办法了就直接用mysql先抗吧,毕竟小商城,既要有秒杀这种活动,又不想多花钱,但好在用户量也不大,所以用mysql,我觉得用的好的话,也能抗住不少。
比如可以写存储过程,将insert和update放在一个存储过程中,这样的好处是可以减少java客户端与mysql的执行次数,最终是减少了事务锁的时间。
但存储过程,我并没有在真正的生产环境见过这样用的,甚至一些大并发的部门,都是禁止用存储过程的,所以这个用存储过程,也只是一种民间说法,可以作为一个思考方向吧。
缓存
cdn缓存
一般秒杀页的静态资源,包括一些基本不变的css,js都可以静态化处理推送在cdn缓存上。
redis缓存
redis缓存,为了挡住大部分到数据库的请求。
缓存和数据库的一致性考虑,需根据业务不同自行判断。
对于几乎不可改的数据,可以用redis的过期时间,过期了就再从数据库查一次。
对于常改的数据,就需要每次改完数据库都同步一下redis了。
普通业务,只要产品经理靠谱点,都会搞成不可改的数据,如果错了就废弃掉,新建一条。
序列化
redis由于不能存对象,一般可以存json,或者byte数组。
一般使用java原生的序列化也行,就存byte数组,或者糙一点,存json字符串也有,但讲究点的,可以选择换种序列化方式。
这里个人推荐用protobuf序列化。
protostuff序列化包,使用protobuf序列化协议,相比java原生的序列化,速度快,占用空间还小。
为什么又快又小,可参考:https://github.com/eishay/jvm-serializers/wiki