还记得我们系统的设计目标吗?
1. 重申目标
请允许我再重复啰嗦一次,因为很重要,如果没有目标,系统会被建设的一塌糊涂,这跟我们的人生是一样一样的。
模块 | 指标 | 计算方式 | 统计方式 | 结果 |
---|---|---|---|---|
awardMgtService | 创建奖券成功率 | 每秒的成功数/每秒的请求总数 | 监控埋点 | 99.99% |
awardAssignService | 自动分配券成功率 | 每秒的自动分配奖券成功数/每秒的请求总数 | 监控埋点 | 99.99% |
QPS | 每秒请求总数 | 监控埋点 | 1w QPS TP95延时 50ms | |
awardReceiveService | 兑换奖券成功率 | 每秒的兑换成功数/每秒的请求总数 | 监控埋点 | 99.99% |
QPS | 每秒请求总总数 | 监控埋点 | 2w QPS TP95延时 30ms |
1.1 还有哪些问题需要考虑?
每个模块的设计目标并不一样,在做这次重建之前,有一个至关重要的前提是,新建系统要兼容旧系统,那就意味着有些组件不能被替换掉,尤其是数据库部分。
哪些组件不能被替换掉呢?
- 数据库,目前用的mysql,如果你说我就要替换掉,那也不是不可能,但就是成本比较高,但又有什么理由去做一个费力不讨好的事儿呢?
- 数据库主键生成策略目前采用的是分布式id生成器,系统已经运行了好几年了,但我们目前的数据库表的记录不到100w,我真不知道当时为啥选择分布式id生成器,用主键不香吗?一般用分布式ID主要是分库分表,但我们目前的业务增长量好像近期也不需要分库分表啊。而且分布式id生成器需要进行网络通讯,万一网络抖动了导致无法生成主键id,那岂不是很麻烦?凡是依赖于网络的都存在不可靠的因素。
有哪些可以被替换掉的?
代码,代码还是代码,代码做好兼容性就好了,也许会费点时间,那又何妨,换来一个清爽的,简约的结构不爽吗?
2. 技术方案选型
先讲个故事,我有三个朋友,他们是大C,M,和小D。
大C做事情非常麻利从来不拖泥带水,交给他的事情他都能帮你办的妥妥的。
小D慢性子,社恐,但做事情非常仔细,循规蹈矩,不出格,你交给他的事情除了慢一点,没别的毛病。
M呢,和事佬,经常调节大C和小D之间的矛盾,大C总是嫌小D做事情拖拖拉拉,慢慢腾腾的,他们两个一旦有矛盾,M总会出现。
我这三个朋友他们分别是 CPU,Memory,Disk。
我偷偷的告诉你,后来大C觉得总是麻烦M,很不落忍,于是他经常把自己和小D的矛盾积攒在一起,然后一次性交给M来帮忙解决,积攒在一起的这个地方叫 L Cache。
如果要做到高性能,业内通常的做法是加缓存,在快和慢之间。
如果要做到高并发,那肯定不能一个人全把活干了,需要多个像CMD这样的组合,这就是横向扩展。
在上边的故事中,我们捋清楚了他们各自的角色,但有一点需要特别注意,小D的工作任务怎么能有条不紊的交给M呢,他们之间是不是得有条航线啊,这条航线叫操作系统。 万一这条航线断掉怎么办呢?
2.1 mysql和Redis如何保持一致性
说了半天,其实我想跟你探讨如果小D是mysql,M是Redis,怎么保证mysql和Redis的数据一致性呢,我调研了业内的一些做法,别嫌麻烦,看一看也许能让你年薪50w,当然也有可能100w,总之祝福你。
经典的缓存方式有三种:
-
read/write Through : 读/写直接操作缓存,如果缓存未命中,读/写把数据库数据加载到缓存。整个操作有缓存中间件去完成。
-
write behind :先写缓存,后写数据库,会带来不一致。
-
cache aside:
失效:应用程序从缓存中取,如果未命中,则从数据库中取,然后放到缓存。
命中: 缓存命中,直接取缓存中数据
更新:先更新数据库,然后让缓存失效。
根据cache aside的几种情况,详细拆解为以下几种情况。
ini
策略1:先写数据库,后更新缓存
case1:数据库成功,缓存失败:
数据库值最新的,缓存值是旧的; 这将导致不一致。
解决方法:重试一直到缓存更新成功,在重试之前会存在短暂的不一致,但会最终一致。
case2:并发场景::
线程A更改数据库FieldA=1,线程B更改数据库FeildA=2, 线程B对缓存的更改晚于线程A,
导致缓存结果是FieldA=1而数据库结果是FieldA=2 --不一致。
解决方法:数据按照更新的顺序同步到缓存,在更新到缓存之前会出现短暂的不一致,但最终会一致。
case3:大量更新操作
如果存在大量的更新操作会影响性能;会出现非热点数据长期在缓存中,浪费内存空间的问题。
解决方法:适用于低频的写操作;同时给缓存数据设置过期时间
css
策略2:先写数据库,后删缓存
case1:数据库成功,缓存失败:
数据库值是最新的,缓存是旧值:-不一致。
解决方法:重试机制会最终一致,但在重试成功之前会有短暂的不一致。
case2:并发场景:
线程A更改FieldA=1,线程B更改FeildA=2, 线程B对缓存的更改晚于线程A;
不会出现不一致场景,此刻是缓存被删除了,最终数据一致
case3:并发场景:
读取FieldA,这个时候缓存恰好失效,线程A需要从数据库中读取数据,同时有个并发的写操作对FieldA进行更改,
数据库更新完成后,使缓存失效,
恰好这个时候读操作把旧值放到缓存,导致数据不一致。
解决方法:这种场景发生需要具备 读的速度要慢于写的速度并且有并发写操作下,
一般这种数据库读速度是远大于写的速度,这种事情发生的概率很小
case4:网络抖动
线程A更改FieldA=1,线程B更改FeildA=2;数据库目前最新值是FeildA=2,
由于网络抖动导致缓存未同步,读操作取的还是缓存旧值,等待网络恢复,缓存被删除。- 最终一致
策略3:先写缓存,后写数据库
case1: 缓存成功,数据库失败:此时缓存的数据是脏数据
策略4: 先删除缓存,后更新数据库
case1: 缓存成功,数据库失败:丢失新的请求
猜猜按照我们的业务场景,最终选择了哪一种呢?
奖券的修改属于读多写少的场景,同时写操作属于低频操作,并不会存在因大量更新导致的性能低下。
如果采用策略2,如果有大量缓存失效,那将会有大量请求分发到数据库中,导致数据库压力上升,目前在读多写少的场景中,希望更多的命中缓存的方式。
如果采用策略1需要解决的问题是:消息的顺序性;容忍短暂的不一致
通过调研canal在同步binlog的机制中可以按照顺序进行同步 在高并发场景中不会出现错误,所以在业务场景中,我们选择了策略1。
但这样的方案仍然存在隐患,如果canal断了,我们该如何解决呢?
参考文章链接