视频地址:
http://www.imooc.com/learn/587
http://www.imooc.com/learn/631
http://www.imooc.com/learn/630
http://www.imooc.com/learn/632
这个视频一共分为四篇,前面三篇比较基础,我基本快速过了一下,没怎么记录,最后一篇开始讲高并发,这一篇还是很精彩的,很多干货。对Spring、Mybatis有基础的可以直接看最后一篇。
一、秒杀业务分析
上图是天猫的秒杀系统,比较复杂,本案例并不会实现这个,而是实现一个秒杀的基本功能,如下图所示。
通过这个简易版秒杀流程可以看出:秒杀系统有两个角色:商家和用户。
- 对于商家来说,是添加秒杀商品,然后添加/调整库存,然后发货/合账。
- 对于用户来说,参与秒杀,付款/退货。
秒杀的核心在于库存的管理。
因为判断用户的秒杀是否成功,是否成功取决于库存,被秒杀的数量和库存量必须一致,秒杀不能多(货不够),也不能少(货没卖掉)。
二、秒杀系统数据库原型
1、秒杀的事务性
一个基本的秒杀操作至少包含两个,一是减库存(update操作,库存数量-1),二是记录购买明细。
这两个操作都存在才能算是一个完整的秒杀,少一个就有问题了,所以两个操作必须在同一个事务中。
2、秒杀的并发性
秒杀跟普通的业务相比,在于短时间内的大量并发操作,对于同一商品有限库存的竞争。
3、决定使用mysql
nosql类数据库能提供快速的插入能力,可以处理大量并发,但是在事务方面不行,所以我们关系型数据库mysql来保证数据的事务性。
mysql的行级锁可以保证数据的事务性,并且可以提供1秒2w左右的并发(单实例),很适合秒杀场景。
三、秒杀系统增删改查
1、技术分层
跟普通系统一样分三层:dao、service、controller
dao这里选用mybatis,因为是自己写SQL,性能显然比hibernate、springdataJPA等高效一点,也为后面优化做铺垫。
service和controller就用spring和springmvc,提供rest接口,这块儿没什么特殊的
2、业务逻辑
1)【用户】查看秒杀商品;
2)【系统】返回秒杀商品详情介绍界面
3)【用户】浏览器端用js做一个倒计时,到时间自动向服务器请求秒杀链接;
4)【系统】判断是否在秒杀期限内,如果在期限内,则给用户返回秒杀链接,否则返回等待
5)【用户】得到秒杀链接,点击秒杀;
6)【系统】判断库存,如果有则减库存并记录秒杀日志;如果库存为0则返回秒杀失败。
四、秒杀系统优化
这里系统做的主要是步骤2、4、6,先来说2。
1、步骤2优化
第2步需要给用户返回商品详情,详情页面内容比较多,包括数据、图片、介绍视频等,会对数据库和web服务器的带宽造成压力。
这里因为秒杀的商品信息是不会变化的,所以可以认为是一个静态资源,我们可以采用cdn来加速用户访问,用户不需要连接到真的服务器就可以查询这个信息,也就减轻了服务器的压力。
ps:cdn也有个问题,他相当于一个缓存,如果你修改了秒杀商品,cdn并不会马上更新,所以会导致用户看到的商品跟服务器不一致。所以为了避免这个情况,一般都不去修改秒杀商品。如果非要修改,就把商品逻辑删除,重新发布一个商品。
2、步骤4优化
上面的业务逻辑中,实际上已经对步骤4做了优化,就是第3步这里,给用户做了一个倒计时,等秒杀开始,才给他返回第4步的秒杀链接,避免用户自己频繁刷新浏览器给第4步造成很大压力。
但是当秒杀开启短时间内,特别是秒杀开启瞬间,大家都都来请求秒杀链接,虽然是根据主键查询,但是仍然不够。这里我们可以采用redis,将主键查询的对象直接放到redis中,下次再有用户请求,直接从redis中获取返回。
关于redis,还有一个优化点是序列化,因为redis中存储的是bytes,我们需要将对象做序列化,这里建议使用google的protostuff框架做序列化,而不是用jvm自带的Serializable接口。下面的链接可看看常用序列化框架的对比,差距真的很大。
查看更多对比:https://github.com/eishay/jvm-serializers/wiki
3、步骤6优化
这里是秒杀的最大难点,也就是处理竞争。我们前面用到行级锁来保证事务,相当于所有用户都要在这里排队,一个用户锁定后,只要他不释放锁,其他用户全部都要等待,所以我们肯定是尽量缩短用户对行级锁的占用时间。
如何做呢?
上面这张图我们前面已经提到,再来看一次,基于行级锁的机制,用户1秒杀操作过来的时候,我们在update这一步锁掉行记录、减库存、insert购买明细,然后提交/回滚,然后解除行级锁。
所以用户1占用行级锁的时间是:锁记录、减库存、insert购买明细、提交/回滚、解除锁。
我们注意到,这里有一个insert购买明细的操作,如果我们把它拿到外面去,流程改为insert购买明细、锁记录、减库存、提交/回滚、解除锁,那么用户1占用行级锁的时间就缩短了。
不过,insert操作仍然是需要占用数据库资源的,不过,这个操作因为不在锁内,就可以并发执行的,所以可以忽略。
五、网络层面进一步优化步骤5、6
经过上一步,步骤1、2我们采用cdn,基本完全屏蔽了对服务器的压力;步骤3、4在业务逻辑中自动优化了;步骤5、6通过把insert逻辑踢出锁,也提高了性能。
大家经过这些优化后可以做一下性能测试,如果已经满足了自己的需求,就可以到此为止了,如果你的需求比较高,那么我们还可以进一步优化第6步。来进一步分析。
第6步是先执行减库存(update)
如果update成功,则执行commit,如果update失败,则执行rollback
前面我们提到过用户和系统的交互,刚才这个过程,我们也可以进一步细化为三个角色的交互:java、db和网络
1、【java】请求执行update
2、【网络】传输update请求
3、【db】执行update并返回结果
4、【网络】返回update结果
5、【java】判断update结果是否成功,请求commit/rollback
6、【网络】传输commit/rollback请求
7、【db】执行commit/rollback并返回结果
8、【网络】传输commit/rollback请求
9、【java】返回秒杀结果
这里可以看到4次网络传输的过程,有同学可能会说,这是不是过分了,程序不都是这么写的么,从来没想过网络传输的问题。
但是我们是秒杀系统,这些过程都是串行执行的,我们来做个数学运算。
通常来说,同城机房的网络传输是1ms,异地机房(假设北京到上海)的时间第5ms。
假设一次网络传输是1ms,那么一次秒杀请求至少需要串行4ms,那么1秒的秒杀次数=1000ms/4ms=250次。
假设一次网络传输是5ms,那么一次秒杀请求至少需要串行20ms,那么1秒的秒杀次数=1000ms/20ms=50次。
此外,如果中间某一次返回java的时候,来一下gc,就又是50ms。
所以如果你要抗1w人的在线秒杀,你就需要至少40台的mysql集群,而且保证你的mysql不受其他影响,网络也都是最好。
那么我们的优化思路也就很清晰了:减少网络交互。有两种方法,效果都是一样,就是把java跟db的两次请求(一次是update,一次是commit/rollback),改成一次请求。在第3步完成时,就完成了事务并释放行级锁,到这里网络请求只经过了一次。所以我们就可以成功的把4次网络交互缩短到1次,并且一定程度上避免了并发过程中的gc,大大提高并发。
1、修改mysql源码,发送定制SQL,如下图所示,我们的sql中加入了一部分/* */格式的注释,告诉mysql,如果update成功就commit,如果update失败就rollback
这种方式使用起来很简单,但是需要改mysql源码,很多公司可能不具备这样的能力
2、另外一种就是使用db自带功能:存储过程,也可以实现跟第一个类似的功能。
具体存储过程没什么特殊的,就是把我们上面的逻辑用存储过程实现一遍,我就不贴代码了。
六、总结
经过一系列的优化,我们就实现了高并发的秒杀API。
1、秒杀商品请求采用vpn
2、秒杀链接到时间才提供,避免无效请求;使用redis缓存秒杀链接
3、无效操作尽量不要放到行级锁过程中(insert秒杀记录日志)
4、减少行级锁过程中的网络交互
如果觉得有用就关注一下吧~