一种高并发电商秒杀系统的设计与实现
2019-03-02刘磊
刘磊
(广东开放大学,广州 510000)
1 秒杀业务分析
作为现代电商的重要促销手段,商品秒杀、抢红包这类“短时间、高并发”的需求越来越多。一个典型的电商秒杀系统围绕着商家、库存、用户、订单四者展开[1]。首先,商家会添加、调整商品库存,库存则会反映商家的发货、核帐信息;用户成功秒杀商品,库存进行减法操作,同时用户订单进行加法操作,插入一条秒杀记录明细;用户的付款、退货行为,也会反映在库存和订单。秒杀系统处理的是大量用户瞬时对有限商品的激烈竞争,它的高并发点出在前端用户秒杀商品环节,其核心是库存的高效处理。
用户每个成功的秒杀行为都包括两个动作:减去商品库存和记录购买明细,业务要求这两个动作必须同时成功,若减库存而没有记录购买明细就会出现超卖,记录了明细却没有减库存就出现了少卖,这两种现象都是不正常的。因此,减库存与记明细这两个原子操作要么都成功,要么都不成功,它们组成了一个完整的事务,秒杀业务场景具有典型的“事务”特性,如图1所示。要保证以上操作数据顺利存储,有两种方案:使用RDBMS(关系型数据库)或使用NoSQL(非关系型数据库),NoSQL的重要场景在缓存,对事务支持较差;RDBMS支持鲁棒性事务,业界研究表明,事务机制依然是目前最可靠的数据落地方案[2-3]。
图1 秒杀的事务特性
2 系统设计
2.1 功能设计
为着重验证秒杀功能,笔者设计了一个业务具有典型性的秒杀系统[4],功能设计如下:
(1)用户注册登录:用户注册账号,使用账号登录系统。
(2)秒杀列表查询:以列表形式展示秒杀商品。
(3)秒杀详情查询:以单页面展示秒杀商品详细信息,包括秒杀倒计时、秒杀按钮。
(4)生成秒杀地址:为了避免用户作弊,即秒杀开启前提前在浏览器输入秒杀地址进入秒杀,设计只有在秒杀开始时才能暴露秒杀地址,使用MD5加盐加密生成地址,防止用户撞库破解。
(5)执行秒杀:系统最核心功能,用户执行秒杀,成功则依次进行减库存和记录购买明细两个动作,同时返回秒杀成功响应给前端用户,秒杀失败也返回相应失败信息。
(6)用户中心:以列表形式展示用户成功秒杀的商品,对接付款模块,显示订单状态。
2.2 数据库设计
围绕秒杀业务和功能分析,本方案数据库设计三个核心实体对象:用户、商品和订单。用户实体记录注册登录信息,用户ID设置为主键;商品实体记录一件商品的名称、库存量、秒杀开始时间和结束时间等库存详情,商品ID设置为主键;订单记录秒杀成功后的明细,即哪个用户秒杀了哪件商品,当前状态是未付款还是已付款。根据业务分析,一个用户可以秒杀多个商品,一个商品也可以被多个用户秒杀,用户与商品是多对多的关系,同时秒杀业务要求同一用户对同一商品不能重复秒杀,也就是订单表不能插入用户ID和商品ID两者都相同的记录,解决策略是只需要将商品ID与用户ID组成订单表的联合主键即可。本秒杀系统数据库模型设计如图2所示。
图2 数据库模型图
2.3 流程设计
用户秒杀商品的典型流程是:①首先,用户查看所有的秒杀商品列表,点击选中的秒杀进入商品详情页;②详情页除了显示商品名称、详情、开始时间、结束时间等“静态信息”外,每次加载详情页,都要动态请求服务器标准系统时间,若系统时间小于开始时间,则返回前端提示,页面使用特效展示秒杀倒计时;③若系统时间大于开始时间并且小于结束时间,则意味着秒杀进行中,系统生成秒杀地址并返回前端显示秒杀按钮,用户需要登录或提前登录执行秒杀,成功秒杀则后端数据库执行减库存和记明细两个事务操作,返回前端秒杀结果;④若系统时间大于结束时间,则意味着秒杀结束,返回前端结束信息。用户在商品详情页的交互流程如图3所示。
图3 用户秒杀流程图
3 技术实现
本系统实现方案采用业界流行的技术组合:Linux+Tomcat/Nginx+SSM+Bootstrap+MySQL+Redis+Maven,这套组合被阿里、京东等互联网公司广泛应用于大中型Web应用,非常适于开发高并发、高性能、高扩展的Web系统。
(1)SSM:SpringMVC、Spring、MyBatis三大框架的完美组合,主流的J2EE企业级开发框架,具有轻量级、代码侵入性低、技术成熟的特点,支持典型的三层架构:DAO(数据层)、Service(业务层)、Web(表示层)[5]。MyBatis是数据层框架,支持定制SQL语句,传参自由、灵活,结果集自动赋值,接口设计和SQL语句的分离,方便代码Review。Spring提供了一个统一托管对象的容器工厂,允许通过一致的访问接口,访问工厂里的任意实例,也就是对象控制反转(IoC);Spring还支持声明式事务,对于高并发应用,带事务的方法往往是瓶颈所在,一不小心就可能导致数据库访问延迟,使用声明式事务可以方便地开发单一、纯净的事务方法。SpringMVC是Web层框架,支持RESTful风格的URL和MVC开发模式。
(2)Bootstrap:简洁、直观、强悍的前端开发框架,基于 HTML5、CSS3、jQuery技术构建,提供了导航、分页、面板等可复用的静态组件,下拉菜单、标签页、弹出框等动态插件,强大灵活的栅格布局系统,使用Bootstrap可以快速、高效地开发健壮和优雅的响应式静态页面[6]。
(3)MySQL:最流行的用于Web开发的关系型数据库,支持事务和锁机制,可以单点、主从复制、集群多种规模运行,经测试,MySQL执行同一条update压力测试约为4万QPS,即一秒可以卖4万个商品,能够顶住大部分秒杀压力,本方案使用MySQL持久化数据[7-8]。
(4)Redis:基于内存的NoSQL型数据库,经测试Redis性能非常高,每秒钟SET操作可执行110000次、GET操作可执行81000次,本方案使用Redis缓存热点数据[9-11]。
(5)Maven:强大的项目构建工具,以标准化的方式管理编译、生成、发布、文档等整个项目建设生命周期,提供更佳的项目依赖和持续集成管理。
(6)Tomcat/Nginx:Tomcat轻量、稳定,用作应用服务器,Nginx具备强大的并发处理、灵活的反向代理能力,用作负载均衡服务器。
使用以上技术组合,编码分为SQL编码、DAO层编码、Service层编码、Web层编码,系统各层关键接口对接情况如表1所示。
表1 系统关键接口对接表
4 高并发瓶颈分析及优化
秒杀的本质是大量用户瞬时集中竞争有限的商品,秒杀系统最大的瓶颈点在商品秒杀环节。当秒杀开启时,大量用户涌入查询商品详情页、同时点击秒杀按钮,请求流量瞬间达到高峰,若将所有请求都直接发送到服务器,将耗损服务器大量响应资源,单台服务器并发处理能力有限,严重时可能导致服务器宕机,为了保证大量请求的及时处理,使用其他服务器分流依然是最有效的应对策略。
一个成功的秒杀行为在服务器要执行两个业务逻辑:减库存、记明细,反映在数据库层面,这两个原子操作组成一个事务,要么都提交,要么都不提交,示意代码如下:
Start Transaction
Update减库存
Insert记明细
Commit/Rollback
大量用户在有限的时间内竞争有限商品,意味着很多用户同时去执行同一个商品ID的减库存操作(如:update table set num=num-1 where id=1001),但是数据库对同一行数据修改是有行级锁机制的,即一次运行完提交后,下一个才能运行,这就让很多用户“堵塞”在update操作,并发量大时系统响应性能呈指数级下降,这也是秒杀业务逻辑的并发瓶颈所在。对于秒杀系统来说,高并发优化就是疏通“堵塞”、引导“分流”,让更多人更快通过“瓶颈”参与“竞争”[5-6]。
针对以上分析提出以下具体优化措施[12-13]。
4.1 使用C C D D N N缓存静态化资源
CDN即内容分发网络,通过在网络各处放置节点服务器构成智能虚拟网络,能够实时将用户请求重定向到响应最快的服务节点,使用户就近取得所需内容,开发人员可以自己搭建或租用云CDN服务。商品秒杀详情页是并发请求量最集中的页面,为减少服务器请求,可将详情页里静态化的资源剥离出来推送到CDN节点,例如JavaScript脚本、CSS、图片等不常变化的资源,也就是在前端缓存静态内容,这样一部分请求就被重定向到CDN节点,如果命中资源就不需访问后端服务器,减轻了服务器压力,也提高了响应速度。
4.2 使用Redis优化生成秒杀地址
详情页里有部分资源是不能缓存到CDN节点的,例如请求服务器标准时间,Java执行一次new Date()约只需10ns,因此不会成为瓶颈。请求秒杀地址也不能缓存到CDN,秒杀地址为防止被猜到,是由服务端在秒杀期间动态生成的,每次查询详情页都要请求一次秒杀地址是否开启,如果开启则通过MD5加盐后再加密生成不可逆的地址字符串发送到前端,判断是否开启需要查询秒杀商品的开始时间和结束时间,为提高响应速度,可将秒杀商品查询结果缓存在服务端。本方案选用高性能的Redis用作服务端缓存,方法是首先在Redis查找商品,若找到则直接返回;找不到则查询数据库,同时缓存一份在Redis。为避免单机故障,也可以做成Redis集群使用。
4.3 使用存储过程优化行级锁持有时间
上面分析出秒杀环节的瓶颈点在对同一商品“竞争”执行update操作,数据库对一行数据执行update操作时,会加上行级锁,此时其他update操作处于等待,等事务提交或回滚后释放行级锁,其他update操作才能依次执行,而insert操作是可以并发运行的,也就是行级锁持有时间阻塞了秒杀进程;Java与MySQL交互,使用Java执行SQL语句的耗时,包括SQL语句执行耗时、发送语句到MySQL和返回结果的网络延迟、GC操作耗时,一次秒杀行为的耗时计算公式为:一次秒杀耗时=update耗时+insert耗时+网络延迟+GC耗时+其他耗时。所有耗时加起来,秒杀行为的总耗时就变长了,其实Java不慢,MySQL也不慢,但是使用Java操作MySQL,中间耗时浪费,响应就慢了[14-16]。
因此减少行级锁的持有时间和降低网络延迟即可大幅降低一次秒杀耗时,方法是:①将insert调到update之前运行,insert可以并发执行,若update成功,则可以同时提交,否则,则同时回滚,只有update需要锁住,性能提升一倍;②将业务逻辑封装成存储过程放在MySQL服务端运行,Java客户端只需要拿到执行结果即可,MySQL本地执行存储过程是很快的,这样大部分网络延迟和GC耗时会被消灭掉。行级锁持有时间优化前后对例如图4所示。
经过多组测试,对同一行数据执行一次update操作,使用存储过程事务的行级锁持有时间大约为6ms,使用Java客户端托管的事务行级锁持有时间大约为40ms,相差34ms,这意味者如果有500人同时竞争同一个热点商品,优化后的事务排队时间可以减少17s(34ms×500)。
图4 行级锁持有时间优化
4.4 使用集群化部署提高系统抗压能力
理论上,一台服务器若能顶住1000的并发量,两台服务器就能顶住2000的并发量,一般来说,当上线的Web系统并发量达到一定数量级时,都可以通过大规模服务器集群化部署提高系统的抗压能力,也就是常说的“当一头牛拉不动时,那就用三头牛”。本方案设计的系统架构、选用的开发框架都非常适于以集群的方式运行,对于实际线上的电商秒杀系统,通过不断增加服务器可以快速提高系统的并发处理能力[17-18]。本系统线上集群化部署可以分为五层:①CDN缓存层:缓存静态化资源;②负载均衡层:根据访问量,负责将请求智能转发到不同服务器;③Web应用层:在Tomcat集群上部署相同的Web应用;④Redis缓存层:缓存数据库查询结果;⑤数据库存储层:数据落地,使用主从库同步、读写库分离、分库分表等技术增强抗压能力。线上秒杀系统集群化部署方案参考图5所示。
图5 秒杀系统集群化部署方案
5 测试与应用
为了测试本文设计的秒杀系统的性能、可用性和并发处理能力,笔者使用Apache JMeter进行了三组压力测试,为避免硬件带来的误差,三组测试环境使用了相同的机器配置,统一CPU为Intel Core i5 3.3GHz、内存为8GB,网络环境为实验室局域网。三组测试分别为:①测试一使用1台服务器,MySQL与Tomcat安装在一起;②测试二使用2台服务器,1台安装Tomcat,1台安装MySQL,Web服务器与数据库分开部署;③测试三使用10台服务器模拟小型集群环境,2台安装Nginx作负载均衡服务器,1台安装Nginx作静态资源服务器,3台安装Tomcat作应用服务器,1台安装Redis作缓存服务器,3台安装MySQL作数据库服务器。从测试一到测试二再到测试三,不断增加服务器数量和并发数,以验证系统并发承载能力,三组测试得到的并发数、服务器CPU利用率、平均响应时间如表2-4所示。
表2 测试一
表3 测试二
表4 测试三
由此可见,通过不断增加服务器,分层部署系统,将请求流量分发到不同节点;采取一定的优化策略,将静态化资源分流,将部分查询结果缓存,将网络延迟降至最低。这些措施可有效提高秒杀系统的性能、可用性和并发处理能力。本方案实现的秒杀系统作为笔者参与的电商平台核心模块已经上线运行,通过生产环境中的并发优化和集群化部署,在多次的促销活动中,顶住了上万的并发压力,表现稳定。
6 结语
本文设计和实现的电商秒杀系统解决方案,使用SSM框架实现高扩展性,使用MySQL事务机制保证秒杀数据完整性,使用Redis缓存提高系统访问性能,使用集群化部署增强系统并发处理能力,本方案具有典型性,对于解决秒杀、抢红包这类“瞬时高并发抢资源”的需求有较普遍的参考价值,本文没有更多讨论秒杀系统的安全性,有待进一步研究。当然秒杀作为各大电商的促销手段,其线上实现方案不止一种,也有利用分布式MQ记录行为消息、再异步数据落地或者利用队列在内存中操作的方案,没有最佳和一劳永逸的方案,只有最适应业务需求的方案,读者可以根据场景灵活选用。