分布式
2021-09-06 21:46:01 0 举报
AI智能生成
分布式
作者其他创作
大纲/内容
客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器
可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的**原子性**。
Why Lua?
这里KEYS[1]代表的是你加锁的那个key,比如说:RLock lock = redisson.getLock(\"myLock\");这里你自己设置了加锁的那个锁key就是“myLock”。
KEYS[1]
代表的就是锁key的默认生存时间,默认30秒
ARGV[1]
代表的是加锁的客户端的ID,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1
ARGV[2]
参数含义
判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁
exists myLock
加锁
hset myLock hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
设置myLock这个锁key的生存时间是30秒
pexpire myLock 30000
第一段if
判断这个key已经存在
hexists myLock
hincrby myLock UUID
第二段 if
逻辑含义
LUA脚本
NX:仅在不存在 key 的时候才能被执行成功;PX:失效时间,传入 30000,就是 30s 后自动释放锁;UUID:就是随机值,可以是线程号之类的。主要是为了更安全的释放锁,释放锁的时候使用脚本告诉 Redis: 只有 key 存在并且存储的值和我指定的值一样才能删除成功。TTL: 过期时间
set key UUID NX PX TTL
一个原子操作的命令
两种方式
加锁机制
锁互斥机制
假如一个客户端在持有锁的时候崩溃了,没有释放锁,那么别的客户端无法获得锁,则会造成死锁,所以要保证客户端一定会释放锁。
防死锁
只要客户端1一旦加锁成功,就会启动一个watch dog看门狗
watch dog
watch dog自动延期机制
可重入加锁机制
加锁和解锁必须是同一个客户端,客户端A的线程加的锁必须是客户端A的线程来解锁,客户端不能解开别的客户端的锁。
释放锁机制(持锁人解锁)
缺点
多个客户端在一个redis实例上加锁
对集群的每个节点进行加锁,如果大多数(N/2+1)加锁成功了,则认为获取锁成功。在Redis的分布式环境中,我们假设有N个完全互相独立的Redis节点,在N个Redis实例上使用与在Redis单实例下相同方法获取锁和释放锁。现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:1. 获取当前Unix时间,以毫秒为单位2. 依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁 - 当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免客户端死等3. 客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。 - 当且仅当从半数以上的Redis节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间,这个很重要5. 如果因为某些原因,获取锁失败(没有半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁, - 无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了,毕竟多释放一次也不会有问题
所以这个时候就存在两个客户端A B 都认为自己获得了锁
获取锁的客户端在持有锁时可能会暂停一段较长的时间,尽管锁有一个超时时间,避免了崩溃的客户端可能永远持有锁并且永远不会释放它,但是如果客户端的暂停持续的时间长于锁的到期时间,并且客户没有意识到它已经到期,那么它可能会继续进行一些不安全的更改,换言之由于客户端阻塞导致的持有的锁到期而不自知。
无法解决客户端阻塞时间超过锁的持续时间
存在的问题思考
使用redlock
redis 实现
他是个数据库,文件存储系统,并且有监听通知机制(观察者模式)
What is zookeeper?
持久化节点(zk断开节点还在)持久化顺序编号目录节点临时目录节点(客户端断开后节点就删除了)临时目录编号目录节点
节点
create /test laogong // 创建永久节点
create -e /test laogong // 创建临时节点
create -s /test // 创建顺序节点
create -e -s /test // 创建临时顺序节点
节点怎么创建?
zk存储了什么?
zookeeper是集群
zk实现分布式锁的原理
创建临时节点
如何避免死锁
监听,是所有服务都去监听一个节点的,节点的释放也会通知所有的服务器(服务器数量可能很大)
一个释放的消息,就好像一个牧羊犬进入了羊群,大家都四散而开,随时可能干掉机器,会占用服务资源,网络带宽等等。
羊群效应
全部监听一个节点问题很大,那我们就监听我们的前一个节点,因为是顺序的,很容易找到自己的前后。
每个节点只监听了自己的前一个节点,释放当然也是一个个释放下去,就不会出现羊群效应了。
创建临时顺序结点
解决方法
所有客户端监听结点的弊端
因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。
Zk性能上可能并没有缓存服务那么高。
ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。
zk实现分布式锁的机制
服务注册与订阅(共用节点)分布式通知(监听znode)服务命名(znode特性)数据订阅、发布(watcher)分布式锁(临时节点)
zk的应用场景
zookeeper 实现
多客户端访问一个缓存的key(key过期了)
应用场景
分布式锁
- 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。- 简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,- 分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
需要请求多个服务
每个业务都维护着自己的数据库,数据的交换只能进行RPC调用。
创建订单和扣减库存,需要同时对订单DB和库存DB进行操作
两步操作必须同时成功,否则就会造成业务混乱
需要对多个服务进行操作
此时我们只能保证自己服务的数据一致性,无法保证调用其他服务的操作是否成功
所以为了保证整个下单流程的数据一致性,就需要分布式事务介入。
分布式事务控制数据一致性
以订单请求为例
什么是分布式事务
- service产生多个节点,- resource产生多个节点。
分布式事务产生的原因
- 对某个指定的客户端来说,读操作能返回最新的写操作。- 对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,- 如果有某个节点没有读取到,那就是分布式不一致。
C (一致性) Consistency:
- 非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。- 可用性的两个关键一个是合理的时间,一个是合理的响应。- 合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。- 合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。
A (可用性) Availability:
- 当出现网络分区后,系统能够继续工作。- 打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。
P (分区容错性) Partition tolerance:
CAP
分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
Basically Available(基本可用)
允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。
Soft state(软状态)
最终一致是指经过一段时间后,所有节点数据都将会达到一致。
span style=\
BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态
BASE
TCC与 2PC、3PC一样,都只是实现分布式事务的一种方案而已。
`两段提交`顾名思义就是要进行两个阶段的提交:第一阶段,准备阶段(投票阶段) ; 第二阶段,提交阶段(执行阶段)。
2PC 原理
通知参与者执行事务操作
Prepare
Commit
2PC 过程
- 一个下单流程就会用到多个服务,各个服务都无法保证调用的其他服务的成功与否,- 这个时候就需要一个全局的角色(`协调者`)对各个服务(`参与者`)进行协调。
- 如果`协调者`收到了`参与者`的失败消息或者超时,直接给每个`参与者`发送`回滚(Rollback)`消息- 否则,发送`提交(Commit)`消息;
一个下单请求过来通过`协调者`,给每一个`参与者`发送`Prepare`消息,执行本地数据脚本但不提交事务。
显然`2PC`做到了所有操作要么全部成功、要么全部失败。
`参与者`根据`协调者`的指令执行提交或者回滚操作,释放所有事务处理过程中被占用的资源,
下单扣库存案例
- 第二阶段中`协调者`向`参与者`发送`commit`命令之后,一旦此时发生网络抖动,- 导致一部分`参与者`接收到了`commit`请求并执行,可其他未接到`commit`请求的`参与者`无法执行事务提交。- 进而导致整个分布式系统出现了数据不一致。
网络抖动导致的数据不一致
`2PC`中的所有的参与者节点都为`事务阻塞型`,当某一个`参与者`节点出现通信超时,其余`参与者`都会被动阻塞占用资源不能释放。
超时导致的同步阻塞问题
- 由于严重的依赖`协调者`,一旦`协调者`发生故障,而此时`参与者`还都处于锁定资源的状态,无法完成事务`commit`操作。- 虽然协调者出现故障后,会重新选举一个协调者,可无法解决因前一个`协调者`宕机导致的`参与者`处于阻塞状态的问题。
单点故障的风险
两段提交(2PC)的缺点
2PC 两阶段提交
三段提交(3PC)是对两段提交(2PC)的一种升级优化,`3PC`在`2PC`的第一阶段和第二阶段中插入一个准备阶段canCommit。
在协调者和参与者中都引入超时机制,当`参与者`各种原因未收到`协调者`的commit请求后,会对本地事务进行commit,不会一直阻塞等待
但`3PC` 还是没能从根本上解决数据一致性的问题。
超时
保证了在最后提交阶段之前,各参与者节点的状态都一致。
PreCommit
解决了2PC的问题
协调者向所有参与者发送CanCommit命令,询问是否可以执行事务提交操作。
类似于2PC中的第二个阶段中的Ready阶段,是一种事务询问操作(被调用方检查自己是否有能力进行事务操作)
CanCommit
参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。
`协调者`向所有`参与者`发送`PreCommit`命令,询问是否可以进行事务的预提交操作
- 参与者接收到PreCommit请求后,如参与者成功的执行了事务操作,则返回`Yes`响应,进入最终commit阶段。- 一旦参与者中有向协调者发送了`No`响应,或因网络造成超时,协调者没有接到参与者的响应,协调者向所有参与者发送`abort`请求,参与者接受abort命令执行事务的中断。
在前两个阶段中所有参与者的响应反馈均是`YES`后,协调者向参与者发送`DoCommit`命令正式提交事务,
如协调者没有接收到参与者对于DoCommit的ACK响应,会向所有参与者发送`abort`请求命令,执行事务的中断。
DoCommit
3PC 过程
相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制
避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
3PC对于2PC的优化
3PC 三阶段提交
`TCC`(Try-Confirm-Cancel)又被称`补偿事务`,`TCC`与`2PC`的思想很相似,事务处理流程也很相似,但`2PC` 是应用于在DB层面,TCC则可以理解为在应用层面的`2PC`,是需要我们编写业务逻辑来实现。
`TCC`它的核心思想是:\"针对每个操作都要注册一个与其对应的确认(Try)和补偿(Cancel)\"。
通过Try接口执行对应的事务操作
Try
确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性。要求具备幂等设计,Confirm失败后需要进行重试。
strong style=\
取消执行,释放Try阶段预留的业务资源 Cancel操作满足幂等性Cancel阶段的异常和Confirm阶段异常处理方案基本上一致。
只要涉及到的相关业务中,有一个业务方未成功,则取消所有请求。
Cancel
TCC 过程
TCC由于基于在业务层面,至使每个操作都需要有 `try`、`confirm`、`cancel`三个接口。
应用侵入性强
代码开发量很大,要保证数据一致性 `confirm` 和 `cancel` 接口还必须实现幂等性
开发难度大
TCC的缺点
TCC 补偿事务
2PC 3PC TCC
数据库中对于一条数据的全局唯一标识
随着数据日渐增长,主从同步也扛不住了,就需要对数据库进行分库分表,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;特别一点的如订单、优惠券也都需要有`唯一ID`做标识。此时一个能够生成`全局唯一ID`的系统是非常必要的。那么这个`全局唯一ID`就叫`分布式ID`。
什么是分布式ID?
必须保证ID是全局性唯一的,基本要求
全局唯一
高可用低延时,ID生成响应要块,否则反倒会成为业务瓶颈
高性能
100%的可用性是骗人的,但是也要无限接近于100%的可用性
高可用
要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单
好接入
最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求
趋势递增
分布式ID需要满足哪些条件
不推荐
生成一个16 字节128位,36位长度的字符串
生成足够简单,本地生成无网络消耗,具有唯一性
优点
无序的字符串,不具备趋势自增特性
没有具体的业务含义
UUID
基于数据库的`auto_increment`自增ID完全可以充当`分布式ID`,具体实现:需要一个单独的MySQL实例用来生成ID,
当我们需要一个ID的时候,向表中插入一条记录返回`主键ID`
实现简单,ID单调自增,数值类型查询速度快
数据库自增ID
双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。
需要人工修改一、二两台`MySQL实例`的起始值和步长,把`第三台机器的ID`起始生成位置设定在比现有`最大自增ID`的位置远一些,但必须在一、二两台`MySQL实例`ID还没有增长到`第三台MySQL实例`的`起始ID`值的时候,否则`自增ID`就要出现重复了,必要时可能还需要停机修改
扩容
解决DB单点问题
不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。
数据库多主模式
号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围
由于多业务端可能同时操作,所以采用版本号`version`乐观锁方式更新,这种`分布式ID`生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。
号段模式
利用`redis`的 `incr`命令实现ID的原子性自增。
`RDB`会定时打一个快照进行持久化,假如连续自增但`redis`没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
`AOF`会对每条写命令进行持久化,即使`Redis`挂掉也不会出现ID重复,但由于incr命令的特殊性,会导致`Redis`重启恢复的数据时间过长。
要考虑到redis持久化的问题。`redis`有两种持久化方式`RDB`和`AOF`
Redis
`Snowflake`生成的是Long类型的ID
- Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
第一个bit位(1bit)
毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
时间戳部分(41bit)
也被叫做`workId`,这个可以灵活配置,机房或者机器号组合都可以。
工作机器id(10bit)
自增值支持同一毫秒内同一个节点可以生成4096个ID
序列号部分(12bit)
ID的组成
根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。
雪花算法(SnowFlake)
`uid-generator`支持自`定义时间戳`、`工作机器ID`和 `序列号` 等各部分的位数,
`uid-generator`中采用用户自定义`workId`的生成策略。
`uid-generator`需要与数据库配合使用,需要新增一个`WORKER_NODE`表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增ID就是该机器的`workId`数据由host,port组成。
`uid-generator`是基于`Snowflake`算法实现的
`workId`,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,
和原始的`snowflake`不太一样,时间的单位是秒,而不是毫秒,
`workId`也不一样,而且同一应用每次重启就会消费一个`workId`。
对于`uid-generator` ID组成结构:
百度 (Uidgenerator)
`Leaf`同时支持号段模式和`snowflake`算法模式,可以切换使用。
`Leaf`的snowflake模式依赖于`ZooKeeper`
span md-inline=\"plain\" class=\"md-plain md-expand\" style=\
每个应用在使用`Leaf-snowflake`时,启动时都会都在`Zookeeper`中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个`workId
snowflake模式
美团(Leaf)
滴滴出品(TinyID)
分布式ID都有哪些生成方式?
分布式ID
分布式事务
etcd
sentinel 选举局部领头对监控主服务器进行故障转移
redis
zookeeper
共识算法(raft)
一致性哈希
负载均衡算法
计数器法漏桶法令牌桶法
分布式限流
降级
熔断策略
分布式
0 条评论
回复 删除
下一页