本次的项目实践是使用 SpringBoot+Mybatis 对电商项目中秒杀模块的实现. 目前阶段使用4台云服务器来做分布式集群,提高并发处理性能.
-
SpringBoot
-
Mybatis
-
Redis
-
Rocketmq
-
Mybatis-generator
-
joda-time
-
lombok
-
guava
部署环境为4台服务器,一台作为nginx反向代理服务器,两台作为WebServer服务器,一台作为数据仓库, 拓扑图如下:
-
接入层模型 ViewObject:
- View Object与前端对接的模型, 隐藏内部实现,供展示的聚合类型
-
业务层 Model:
- 领域模型,贫血+调用服务来提供输出能力
-
DataObject
- 为领域模型的具体存储形式,一个领域模型由一个或多个DataObject组成,每一个DataObject对应一张表,以ORM方式操作数据库.
-
自定义异常类属性:
-
errCode: 业务逻辑错误码 ( 如:1开头的错误码为通用错误,2开头的错误码为用户相关错误,3开头的错误码为交易信息相关错误等.. )
-
errMsg: 错误提示
-
-
用户模块:
- 注册(使用手机获取验证码短信来注册)
- 登陆
-
交易模块:
- 下单操作
-
商品模块:
- 创建商品
- 展示商品
-
促销模块
- 维护促销商品列表
-
- 问题:分布式部署时,session存储在Web服务器端,下次用户请求过来,通过负载均衡,可能会分发到其他服务器节点,导致了session不一致问题
- 解决方法:
- 使用redis来做一个集中式缓存,将session存放到redis中,每次请求过来时,会从同一个redis节点中读取session信息
-
- 技术点:设置多级缓存,减少对数据库的访问次数,对于本地热点缓存,通过设置过期时间来保证数据库和缓存的数据一致性。
- 本地热点数据缓存
- 特点:
- 使用的是JVM的缓存来对热点数据进行存储;因为当数据库对缓存中的数据进行修改后会造成脏读,所以本地缓存的过期时间要设的尽可能的短.
- 第三方库:
- Guava:
- 提供了这样一种可控制大小和超时时间的线程安全的Map——即Guava Cache
- 可配置lru策略(置换算法配置)
- Guava:
- 特点:
- Redis集中式缓存
-
- 技术点:将一些写操作从数据库移动缓存中,使用异步消息队列保证缓存和数据库的最终一致性。
- 用户风控, 活动校验等环节能用读缓存解决的就尽量不要读数据库.
- 库存的行锁优化
-
将扣减库存操作放到redis中操作:
- 预先将库存信息刷入redis中
- 落单后在redis中减库存
- 使用异步消息队列Rocketmq异步同步数据库内,从而保证库存数据库最终一致性保证
-
问题的发现与解决:
- 使用消息队列异步同步数据库出现问题时,仍然无法保证缓存和数据库的一致性,例如:
- 异步消息发送失败
- 扣减操作失败
- 下单失败无法正确补回库存
- 解决办法,将 “创建订单事务提交成功” 和 “消息发出” 这两个动作再次绑定成一个事务,即可解决,也就是说,发一条事务型消息。
- 使用消息队列异步同步数据库出现问题时,仍然无法保证缓存和数据库的一致性,例如:
-
问题的发现与解决:
- 遇到问题:
- 在定期checkLocalTransaction的UNKNOWN状态的消息时,仅以当前传入消息的参数还不够(仅传入了ItemId和扣减个数amount)。
- 解决问题:引入库存流水:
- 数据类型:
- 主业务数据(例如ItemModel)
- 操作型数据(记录某个操作,便于追踪该操作的状态,保证异步操作的正确执行(例如具备回滚的能力等等)), 例如配置,中间状态的记录,等等
- 数据类型:
- 遇到问题:
-
问题的发现与解决:
- redis缓存在某些时刻仍无法保证与数据库一致性, 例如:
- 在缓存中扣减之后,出现异常,回滚之后,缓存里的值并没有被回滚.
- 在消息队列中,若线程在createOrder之后挂掉了,既无法进入catch代码块,也没有跳出try块,消息的状态将用于处于UNKONWN的状态..
- 问题的解决:
- 严格的缓存, db一致性将是以时间开销为代价的,因此,如果仅仅是保证缓存与数据库的最终一致性,可以根据具体的业务场景来决定高可用技术的实现,例如在本次的秒杀场景中的原则为:宁可少卖,不能超卖。
- 解决方法:
- 因为宁可少卖,不能多卖的业务场景,可以允许redis比实际数据库中少.
- 超时自动释放(若订单流水在一定时间内没有从unkonwn状态转变,那么将会将此订单作废)
- 分布式锁(日后看
- redis缓存在某些时刻仍无法保证与数据库一致性, 例如:
-
库存售罄
- 加库存售罄标示;售罄后不去操作后续流程;售罄后通知各系统售罄;回补上新;
-
在多线程场景下保证“库存扣减”的安全,防止超卖
- 在进行库存扣减的时候,使用分布式锁来保证线程安全.(本次使用)
- Redis + Lua脚本实现CAS也能够保证线程安全.
-
-
- 技术点:秒杀令牌
- 秒杀令牌:
- 原理:
- 秒杀接口需要依靠令牌才能进入
- 秒杀的令牌由秒杀活动模块负责生成
- 秒杀活动模块对秒杀令牌生成全权处理,逻辑收口
- 秒杀下单前需要先获得秒杀令牌
- 解决的问题:
- 防止机器人刷下单接口。
- 具体实现:
- 下单时,前端ajax代码的click操作中,在下单逻辑之前,增加一个生成秒杀令牌的操作,然后服务端根据userid, itemid, promoid生成对应的秒杀令牌字符串,存到redis中并返回给前端,然后前端使用这个令牌作为钥匙,来去下单,这就确保了下单操作是在网页端进行的。
- 缺陷:
- 当上亿用户在浏览器手动点的时候,瞬间并发量也是很惊人的, 会把redis爆破..解决方案为秒杀大闸
- 原理:
-
-
验证码错峰:
- 包装秒杀令牌前置, 用数学公式生成验证码
-
限流:
-
借助Guava实现接口限流:
- Guava:
- 初始化:
RateLimiter rl = RateLimiter.create(10);
- 获取令牌:
bool canGetToken = rl.tryAcquire() ;
- 初始化:
- Guava:
-
Guava的限流方法是对令牌桶算法的实现:
- 令牌痛算法:
- 算法介绍:
- 服务器端维护一个桶,每访问服务端一次桶内减少一个令牌,同时,每秒钟往桶内增加x个令牌.
- 解决场景:限制某一秒流量的最大值,用于保护系统不会因浪涌流量而发生崩溃。
- 算法介绍:
- 令牌痛算法:
-
其他限流方法:
- (额外补充)漏桶算法:
- 算法介绍:
- 服务端维护一个桶,桶内最多只能有k个令牌,每访问服务端一次桶内增加1个令牌,每秒桶内减少1个令牌。
- 解决场景:希望网络流量以平滑平稳的方式流入,对于浪涌流量应对很差.
- 算法介绍:
- 线程池/信号量也可以进行一定程度的限流
- (额外补充)漏桶算法:
-
-