分布式系统技术原则
2023-12-18 11:10:14 0 举报
AI智能生成
分布式系统技术概览
作者其他创作
大纲/内容
为什么要进行业务拆分
业务清晰
模块隔离
职责单一化
如何进行系统拆分
系统拆分布式系统,拆成微服务架构,是要拆很多轮的。
如果多人维护一个服务,<=3个人维护一个服务;最理想情况下,1个人负责1个或2~3个服务。
一个服务的代码不要太多,1万行左右,两三万撑死
比如第一次拆分,就是将以前多个模块拆分开来,比如订单系统,商品系统,仓储系统,用户系统等等。
但是后面每个系统又变的越来越复杂,比如采购系统里面又分成了供应商管理系统,采购单管理系统,订单系统又拆分成购物车系统,价格系统,订单管理系统。
但是后面每个系统又变的越来越复杂,比如采购系统里面又分成了供应商管理系统,采购单管理系统,订单系统又拆分成购物车系统,价格系统,订单管理系统。
spi机制
定义
Service Provider Interface
一种类似于服务发现的机制
SPI机制的作用就是服务发现
jdk spi
比如有个工程A,有个接口A,接口A在工程A里是没有实现类的——>系统在运行时,怎么给接口A选择一个实现类呢?
可以搞一个jar包,META-INF/services/,放上一个文件,文件名就是接口名,接口A,接口A的实现类=com.xxx.service.实现类A2.
让工程A来依赖你的这个jar包,然后在系统运行的时候,工程A跑起来,对接口A,就会扫描自己依赖的所有的jar包,在每个jar里找找,有没有META-INF/services文件夹,如果有,在里面找找,有没有接口A这个名字的文件,如果有,在里面找一下你指定的接口A的实现是你jar包里的哪个类?
让工程A来依赖你的这个jar包,然后在系统运行的时候,工程A跑起来,对接口A,就会扫描自己依赖的所有的jar包,在每个jar里找找,有没有META-INF/services文件夹,如果有,在里面找找,有没有接口A这个名字的文件,如果有,在里面找一下你指定的接口A的实现是你jar包里的哪个类?
场景主要用在插件扩展的场景
比如java定义了一套jdbc的接口,但是java没有提供实现类
实际项目跑的时候,要使用jdbc接口的哪些实现类呢?
一般来说,我们要根据自己使用的数据库,比如mysql,就将mysql-jdbc-connector.jar引进来;oracle,就把oracle-jdbc-connector.jar引进来
一般来说,我们要根据自己使用的数据库,比如mysql,就将mysql-jdbc-connector.jar引进来;oracle,就把oracle-jdbc-connector.jar引进来
分布式接口的幂等性如何设计?
(比如不能重复扣款)
(比如不能重复扣款)
解决方式,创建一个set根据id存储,下次发现已经存在就说明重复了,分布式系统,可以用redis set去存储
(1)每个请求必须有唯一的标识,比如订单支付请求,肯定得包含订单id,一个订单id只能支付一次。
(2)每次处理完请求,必须有一个记录标识这个请求处理过了,比如常见的方案是在mysql中记录个状态。
比如支付之前记录一条这个订单的支付流水,而且支付流水采用orderId作为唯一键(unique key)。只有成功插入这个支付流水,才可以执行实际的支付扣款。否则整个事务回滚。
比如支付之前记录一条这个订单的支付流水,而且支付流水采用orderId作为唯一键(unique key)。只有成功插入这个支付流水,才可以执行实际的支付扣款。否则整个事务回滚。
(3)不通过数据库做,可以使用redis,set order_id payed,下一次重复请求过来了,去redis发现已经支付过了,就不再扣款了。
zookeeper使用场景
分布式协调
A系统通过MQ发送数据给B系统,系统A在zookeeper里写入【orderId=1】的node节点,然后注册一个监听器。系统B处理完MQ的信息后,去zookeeper修改【orderId=1】的node节点更新它的值为finish。然后系统A通过zookeeper监听到系统B已经执行成功了。
分布式锁
zookeeper如果拿到锁就加锁,拿不到锁就对这个锁注册一个监听器。等锁被释放了以后,zookeeper就会通知监听的系统。
配置信息管理
kafka,dubbo基于zookeeper保存元数据
HA高可用性
比如hdfs,yarn等很多大数据系统,都基于zk来开发HA高可用性机制,就是一个重要进程一般都会做主备两个,主进程挂了,立马通知zk感知到进行主备切换。
分布式锁
Redis分布式锁
Redis普通分布式锁
set my:lock 随机值 NX PX 30000
set orderId:1:lock uuid NX PX 30000
set orderId:1:lock uuid NX PX 30000
随机值一定要随机,比如UUID。根据lua脚本进行解锁(过期时间问题,需要对比value值进行锁删除)
NX:设置必须没有这个key值才能成功
如果redis没有orderId:1:lock,那么设置成功。如果有就返回失败。
PX 30000:过了30s之后,key自动过期,被删除
几个系统同时去redis使用上面的命令添加锁,但是通过NX只有一个系统能获取并加锁。
拿到锁的系统去执行一些处理,然后释放这把锁
如果其他系统没有拿到这把锁,那么需要自己每隔1s自动尝试一下,看是否能拿到这把锁
拿到锁的系统去执行一些处理,然后释放这把锁
如果其他系统没有拿到这把锁,那么需要自己每隔1s自动尝试一下,看是否能拿到这把锁
删除锁执行lua脚本
删除这把锁的时候,执行lua脚本,找到那个key的value,跟自己传过去的value比较下,如果是一样的,那么才会删除这个key
为什么要比较value?
如果设置了锁之后,过了30s,当初设置的锁已经过期不存在了,然后被其他系统写了锁。
30s以后执行完了,然后如果只根据key去删除,那么可能删除了别的系统的锁。
如果设置了锁之后,过了30s,当初设置的锁已经过期不存在了,然后被其他系统写了锁。
30s以后执行完了,然后如果只根据key去删除,那么可能删除了别的系统的锁。
缺点
redis如果只做了单机,那么分布式锁就失效了
redis主从架构,master保存了锁数据,但是还没同步到slave就挂掉了。导致分布式锁机制失效
RedLock算法
1.获取当前时间戳,单位是毫秒
2.跟上面类似,轮流尝试在每隔master节点创建锁,过期时间较短,一般就几十毫秒。
3.尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n/2+1)。
4.客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
5.要是锁建立失败了,那么就依次删除这个锁
6.只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
2.跟上面类似,轮流尝试在每隔master节点创建锁,过期时间较短,一般就几十毫秒。
3.尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n/2+1)。
4.客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
5.要是锁建立失败了,那么就依次删除这个锁
6.只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
Redission
看门狗:如果锁到了过期时间失效了,Redission能够续期。防止由于到了过期时间锁释放的问题。
zk分布式锁
获取锁:尝试去创建一个临时节点,如果这个临时节点之前并不存在,就创建成功了,那么这个锁就属于你了。
其他节点尝试创建相同名称的临时节点,如果已经存在了,说明别人已经占有了这把锁,你就失败了。然后就对这个临时节点注册一个监听器。
释放锁:删除那个临时节点就可以了,然后一旦临时节点被删除,zk就会通知别人这个节点被干掉了,相当于锁释放掉了。
其他节点尝试创建相同名称的临时节点,如果已经存在了,说明别人已经占有了这把锁,你就失败了。然后就对这个临时节点注册一个监听器。
释放锁:删除那个临时节点就可以了,然后一旦临时节点被删除,zk就会通知别人这个节点被干掉了,相当于锁释放掉了。
使用临时节点原因:如果系统创建了锁,然后挂掉了。容易产生死锁,但是临时节点,zk感觉到连接挂掉了,那么就会删除这个临时节点
临时顺序节点实现zookeeper
如果有一把锁,被多个人竞争,此时多个人排队,第一个拿到锁的人会执行,然后释放锁。后面的每个人都会去监听排在自己前面的那个人创建的node上,一旦某个人释放了锁,排在自己后面的人就会被zookeeper给通知,一旦被通知了之后,就ok了,自己就获取到了锁,就可以自己执行代码了
redis分布式锁与zookeeper分布式锁对比
redis需要自己不断去尝试获取锁,比较耗性能
zookeeper获取不到锁只需要监听,不需要不断轮询尝试获取锁
zookeeper获取不到锁只需要监听,不需要不断轮询尝试获取锁
分布式Session
tomcat+redis
Tomcat RedisSessionManager的东西,让所有我们部署的tomcat都将session数据存储到redis中
问题:分布式会话的这个东西重耦合在tomcat中,如果将web容器换成jetty,就会很麻烦
spring session+redis
分布式事务
XA方案 两阶段提交方案
比如团建
第一阶段:tb主席问每个人能不能去,如果能去,那么ok,一起去这次tb。
如果这个阶段里,任何一个人回答说,去不了,那么tb主席取消这次活动
如果这个阶段里,任何一个人回答说,去不了,那么tb主席取消这次活动
第二阶段:那下周六大家就一起去滑雪+烧烤了
事务管理器
相当于team building主席
第一阶段:询问
事务管理器去问每个系统,能不能执行这个操作?
第二阶段:执行
挨个通知各个系统进行数据处理
常见于协调一个系统需要对多个数据库进行写入,基于spring+JTA就可以搞定
这种分布式事务方案,比较适合单块应用,跨多个库的分布式事务,因为严重依赖于数据库层面来搞定复杂的事务
但这这种一个服务多个库不符合开发规范了
TCC方案
用了补偿的概念
全程是:Try,Confirm,Cancel
1.Try阶段:对各个服务的资源做检测以及对资源进行锁定或预留
2.Confirm
这个阶段是在各个服务中执行实际的操作
3.Cancel
如果任何遗憾个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑回滚操作。
try阶段:先去冻结银行紫金
confirm阶段
调用银行B的接口,扣款。
调用银行C的接口,转账
调用银行C的接口,转账
如果执行失败,进行Cancel阶段
Cancel阶段
通过跟业务相关的代码,将银行B扣的款给加回去
比较适合的场景:
这个除非你是真的要求一致性要求太高,是你系统中核心之核心的场景,跟钱相关的资金类的场景,那你可以用TCC方案,自己编写大量的业务逻辑,自己判断各个阶段是否OK,还得确保每个系统执行的时间都比较短。
但是说实话,一般尽量别这么搞,自己手写回滚逻辑,实在太恶心。业务代码都很难维护。
本地消息表方案
流程
(1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表中。
(2)接着A系统将这个消息发送到MQ中去
(3)B系统接收到消息之后,在一个事务里,先往自己本地消息表里插入一条数据,同时执行其他业务操作,如果这个消息已经被处理过了,那么此时事务会回滚,保证不会处理重复消息。
(4)B系统执行成狗之后,就会更新自己本地消息表的状态以及A系统消息表的状态(可以用zookeeper,A订阅OrderID,b成功去修改值)
(5)如果B系统处理失败了,那么久不会更新消息表的状态,那么A系统定时扫描自己的消息表,如果有没有没有处理的消息,那么会再次发送到MQ中去,让B再次处理
(6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止。
(2)接着A系统将这个消息发送到MQ中去
(3)B系统接收到消息之后,在一个事务里,先往自己本地消息表里插入一条数据,同时执行其他业务操作,如果这个消息已经被处理过了,那么此时事务会回滚,保证不会处理重复消息。
(4)B系统执行成狗之后,就会更新自己本地消息表的状态以及A系统消息表的状态(可以用zookeeper,A订阅OrderID,b成功去修改值)
(5)如果B系统处理失败了,那么久不会更新消息表的状态,那么A系统定时扫描自己的消息表,如果有没有没有处理的消息,那么会再次发送到MQ中去,让B再次处理
(6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止。
问题
最大的问题在于严重依赖于数据库的消息表来管理事务。这个如果是高并发场景咋办?咋扩展呢?所以一般确实很少用
靠谱方案:可靠消息最终一致性方案
阿里的RocketMQ支持实现消息事务
流程
1.A系统发送parpared消息到MQ,如果这个prepared消息发送失败那么直接取消操作别执行就行
2.如果消息发送成功了,那么接着执行本地事务,如果成功了就告诉MQ发送确认消息confirm,如果失败久告诉MQ回滚消息。
假如A系统发送确认消息的时候失败了,那么消息还停留在prepard状态
RocketMQ会自动轮询所有prepared消息,如果RocketMQ发现消息一直在Prepared状态,那么它会回调A系统的一个接口,问是回滚还是重新发送一次确认消息?那么A系统就选择重新发送确认消息(confirm)
3.一旦A系统发送confirm消息,那么MQ就会让B系统消费这条消息,执行本地事务
4.B系统如果失败了,可以通过MQ或者zookeeper,告诉A系统再重新发送一次。B系统要保证下幂等性,通过记录标志确定没有重复处理。
seata
术语
TC 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM 事务管理器
定义全局事务的范围:开始全局事务,提交或回滚全局事务。
RM 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
undo_log回滚日志表
undo_log必须在每个业务数据库中创建,用于保存回滚操作数据。
当全局提交时,undolog记录直接删除。
当全局回滚时,将现有数据撤销,还原至操作前的状态
beforeImage
操作前镜像
afterImage
操作后镜像
模式
AT,TCC,SAGA,XA模式
AT模式
对业务无侵入,二阶段提交
第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
第二阶段
提交异步化,非常快速的完成。
回滚通过一阶段的回滚日志进行反向补偿
具体步骤
1.TM端是有@GlobalTransaction 进行全局事务开启,提交,回滚
2.TM开始RPC调用远程服务。
3.RM端seata-client通过扩展DataSourceProxy,实现自动生成 UNDO_LOG与TC上报
4.TM告知TC提交/回滚全局事务
5.TC通知RM各自执行commit/rollback 操作,同时清除undo_log
AT模式原理解析
TC相关表
global_table:全局事务
每当有一个全局事务发起后,就会在该表中记录全局事务的ID
branch_table:分支事务
记录每一个分支事务的ID,分支事务操作的哪个数据库等信息
lock_table:全局锁
xid:全局事务的ID ip+端口+事务ID
AT模式如何做到对业务的无侵入
一阶段提交
1.TM 方法执行时,由于该方法被@GlobalTranscation修饰,该TM会向TC发起全局事务,生成XID(全局锁)
2.RM 进行写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果
RM写表过程,Seata会拦截业务SQL,首先解析SQL语句,在业务数据被更新前,将其报错成before image,然后执行业务SQL,在业务数据更新之后,再将其保存为after image,最后生成行锁。以上操作全部在一个数据库事务哪完成,这样保证了一阶段操作的原子性。
二阶段步骤
因为业务SQL在一阶段已经提交至数据库,所以Seata只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
正常:TM执行成功,通知TC全局提交,TC此时通知所有的RM提交成功,删除UNDO_LOG回滚日志
异常:TM执行失败,通知TC全局回滚,TC此时通知所有RM进行回滚,根据UNDO_LOG反向操作,使用before image还原业务数据,删除UNDO_LOG,但在还原前要首先校验脏写,对比数据库当前业务数据和 after image ,如果两份数据完全一致说明没有脏写,可以还原业务数据。如果出现脏写,那么就需要转人工处理。
你们公司怎么处理分布式事务
找一个严格资金绝对不能错的场景,可以说用TCC方案
一般的分布式事务,库存数据没有那么敏感,可以用可靠消息最终一致性方案
RocketMQ 3.2.6之前的版本,后来的回调被砍掉了
分布式事务太复杂,导致系统吞吐量大幅度下跌。
正常就系统A调用系统B,系统B调用系统C。调用报错打印异常日志。
99%分布式接口调用,不要做分布式事务
只需要监控调用报错(发邮件,发短信)
记录日志(一旦出错,完整的日志)
事后快速定位,排查和出解决方案,修复数据。
记录日志(一旦出错,完整的日志)
事后快速定位,排查和出解决方案,修复数据。
比你做50个分布式事务的成本要低上百倍几十倍。
要权衡
要用分布式事务的时候,一定有成本,代码会很复杂,性能会下跌,系统更加复杂更加脆弱,更加容易出bug
好处:做好了的话TCC,可靠消息最终一致性方案,一定可以100%保证你那块数据不会出错
读写隔离
1.写隔离
一阶段本地事务提交前,需要确保先拿到 全局锁
拿不到全局锁,不能提交本地事务
拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
一阶段
tx1先开始,开启本地事务,拿到本地锁,更新操作m = 1000-100 = 900。本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁。tx2后开始,开启本地事务,拿到本地锁,更新操作m = 900 - 100 = 800。本地事务提交前,尝试拿到该记录的全局锁,tx1全局提交前,该记录的全局锁被tx1持有,tx2需要重试等待全局锁。
二阶段全局提交,释放全局锁。tx2拿到全局锁,提交本地事务。
如果二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支回滚。
此时,如果tx2仍在等待该数据的全局锁,同时持有本地锁,则tx1的分支回滚会失败。分支回滚会一直重试,直到tx2的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。
因为整个过程 全局锁在tx1结束前一直是被tx1持有的,所以不会发生脏写问题。
2.读隔离
在数据库本地事务隔离级别 读已提交或以上的基础上,Seata AT模式的默认全局隔离级别是读未提交。
如果在特定场景下,必须要全局的读已提交,目前Seata方式是通过Select for update
select for update 语句执行会申请 全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被block住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。
Eureka服务中心的原理
流程
服务一启动,就进行服务注册,到服务注册表
其他服务进行服务发现,就去eureka去拉去服务注册表,可配置每隔30秒拉取一次服务注册表。
多级缓存
ReadOnly缓存
ReadWrite缓存
服务注册表
1.修改服务注册表后,会立马同步到ReadWrite缓存中。
2.在ReadOnly缓存中没有的话,就会去ReadWrite缓存中查询,然后保存到ReadOnly缓存中
3.后台会有一个定时的线程,把ReadWrite缓存定时同步到ReadOnly缓存里。(定时可配置)
2.在ReadOnly缓存中没有的话,就会去ReadWrite缓存中查询,然后保存到ReadOnly缓存中
3.后台会有一个定时的线程,把ReadWrite缓存定时同步到ReadOnly缓存里。(定时可配置)
目的:优化并发冲突
如果只有服务注册表,可能在高并发的时候有写和读的冲突然后加锁等问题
心跳与故障
所有服务启动注册完以后,会不停发送心跳。(时间可设置)
服务注册中心会维护所有注册服务的心跳。有一个线程定时会检查心跳。(时间可设置)
一旦发现某个服务超过一定时间没有心跳,就认为服务故障,就会摘掉这个服务。
直接删掉ReadWrite缓存的所有数据。
已经拉取注册表的请求可能使用的还是原来的注册表,往已经挂掉的服务发送请求,发现每次都请求失败。
每隔一段时间才能刷新
Ribbon
Feign
他是对一个接口打一个注解,他一定会针对这个注解标注的接口生成动态代理,然后针对feign的动态代理去调用他的方法的时候,此时会在底层生成http协议格式的请求,/order/create?productId=1
底层:使用http通信的框架组件,HttpClient,先得使用Ribbon去Eureka从本地的Eureka注册表的缓存里获取出来对方的机器列表,然后进行负载均衡,选择一台机器出来,接着针对那台机器发送http请求过去即可。
Zuul
配置一下不同的请求路径与服务的对应关系,你的请求到了网关,他直接查找到匹配的服务,然后就直接把请求转发给那个服务的某台机器。Ribbon从Eureka本地的缓存列表中获取一台机器,负载均衡,把请求直接通过http通信框架发送到指定机器上去。
springCloud的好处
主打微服务架构,组件齐全,功能齐全。网关直接提供了分布式配置中心,授权认证,服务调用链路追踪,Hystrix做资源隔离,熔断降级,服务请求QPS监控。
胜是胜在功能齐全,中小型公司开箱即用,直接满足系统的开发需求。
hystrix限流,降级,熔断
如何设计一个高并发系统?
1.系统拆分(2000万用户,2000/s请求)
一个系统拆成多个系统,每个系统访问一个数据库。
2.缓存(6000万个用户,6000/s请求)
大量的读请求5000/s走缓存,1000/s写请求走数据库。缓存可以轻松抗几万的并发
3.MQ(1亿用户,1万请求/s)
2000/s个请求写请求到MQ,每秒1000处理请求
4.分库分表(2亿用户,2万请求/s)
分库分表,增加写入数据库的请求/s
5.数据库读写分离(5亿用户,每秒5万个请求)
由于缓存LRU部分数据还是得去数据库里读,压力比较大。写的话写到主库,读的话读从库。写的话每秒600个请求。读每秒1000个请求
6.ElasticSearch
玩搜索,一开始数据量很小,你可以自己基于lucene包缝状搜索引擎。但是后期数据量越来越大,单机lucene已经放不下了,而且单机也支撑不了那么高的并发量。
es集群是分布式的,可以放几十亿的数据量,用20台机器,每秒可以支撑几万的请求
分布式系统中的接口调用如何保证顺序性
调用接口可以带一个order id,然后创建一个接入服务,如果需要进行顺序执行,就使用同一个orderId,然后中间接入服务根据orderId给到同一个系统中执行。
如果一个系统中有多个线程处理接口,可以创建多个内存队列对应每个线程,根据orderId放到同一个内存队列中,让同一个线程按顺序执行队列中的请求
高并发原因,在用户发送请求网络原因导致发到接入服务就顺序不对,非要严格要求顺序,使用分布式锁,保证顺序性。然后根据排序序号或者时间,去判断已经执行到第几个,保证顺序性。
如果要用分布式锁,系统可能会很慢,最好使用MQ进行处理。
如果要用分布式锁,系统可能会很慢,最好使用MQ进行处理。
群发红包如何设计
发红包
判断账户额度是否大于金额,如果大于就保存红包到redis和数据库,然后更新账户
抢红包
判断红包个数,然后在redis更新缓存,并插入红包记录。然后通过MQ进行异步解偶写入数据库。
金额算法
线性切割法
把总金额看成一根绳子,切N-1刀
二倍均值法
每次抢到的金额=随机区间(0,M/N✖️2)
如果为100的红包5个人去抢(除去最后一次,任何一个人抢到的红包都不会大于人均的两倍)
超买超卖
分布式锁
单体使用synchronized
分布式使用redis或者zookeeper
redis速度快
并发量不是特别大,支持可靠性选择zookeeper
数据一致性
分布式事务
seata
数据可靠性投递
rabbitMQ保证数据不丢失
生产者发送消息confirm机制,发送完消息等待异步回调确保消息发送成功
数据持久化
消费端手动ack
0 条评论
下一页