分布式锁
以商场系统超卖现象举例
超卖现象一
现象:
商品卖出数量超出了库存数量。
产生原因:
扣减库存的动作在程序中进行,在程序中计算剩余库存,在并发场景下,导致库存计算错误。
代码复现
es.shutdown();
cyclicBarrier 作用是确保多个线程同时扣减库存。
countDownLatch 确保执行完之前,线程池不关闭
解决方法
注意,库存做个大于0的判断
超卖现象二
现象
系统中库存变为负数
产生原因
1 并发检验库存,造成库存充足的假象
2 update更新库存,导致库存为负数
1 检索商品库存,如果商品库存为负数,抛出异常,则更新操作回滚
2 加锁
单体加锁
synchronized 关键字
当使用 synchronized 关键字时,可以分别应用于实例方法、静态方法和代码块,以确保线程安全。以下是分别对这三种情况的加锁示例:
1. 实例方法加锁示例:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
在上面的示例中,increment 方法使用 synchronized 关键字修饰,因此在每次调用该方法时,会对当前对象进行加锁,确保同一时刻只有一个线程可以执行 increment 方法。
2. 静态方法加锁示例:
public class SynchronizedExample {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}
在这个示例中,increment 方法被声明为静态方法并使用了 synchronized 关键字修饰。这会导致对类的 Class 对象进行加锁,确保同一时刻只有一个线程可以执行这个静态方法。
3. 代码块加锁示例:
public class SynchronizedExample {
private static final Object lock = new Object();
private int count = 0;
public void increment() {
synchronized (lock) {
count++;
}
}
}
在这个示例中,使用了一个私有的静态对象 lock 作为锁,并在 increment 方法中使用 synchronized 块来对一段代码进行加锁,确保同一时刻只有一个线程可以执行这段代码块。
以上这些示例展示了 synchronized 关键字在不同场景下的应用,可以确保线程安全,并避免出现并发访问问题。
ReentrantLock
当需要更灵活地控制加锁和解锁的时候,ReentrantLock 是一种好的选择。下面是一个使用 ReentrantLock 加锁的示例:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
在上面的示例中,首先创建了一个 ReentrantLock 对象,然后在 increment 方法中使用 lock() 来获取锁,在操作完成后使用 unlock() 来释放锁。这样可以确保在加锁的过程中发生异常时,锁仍然能够被正确释放,避免出现死锁的情况。
ReentrantLock 还提供了更多的灵活性,如可以实现公平锁和非公平锁、可以中断等待锁的线程等功能,更适用于复杂的并发控制场景。
单体加锁的局限性
上述示例中单体加锁的两种方式(synchronized 关键字和 ReentrantLock)虽然可以在某些情况下提供线程安全性,但也存在一些局限性,这些局限性包括:
1. 互斥性:单体加锁是一种悲观锁机制,当一个线程持有锁执行临界区代码时,其他线程必须等待释放锁才能执行。这可能导致性能问题,特别是在并发度较高的情况下。
2. 潜在的死锁风险:如果在加锁的过程中发生异常或逻辑错误,可能导致锁没有被正确释放,从而引发死锁问题。
3. 缺乏灵活性:synchronized 关键字和 ReentrantLock 的加锁范围仅限于方法或代码块内部,这可能限制了并发控制的粒度和灵活性。有些复杂的并发控制场景可能需要更细粒度的加锁或释放锁的控制,此时单体加锁机制可能无法满足要求。
5. 无法跨节点同步:单体加锁是基于进程内的锁机制,无法在分布式环境中实现跨节点的同步。在多个节点之间无法保证互斥性,并且无法实现全局的锁机制。
6. 性能瓶颈:在分布式环境中,如果使用单体加锁来保护共享资源,会导致性能瓶颈。由于加锁的范围扩大到整个服务实例或整个代码块,会造成并发度的降低,影响整体的吞吐量和性能。
7. 高开销和复杂性:由于需要维护全局一致的状态,单体加锁需要进行大量的网络通信和同步操作,增加了额外的开销和复杂性。
为了解决这些问题,分布式环境中常用的并发控制机制是分布式锁。分布式锁是一种基于分布式协议和算法实现的锁机制,可以在分布式环境中保证互斥性和一致性。常见的分布式锁实现包括基于数据库的锁、基于缓存的锁(如 Redis 锁),以及基于 ZooKeeper 等分布式协调服务的锁。
使用分布式锁可以解决分布式环境中跨节点同步和锁竞争的问题,提高性能和可伸缩性。但是需要注意,分布式锁也可能引入其他问题,如死锁、性能瓶颈等。因此,在设计和使用分布式锁时需要谨慎考虑,并综合考虑系统的一致性、性能和可伸缩性需求。
常见分布式锁
当在分布式系统中实现分布式锁时,可以使用不同的技术作为后端存储和同步机制,常见的包括 Redis、数据库和 ZooKeeper。下面我们将分别讨论它们作为分布式锁的原理、优缺点等。
当在分布式系统中实现分布式锁时,可以使用不同的技术作为后端存储和同步机制,常见的包括 Redis、数据库和 ZooKeeper。下面我们将分别讨论它们作为分布式锁的原理、优缺点等。
Redis 分布式锁
原理:
- 使用 Redis 作为后端存储,基于 Redis 的 SETNX(set if not exists)命令来实现分布式锁。当某个客户端成功获取锁时,会在 Redis 中创建一个对应的锁键,并设置合适的超时时间,其他客户端获取不到锁。
优点:
- **高性能:** Redis 是内存数据库,读写性能出色,适合作为分布式锁的后端存储。
- **支持阻塞和非阻塞式获取锁:** Redis 提供了支持阻塞以及非阻塞式的锁获取方式(通过 BLPOP 或者 SETNX 结合 EXPIRE 命令)。
- **支持可重入:** 可以通过为每个锁键加上唯一标识,实现可重入性。
缺点:
- **单点故障:** Redis 单节点故障时可能导致锁不可用,需要通过 Redis Sentinel 或 Redis Cluster 等方式解决高可用性问题。
- **并发数受限:** Redis 作为单机或者主从部署模式下,可能受限于单机的并发连接数。
数据库分布式锁
原理:
- 使用关系型数据库的乐观锁或者悲观锁来实现分布式锁,可以利用数据库的事务和唯一索引来保证原子性和唯一性。
优点:
- **成熟稳定:** 数据库作为企业级数据存储系统,具有成熟的高可用、一致性和持久性解决方案。
- **适用性广:** 已有的数据库系统可以直接用于存储锁信息,无需引入新的中间件。
缺点:
- **性能:** 与 Redis 相比,数据库的读写性能通常较差,可能影响锁的获取和释放性能。
- **数据库连接资源消耗:** 大量并发获取锁可能导致数据库连接池耗尽。
- **死锁风险:** 数据库锁对于跨事务并发控制必须慎重处理,避免出现死锁。
当使用 ZooKeeper 作为分布式系统中的分布式锁时,通常会利用 ZooKeeper 的顺序节点(Sequential ZNode)和 Watches 机制来实现分布式锁。下面是详细的介绍以及和 Redis 的对比:
ZooKeeper
工作原理
1. 创建锁节点: 客户端在尝试获取锁时,在 ZooKeeper 上创建一个临时的、有序的顺序节点,代表自己的锁请求。
2. 获取锁: 客户端检查自己创建的节点是否是当前序列中最小的节点,如果是,则表示获得了锁;如果不是,则注册前一个节点的 Watcher 监听,然后进入等待状态。
3. 释放锁:*当客户端不再需要锁时,直接删除自己创建的节点,其他等待的客户端会通过监听到节点删除事件来尝试获取锁。
优点
- 高可用性:*ZooKeeper 提供了高可用的分布式协调服务,适合作为分布式锁的后端存储,具有良好的稳定性和一致性。
- 顺序性:*ZooKeeper 的顺序节点可以使客户端获取锁的顺序有序,并且通过 Watches 机制实现高效的事件通知,避免了轮询导致的资源浪费。
缺点
- 复杂性:*使用 ZooKeeper 需要额外的运维成本和学习成本,部署和维护相对复杂。
- 性能: ZooKeeper 的性能可能不如 Redis 那样出色,尤其在高并发场景下可能受到影响。
ZooKeeper 和 Redis 分布式锁的对比
- 一致性和可靠性:ZooKeeper 提供了较高的一致性和可靠性,适合作为分布式锁的后端存储;而 Redis 虽然性能出色,但在一致性和可靠性方面略逊一筹。
- 部署复杂性:ZooKeeper 的部署和维护相对复杂,需要专门的运维人员进行管理;而 Redis 相对来说更加简单和容易上手。
- 性能: Redis 作为内存数据库,读写性能通常更优,适用于对性能要求较高的场景;ZooKeeper 在高并发场景下可能性能较为有限,需要权衡选择。
最终选择 ZooKeeper 还是 Redis 作为分布式锁的后端存储,需要根据具体的业务需求、系统的性能要求以及运维成本等方面进行综合考虑。
curator分布式锁和redisson分布式锁
Curator是Netflix开源的一组Apache ZooKeeper客户端库的高级API。Curator 提供了一套易用
基于分布式锁解决定时任务的重复问题
当使用 Redis 实现分布式锁来解决定时任务的重复问题时,可以通过以下步骤来实现:
1. 首先,实现一个定时任务,例如每隔一定时间执行某个任务。
2. 在任务执行之前,客户端尝试获取 Redis 中特定的锁,比如使用 SETNX 命令来设置一个特定的键。如果返回成功,即获取到了锁,就可以执行任务;否则,任务不应该被执行。
3. 在任务执行完毕后,释放锁,通过 DEL 命令来删除锁对应的键。
这是一个基本的使用 Redis 实现分布式锁解决定时任务重复问题的例子。然而,在实现过程中可能会遇到一些问题,如下所示:
- 锁竞争问题:*在高并发的情况下,多个客户端可能同时尝试获取同一个锁,从而导致竞争。只有一个客户端能够成功获取到锁,其他客户端则不能执行任务。
- 锁超时问题:如果设置的锁超时时间过长,那么在某个客户端执行任务期间,该客户端意外崩溃或失去连接,没有及时释放锁,就会导致其他客户端无法获取锁,进而无法执行任务。
为了解决这些问题,可以采取以下方案:
- 设置锁的有效期限制: 在获取锁的时候,使用 SETNX 命令设置一个过期时间,避免某个客户端崩溃或失去连接而不能释放锁。可以使用 PEXPIRE 命令来设置锁的过期时间。
- 添加唯一标识符:当某个客户端成功获取到锁时,在锁的键名中添加一个唯一标识符,使得只有拥有相应标识符的客户端才能释放该锁。这样可以避免其他客户端误释放锁。(当两个客户端在几乎相同的时间内向 Redis 请求锁时,根据 Redis 的单线程执行特性,Redis 会依次处理这两个客户端的请求。由于执行速度非常快,Redis 无法辨别两个请求的先后顺序,因此有可能会出现两个客户端同时获得锁的情况。)
- 使用 Redlock 或 RediLock 算法:Redlock 或 RediLock 是一种分布式锁算法,在 Redis 集群环境中使用多个 Redis 实例来实现分布式锁,提供更高的可靠性和鲁棒性。
红锁(Redlock)是一种分布式锁算法,被设计用于在Redis集群环境中实现分布式锁。它旨在提供更高的可靠性和鲁棒性,尤其针对网络分区和故障恢复的情况。
红锁算法的原理如下:
1. 客户端根据一致性哈希算法将锁映射到多个Redis实例上,形成一个由n个实例组成的锁集群。
2. 客户端尝试在每个Redis实例上获取锁。通过使用`SETNX`命令来设置一个特定键和值,如果返回成功则客户端获取到了锁。
3. 客户端记录获取锁成功的实例数量,并计算时间戳。
4. 客户端检查是否超过了超时时间(通常是锁的有效期的一半),以及是否获得了大部分实例锁的数量。如果是,则客户端认为成功获取到了锁。
5. 如果客户端无法获取到过半数的实例锁,或者超时了,则客户端尝试释放已经获取的锁。
红锁算法的优点是考虑了分布式环境可能出现的网络分区和故障恢复问题。通过使用多个Redis实例进行锁的获取和释放,能够提供更高的可靠性和鲁棒性,同时保证只有一个客户端能够获得锁。
针对定时任务的重复问题,可以使用红锁算法结合定时任务的处理方式来解决。具体实现步骤如下:
1. 在每个定时任务执行前,客户端使用红锁算法获取一个分布式锁。
2. 获取锁成功后,执行定时任务。
3. 定时任务执行完毕后,客户端释放分布式锁。
这样可以确保在分布式环境中,同一时间只有一个客户端能够获取到锁,从而避免了定时任务的重复执行问题。同时,红锁算法的可靠性和鲁棒性能够应对网络分区和故障恢复的情况,提供更强大的分布式锁功能。
需要注意的是,使用 Redis 实现分布式锁时,仍然需要保证 Redis 的高可用性和故障恢复能力,可以通过 Redis Sentinel 或 Redis Cluster 来实现。同时,还需要谨慎处理 Redis 连接池的资源管理,以避免资源耗尽的问题。
常见面试题
如何解决分布式事务
在解决分布式事务问题时,可以采用以下几种常见的解决思路:
1. 强一致性分布式事务(Two-Phase Commit,2PC):2PC 是一种经典的解决方案,基于协调者和参与者之间的协议来保证所有参与者要么全部提交,要么全部回滚。协调者负责协调参与者的状态,并决定是否进行提交。然而,2PC 存在单点故障问题,并且在分布式系统规模较大时性能和可扩展性会受到限制。
2. 最终一致性分布式事务(Saga Pattern):Saga Pattern 将一个大事务拆分为多个小事务,并通过补偿操作来保证最终一致性。每个小事务可以独立执行,并且在失败时可以执行相应的补偿操作。Saga Pattern 对于长时间运行的分布式事务和异步消息的处理非常有用。但需要注意,Saga Pattern 的设计和实现可能会增加系统的复杂性。
3. 基于消息的分布式事务(Transactional Messaging):在分布式系统中,可以将事务性操作与消息传递相结合。通过消息队列来确保事务的一致性。当一个服务完成其本地事务后,会向消息队列发送消息,其他服务在接收到消息后执行相应的操作。这种方法可以实现比较灵活的事务处理,但需要考虑消息队列的可靠性和幂等性。
4. 分布式事务中间件:一些开源和商业的分布式事务中间件,如Seata、TCC-Transaction、Hmily等,提供了分布式事务的解决方案,可以简化分布式事务的开发和管理。这些中间件通常提供了一致性协议、分布式事务管理和补偿机制等功能。
需要根据具体的业务场景、系统规模和性能要求来选择合适的分布式事务解决方案。同时,需注意分布式事务的设计和实现可能会增加系统的复杂性,并需要充分考虑事务一致性、性能、可靠性和开发复杂度等因素之间的权衡。