项目技术总结

零、多数据源管理方案

1、核心是使用Spring提供的AbstractRoutingDataSource抽象类,注入多个数据源。

2、使用MyBatis注册多个SqlSessionFactory

3、dynamic-datasource是MyBaits-plus作者设计的一个多数据源开源方案。使用这个框架需要引入对应的pom依赖

一、分库分表、读写分离

1. 对于前端的重复点击造成的订单问题?

前端进入确认订单界面的时候,就先生成一个订单号ID。在订单点击提交的时候,订单号和商品一起提交,提交的按钮就置灰,防止重复点击。

由于网络的不可靠,后端通过前端提交的订单号校验是否重复提交是非常有必要的。可以用布隆过滤器校验。

2. 对于订单的ABA问题,使用版本号解决。

3. 对于分库分表来说,能不分就不分。如果单表数据量太大,就分表;如果并发请求量高,就分库。阿里给出参考建议:对于单表在短期内会超过500万,做分表处理。或者所有表加起来超过2GB,则考虑分库。

分表一般取2的幂次方,关联的信息表绑定同步的分片表,比如订单分表1对应订单详情分表1。

在之后做数据的迁移归档,我们总是在MySQL中保留3个⽉的订单数据,超过三个⽉的数据则迁出。假设预估每⽉订单2000W,⼀张订单下的商品平均为10个,如果只保留3个⽉的数据,则订单详情数为6亿,分布到32 个表中,每个表容纳的记录数刚好在2000W左右,那分库分表将订单表就设定为32个。

订单的分片键要如何划分?——》订单可以根据订单号+用户ID的后两位做组合,取模。这样同一个用户的数据不用跨表,在一个表内就可以完成根据订单号查询订单详情,或者根据用户查询订单信息的需求。比如在后续的对账过程中,根据用户下的订单金额和银行的流水金额是否匹配。

4. 对于读多写少的要做读写分离,提高数据库的并发能力。读写分离分数据库层面和应用层面,数据库层面做主从备份的机制。在应用层面要做路由控制,主节点写,从节点读,减轻数据库的压力。

5. 读写分离的数据不一致问题?

主从同步延迟带来的读写不一致问题,尽量规避更新数据后立即去从库查询刚刚更新的数据。如果一定要查,这两个步骤可以放到一个数据库事务中,同一个事务中的查询操作也会被路由到主库,这样就可以规避主从不一致的问题了,还有一种解决方式则是对查询部分单独指定进行主库查询。

常规做法是增加了一个支付完成页面,这个页面其实没有任何新的有效信息,就是告诉你支付成功的信息。如果想再查看一下刚刚支付完成的订单,需要手动选择,这样就能很好地规避主从同步延迟的问题。

6. 如何在代码中实现读写分离和分库分表呢?

使用像Sharding-JDBC 这些组件集成在应用程序内,用于代理应用程序的所有数据库请求,并把请 求自动路由到对应的数据库实例上。

7. 当每个月的订单超过2000w,数据量太大,严重影响数据库的性能,我们就要对它进行拆。其实 分库分表很多的时候并不是⾸选的⽅案,应该先考虑归档历史数据。

银行更关注近一个月的数据查询,电商参考京东,关注近3个月的数据。

所以,把超过关注时间的数据迁移到别的数据库或者别的存储系统(mangoDB,Hive,click house,ES等)。

进一个月/三个月的数据放到拆分的表里面,基本上只有查询统计类的功能会查到历史订单,这些都需要稍微做些调整。按照查询条件中的时间范围,选择去订单表还是历史订单中查询就可以了。

应该尽量选择在闲时迁移⽽且每次数据库操作的记录数不宜太多。按照⼀般的经验,对MySQL 的操作的记录条数每次控制在10000⼀下是⽐较合适,迁移前一定会做备份以免误删。

迁移的流程:

逐表批次删除,对于每张订单表,先从MySQL从获得指定批量的数据,写⼊ MongoDB,再从MySQL中删除已写⼊MongoDB的部分。这⾥并不需要分布式事务,解决的关键在于写⼊订单数据到MongoDB 时,我们要记住同时写⼊当前迁⼊数据的最大订单ID,让这两个操作执⾏在同⼀个事务之中。

定时迁移——》XXL Job。

停止迁移以及恢复迁移的问题——》只要记录了当前迁入数据的最大订单ID,下一次从MySQL获取数据,就从最大的订单ID开始,就不会漏数据。

如何防止mysql和mangoDB的数据对不上的问题。——》同上

如何批量删除⼤量数据。——》根据主键删除。

由于条件变成了主键⽐较,⽽在MySQL的InnoDB存储引擎中,表数据结构就是按照主键 组织的⼀棵B+树,同时B+树本身就是有序的,因此优化后不仅查找变得⾮常快,⽽且也不需要再进⾏额外的排序操 作了。 按ID排序后,每批删除的记录基本上都是ID连续的⼀批记录,由于B+树的 有序性,这些ID相近的记录,在磁盘的物理⽂件上,⼤致也是存放在⼀起的,这样删除效率会⽐较⾼,也便于 MySQL回收⻚。 关于⼤批量删除数据,还有⼀个点需要注意⼀下,执⾏删除语句后,最好能停顿⼀⼩会,因为删除后肯定会牵涉 到⼤量的B+树⻚⾯分裂和合并,这个时候MySQL的本身的负载就不⼩了,停顿⼀⼩会,可以让MySQL的负载更加 均衡。

8. 使用RocketMQ的事务消息对消息状态进行不断的确认,优化订单超时取消流程。回查间隔可以通过参数 transactionCheckInterval 定制。并基于业务做对应的正向或者反向通知,最后采取定时任务做兜底批量回退超时的订单。。

核心代码流程:

1、支付宝预下单时发送事务消息。 OmsPortalOrderController#tradeQrCode: 使用 orderMessageSender.sendCreateOrderMsg(orderId,memberId); 发送消息,这个消息实际上是用来通知 下游服务进行订单取消的。

2、发送消息后,就会先执行本地事务。 TransactionListenerImpl#executeLocalTransaction方法。在这个 方法中会将订单ID放到Redis中,这样可以在后续进行支付状态检查时,快速找到对应的业务信息。只要下单 成功,就会返回UNKOWN状态,这样RocketMQ会在之后进行状态回查。

3、然后在事务状态回查时,会执行 TransactionListenerImpl#checkLocalTransaction方法。在这个方法里 会自行记录回查次数,超过最大次数就直接取消订单。 注意,这里最大回查次数需要根据业务要求进行定制。 如果没有超过最大次数,就可以去支付宝中查询订单支付状态。 如果已经支付完成,则返回ROLLBACK状态,消息取消,后续就不会再进行本地订单取消了。 如果未支付,则记录回查次数后,返回UNKNOWN状态,等待下次回查。

4、如果事务消息最终发送出去,也就是订单已经超时,就会将消息发送到RocketMQ的 ${rocketmq.tulingmall.asyncOrderTopic}这个Topic下。下游的消费者RocketMqCancelOrderReciever就 会完成取消本地订单,释放库存等操作。

通过事务消息通知下游服务订单取消,这其实就是一 种反向通知的方式。

使用正向通知,即通过事务消息通知下游服务进行订单支付 确认,这样这个下单的消息就容易扩展更多的下游消费者。

订单下单确认是用户完成支付 后,支付宝发起的通知来确认的。这时,如果订单确认的下游服务实现了幂等控制,就完全可以将事务消息 机制改为正向通知。即在事务消息回查过程中,确认用户已经完成了支付,就发送消息通知下游服务订单支 付成功。这样也可以防止支付宝通知丢失造成的订单状态缺失。 而用户订单超时判断,则可以在事务消息的checkLocalTransaction状态回查过程中,通过记录回查次数 判断。如果已经超时,则返回Rollback。同时启动另外一个消息生产者,往下游服务发送一个订单取消的消 息,这样也是可以的。

二、分布式事务

基础理论

1. CAP理论

C-更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致

A-服务一直可用,而且是正常响应时间

P-当部分节点出现消息丢失或者分区故障的时候,分布式系统仍然能够对外提供满足一致性和可用性的服务。

2. BASE理论

基本可用:系统能够基本运行,一直提供服务。基本可用强调了分布式系统在出现不可预知故障的时候,允许损失部分可用性,相比正常的系统,可能是响应时间延长,或者是服务被降级。

软状态:软状态则是允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

最终一致性:

数据不可能一直是软状态,必须在一个时间期限之后达到各个节点的一致性,在期限过后,应当保证所有副本保持数据一致性,也就是达到数据的最终一致性。

在系统设计中,最终一致性实现的时间取决于网络延时、系统负载、不同的存储选型、不同数据复制方案设计等因素。

3. Base 理论则是对 CAP 理论的实际应用,也就是在分区和副本存在的前提下,通过一定的系统设计方案,放弃强一致性,实现基本可用,这是大部分分布式系统的选择,比如 NoSQL 系统、微服务架构。

事务模型

2PC 两阶段提交

第一阶段

TM(事务管理器)通知各个RM(资源管理器)准备提交它们的事务分支。如果RM判断自己进行的工作可以被提交,那就对工作内容进行持久化,再给TM肯定答复;要是发生了其他情况,那给TM的都是否定答复。

第二阶段

TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare失败的话,则TM通知所有RM回滚自己的事务分支。

2PC存在的问题

二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:
  • 同步阻塞问题
2PC 中的参与者是阻塞的。在第一阶段收到请求后就会预先锁定资源,一直到 commit 后才会释放。
  • 单点故障
由于协调者的重要性,一旦协调者TM发生故障,参与者RM会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
  • 数据不一致
若协调者第二阶段发送提交请求时崩溃,可能部分参与者收到commit请求提交了事务,而另一部分参与者未收到commit请求而放弃事务,从而造成数据不一致的问题。
seata

Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式。

在 Seata 的架构中,一共有三个角色:

  • TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

在 Seata 中,一个分布式事务的生命周期如下:

  1. TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
  2. RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
  3. TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
  4. TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。

Seata AT模式的设计思路(仅限AT模式需要undo_log表)

Seata AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如下:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 分布式事务操作成功,则TC通知RM异步删除undolog
    • 分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

注意:Seata的配置中心是作用于Seata自身的,和Spring Cloud的配置中心无关

核心注解:@GlobalTransactional

 Apache ShardingSphere 分布式事务

如果表用了分库分表技术(shardingsphere),seata不能对逻辑表进行解析。不能简单的在全局事务发起方使用@GlobalTransactional,需要换成@ShardingTransactionType(TransactionType.BASE), 同时需要关闭数据源自动代理,交给sharding-jdbc那边。

seata TCC
在Seata中,AT模式与TCC模式事实上都是两阶段提交的具体实现, 他们的区别在于:
AT 模式基于 支持本地 ACID 事务的关系型数据库:
  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚。
相应的, TCC 模式不依赖于底层数据资源的事务支持:
  • 一阶段 prepare 行为:调用自定义的 prepare 逻辑。
  • 二阶段 commit 行为:调用自定义的 commit 逻辑。
  • 二阶段 rollback 行为:调用自定义的 rollback 逻辑。
简单点概括, SEATA的TCC模式就是手工的AT模式,它允许你自定义两阶段的处理逻辑而不依赖AT模式的undo_log。
TCC的空回滚问题如何产生?
当参与者A因为机器宕机或者网络等原因,造成A没有执行成功,却需要回滚的情况。

要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,Seata 是如何做的呢?

Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。

如何处理悬挂?

悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。

解决方案:在 TCC 事务控制表记录状态的字段 status 中增加一个状态:
  • suspended:4
当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表没有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。
可靠消息的最终一致性
1. 本地消息表
2. RocketMQ事务消息

RocketMQ事务消息也比较挑业务场景,同步性强的处理链路不适合。

- 要求下游MQ消费方一定能成功消费消息。否则转人工介入处理。【重要】
- 千万记得实现幂等性。【重要】

3. 最大努力通知

使用建议:

尽量避免分布式事务,单进程用数据库事务,跨进程用消息队列。

互联网业务主流实现分布式系统事务一致性的方案:

1. 基于MQ的可靠消息投递的机制
2. 基于重试加确认的的最大努力通知方案。

三、全局唯一性ID

分库分表之后,数据库的自增 ID 已经无法满足需求,需要有一个唯一 ID 来标识一条数据或消息。此时一个能够生成全局唯一 ID 的系统是非常必要的。

全局唯一性:不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求。

趋势递增、单调递增:保证下一个 ID 一定大于上一个 ID。无序性可能会引起数据位置频繁变动,严重影响性能。

信息安全:如果 ID 是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定 URL 即可;如果是订单号就更危险了,竞对可以直接知道我们 一天的单量。所以在一些应用场景下,会需要 ID 无规则、不规则。

常见方法介绍:

1. UUID

优点: 性能非常高:本地生成,没有网络消耗。

缺点:

不易于存储:UUID 太长,16 字节 128 位,通常以 36 长度的字符串表示, 很多场景不适用。

信息不安全:基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露, 这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID 就非常不适用:

① MySQL官方有明确的建议主键要尽量越短越好[4],36个字符长度的UUID 不符合要求。

② 对 MySQL 索引不利:如果作为数据库主键,在 InnoDB 引擎下,UUID 的 无序性可能会引起数据位置频繁变动,严重影响性能。在 MySQL InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据, 在主键的选择上面我们应该尽量使用有序的主键保证写入性能。

2. 雪花算法及其衍生

长度64-bit刚好表示一个long类型的数字。

第 0 位: 符号位(标识正负),始终为 0,没有用,不用管。

第 1~41 位 :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年)。

(时间戳就是64-bit,如何压缩到42位?——》当前时间减去一个时间的起点位)

第 42~52 位 :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表 示机器 ID(实际项目中可以根据实际情况调整),这样就可以区分不同集群/机 房的节点,这样就可以表示 32 个 IDC,每个 IDC 下可以有 32 台机器。

第 53~64 位 :一共 12 位,用来表示序列号。 序列号为自增值,代表单 台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最 多可以生成 4096 个 唯一 ID。

优点: 毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的。 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成 ID 的性能也是非常高的。 可以根据自身业务特性分配 bit 位,非常灵活。

缺点: 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

时钟回拨问题?——》如果当前时间小于之前的时间,就休眠一段时间,再去获取。也可以抛异常,之后再获取。

时间相同,则sequence自增。

时间往前正常走,但是sequence为0,在分库分表,数据取模会导致数据分布不均,如何解决?——》加一个震荡方法,这样取模会自增,让数据分片分表更友好。

3. 数据库生成

MySQL

redis

4. 分布式 ID 微服务——》美团 Leaf 方案实现

Leaf 分别在 MySQL 和雪花上做了相应的优化,实现了 Leaf-segment 和 Leaf-snowflake 方案。

1)Leaf-segment数据库方案

每个业务每次根据step取一批数据。

数据库表设计如下: 重要字段说明:biz_tag 用来区分业务,max_id 表示该 biz_tag 目前所被分配 的 ID 号段的最大值,step 表示每次分配的号段长度。

这种模式有以下优缺点:

优点:

  • Leaf 服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
  • ID 号码是趋势递增的 8byte 的 64 位数字,满足上述数据库存储的主键要求。
  • 容灾性高:Leaf 服务内部有号段缓存,即使 DB 宕机,短时间内 Leaf 仍能正 常对外提供服务。
  • 可以自定义 max_id 的大小,非常方便业务从原有的 ID 方式上迁移过来。

缺点:

  • ID 号码不够随机,能够泄露发号数量的信息,不太安全。
  • TP999 数据波动大,当号段使用完之后还是会在获取新号段时在更新数据库 的 I/O 依然会存在着等待,tg999 数据会出现偶尔的尖刺。
  • DB 宕机会造成整个系统不可用。

问题1:

我们会一次性插入一条订单记录和多条订单详情记录, 如果对于订单详情记录的 ID 每次都从唯一 ID 服务取,这个无疑会对性能有影响。

解决办法有两个:

1、订单详情记录的 ID 不保证全局唯一,依然使用数据库的自增主键;

2、订单详情记录的 ID 需要全局唯一,但并不每次从唯一 ID 服务,而是在生成订单时,一次性从唯一 ID 服务获得。

问题2:

针对第二个缺点做双 buffer 优化。

采用双 buffer 的方式,Leaf 服务内部有两个号段缓存区 segment。当前号段 已下发 10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。

当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前 segment 接着下发,循环往复。 通常推荐 segment 长度设置为服务高峰期发号 QPS 的 600 倍(10 分钟), 这样即使 DB 宕机,Leaf 仍能持续发号 10-20 分钟不受影响。

每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

2)Leaf-snowflake 方案

启动 Leaf-snowflake 服务,连接 Zookeeper,在 leaf_forever 父节点下检查自己是否已经注册过(是否有该顺序子节点)。 如果有注册过直接取回自己的 workerID(zk顺序节点生成的 int 类型 ID 号), 启动服务。 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取 回顺序号当做自己的 workerID 号,启动服务。

弱依赖 ZooKeeper

除了每次会去 ZK 拿数据以外,也会在本机文件系统上缓存一个 workerID 文 件。当 ZooKeeper 出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。这样做到了对三方组件的弱依赖。

时钟回拨的问题:(我们采取第二点)

美团建议有三种解决方案,一是可以直接关闭 NTP 同步;二是在时钟回拨的时候直接不提供服务直接返回 ERROR_CODE,等时钟追上即可,三是做一层重试,然后上报报警系统,更或者是发现有时钟回拨之 后自动摘除本身节点并报警。

四、分布式Session

五、分布式链路跟踪

六、日志收集与展示

七、商品搜索

1. 统计类,redis缓存、宽表

2. 搜索:ES

八、分布式锁

九、服务降级/限流/熔断/隔离

        在遇到大流量时,更多考虑的是运行阶段如何保障系统的稳定运行,常用的手 段:限流,降级,拒绝服务。

1. 限流

(1)客户端限流

缺点:当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果设的太大,则起不到限制的作用。

(2)服务端限流

缺点:被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。

2. 限流的方案

(1)前端限流

(2)接入层 nginx 限流

(3)网关限流
        1)基于 redis+lua 脚本限流

        gateway 官方提供了 RequestRateLimiter 过滤器工厂,基于 redis+lua 脚本方式采用令牌桶算 法实现了限流。

        2)整合 sentinel 限流

        利用 Sentinel 的网关流控特性,在网关入口处进行流量防护,或限制 API 的调用频率。

        提供两种资源维度的限流:

        route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId。

        自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组。

(4)应用层限流
        1)微服务接入 sentinel

        a. 热点参数限流

        注意:

        热点规则需要使用@SentinelResource("resourceName")注解,否则不生效;

        参数必须是 7 种基本数据类型才会生效

2. 降级

        降级就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而 把有限的资源保留给更核心的业务。

        (1)慢调用(响应时间)

        (2)异常比例

        (3)异常数

3. 拒绝服务

        拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。

        (1)Nginx上设置过载保护,当机器负载达到某个值时直接拒绝HTTP请求并返回503错误码。

        (2)在Java层过载保护,比如Sentinel提供了系统规则限流:

        Load、CPU使用率、总体平均RT、入口QPS和并发线程数等几个维度的监控指标。

十、页面静态化

十一、分布式任务调度

十二、数据迁移方案

        在大型互联网企业中、其核心业务数据,以不同的数据结构和存储方式,保存几十甚至上百份,以满足不同业务对数据查询的需求,都是非常正常的。

        如何在不停机的情况下,安全地迁移数据、更换数据库呢?

        设计迁移方案的时候,一定要保证每一步都是可逆的。也就是必须保证,每执行完一个步骤,一 旦出现任何问题,都能快速回滚到上一个步骤。

        (1)使用Binlog实现两个异构数据库之间数据的实时同步。这一步不需要回滚,因为这里只增加了一个新库和一个同步程序,对系统的旧库和程序没有任何改变。即使新上线的同步程序影响到了旧库,停掉同步程序也就可以了。

        (2)业务逻辑部分不需要变动,数据访问的DAO层需要进行如下改造:

                1)支持双写新旧两个库,并且预留热切换开关,能通过开关控制三种写状态:只写旧库、只写新库和同步 双写。

                2)支持读取新旧两个库,同样预留热切换开关,控制读取旧库还是新库。

        (3)然后上线新版的订单服务,这个时候订单服务仍然是只读写旧库,不读写新库。让这个新版的订单服务稳定运行至少一到两周的时间,其间我们不仅要验证新版订单服务的稳定性,还要验证新旧两个订单库中 的数据是否保持一致。这个过程中,如果新版订单服务出现任何问题,都要立即下线新版订单服务,回滚到旧版本的订单服务。

        (4)稳定一段时间之后,就可以开启订单服务的双写开关了。开启双写开关的同时,需要停掉同步程序。 这里有一个需要特别注意的问题是,这里双写的业务逻辑,一定是先写旧库,再写新库,并且以旧库的结果 为准。

        (5)用类似灰度发布的方式把读请求逐步切换到新库上。同样,运行期间如果出现任何问 题,都要再切回到旧库。

        (6)将全部读请求都切换到新库上之后,其实读写请求已经全部切换到新库上了,虽然实际的切换已经完 成,但后续还有需要收尾的步骤。 再稳定一段时间之后,就可以停掉比对程序,把订单服务的写状态改为只写新库。至此,旧库就可以下线 了。注意,在整个迁移过程中,只有这个步骤是不可逆的。由于这一步的主要操作就是摘掉已经不再使用的旧 库,因此对于正在使用的新库并不会有什么影响,实际出问题的可能性已经非常小了。

        如何实现比对和补偿程序?

        数据上带有更 新时间,那么比对程序就可以利用这个更新时间,每次从旧库中读取一个更新时间窗口内的数据,到新库中查 找具有相同主键的数据进行比对,如果发现数据不一致,则还要比对一下更新时间。如果新库数据的更新时 间晚于旧库数据,那么很可能是比对期间数据发生了变化,这种情况暂时不要补偿,放到下个时间窗口继续 进行比对即可。另外,时间窗口的结束时间不要选取当前时间,而是要比当前时间早一点,比如1分钟之 前,这样就可以避免比对正在写入的数据了。

        如果数据没带时间戳信息,那就只能从旧库中读取Binlog,获取数据变化信息后到新库中查找对应的数据 进行比对和补偿。

十三、数据同步方案

1. 使用Canal来实时接收Binlog更新Redis缓存

        它通过模拟MySOL主从复制的交互协议,把自己伪装成一个MySOL的从节点,向 MySOL主节点发送 dump请求。MySOL收到请求后,就会向 Canal开始推送Binlog,Canal解析Binlog字节流之后,将其转换为 便于读取的结构化数据,供下游程序订阅使用。

        先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式。

2. 基于 Binlog实现跨系统实时数据同步

        Canal把自己伪装成一个 MySQL 的从库,从MySQL 数据库中实时接收 Binlog,然后修改Redis缓存。所以实现异构数据库的同步也可以采用这个方法。

        但是下游的每个数据库,在写入之前可能还要处理一些数据转换和过滤 的工作。所以一般会增加一个消息队列来解耦上下游。

3. 安全地实现数据备份和恢复

(1)全量备份

(2)MySQL自带的 Binlog就是一种实时的增量备份工具。       

        在执行备份和恢复的时候,大家需要特别注意如下两个要点。

        第一,无论是全量备份还是Binlog,都不要与数据库存放在同一个服务器上。最好能存放到不同的机房,甚至不同城市离得越远越好。这样即使出现机房着火、光缆被挖断甚至地震也不怕数据丢失。

        第二,在回放 Binlog的时候,指定的起始时间可以比全量备份的时间稍微提前一点儿,这样可以确保全量 备份之后的所有操作都在恢复的Binlog 范围内,从而保证数据恢复的完整性。

         注意:为了确保回放的幂等性,需要将Binlog的格式设置为ROW格式。

十四、多级缓存、缓存预热

从浏览器到网络,再到应用服务器,甚至到数据库,通过在各个层面应用缓存技术,整个系统的性能将大幅提高。

1. 缓存的分类

        (1)客户端缓存:对于互联网应用而言,也就是通常所说的BS架构应用,可以分为页面缓存和浏览器缓存。对于移动互 联网应用而言,是指APP自身所使用的缓存。

                a. 页面缓存

                        将之前渲染的页面保存为文件,当用户再次访问时可以避开网络连接,从而减少负载,提升性 能和用户体验。

                        HTML5提供的离线应用缓存机制。

                        cookie、vuex。

                b. 浏览器缓存

                        浏览器会在硬盘上专门开辟一个空间来存储资源副本作为缓存。在用户触发“后退”操作或点 击一个之前看过的链接的时候,浏览器缓存会很管用。

                c. APP上的缓存

                        APP可以将内容缓存在内存、文件或本地数据库中。

        (2)网络缓存:网络中的缓存位于客户端和服务端之间,代理或响应客户端的网络请求,从而对重复的请求返回缓存中的 数据资源。同时,接受服务端的请求,更新缓存中的内容。

                a. Web代理缓存

                        常用的Web代理分为正向代理、反向代理和透明代理。

                        Web代理缓存有很大的存储空间,不断将新获取的数据储存到本地的存储器上,如果浏览器所请求的数据在Web代理的缓存上已经存在而且是最新 的,那么就不重新从Web服务器取数据,而是直接将缓存的数据传送给用户的浏览器。

                        反向代理缓存可以缓存原始资源服务器的资源,而不是每次都要向原始资源服务器请求数据,特别是一些静态的数据,比如图片和文件

                b. 边缘缓存

                        边缘缓存在网络上位于靠近用户的一侧,可以 处理来自不同用户的请求,主要用于向用户提供静态的内容,以减少应用服务器的介入。

                        边缘缓存中典型的商业化服务就是CDN。

        (3)服务端缓存

                a. 数据库缓存

                        innodb_buffer_pool_size参数,用来设置用于缓存InnoDB索引及数据块的内存区域大小,简单来说,当操作一个InnoDB表的时候,返回的所有数据或者查询过程中用到的任何一个索引块,都会在这个内存区域中去查询一遍。

                        所以如果有足够的内存,尽可将该参数设置到足够大,将尽可能多的InnoDB的索引及数据 都放入到该缓存区域中,直至全部。

                b. 应用级缓存

                        Voldemort是一款基于Java开发的分布式键值缓存系统,Voldemort同样支持多台服务器之间的缓存同步,以增强系统的可靠性和 读取性能。

                        coffine基于内存做本地缓存,性能较高,内存较小。数据不安全,不持久。

                        RocksDB缓存,基于磁盘持久化缓存。

                c. 平台级缓存

                        不论是Redis还是MongoDB,以及Memcached都可以作为平台级缓存的重要技术。

2. 如何保证缓存的数据一致性

        (1)先删除缓存,后更新DB。

·                采用延时双删策略:

                先淘汰缓存 ——》再写数据库 ——》休眠1秒,再次淘汰缓存。

                1秒的时间=读数据业务逻辑的耗时+主从同步的延时时间+几百ms。

        (2)先更新DB,后删除缓存

        

                        通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通 过ACK机制确认处理删除缓存。

3. 缓存预热

重启导致缓存失效。启动时从远程获得相关的数据写入本地的Caffeine缓存中。

4. 数据一致性

(1)本地Caffeine缓存:

        设置了过期时间 n 分钟。

        异步的刷新缓存,每分钟检查一次本地Caffeine缓存是否已无效,无效则刷新缓存

(2)redis缓存:

        利用Canal监测数据库的 binlog日志 更新,然后删除缓存中的对应部分。

5. 双缓存技术

        在本地的缓存我们保存了两份,并且将备份缓存作为降级和兜底方案。

        在数据的过期机制上可以看到,我们使用了不同的策略,正式缓存是最后一次写入后经过固定时间过期,备份缓存是设置最后一次访问后经过固定时间过期。

        在本地缓存的异步刷新机制上,正式缓存只有无效才会被重新写入,备份缓存无论是否无效都会重新写入,一则可以保证备份缓存中的数据不至于真的永久无法过期而太旧,二则使备份缓存的过期时间不管用户是否访问首页都可以不断后延。

6. 专门的缓存管理接口

        缓存管理接口Controller,强制本地缓存失效和手动刷新本地缓存。

7. 缓存穿透

        布隆过滤器化

十五、高并发秒杀系统实现(秒杀系统的商详页静态化、秒杀系统的隔离、秒杀的削峰和限流等等)

秒杀系统需要单独搭建。

秒杀预约,小米在预约的时候,就设置好了某个参数,秒杀是否可以成功。

1. 秒杀系统的挑战

(1)巨⼤的瞬时流量

(2)热点数据问题

(3)刷⼦流量

nginx的master和worker进程本质上是单线程,适合lua脚本运行。

十六、互联网思维

1. 自定义实现MyBatis-Plus逆向工程

2. 使用Freemarker模板引擎实现一键开发模式

springboot可以结合freemarker,根据一个表生成对应的controller,service,dao,mapper,pojo,以及统一的前端界面。

3. 结合CBoard报表工具实现拖拽式报表开发

十七、海量数据高并发场景

1. 高并发读

策略1:加缓存/读副本——通过对数据进行冗余,达到空间换时间的效果

        方案1:本地缓存或集中式缓存

        方案2:MySQL的 Master/Slave

        方案3:CDN/静态文件加速/动静分离

策略2:并发读——串行改并行

        方案1:异步 RPC

        方案2: 冗余请求

策略3:重写轻读

        方案1:微博Feeds流的实现

        方案2:多表的关联查询:宽表与搜索引擎

总结:读写分离(CQRS 架构)

        (1)分别为读和写设计不同的数据结构。

        (2)写的这一端,通过分库分表抵抗写的压力。读的这一端为了抵抗高并发压力,针对业务场景,可能是缓存,也可能是提前做好Join的宽表,又或者是ES搜索引擎。

        (3) 读和写的串联。定时任务定期把业务数据库中的数据转换成适合高并发读的数据结构;或者是写的一端把 数据的变更发送到消息中间件,然后读的一端消费消息;或者直接监听业务数据库中的 Binlog,监听数据库 的变化来更新读的一端的数据。

        (4)读比写有延迟。

2. 高并发写

策略1:数据分片

        数据库的分库分表。ES的分布式索引。

策略2:异步化

        案例1:短信验证码注册或登录——消息队列

        案例2:电商的订单系统——拆单

策略3:批量写

        案例1:广告计费系统的合并扣费

        案例2:MySQL的小事务合并机制

十八、存储系统的技术选型

1. 系统的类型

        在线业务系统,还是⼀个分析系统。

2. 数据量

考虑存量数据和增量数据两个部分。时间维度2-3年内。

1) 1GB以下量级,或者数据的条数在千万以下。对于这个量级内的数据来说,⼏乎所有的存储产品其性能都还 不错,因此不需要过多考虑数据量和性能,重点考虑其他维度即可。

2) 1GB 以上、10GB 以下量级,或者数据的条数在⼀亿以内。这个量级基本上是单机存储系统能够处理的上 限。

3)超过10GB量级,或者数据的条数超过⼀亿。这个量级的数据必须使⽤分布式存储,只有将数据分⽚,才能获 得可以接受的性能。

3. 总体拥有成本

        成本主要来⾃如下三个⽅⾯。

        第⼀,也是最重要的,团队是否熟悉该产品?如果不熟悉则意味着使⽤过程中可能要踩坑,然后填坑。踩坑和填 坑的代价可能是系统宕机、丢数据或者开发进度延期。

        第⼆,需要考虑该产品是否简单、易于学习和使⽤。

        第三,需要考虑系统上线后的运维成本

在线业务系统

1)由于需要频繁地对数据进⾏增删改的操作,因此存储产品需要有较好的写性能。

2)由于在线业务直接服务于前端,需要快速响应,因此每次存储访问必须要快,⾄少要达到毫秒级的响应。

3)另外,存储产品需要能够⽀撑⾜够多的并发请求,以满⾜⼤量⽤户同时访问的需求。

4) 最后,由于在线业务系统的需求⼀直都在不停地变化,因此存储产品需要能够提供相对⽐ 较强⼤的查询能⼒,以便应对频繁变化的需求。

关系型数据库:MySQL首选

KV存储:Redis这种基于内存的存储,具有⾮ 常好的读写性能,能提供有限的查询功能,但是其并不能保证数据的可靠性,⼀般来说,Redis都是配合MySQL 数 据库作为缓存来使⽤。

在线业务系统需要存储产品能够⽀持⾼性能写⼊、毫秒级响应以及⾼并发。MvSOL加 Redis的经典组合可以应对 ⼤部分的场景需求。

分析系统

⽽分析系统则需要存储产品能够促在海量数据,并且能够⽀持在海量数据上快速聚合、分析和 查询,⽽对写⼊性能、响应时延和并发的要求并不⾼。

⼀般来说量级在GB以内的可以使⽥MySQL ;量级超过GB的数据并且如果还是需要做实时的分析和查询,则可 以优先考虑ES,Hbase、Cassandra和ClickHouse这些列式数据库也可以视情况选择。量级超过 TB的数据,⼀般 只能事先对数据做聚合计算,然后再在聚合计算的结果上进⾏实时查询,这种情况下,⼀般选择把数据保存在 HDFS 中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值