淘宝大秒系统设计详解( 三 )


  • 直接使用Servlet处理请求 。避免使用传统的MVC框架也许能绕过一大堆复杂且用处不大的处理逻辑 , 节省个1ms时间 , 当然这个取决于你对MVC框架的依赖程度 。
  • 直接输出流数据 。使用resp.getOutputStream()而不是resp.getWriter()可以省掉一些不变字符数据编码 , 也能提升性能;还有数据输出时也推荐使用JSON而不是模板引擎(一般都是解释执行)输出页面 。
 
同一商品大并发读问题你会说这个问题很容易解决 , 无非放到Tair缓存里面就行 , 集中式Tair缓存为了保证命中率 , 一般都会采用一致性Hash , 所以同一个key会落到一台机器上 , 虽然我们的Tair缓存机器单台也能支撑30w/s的请求 , 但是像大秒这种级别的热点商品还远不够 , 那如何彻底解决这种单点瓶颈?答案是采用应用层的Localcache , 即在秒杀系统的单机上缓存商品相关的数据 , 如何cache数据?也分动态和静态:
  • 像商品中的标题和描述这些本身不变的会在秒杀开始之前全量推送到秒杀机器上并一直缓存直到秒杀结束 。
  • 像库存这种动态数据会采用被动失效的方式缓存一定时间(一般是数秒) , 失效后再去Tair缓存拉取最新的数据 。
 
你可能会有疑问 , 像库存这种频繁更新数据一旦数据不一致会不会导致超卖?其实这就要用到我们前面介绍的读数据分层校验原则了 , 读的场景可以允许一定的脏数据 , 因为这里的误判只会导致少量一些原本已经没有库存的下单请求误认为还有库存而已 , 等到真正写数据时再保证最终的一致性 。这样在数据的高可用性和一致性做平衡来解决这种高并发的数据读取问题 。
同一数据大并发更新问题解决大并发读问题采用Localcache和数据的分层校验的方式 , 但是无论如何像减库存这种大并发写还是避免不了 , 这也是秒杀这个场景下最核心的技术难题 。
同一数据在数据库里肯定是一行存储(MySQL) , 所以会有大量的线程来竞争InnoDB行锁 , 当并发度越高时等待的线程也会越多 , TPS会下降RT会上升 , 数据库的吞吐量会严重受到影响 。说到这里会出现一个问题 , 就是单个热点商品会影响整个数据库的性能 , 就会出现我们不愿意看到的0.01%商品影响99.99%的商品 , 所以一个思路也是要遵循前面介绍第一个原则进行隔离 , 把热点商品放到单独的热点库中 。但是无疑也会带来维护的麻烦(要做热点数据的动态迁移以及单独的数据库等) 。
分离热点商品到单独的数据库还是没有解决并发锁的问题 , 要解决并发锁有两层办法 。
  • 应用层做排队 。按照商品维度设置队列顺序执行 , 这样能减少同一台机器对数据库同一行记录操作的并发度 , 同时也能控制单个商品占用数据库连接的数量 , 防止热点商品占用太多数据库连接 。
  • 数据库层做排队 。应用层只能做到单机排队 , 但应用机器数本身很多 , 这种排队方式控制并发仍然有限 , 所以如果能在数据库层做全局排队是最理想的 , 淘宝的数据库团队开发了针对这种MySQL的InnoDB层上的patch , 可以做到数据库层上对单行记录做到并发排队 , 如图6所示 。
 
淘宝大秒系统设计详解

文章插图
 
图6 数据库层对单行记录并发排队
你可能会问排队和锁竞争不要等待吗?有啥区别?如果熟悉MySQL会知道 , InnoDB内部的死锁检测以及MySQL Server和InnoDB的切换会比较耗性能 , 淘宝的MySQL核心团队还做了很多其他方面的优化 , 如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的patch , 配合在SQL里面加hint , 在事务里不需要等待应用层提交COMMIT而在数据执行完最后一条SQL后直接根据TARGET_AFFECT_ROW结果提交或回滚 , 可以减少网络的等待时间(平均约0.7ms) 。据我所知 , 目前阿里MySQL团队已将这些patch及提交给MySQL官方评审 。
大促热点问题思考以秒杀这个典型系统为代表的热点问题根据多年经验我总结了些通用原则:隔离、动态分离、分层校验 , 必须从整个全链路来考虑和优化每个环节 , 除了优化系统提升性能 , 做好限流和保护也是必备的功课 。


推荐阅读