目录
1、分布式事务
分布式事务就是在一个交易中各个服务之间的相互调用必须要同时成功或者同时失败,保持一致性和可靠性。在单体项目架构中,在多数据源的情况下也会发生 分布式事务问题。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
在传统的单机环境中,事务处理通常符合ACID属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。在这种情况下,一个事务要么完全执行成功,将所有的更改提交到数据库中,要么完全失败,将所有的更改回滚,数据库状态不受影响。
分布式锁:分布式锁的主要目的是在分布式系统中确保同一时刻只有一个进程(或服务实例)能够访问某一共享资源。通常用于防止多个节点或进程并发地操作同一数据,避免出现竞态条件。(大额存单额度的控制,防止超卖问题)。可以使用redis解决分布式锁的问题。
2、CAP理论
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
Availability(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
Partition tolerance(分区容错性)
Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务。
在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此P不可避免。当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足。
3、BASE理论
Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。 Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
解决分布式事务的思想和模型:
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
全局事务:整个分布式事务
分支事务:分布式事务中包含的每个子系统的事务
最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据 强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚
4、seata架构
Seata事务管理中有三个重要的角色:
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状。
Seata提供了四种不同的分布式事务解决方案:
XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入。
TCC模式:最终一致的分阶段事务模式,有业务侵入 AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式。
SAGA模式:长事务模式,有业务侵入。
4.1.XA模式
什么是XA?
XA则规范了TM与RM之间的通信接⼝,在TM与多个RM之间形成⼀个双向通信桥梁 是数据库级别的规范。
查看数据库是否支持XA规范:show engines
XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。
RM一阶段的工作:
a. 注册分支事务到TC b.执行分支业务sql但不提交 c.报告执行状态到TC
TC 二阶段的工作:
TC检测各分支事务执行状态:
a.如果都成功,通知所有RM提交事务
b.如果有失败,通知所有RM回滚事务 RM
二阶段的工作: 接收TC指令,提交或回滚事务。
XA模式的优点是什么?
事务的强一致性,满足ACID原则。
常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差。
依赖关系型数据库实现事务。
4.2.AT模式
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
阶段一RM的工作:
a. 注册分支事务 b.记录undo-log(数据快照) c.执行业务sql并提交 d.报告事务状态
阶段二提交时RM的工作:
删除undo-log即可。
阶段二回滚时RM的工作:
根据undo-log恢复数据到更新前
AT模式与XA模式最大的区别是什么?
1、XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
2、XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
3、XA模式强一致;AT模式最终一致。
AT模式的优点:
一阶段完成直接提交事务,释放数据库资源,性能比较好。
利用全局锁实现读写隔离。
没有代码侵入,框架自动完成回滚和提交。
AT模式的缺点:
两阶段之间属于软状态,属于最终一致。
框架的快照功能会影响性能,但比XA模式要好很多。
4.3.TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
Try:资源的检测和预留。
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
Cancel:预留资源释放,可以理解为try的反向操作。
TCC模式的每个阶段是做什么的?
Try:资源检查和预留
Confirm:业务执行和提交
Cancel:预留资源的释放
TCC的优点是什么?
一阶段完成直接提交事务,释放数据库资源,性能好。
相比AT模型,无需生成快照,无需使用全局锁,性能最强。
不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库。
TCC的缺点是什么?
有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦。
软状态,事务是最终一致。
需要考虑Confirm和Cancel的失败情况,做好幂等处理。
4.4.SAGA模式
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga也分为两个阶段:
一阶段:直接提交本地事务。
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚。
优点:
事务参与者可以基于事件驱动实现异步调用,吞吐高。
一阶段直接提交事务,无锁,性能好。
不用编写TCC中的三个阶段,实现简单。
缺点:
软状态持续时间不确定,时效性差。
没有锁,没有事务隔离,会有脏写。
4.5.四种模式对比
5、分布式锁的实现方案
分布式锁侧重于单一资源或操作的并发控制,确保同一时间只有一个进程/线程可以访问某个共享资源。
分布式事务侧重于多系统或服务之间的事务一致性,确保多个操作要么全部成功,要么全部回滚。
两者的关系:分布式锁有时会作为分布式事务的一个工具,帮助协调资源操作,但它们的核心目标和解决的问题是不同的。
5.1、redis实现的分布式锁
在银行业务中,大额存单的额度更新问题需要处理好并发控制,避免因并发操作导致的“超卖”问题。具体来说,假设系统的总额度是 1000w,当多个客户同时购买时,我们需要确保在多个微服务/分布式节点间同步更新额度,并确保没有超卖。
Redis 是一种常用的高效分布式缓存系统,可以用来处理这类并发控制问题,尤其是在分布式环境下。为了避免超卖问题,Redis 提供了几个机制,可以用于解决这个问题:
1. 使用 Redis 原子操作:
-
通过 Redis 的原子操作(如
INCRBY
)来保证对额度的修改是线程安全的,并且确保在任何时刻额度不会超卖。
2. 使用 Redis 锁来控制并发:
-
为了避免并发时多个请求同时修改额度,我们可以使用 Redis 的分布式锁机制。在购买过程中,只有一个请求能够获得锁并进行额度更新,其他请求会被阻塞或者返回错误提示,直到锁被释放。
3. 额度校验与更新步骤:
-
客户在购买时,首先需要通过 Redis 查询当前的可用额度。
-
然后,系统通过 Redis 分布式锁 获取锁,防止多个请求同时操作额度。
-
锁住额度后,系统会检查剩余额度是否足够购买当前请求的额度。如果足够,就更新额度,若不够,则拒绝该请求。
- 释放锁后,其他请求可以继续操作。
4.使用redis的setnx(key,value),如果返回1则说明,redis中没有该key值,获取锁成功。返回0则说明redis中有该key值,获取锁失败。
当多个用户来购买大额存单时,可以使用大额的存单的期次号作为key,这笔交易的全局流水号作为value值。释放锁时要比较不能使用线程id作为value,因为线程 ID 不具备全局唯一性。线程 ID 是 JVM 内部的唯一标识符,仅在单个进程或 JVM 实例中有效。不同的 JVM 进程会有不同的线程 ID,而且线程 ID 可能会重复(如果线程池复用线程的话),这会导致跨进程或跨机器的锁无法正确区分。例如,在分布式系统中,线程 ID 不能保证在不同的机器或不同的 JVM 实例之间是唯一的。因此,使用线程 ID 作为锁的 value
并不能有效区分不同节点上的线程。
锁的 value
值选择原则:
-
唯一性:每个请求(或者说每个操作)都应该有一个唯一的标识符来作为锁的
value
,这样才能确保锁的持有者是唯一的。当锁被释放时,需要检查这个value
是否匹配,以避免误删锁。 -
防止死锁:
value
的设计应该避免冲突或者错误释放的风险。例如,如果多个线程使用相同的value
,在释放锁时容易误删其他线程的锁,导致系统出现死锁或资源抢占错误。
String lockKey = "cd_lock_" + periodCode; // 期次号作为锁的 key,例如 "cd_lock_2025-01"
String lockValue = channlSeqNo; // 唯一的锁标识
// 尝试获取分布式锁
boolean isLockAcquired = jedis.setnx(lockKey, lockValue) == 1;
if (isLockAcquired) {
// 设置锁的过期时间,防止死锁
jedis.expire(lockKey, 30);
try {
// 获取当前剩余额度
Long availableAmount = jedis.get("cd_amount_" + periodCode);
// 如果剩余额度足够
if (availableAmount >= requestedAmount) {
// 扣除额度
jedis.decrBy("cd_amount_" + periodCode, requestedAmount);
// 提交业务处理(如订单生成)
} else {
// 剩余额度不足,拒绝购买
return "额度不足,无法购买";
}
} finally {
// 释放锁
String currentLockValue = jedis.get(lockKey);
// 释放锁时检查锁的持有者
if (currentLockValue != null && currentLockValue.equals(lockValue)) {
jedis.del(lockKey); // 删除锁
}
}
} else {
//重试机制
return "系统繁忙,请稍后再试";
}
注意:
1 必须要给redis设置的key设置过期时间,防止因为业务执行异常,导致当前线程一直持有锁而没有释放锁造成死锁。
2 如果业务执行的时间大于锁的生效时间。当前业务还没有执行完,锁就失效了。此时并发时会导致其它线程也能够获取到该锁,造成数据不一致。此时就需要使用redis的看门狗机制进行解决:
Redis 的看门狗机制,通常是指锁的续期机制(又称自动续期机制),它的核心作用是防止在持有分布式锁的过程中,由于业务处理时间超过锁的有效期,导致锁在业务未完成时提前失效,从而出现并发问题。
1. 问题背景
在分布式系统中,通常使用 Redis 来实现分布式锁(比如通过 SETNX
命令)。为了防止死锁,分布式锁通常会设置一个过期时间(TTL)。然而,如果锁的持有者在锁过期之前没有及时释放锁,而业务处理时间又比较长,锁会在业务未完成时被意外释放,从而导致其他客户端获取锁并执行相同的业务,最终引发并发问题,造成数据不一致。
2. 看门狗机制的作用
Redis 看门狗机制的目的是自动延长锁的有效期,保证在业务处理的过程中,锁不会因超时而自动失效。看门狗机制通常在锁被获取后,周期性地检查锁的剩余有效时间,并根据需要延长锁的过期时间,直到业务执行完毕,锁才会被释放。
Redis 的看门狗机制本质上就是在持有锁的过程中,通过周期性地延长锁的过期时间,确保锁在业务执行完成之前不会失效。它是防止分布式锁失效而导致的数据不一致和并发问题的一个非常有效的手段。在实际开发中,可以通过合理设计锁的过期时间和续期策略,确保业务逻辑的正确执行和数据的一致性。
3. 如何实现 Redis 看门狗机制
思路:
- 获取锁后,设定一个过期时间(TTL)。
- 业务逻辑开始执行时,定期检查锁的剩余有效时间,并在必要时延长锁的过期时间,防止锁在业务未完成时过期。
- 业务完成后,释放锁。这个地方需要注意,如果发生异常,导致业务一直无法执行完成,不能无限续约,否则也会造成死锁,需要设置最大续约次数。
5.2、使用DB实现
①悲观锁
多个线程并发执行select for update,只有一个线程都成功获取到锁。当这个线程执行commit操作后,才能释放锁。问题:连接池爆满(加锁的时候,select会占用数据库连接,获取不到行锁的时候连接也不释放,就会导致连接池爆满)。事务超时(事务处理过长,到了超时时间就会自动回滚,导致锁释放,这个时间要把控好)。行锁升级为表锁(InnoDb的行锁是加在索引上的,如果不走索引就会变成表锁,所以必须使用索引字段)。
注意事项
- 锁粒度:
SELECT FOR UPDATE
锁定的是查询到的行,因此需要确保查询条件精确,以免锁定不必要的数据。 - 死锁问题:如果多个进程或线程按不正确的顺序请求锁,可能会发生死锁。为避免死锁,确保锁的获取顺序统一或设置超时重试机制。
- 性能问题:
SELECT FOR UPDATE
在高并发情况下可能会导致性能瓶颈,特别是当涉及的行数较多时。因此,应尽量缩小锁定的范围,避免锁住不必要的数据行。 - 锁的超时:在一些数据库系统中,可以设置锁的超时时间,避免锁被长时间持有。比如 MySQL 5.5 及以后版本可以使用
innodb_lock_wait_timeout
参数来设置锁的超时时间。
优缺点
优点:
- 原生支持:不需要额外的分布式缓存工具(如 Redis、Zookeeper),直接依赖数据库的锁机制,减少了额外的系统复杂性。
- 事务一致性:借助数据库的事务机制,保证了操作的原子性、一致性、隔离性。
缺点:
- 性能问题:在高并发情况下,数据库锁可能成为瓶颈,影响系统的吞吐量。
- 数据库依赖性:锁机制依赖于数据库的事务和锁功能,不同的数据库系统可能会有不同的表现和限制。
- 死锁风险:如果设计不当,可能会导致死锁问题,进而影响系统的可靠性。
总结
使用 SELECT FOR UPDATE
实现分布式锁是利用数据库的事务机制,保证同一时刻只有一个进程能够访问某些资源。这种方法简单直接,适用于大多数基于数据库的分布式系统。然而,需要注意在高并发和高负载情况下,SELECT FOR UPDATE
可能带来性能瓶颈,需要小心设计锁的粒度、事务的超时等问题。
②乐观锁
乐观锁使用一个version字段来表示当前行的版本,如果有线程修改了这一行,则version+1。当前线程首先获取到这一行的version值,然后做业务处理,判断这一行的version是否发生变化,如果没有则update这一行的version为version+1,表示操作成功,这期间没有其他事务来操作过这一行。否则,操作失败,需要回滚事务。问题:乐观锁不太适合写入操作频繁的场景,会导致大量的事务回滚。