作者:后端小肥肠
🍇 我写过的文章中的相关代码放到了gitee,地址:xfc-fdw-cloud: 公共解决方案
🍊 有疑问可私信或评论区联系我。
🥑 创作不易未经允许严禁转载。
目录
4.2. SpringBoot使用Redission分布式锁
1. 前言
在当今快速发展的分布式系统中,多个节点之间的协调和一致性成为了一个日益重要的挑战。随着云计算、微服务架构和大数据处理的普及,系统的复杂性显著增加,这使得并发操作的管理愈发困难。在这样的背景下,分布式锁作为一种重要的机制,能够有效地防止数据竞争和不一致性问题,确保系统的稳定与可靠。本文将深入探讨分布式锁的原理、实现方式以及在实际应用中的重要性。
2. 为何要使用分布式锁?
在系统开发中,尤其是高并发场景下,多个线程同时操作共享资源是常见的需求。例如,多个线程同时售票、更新库存、扣减余额等。这些操作如果没有妥善管理,很容易导致资源竞争、数据不一致等问题。在单机环境中,我们可以通过锁机制(如 synchronized
或 ReentrantLock
)解决这些问题,但在分布式环境中,这些机制无法直接使用,需要更复杂的分布式锁方案。
2.1. 单机场景里的锁
在单机环境中,可以使用线程安全的操作来避免多线程竞争。在以下代码中,我们通过三种方式逐步引入锁机制来保障线程安全。
以普通的售票代码为例,原始代码:
public class SaleTicket {
public static void main(String[] args) throws Exception {
Ticket ticket = new Ticket();
for (int j = 0; j < 5; j++) { // 创建5个线程模拟并发
new Thread(() -> { // 每个线程执行售票操作
for (int i = 1; i <= 10000; i++) {
ticket.sale();
}
}).start();
}
Thread.sleep(5000); // 等待线程执行完成
ticket.print(); // 打印剩余票数
}
}
// 无锁资源类
class Ticket {
// 总票数
private Integer number = new Integer(50000);
// 售票方法,无线程安全保障
public void sale() {
if (number > 0) {
number--;
}
}
public void print() {
System.out.println("剩余票:" + number);
}
}
运行以上代码,可能会出现以下问题:
- 票数不一致:多个线程可能同时读取和修改
number
,导致最终票数小于 0 或大于实际值。 - 数据竞争:线程之间没有同步机制,数据容易被破坏。
解决这一问题的关键在于引入锁机制,下面我们介绍三种常见的单机锁实现方式。
1. 使用 AtomicInteger
AtomicInteger
是 Java 提供的线程安全类,使用 CAS(Compare-And-Swap)原子操作实现多线程数据一致性。它适合简单场景,例如递增、递减等操作。
代码示例如下:
import java.util.concurrent.atomic.AtomicInteger;
public class SaleTicket {
public static void main(String[] args) throws Exception {
Ticket ticket = new Ticket();
for (int j = 0; j < 5; j++) {
new Thread(() -> {
for (int i = 1; i <= 10000; i++) {
ticket.sale();
}
}).start();
}
Thread.sleep(5000); // 等待线程完成
ticket.print(); // 打印剩余票数
}
}
class Ticket {
private AtomicInteger number = new AtomicInteger(50000); // 线程安全的票数
public void sale() {
if (number.get() > 0) {
number.decrementAndGet(); // 原子操作
}
}
public void print() {
System.out.println("剩余票:" + number.get());
}
}
优点:
- 原子操作,无需显式加锁。
- 性能较高,适合简单的并发场景。
缺点:
- 不适合复杂业务逻辑,例如多个共享资源需要同时操作的场景。
2. 使用 synchronized
Synchronized
是 Java 提供的关键字,可以用来保证方法或代码块的线程安全。它通过内部锁(Monitor)机制,确保同一时间只有一个线程能够执行加锁的代码。
代码示例如下:
public class SaleTicket {
public static void main(String[] args) throws Exception {
Ticket ticket = new Ticket();
for (int j = 0; j < 5; j++) {
new Thread(() -> {
for (int i = 1; i <= 10000; i++) {
ticket.sale();
}
}).start();
}
Thread.sleep(5000); // 等待线程完成
ticket.print(); // 打印剩余票数
}
}
class Ticket {
private Integer number = new Integer(50000); // 总票数
public synchronized void sale() {
if (number > 0) {
number--; // 在锁保护下操作
}
}
public void print() {
System.out.println("剩余票:" + number);
}
}
优点:
- 简单易用,内置关键字,便于开发者理解和使用。
- 适合多线程复杂操作。
缺点:
- 性能较低,因为线程竞争会导致阻塞。
- 粒度较大,可能降低系统并发性。
3. 使用 ReentrantLock
ReentrantLock
是 Java 并发包中的显式锁,与 synchronized
相比,它提供了更丰富的功能,例如支持公平锁、非公平锁、条件变量等。
代码示例如下:
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicket {
public static void main(String[] args) throws Exception {
Ticket ticket = new Ticket();
for (int j = 0; j < 5; j++) {
new Thread(() -> {
for (int i = 1; i <= 10000; i++) {
ticket.sale();
}
}).start();
}
Thread.sleep(5000); // 等待线程完成
ticket.print(); // 打印剩余票数
}
}
class Ticket {
private Integer number = new Integer(50000); // 总票数
private final ReentrantLock lock = new ReentrantLock(); // 显式锁
public void sale() {
lock.lock(); // 加锁
try {
if (number > 0) {
number--; // 线程安全操作
}
} finally {
lock.unlock(); // 确保释放锁
}
}
public void print() {
System.out.println("剩余票:" + number);
}
}
优点:
- 灵活,支持公平锁、非公平锁等特性。
- 更适合复杂的并发场景。
缺点:
- 必须显式加锁和释放锁,代码复杂度较高。
- 需要正确处理异常,防止死锁。
在单机场景中,使用锁机制可以有效解决线程安全问题。对于简单的操作(如计数器),可以优先使用 AtomicInteger
;如果需要保护复杂的业务逻辑,可以选择 synchronized
或 ReentrantLock
。
2.2. 分布式场景里的锁
在单机环境中,使用线程锁(如 synchronized
、JUC)可以有效地管理并发操作,保证数据的一致性。但在分布式系统中,多个节点可能并行执行相同的操作,访问的是共享资源(如数据库、缓存、队列等)。这就带来了一个新的问题:如何在不同的节点之间协调资源的访问?
光说可能你不是很理解,我来举个例子:
假设你有一个售票系统,多个用户同时请求购买同一张票。如果没有分布式锁,可能会发生如下情况:
- 用户 A 和用户 B 同时查询到有票可买。
- 用户 A 和用户 B 分别进行扣款操作,且系统仍认为票数未减少,这会导致“超卖”情况。