对分布式系统课程电商秒杀项目的再思考

本文探讨了一次分布式系统课程电商秒杀项目中的架构迭代,从基于MySQL乐观锁到Redis/Zookeeper分布式锁的转变。分析了性能、业务逻辑、数据一致性、耦合性和可扩展性等多个角度,提出了优化方案,包括减少请求数据、合并请求、优化路径、减少依赖、避免单点等。同时,详细讨论了Redis、Kafka、MySQL在秒杀场景下的性能优化策略,如使用分布式锁、消息队列幂等性、分库分表、读写分离等,旨在提高系统性能和数据一致性。
摘要由CSDN通过智能技术生成

项目源码: https://github.com/AlexanderChiuluvB/DistrubutedSystemProject

本项目经历过一次架构的迭代,核心思想是把保证数据安全的机制从基于mysql乐观锁机制迁移到redis/zookeeper基于分布式锁机制,默认只要redis减库存成功就给用户返回秒杀成功的消息,但在实际应用场景中这种方法是有问题的:一是并没有考虑到如何保证缓存和数据库的最终一致性,可能redis减库存成功了,把消息发送给kafka失败了或者mysql消费失败了,最终mysql并没有成功创建订单,也没有做库存的持久化。

这种方法默认Redis减库存成功之后就秒杀成功,或者说是"预减库存成功",那么接下来的事情只是如何
来保证mysql接收到这个减库存的消息。但如果Redis减库存之后的操作失败了呢?Redis事务是不支持回滚的!我们这里的做法是Redis变化影响导致Mysql发生变化,但更为理想的做法应该是Mysql的变化导致Redis的变化,因为其实保存在Redis的数据其实是很不稳定的。万一Redis崩了,很容易发生数据的丢失。比较保险的是只有保证Mysql减库存成功才能返回给用户秒杀成功。

而且Redis减库存成功了,但是Mysql还没有落单,如果减库存成功而支付失败了,这时候同样会造成缓存和Mysql数据不一致的情况,而Redis事务是不支持回滚的,怎么办? 或许可以Mysql定时异步更新Redis的库存量,或许Redis可以订阅Mysql的binlog?

但这也并不意味着原来的方案是错误的,其实秒杀架构并没有唯一解,可以有多种思路。所以我的想法只是抛砖引玉而已。

于是本文从性能角度,业务逻辑角度,保持缓存数据库一致性角度,架构耦合性,可扩展性角度分析2019秋复旦大学分布式系统秒杀课程项目的一些可改进的地方。

核心解决问题:

一般Redis和Mysql的数据一致性是怎么保证的?

如何解决Kafka消费失败导致的缓存数据库不一致?

如何解决Kafka重复消费(幂等性)问题?

可以从哪些角度提高系统性能?

在进入正文之前,不妨可以分析一下,设计一个大并发,高性能,高可用的分布式秒杀系统应该要考虑什么?

架构原则:“4要1不要”

以下规则参考的是许令波的《设计秒杀系统》极客时间课。

1.数据要尽量少

用户请求的数据尽量能少就少,这里的数据指的是上传给系统的数据以及系统返回给用户的数据。

首先,数据在网络上传输需要时间,服务器处理数据的时候通常还要做压缩和字符编码操作,这些操作都是十分消耗CPU的,所以应该尽可能减少传输的数据量来减少CPU的使用。具体做法可以是简化秒杀页面的大小,去掉不必要的页面装修效果

其次,尽可能减少系统依赖的数据。调用服务会涉及到数据的序列化和反序列化,这也是CPU的一大杀手,同样会增加延时。所以尽可能减少和数据库打交道的机会。

2.请求数量要尽量少

对于秒杀系统来说,HTTP请求大多是短请求(建立的都是非持久性链接)。浏览器每发出一个请求多少都会有一些消耗:如建立连接时候TCP三次握手,如DNS域名解析。

减少请求数量最常用一个实践是合并CSS和JavaScript文件。

把多个 JavaScript 文件合并成一个文件,在 URL 中用逗号隔开(https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js)。这种方式在服务端仍然是单个文件各自存放,只是服务端会有一个组件解析这个 URL,然后动态把这些文件合并起来一起返回。

3.路径要尽量短

尽可能减少用户发出请求到返回数据过程中需要经历的中间的节点数。

一方面尽可能减少RPC的序列化&反序列化,而是可以减少网络传输的延时

另外,每增加一个链接都会增加新的不确定性,举个例子:假如每个节点的可用性是99.9%,那么经过五个节点,整个请求的可用性就是 99.9%的五次方。

具体做法: 可以把多个强相互依赖的应用合并部署在一起,把RPC变成JVM内部之间的方法调用。

4.依赖要尽量少

所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。

例如一个秒杀页面,页面必须强依赖商品信息,用户信息,还有其他如优惠券,成交列表这些弱依赖的信息。

可以通过系统的重要程度给系统进行分级,尽可能减少高级系统对低级系统(如减少支付系统对优惠券系统)的依赖。

5.不要有单点

单点意味着没有备份,设计分布式系统一个重要考量就是避免单点。

避免单点:服务的无状态化——避免把服务的状态和机器绑定。

那如何把服务的状态和机器解耦?

例如把和机器相关的配置动态化,这些参数可以通过配置中心来动态推送。在服务启动的时候拉取下来,就可以在这些配置中心设置一些规则来方便改变这些映射关系。

先给出文章中给的一个系统架构思路:
在这里插入图片描述

1.动静数据的分离,不需要每次点击秒杀的时候重新刷新页面,每一次刷新页面肯定要涉及数据的传输,网络的开销
2.引入秒杀答题提交系统,类似于限流的思想
3.然后读Redis,如果是空的话从Mysql填库存,然后之后热点商品都读Redis,这里Redis更像是保护Mysql,只是起一个判断库存是否充足的作用。这时Redis应该设置超时时间,然后可以通过主动更新或者定时更新的方法来更新Redis库存,这时候的库存数据应该设置超时时间,通过主动失效来失效缓存。
4.最后Mysql减库存成功才能算是下单成功,推测靠乐观锁来防止超卖。

正文开始

1.性能角度

我们主要讨论的是系统服务端的性能:判断秒杀系统性能的一个关键指标就是QPS(Query Per Second,每秒请求数)

还有一个影响也和QPS密切相关,即服务器的响应时间,可以理解为服务器处理相应的耗时。

对于Web系统而言,响应时间一般由CPU的执行时间和线程等待时间(如RPC,IO,线程的休眠,等待)
组成。

但一般真正影响性能的还是CPU的执行时间。因为CPU真正消耗了服务器的资源。

其次,考虑线程数量对QPS的影响。
并不是线程数目越多越好,因为线程切换会有成本,线程切换涉及到用户态和内核态的转换,本质上就是进行上下文切换,保存线程上下文现场。

然后多线程场景你还要分析是CPU密集型还是IO密集型

如果是前者: 线程数 = CPU核数+1
如果是后者: 线程数 = 2*CPU核数+1

那如何判断发现哪些函数CPU耗时比较多?

例如可以用jstack定时地打印调用栈,如果某些函数调用十分频繁或者耗时比较多,那么这些函数就会多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值