1. zookeeper
zk都有哪些使用场景?
ZooKeeper是一个高可用的分布式数据管理与系统协调框架。基于对Paxos算法的实现,使该框架保证了分布式环境中数据的强一致性,也正是基于这样的特性,使得ZooKeeper解决很多分布式问题
值得注意的是,ZK并非天生就是为这些应用场景设计的,都是后来众多开发者根据其框架的特性,利用其提供的一系列API接口(或者称为原语集),摸索出来的典型使用方法。
1.1 分布式协调
分布式协调/通知服务是分布式系统中将不同的分布式组件结合起来。
通常需要一个协调者来控制整个系统的运行流程,这个协调者便于将分布式协调的职责从应用中分离出来,从而可以大大减少系统之间的耦合性,而且能够显著提高系统的可扩展性。
ZooKeeper中特有的Watcher
注册与异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。
常用做法
不同系统都对ZK上同一个znode
进行注册,监听znode
的变化(包括znode本身内容及子节点的),其中一个系统update
了znode,那么另一个系统能够收到通知,并作出相应处理。
1.2 分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,需要使用分布式锁。
ZooKeeper 可用于分布式锁,这个主要得益于zk为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
1.2.1 保持独占
所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。
常用做法
通常的做法是把zk上的一个znode
看作是一把锁,通过create znode
的方式来实现。所有客户端都去创建/distribute_lock
节点,最终成功创建的那个客户端也即拥有了这把锁。
1.2.2 控制时序
控制时序,就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。
常用做法
做法和上面基本类似,只是这里/distribute_lock
已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL
来指定)。Zk的父节点(/distribute_lock
)维持一份sequence
,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
1.3 数据发布与订阅(配置中心)
集群中配置文件的更新和同步十分频繁:
传统的配置文件分发都是需要把配置文件数据分发到每台worker上,然后进行worker的reload,这种方式是最笨的方式,结构很难维护,因为如果集群当中有可能很多种应用的配置文件要同步,而且效率很低,集群规模一大负载很高。
还有一种就是每次更新把配置文件单独保存到一个数据库里面,然后worker端定期pull数据,这种方式就是数据及时性得不到同步。
发布与订阅模型,即所谓的元数据/配置中心,顾名思义就是发布者将数据发布到ZK节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。
- 应用中用到的一些配置信息放到ZK上进行集中管理。这类场景通常是这样:应用在启动的时候会主动来获取一次配置,同时,在节点上注册一个Watcher,这样一来,以后每次配置有更新的时候,都会实时通知到订阅的客户端,从来达到获取最新配置信息的目的。
- 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在ZK的一些指定节点,供各个客户端订阅使用。
- 分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用来分配收集任务单元,因此需要在ZK上创建一个以应用名作为path的节点P,并将这个应用的所有机器ip,以子节点的形式注册到节点P上,这样一来就能够实现机器变动的时候,能够实时通知到收集器调整任务分配。
- 系统中有些信息需要动态获取,并且还会存在人工手动去修改这个信息的发文。通常是暴露出接口,例如JMX接口,来获取一些运行时的信息。引入ZK之后,就不用自己实现一套方案了,只要将这些信息存放到指定的ZK节点上即可。
注意:在上面提到的应用场景中,有个默认前提是:数据量很小,但是数据更新可能会比较快的场景。
1.4 Master节点管理
集群当中最重要的是Master,所以一般都会设置一台Master的Backup。
Backup会定期向Master获取Meta信息并且检测Master的存活性,一旦Master挂了,Backup立马启动,接替Master的工作自己成为Master,分布式的情况多种多样,因为涉及到了网络的抖动,针对下面的情况:
Backup检测Master存活性传统的就是定期发包,一旦一定时间段内没有收到响应就判定Master Down了,于是Backup就启动。
如果Master其实是没有down,Backup收不到响应或者收到响应延迟的原因是因为网络阻塞的问题,此时Backup不知道Master是假的挂了,于是也启动了,这时候集群里就有了两个Master,很有可能部分workers汇报给Master,另一部分workers汇报给后来启动的Backup,这下子服务就全乱了。
Backup是定期同步Master中的meta信息,所以总是滞后的,一旦Master挂了,Backup的信息必然是老的,很有可能会影响集群运行状态。
zookeeper Master选举解决问题:
1)Master节点高可用,并且保证唯一。
Zookeeper为我们保证了数据的强一致性,即:zookeeper集群中任意节点上相同的znode数据一定是相同的。
也就是说,如果同时有多个客户端请求创建同一个临时节点,那么最终一定只有一个客户端 请求能够创建成功。
利用这个特性,就能很容易地在分布式环境中进行 Master 选举了。
成功创建该节点的客户端所在的机器就成为了 Master。同时,其他没有成功创建该节点的 客户端,都会在该节点上注册一个子节点变更的 Watcher,用于监控当前 Master 机器是否存 活,一旦发现当前的Master挂了,那么其他客户端将会重新进行 Master 选举。
2)Meta信息的及时同步。
zookeeper 会分配给注册到它上面的客户端一个编号,并且 zk 自己会保证这个编号的唯一性和递增性,N多机器中只需选出编号最小的 Client 作为 Master 就行,并且保证这些机器的都维护一个一样的 meta 信息视图,一旦 Master 挂了,那么这N机器中编号最小的胜任 Master,Meta 信息是一致的。
2. 分布式锁
一般实现分布式锁都有哪些方式?使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
2.1 redis分布式锁
2.1.1 获取锁
Redis 分布式加锁的主要流程
- 产生随机数,可用UUID,存储起来,一般存储在ThreadLocal中,以便解锁用。
- 调用Redis 的SETNX命令将随机数当作value存入,key为taskId,同时设置过期时间。(实际项目中过期时间的多少主要是取决任务估算的执行时间,一般为估算执行时间*2,如该任务的估算时间是2m,则过期就要设置4m)。
- 如果返回ok,说明加锁成功,否则失败。
redis是一种key-value形式的NOSQL数据库,常用于作服务器的缓存。从redis v2.6.12开始,set命令开始变成如下格式:
1 | SET key value [EX seconds] [PX milliseconds] [NX|XX] |
除 key 和 value 外,EX
是超时时间,NX
表示只有在 key 不存在的时候才会设置 key 的值,而XX
表示在 key 存在的时间才会设置 key 的值。NX机制就是基于redis分布式锁的核心。能够解决以下问题:
节点1获取key,并且设置超时时间后,还没来得及释放就挂掉了
这里
EX
超时时间会发挥作用,超时后自动释放锁。刚获取到锁,还没来得及设置超时时间就挂了
这里设置key和设置超时时间是原子操作,如果出现这种情况,会返回0,即获取不到锁。
2.1.2 释放锁
Redis分布式解锁的主要流程
- 调用lua脚本进行解锁,保证原子性。
- Lua脚本实现:判断key的值和我们存入的UUID随机数是不是相等,是的话,则调用DEL指令进行删除操作。
为了解决非原子操作带来的问题,常采用lua
脚本实现。lua脚本的操作会被认为是原子性的,类似于事务。
伪代码如下:
1 | String luaScript = |
2.2 zk分布式锁
zookeeper是一种分布式协调服务,其中每个节点称为znode,并有自己独立的路径。
znode
有四种类型:
持久节点
默认的节点类型。创建节点的客户端与zk断开连接后,该节点依旧存在 。
持久节点顺序节点
所谓顺序节点,就是在创建节点时,zk根据创建的时间顺序给该节点名称进行编号。
临时节点
和持久节点相反,当创建节点的客户端与zk断开连接后,临时节点会被删除。
临时顺序节点
结合和临时节点和顺序节点的特点:在创建节点时,zk根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
下面看看是怎样基于上面的四类节点实现分布式锁的。
1、获取锁
1)在 zk 当中创建一个持久节,当第一个客户端Client1想要获得锁时,需要在这个节点下面创建一个临时顺序节点。
2)Client1查找持久节点下面所有的临时顺序节点并排序,判断自己所创建的节点是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
3)如果再有一个客户端 Client2 前来获取锁,则在持久节点下面再创建一个临时顺序节点Lock2。
4)Client2查找持久节点下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
5)如果又有一个客户端Client3前来获取锁,则在持久节点下载再创建一个临时顺序节点Lock3。
Client3查找持久节点下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
2、释放锁
释放锁就比较简单了,因为前面创建的临时顺序节点,所以在出现下面两种情况时,都会自动释放锁:
1)任务完成后,Client会释放锁。
2)任务没完成,也就是Client就崩溃了,zk会检测这个client挂了,就会自动释放锁。
3. 分布式会话
集群部署时的分布式session如何实现?
3.1 Tomcat + Redis
分布式系统在集群部署后需要实现session共享,针对 tomcat 服务器的实现方案多种多样,比如 tomcat cluster session 广播、nginx IP hash策略、nginx sticky module等方案,常见的分布式会话是使用 redis 服务器进行 session 统一存储管理的共享方案。
3.1.1 nginx 负载均衡配置
1)修改nginx conf配置文件加入
1 | upstream tomcat { |
然后,配置 相应的server或者location
地址到 http://tomcat
3.1.2 tomcat session共享配置步骤
1)添加redis session集群依赖的jar包到TOMCAT_BASE/lib
目录下
- tomcat-redis-session-manager-2.0.0.jar
- jedis-2.5.2.jar
- commons-pool2-2.2.jar
2)修改TOMCAT_BASE/conf
目录下的context.xml
文件
1 | <Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" /> |
属性解释:
host
:redis服务器地址,可选用,默认”localhost”port
:redis服务器的端口号,可选用,默认”6379”database
:要使用的redis数据库索引,可选用,默认”0”maxInactiveInterval
:session最大空闲超时时间(失效时间,单位秒),如果不填则使用tomcat的超时时长,一般tomcat默认为 1800 即半个小时sessionPersistPolicies
:session保存策略,除了默认的策略还可以选择的策略有:
SAVE_ON_CHANGE
:每次 session.setAttribute() 、 session.removeAttribute() 触发都会保存。注意:此功能无法检测已经存在redis的特定属性的变化。
权衡:这种策略会略微降低会话的性能,任何改变都会保存到redis中。
ALWAYS_SAVE_AFTER_REQUEST
:每一个request请求后都强制保存,无论是否检测到变化。注意:对于更改一个已经存储在redis中的会话属性,该选项特别有用。
权衡:如果不是所有的request请求都要求改变会话属性的话不推荐使用,因为会增加并发竞争的情况。
- sentinelMaster redis集群主节点名称(Redis集群是以分片(Sharding)加主从的方式搭建,满足可扩展性的要求)
- sentinels redis集群列表配置(类似zookeeper,通过多个Sentinel来提高系统的可用性)
3)重启tomcat,session存储即可生效。
3.2 Spring Session + Redis
虽然tomcat + redis
的方案很好用,但是会严重依赖于web容器,不方便将代码移植到其他web容器上去,尤其是换了技术栈,比如换成了spring cloud或者是spring boot之类的。
Spring家族中的一个项目Spring Session
可以来实现session的共享。现在Web项目几乎都使用了Spring,既然Spring全家桶里提供这种方案,所以还是建议使用这种方式。
官方参考文档:
Spring Session 快速入门:https://spring.io/projects/spring-session#samples
3.2.1 pom文件添加依赖
1 | <dependencies> |
注意:
这里我使用的 Spring Boot 版本是 2.1.4 ,如果使用当前最新版 Spring Boot2.1.5 的话,除了上面这些依赖之外,需要额外添加 Spring Security 依赖(其他操作不受影响,仅仅只是多了一个依赖,当然也多了 Spring Security 的一些默认认证流程)。
3.2.2 spring 配置
1 | spring.redis.host=192.168.0.1 |
这里的 Redis 配置虽然配置了四行,但是考虑到端口默认就是 6379 ,database 默认就是 0,所以真正要配置的,其实就是两行。
配置完成后 ,就可以使用 Spring Session 了,其实就是使用普通的 HttpSession ,其他的 Session 同步到 Redis 等操作,框架已经自动帮你完成了:
1 |
|
考虑到一会 Spring Boot 将以集群的方式启动 ,为了获取每一个请求到底是哪一个 Spring Boot 提供的服务,需要在每次请求时返回当前服务的端口号,因此这里注入了 server.port 。
4. 分布式事务
分布式事务了解吗?你们如何解决分布式事务问题的?
4.1 两阶段提交(2PC/XA)
两阶段提交(Two-phase Commit,2PC,又叫 XA Transactions),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
2PC 是一个两阶段提交协议,该协议分为下面两个阶段:
- 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
- 第二阶段:事务协调器要求每个数据库提交数据。
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
优点:
- 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。
缺点:
- 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
- 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。
比如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
总的来说,XA 协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。
4.2 补偿事务(TCC)
TCC 事务机制相比于上面介绍的 XA,解决了如下几个缺点:
- 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
- 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
- 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性。
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
- Try 阶段主要是对业务系统做检测及资源预留
- Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
- Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
注意Confirm 和 Cancel要保证操作的幂等性。
举个简单的例子:比如跨银行转账的时候,要涉及到两个银行的分布式事务,如果用TCC方案来实现,思路如下:
1)Try阶段:先检测两个账户是不可以进行交易(如A转B之前,A的余额足不足,AB是否被冻住等),可以交易则把两个银行账户中的资金给它冻结住,不任何其他人操作。
2)Confirm阶段:执行实际的转账操作,A银行账户的资金扣减,B银行账户的资金增加
3)Cancel阶段:如果任何一个银行的操作执行失败,那么就需要回滚进行补偿,比如A银行账户如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去。
对于 TCC 来说适合一些:
- 强隔离性,严格一致性要求的活动业务。
- 执行时间较短的业务。
这种方案只适用金融金钱等十分核心的业务,因为这个事务回需要适用大量的代码来来控制事务逻辑,代码和业务的耦合度十分高。
4.3 本地消息表(异步确保)
本地消息表这个方案最初是 eBay 提出的,eBay 的完整方案。
此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
通俗地讲:
1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表。
2)接着A系统将这个消息发送到MQ中去。
3)B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息。
4)B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态。
5)如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理。
6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止。
但是这个方案最大的问题就在于严重依赖于数据库的消息表来管理事务,高并发场景不适用,扩展性也不好。
本地消息队列是BASE 理论
,是最终一致模型,适用于对一致性要求不高的情况。实现这个模型时需要注意重试的幂等。
4.4 MQ 事务
在 RocketMQ 中实现了分布式事务,实际上是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部。
基本流程如下:
1)A系统向消息中间件发送一条预备消息
2)消息中间件保存预备消息并返回成功
3)A执行本地事务
4)A发送提交消息给消息中间件
通过以上4步完成了一个消息事务。
对于以上的 4 个步骤,每个步骤都可能产生错误,下面一一分析:
步骤一出错,则整个事务失败,不会执行A的本地操作。
步骤二出错,则整个事务失败,不会执行A的本地操作。
步骤三出错,这时候需要回滚预备消息,怎么回滚?
答案是 A 系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查 A 事务执行是否执行成功,如果失败则回滚预备消息。
步骤四出错,这时候 A 的本地事务是成功的,那么消息中间件要回滚A吗?
答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务。
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个:
消息事务(A系统的本地操作+发消息)
B系统的本地操作
其中B系统的操作由消息中间件的消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息消费消息,去执行本地操作:
如果B本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
消息重投之前会再调用一次A操作的结果,一定是成功的,因为这个消息能被中间件存起来并打算给消费者消息,那么这条消息一定是成功的,只是消费者没有成功消费。
扩展博文
分布式服务框架 Zookeeper – 管理分布式环境中的数据
ZooKeeper学习总结 第一篇:ZooKeeper快速入门
拜托,面试请不要再问我Redis分布式锁的实现原理!【石杉的架构笔记】
七张图彻底讲清楚ZooKeeper分布式锁的实现原理【石杉的架构笔记】
分布式锁的两种实现方式(基于redis和基于zookeeper)
Springboot分别使用乐观锁和分布式锁(基于redisson)完成高并发防超卖
【Linux运维-集群技术进阶】集群/分布式环境下5种session处理策略
基于 spring-session 解决分布式 session 共享问题
分布式-Redis+SpringSession实现session共享
SpringSession系列-分布式Session实现方案
Spring Session + Redis实现分布式Session共享
搭建Tomcat集群&通过Redis缓存共享session的一种流行方案
Tomcat通过Redis实现session共享的完整部署记录
分布式集群Session共享 简单多tomcat8+redis的session共享实现
Spring Boot 一个依赖搞定 session 共享,没有比这更简单的方案了!
蚂蚁金服分布式事务开源以及实践 | SOFA 开源一周年献礼
谈谈web系统中的可重入,幂等性,分布式事务的那些事 – 上
分布式事物,解决分布式系统事务一致性的几种方案对比,你有更好的吗?
视频资料: