目录
- 分布式
- 分布式系统设计理念
- 目标
- 设计思路
- 中心化
- 去中心化
- 基本概念
- 分布式与集群
- Nginx
- RPC
- 消息中间件(MQ)
- NoSQL(非关系型数据库)
- 分布式事务
- 1 事务
- 2 本地事务
- 3 分布式事务
- 4 本地事务VS分布式事务
- 5 分布式事务场景
- 6 CAP原理
- 7 CAP组合方式
- 8 Base理论
- 9 分布式事务解决方案之 2PC
- 简介
- XA方案
- Seata 方案
- 10 分布式事务解决方案之TCC
- 简介
- 三种异常处理
- TCC与 2PC
- 11 分布式事务解决方案之可靠消息最终一致性
- 简介
- 要解决的问题
- 解决方案:本地消息表方案
- 小结
- 12 分布式事务解决方案之最大努力通知
- 简介
- 举例
- 实现方案(利用MQ的ack机制,由MQ向接收通知方发送通知)
- 最大努力通知与可靠消息一致性的不同点
- 13 分布式事务解决方案之对比分析
- 分布式锁
- 为什么使用分布式锁
- 分布式锁应该具备的条件
- 三种实现方式
- 1 基于数据库的实现方式
- 2 基于Redis的实现方式
- 2.1 使用setnx和expire命令实现
- 2.2 基于 Redission 客户端
- 3 基于ZooKeeper的实现方式
- 4 Redis和Zookeeper对比
- 分布式ID
- 微服务中网关调用和RPC调用的区别
分布式
分布式系统设计理念
目标
分布式架构的应用十分广泛:
- 分布式文件系统:比如 Hadoop 的HDFS ,Google 的 GFS,淘宝的 TFS 等;
- 分布式缓存系统:Memcache,Hbase 等;
- 分布式数据库:MySQL ,Mariadb,PostgreSQL 等;
分布式系统的目标是提升系统的整体性能和吞吐量另外还要尽量保证分布式系统的容错性。
设计思路
分布式系统 2 大设计思路:
- 中心化
- 去中心化
中心化
- 2 种角色
分布式集群中的节点机器按照角色分工,大体上分为两种角色:“领导”和“员工”。
- 角色职责
“领导”通常负责分发任务并监督“员工”,发现谁太闲了,就想发设法地给其安排新任务,确保没有一个“干活的”能够偷懒,如果“领导”发现某个“干活的”因为劳累过度而病倒了,则是不会考虑先尝试“医治”他的,而是一脚踢出去,然后把他的任务分给其他人。
- 面临的问题
最大问题是“领导”的安危问题,如果“领导”出了问题,则群龙无首。
去中心化
- 地位平等
- “去中心化”不是不要中心,而是由节点来自由选择中心
集群的成员会自发的举行“会议”选举新的“领导”主持工作。
- 面临的问题
脑裂问题。脑裂指一个集群由于网络的故障,被分为至少两个彼此无法通信的单独集群,此时如果两个集群都各自工作,则可能会产生严重的数据冲突和错误。
一般的设计思路是,当集群判断发生了脑裂问题时,规模较小的集群就“自杀”或者拒绝服务。
基本概念
分布式与集群
分布式指的是一个业务拆分为多个子业务,部署在不同服务器上。比如一个电商系统,用户模块部署在 server1, 订单模块部署在 server2,促销模块部署在 server3,商品模块部署在 server4,他们之间通过远程 RPC 实现服务调用,这就叫分布式。强调的是不同功能模块,单独部署在不同的 server上,所有 server 加起来是一个完整的系统。
集群指的是同一业务,部署在多个服务器上,更多强调的是灾备。比如一个电商系统,完整的部署在 server1 上一个,同样完整的部署在 server2 上,当 server1宕机,server2 仍然可以正常提供请求服务,这叫集群。同样对于某一功能模块,比如用户模块部署在 server1 上,同样部署在 server2 上,也叫做集群。分布式系统的每个功能模块节点,都可以用多机做成集群。
拿做菜示例,假如一个厨师做菜要经历切菜,炒菜两个功能,饭店为了提高速度招了两个厨师,每个厨师的工作一样,都是切菜,炒菜,这是集群。还有另一种方法提高效率,饭店招了一个切菜师傅,配合厨师,厨师不管切菜,只管炒菜了,和切菜师傅共同配合把菜做好,这叫分布式。
Nginx
Nginx 作用是反向代理和负载均衡。
反向代理是指请求真实是到 server1 中,但是系统中为了统一或者做比如单点登录,会在 server2 服务器上安装一个 nginx,里面配置到 server1 的反向代理,那么之后请求 url 就可以写 server2 的地址,发出后到 server2, server2 会转发到 server1 上,类似一种代理的模式。
负载均衡是指如果一个系统的请求很多,我们可以把请求转发到不同的服务器上,用来分流。就类似于接了一个水管放水,水流量很大时候,水压大很可能会让一个水管爆炸,这时候接三个水管,就没问题了(这三个水管就是一个集群)。类似的在 nginx 服务器中配了 3 个 Tomcat 服务器,每个 Tomcat 服务器上都部署了整个系统,那么当请求数大的时候,可以分发到不同的 Tomcat。(其实这里每个 Tomcat 上部署同一个功能模块也叫集群)。
RPC
RPC (Remote Procedure Call) 即远程过程调用,对于分布式系统来讲,Tomcat1 上部署了用户模块,Tomcat2 上部署了订单模块,当用户下单时,请求到 Tomcat2,这时候可能要判断这个用户是否是 vip,或者是否有优惠券,这些方法是在 Tomcat1 用户模块上的,那么 Tomcat2 调用 Tomcat1 的服务获取这些信息,就叫 RPC 调用。
消息中间件(MQ)
MQ 消息中间件在分布式系统中的作用有很多,但是经常用到的还是异步解耦。
比如天猫下单流程,当用户支付后,后台接口执行的操作可能包括:1 验签,2 支付密码校验,3 扣库存,4 用户积分增加等等操作,其实我们希望的是2操作执行成功后立即给用户结果提示,而不是等到后续各个操作完成后才去提示,因为后续的操作往往大部分是rpc调用,方法执行时间相对较长。另外对于下单支付这个操作,3和4是后续业务的需要,在设计上不能和下单支付本身出现强耦合度。所以这里我们可以引入 MQ 解决,也就是说1和2执行完成后,生产者只需要通知下3和4,把后续的操作扔给消息队列,立即返回。这里的 MQ 起到的作用一个是异步调用,一个是解耦。
NoSQL(非关系型数据库)
NoSQL 是所有非关系型数据库的统称,在分布式系统中用到很多,主要用来提高 QPS(query per second)。
Redis 常用作缓存,或者内存数据库,小巧强大,什么数据适合放在 Redis 也就是缓存中,一个是经常查询的,需要频繁磁盘 io 的,另一个是查询数据缓慢的,可以放在缓存中。
分布式事务
1 事务
一次大的活动由不同的小活动组成,这些活动要么全部成功,要么全部失败;
2 本地事务
1 通过关系型数据库来控制事务叫数据库事务 2 应用主要靠关系数据库控制事务,数据库通常和应用在同一个服务器 3 因此基于关系型数据库的事务又被称为本地事务;4 ACID特性;
3 分布式事务
1 分布式系统把一个应用系统拆分为可独立部署的多个服务 2 需要服务与服务之间远程协作才能完成事务操作 3 这种分布式系统环境下由不同服务之间通过网络远程协作完成事务称为分布式事务 3 例如创建订单减库存事务、银行转账事务等都是分布式事务;
4 本地事务VS分布式事务
本地事务依赖数据库本身提供的事务特性来实现:
begin transaction;
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;
分布式环境下:
begin transaction;
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
commit transation;
远程调用让李四增加金额成功了,但网络问题远程调用并没有返回,此时本地事务提交失败回滚张三减少金额的操作,于是张三和李四的数据就不一致了。实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
5 分布式事务场景
1 跨JVM进程产生分布式事务:典型场景微服务架构,微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减少库存
或
2 跨数据库实例产生分布式事务:单体系统需要访问多个数据库实例时,比如用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息需要分别删除用户信息及用户订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据
6 CAP原理
1 一致性、可用性、分区容忍性(Consistency、Availability、Partition tolerance);2 C一致性:写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新状态。(实现:写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁) (特点:(1)由于存在数据同步的过程,写操作的响应会有一定的延迟;(2)为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源;(3)如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据) 2 A可用性:任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。(实现:写入主数据库后,不可将从数据库中的资源进行锁定。即使数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时)(特点:所有请求都有响应,且不会出现响应超时或响应错误)3 P分区容忍性:分布式系统的各结点部署在不同子网,即网络分区,不可避免会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务。(特点:(1)主数据库向从数据库同步数据失败不影响读写操作;(2)一个结点挂掉不影响另一个结点对外提供服务)(实现:(1)尽量使用异步取代同步操作,例如使用异步将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合;(2)添加从数据库结点,其中一个从结点挂掉其它从结点提供服务)
7 CAP组合方式
1 背景:在保证P分区容忍性前提下,如果实现C一致性,在数据同步的时候为防止向从数据库查询不一致的数据则需要将从数据库数据锁定,待同步完成后解锁,如果同步失败从数据库要返回错误信息或超时信息;如果实现A可用性,不管任何时候都可以向从数据查询数据,不会响应超时或返回错误信息。因此在满足P的前提下 C 和 A 存在矛盾性;2 CAP组合:(1)AP:放弃C一致性,追求A可用性和P分区容忍性,前提是用户可以接受所查询到的数据在一定时间内不是最新。如:订单退款,今日退款成功,明日账户到账。(2)CP:放弃P可用性,追求C一致性和P分区容错性。如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。(3)CA:放弃P分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现C一致性和A可用性。那么系统将不是分布式系统,如最常用的关系型数据库,通过事务隔离级别实现每个查询请求都可以返回最新的数据。4 总结:一般保证 P分区容忍性和A可用性 ,舍弃C强一致性,保证最终一致性。
8 Base理论
1 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)2 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态 3 Ba基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网站交易付款出现问题了,商品依然可以正常浏览。4 S软状态:不要求强一致性,允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的"支付中"、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。5 E最终一致性:经过一段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
9 分布式事务解决方案之 2PC
简介
1 即两阶段提交协议:2 指两个阶段,P 指准备阶段,C 指提交阶段 2 整个事务过程由事务管理器和参与者组成 3 事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚 4 准备阶段(事务管理器给每个参与者发送 Prepare 消息,每个数据库参与者在本地执行事务,并写本地的 Undo/Redo 日志,此时事务没有提交(Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件))5 提交阶段(如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。)
XA方案
1 基于数据库的 XA 协议来实现 2PC;2 三个角色 AP、RM、TM。AP 指使用 2PC 分布式事务的应用程序;RM 指资源管理器,控制分支事务;TM 指事务管理器,控制全局事务;3 准备阶段 (RM 执行实际的业务操作,但不提交事务,资源锁定);4 提交阶段 (TM 接受 RM 在准备阶段的执行回复,只要有任一个RM执行失败,TM 会通知所有 RM 执行回滚操作,否则,TM 将会通知所有 RM 提交该事务。提交阶段结束资源锁释放。)4 问题(1)需要本地数据库支持XA协议(定义TM和RM之间通讯的接口规范叫 XA)。(2)资源锁需要等到两个阶段结束才释放,性能较差。
Seata 方案
1 阿里中间件团队发起的开源项目,业务0侵入的 2PC 方案 2 定义了 3 个组件来协议分布式事务:TC事务协调器、TM事务管理器、RM资源管理器 3 TC事务协调器:独立的中间件,需要独立部署运行,维护全局事务的运行状态,接收 TM 指令发起全局事务的提交与回滚,负责与 RM 通信协调各分支事务的提交或回滚。4 TM事务管理器:TM 需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向 TC 发起全局提交或全局回滚的指令。5 RM资源管理器:控制分支事务,负责分支注册、状态汇报,并接收事务协调器 TC 的指令,驱动分支(本地)事务的提交和回滚。6 新用户注册送积分举例流程:(1 用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。2 用户服务的 RM 向 TC 注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局事务的管辖。3 用户服务执行分支事务,向用户表插入一条记录。4 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的 RM 向 TC 注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。5 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。6 用户服务分支事务执行完毕。7 TM 向 TC 发起针对 XID 的全局提交或回滚决议。8 TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。)7 与XA的区别:(1 架构层次方面:传统 2PC 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现;而 Seata 的 RM 是以 jar 包的形式作为中间件层部署在应用程序这一侧的。2 两阶段提交方面:传统 2PC无论第二阶段的决议是 commit 还是 rollback ,事务性资源的锁都要保持到 Phase2 完成才释放。而 Seata 的做法是在 Phase1 就将本地事务提交,这样就可以省去 Phase2 持锁的时间,整体提高效率。)
推荐采用 Seata 实现 2PC。
10 分布式事务解决方案之TCC
简介
1 TCC 是 Try、Confirm、Cancel 三个词语的缩写,TCC 要求每个分支事务实现三个操作:预处理 Try、确认 Confirm、撤销 Cancel;2 Try 操作做业务检查及资源预留,Confirm 做业务确认操作,Cancel 实现一个与 Try 相反的操作即回滚操作。3 TM 首先发起所有的分支事务的 Try 操作,任何一个分支事务的Try操作执行失败,TM 将会发起所有分支事务的 Cancel 操作,若 Try 操作全部成功,TM 将会发起所有分支事务的 Confirm 操作,其中 Confirm/Cancel 操作若执行失败,TM 会进行重试/人工处理;
三种异常处理
1 TCC需注意三种异常处理:空回滚、幂等、悬挂: 1 空回滚(1 在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,出现原因是当一个分支事务所在服务宕机或网络异常,没有执行 Try 阶段,当故障恢复后,分布式事务会调用二阶段的 Cancel 方法,从而形成空回滚。2 解决思路:额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。)2 幂等(1 为保证 TCC 二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源 2 解决思路:在上述"分支事务记录"中增加执行状态,每次执行前都查询该状态)3 悬挂(1 Cancel 接口比 Try 接口先执行 2 出现原因是在 RPC 调用分支事务 Try 时,先注册分支事务,再执行 RPC 调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,TM 就会通知 RM 回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者真正执行 3 解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,"分支事务记录"表中是否已经有二阶段事务记录,如果有则不执行 Try)
TCC与 2PC
1 2PC 通常都是在跨库的 DB 层面,而 TCC 则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。2 不足之处在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现 Try、Confirm、Cancel 三个操作。此外实现难度比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
11 分布式事务解决方案之可靠消息最终一致性
简介
1 可靠消息最终一致性方案是指当事务发起方执行完成本地事务后发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功; 2 利用消息中间件完成;3 事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间、事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题;
要解决的问题
- 本地事务与消息发送的原子性问题:1 事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。2 情形一:先发送消息,再操作数据库。无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败;3 情形二:先操作数据库,再发送消息。如果发送 MQ 消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数据库回滚,但 MQ 其实已经正常发送了,同样会导致不一致;
- 事务参与方接收消息的可靠性问题:事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息;
- 消息重复消费的问题:1 由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。2 要解决消息重复消费的问题就要实现事务参与方的方法幂等性;
解决方案:本地消息表方案
通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
以注册送积分为例来说明:两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
交互流程如下:
- 用户注册:用户服务在本地事务新增用户和增加 “积分消息日志”。由于本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性;
- 定时任务扫描日志:启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试;
- 消费消息:1 使用 MQ 的 ack(即消息确认)机制,消费者监听 MQ,如果消费者接收到消息并且业务处理完成后向 MQ 发送 ack(即消息确认),此时说明消费者正常消费消息完成,MQ 将不再向消费者推送消息,否则MQ会不断重试向消费者发送消息。2 积分服务接收到"增加积分"消息,开始增加积分,积分增加成功后向消息中间件回应 ack,否则消息中间件将重复投递此消息。3 由于消息会重复投递,积分服务的"增加积分"功能需要实现幂等性;
小结
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性:
- 本地事务与消息发送的原子性问题;
- 事务参与方接收消息的可靠性;
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
12 分布式事务解决方案之最大努力通知
简介
发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。
包括:
- 有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知;
- 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息来满足需求;
举例
- 账户系统调用充值系统接口;
- 充值系统完成支付处理向账户发起充值结果通知,若通知失败,则充值系统按策略进行重复通知;
- 账户系统接收到充值结果通知修改充值状态;
- 账户系统未接收到通知会主动调用充值系统的接口查询充值结果;
实现方案(利用MQ的ack机制,由MQ向接收通知方发送通知)
- 发起通知方使用普通消息机制将通知发给MQ;
- 接收通知方监听 MQ;
- 接收通知方接收消息,业务处理完成回应 ack;
- 接收通知方若没有回应 ack 则 MQ 会重复通知。MQ会按照间隔 1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔,直到达到通知要求的时间窗口上限;
- 接收通知方可通过消息校对接口来校对消息的一致性;
最大努力通知与可靠消息一致性的不同点
- 解决方案思想不同:(1)可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证;(2)最大努力通知,发起通知方尽最大努力将业务处理结果通知给接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方;
- 业务应用场景不同:(1)可靠消息一致性关注的是交易过程的事务一致性,以异步的方式完成交易;(2)最大努力通知关注的是交易后的通知事务,即将交易结果可靠地通知出去;
- 技术解决方向不同:(1)可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到;(2)最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息;
13 分布式事务解决方案之对比分析
- 2PC 最大的诟病是一个阻塞协议。RM 在执行分支事务后需要等待 TM的决定,此时服务会阻塞并锁定资源。由于其阻塞机制和最差时间复杂度高,因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以及子事务生命周期较长(long-running transactions) 的分布式服务中;
- 如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC 通常都是在跨库的 DB 层面,而 TCC 则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现Try、Confirm、Cancel 三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。典型的使用场景:满减,登录送优惠券等;
- 可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注册送积分,登录送优惠券等;
- 最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果通知等;
分布式锁
在整个系统提供一个全局、唯一的锁,在分布式系统中每个系统在进行相关操作的时候需要获取到该锁,才能执行相应操作。
为什么使用分布式锁
- 在传统单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制;
- 随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问;
- 这就是分布式锁的由来;
上图可以看到,变量A存在三个服务器内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象),如果不加任何控制的话,变量A同时都会在分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!
分布式锁应该具备的条件
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
- 高可用的获取锁与释放锁;
- 高性能的获取锁与释放锁;
- 具备可重入特性;
- 具备锁失效机制,防止死锁;
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败;
三种实现方式
1 基于数据库的实现方式
- 在数据库中创建一个表,表中包含method_name等字段,并在方法名字段上创建唯一索引;
- 因为对method_name做了唯一性约束,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容;
- 成功插入则获取锁,执行完成后删除对应的行数据释放锁;
- 因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;
- 不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
- 没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
- 不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
2 基于Redis的实现方式
2.1 使用setnx和expire命令实现
在使用Redis实现分布式锁的时候,主要使用到以下三个命令:
- SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0;
- expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁;
- delete key:删除key;
实现思想:
- 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断;
- 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁;
- 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放;
2.2 基于 Redission 客户端
- 调用 api 中的
lock()
和unlock()
方法。它帮我们封装锁实现的细节和复杂度; - 线程去获取锁,获取成功则执行lua脚本,保存数据到redis数据库;
- 如果获取失败: 一直通过while循环尝试获取锁(可自定义等待时间,超时后返回失败),获取成功后,执行lua脚本,保存数据到redis数据库;
- 如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态,会出现锁死的状态,为了避免这种情况发生,锁会设置一个过期时间。这样也存在一个问题,假如一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了Watch Dog 自动延期机制。
- Watch Dog 自动延期机制:Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间(默认30s);
3 基于ZooKeeper的实现方式
ZooKeeper内部是一个分层的文件系统目录树结构,可以使用有序节点来实现:
- 每个线程或进程在 Zookeeper 上的
/lock
目录下创建一个临时有序的节点表示去抢占锁,所有创建的节点会按照先后顺序生成一个带有序编号的 节点; - 线程创建节点后,获取/lock 节点下的所有子节点,判断当前线程创建的节点是否是所有的节点的序号最小的;
- 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功;
- 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听,当前一个被监听的节点释放锁之后,触发回调通知,从而再次去尝试抢占锁;
4 Redis和Zookeeper对比
redis和zk分布式锁的区别,关键就在于高可用性和强一致性的选择,redis的性能高于zk太多了,可在一致性上又远远不如zk。(需要补充)
分布式ID
在分布式系统中,需要对大量的数据和消息进行唯一标识。举个例子,例如数据库分库分表后需要一个唯一的ID来标识一条数据。
- UUID(通用唯一识别码):基于当前时间、硬件标识(通常为无线网卡的MAC地址 )等数据计算生成。优点:生成简单,本地生成无网络消耗,唯一性。缺点:无序不具备趋势自增、无具体业务含义、36位字符太长不适合作索引;
- 基于数据库自增ID:使用一个单独的DB实例,基于数据库的auto_increment来生成分布式ID。优点:实现简单、ID单调自增、数值类型查询速度快;缺点:DB单点存在宕机风险,无法扛住高并发场景(访问量激增时MySQL本身就是系统瓶颈);
- 基于数据库集群模式:每个DB实例单独生产自增ID,每台DB设置不同的初始值,且步长和BD实例数相等。优点:解决DB单点问题;缺点:系统水平扩展比较困难,比如定义好了DB实例数和步长之后,如果要添加DB会比较困难;
- 基于数据库的号段模式:从数据库批量的获取自增ID,比如DistributIdService每次从数据库获取ID时,就获取一个号段,比如(1,1000],这个范围表示了1000个ID,业务应用在请求DistributIdService提供ID时,DistributIdService只需要在本地从1开始自增并返回即可,而不需要每次都请求数据库,一直到本地自增到1000时,也就是当前号段已经被用完时,才去数据库重新获取下一号段。表结构如下:
CREATE TABLE id_generator (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '当前最大id',
step int(20) NOT NULL COMMENT '号段的步长',
biz_type int(20) NOT NULL COMMENT '业务类型',
version int(20) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`)
)
等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,update max_id= max_id + step,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]。
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX
由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。
- 利用 Redis 生成 ID : 通过Redis的INCR命令,能保证生成的id肯定是唯一有序的。优点:性能比较好,灵活方便,不依赖于数据库;缺点:要考虑redis持久化的问题(redis有两种持久化方式RDB和AOF:RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况;AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复数据的时间过长);
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回递增后的数值
(integer) 2
- Snowflake算法:twitter 开源的分布式 id 生成算法,生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特组成的一个Long类型。
- 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0;
- 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年;
- 工作机器id(10bit):也被叫做workId,记录工作机器 id,代表的是这个服务最多可以部署在
2^10
台机器上,即1024台机器。其中 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id,意思就是最多代表2^5
个机房(32个机房),每个机房里可以代表2^5
个机器(32台机器); - 序列号部分(12bit):用来记录同一个毫秒内产生的不同 id,12 bit 可以代表的最大正整数是
2^12 - 1 = 4096
,也就是说支持同一毫秒内同一个节点可以生成4096个id;
- 美团(Leaf):Leaf同时支持号段模式和snowflake算法模式,可以切换使用。1 号段模式需新建数据库表,然后在https://github.com/Meituan-Dianping/Leaf项目中配置数据库信息,并打开号段模式,启动项目 2 snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。
微服务中网关调用和RPC调用的区别
- 适用场景:API网关适用于跨服务的调用,而RPC框架则适用于服务的内部调用;
- 功能:API网关主要用于服务治理和API聚合,提供了路由、转发、流量控制、限流等一系列功能,还可以通过插件机制扩展更多功能。而RPC框架则主要解决分布式系统中服务调用的问题,提供了远程调用、序列化、负载均衡等功能;
- 通信方式:API网关通常采用HTTP(S)协议进行通信,而RPC框架则通常采用自定义的协议或者像Thrift这样的高性能、开源的RPC框架;
- 性能:由于API网关需要处理HTTP(S)协议的解析和转换,因此其性能通常会低于RPC框架。