读写锁分离设计模式详解

大家好,我是 V 哥。商城系统中,用户在浏览商品详情页时可以查看库存数量,这是读操作,频率较高。当用户下单成功时,系统会更新库存数量,这是写操作,但相对较少。这是一个再常见不过的应用场景了,在这种场景下,读写锁分离设计模式就是最好的武器。

V 哥推荐:2024 最适合入门的 JAVA 课程

读写锁分离设计模式是一种多线程设计模式,适合在有读多写少的场景中使用。它通过读写操作的分离,提升了系统的并发性和性能。在这个模式中,读操作是共享的,可以同时被多个线程执行,而写操作需要独占锁,避免并发写入带来的数据不一致问题。

读写锁分离设计模式的原理

咱们再来把读写锁分离的原理先明确一下:

  1. 读写锁(ReadWriteLock)

    • 读写锁提供了两种锁:读锁和写锁。
    • 读锁是共享的,可以允许多个线程同时持有,多个线程可以并发读取数据。
    • 写锁是独占的,只有一个线程能获取写锁。写操作会阻塞所有的读写操作,确保数据一致性。
  2. 适用场景

    • 读操作远多于写操作,数据写入不频繁的场景,如缓存、配置读取、数据分析等。
    • 通过读写分离,可以避免读操作阻塞,从而提高系统的吞吐量和并发性。

特别的爱给特别的你,满足才是硬道理

咱们就拿在电商平台的商品库存管理系统来说,库存数据需要满足如下业务需求:

  1. 高并发访问:商城有大量用户会同时访问商品详情页,查询库存数量。访问这些商品库存的用户数是巨大的,因此读取库存的操作需要具备高并发能力,保证每个用户能够快速查询到最新的库存信息。

  2. 实时更新库存:当用户成功下单,库存需要相应减少。此外,可能会有后台系统进行库存更新操作,比如在补货时增加库存。因此,写操作虽然较少,但必须做到线程安全,确保更新数据的准确性,避免因并发写入导致库存错误。

  3. 数据一致性要求:为了确保库存数据的一致性,写操作必须是独占的,也就是说,只有在没有任何读或写操作时,系统才能进行写操作,从而避免多个线程同时写入导致的库存数据错误。同时,也要保证在进行写操作时,读线程不能获取到库存的中间状态,确保用户获取的是准确的库存信息。

  4. 读多写少:在实际业务中,库存查询的频率远高于更新库存的频率,绝大部分用户操作仅涉及查询商品是否有库存,而少部分用户操作涉及到更新库存,比如完成下单、取消订单、或者后台管理员进行库存调整。

  5. 性能要求:库存查询的响应速度直接影响到用户体验,特别是在大型促销活动中,大量用户同时访问某些热门商品的库存数据,因此必须保证高并发读取的性能。而写操作因为较少,不会频繁发生,能容忍一定的等待时间。

具体操作场景

回到功能业务,通常要实现的具体功能场景是这样的:

  • 库存查询:用户在浏览商品详情页时,都会查看商品库存。一个页面可能会展示多个商品的库存,因此多个用户并发查询不同商品的库存时,系统需要支持多个读线程同时读取数据,而不会互相干扰。

  • 库存更新:当用户下单购买商品时,系统需要减少相应的库存数量。这个写操作必须是独占的,以避免并发写入导致库存数量的不一致。更新操作还会在订单取消、订单失效等情况下发生。此外,后台的库存补货也是一种写操作,更新后的库存应能被用户实时查询到。

  • 促销场景:在大促活动中(如双11、黑五促销),某些商品会有大量用户访问库存。如果不进行读写锁分离,频繁的写锁会阻塞所有的读线程,导致用户体验下降。因此,系统应支持多个用户同时进行读取操作,同时保障写入的独占性,以平衡高并发和数据一致性的需求。

实施目标

有了这样的场景,采用读写锁分离设计模式来优化库存管理,可以达到以下目标:

  1. 提升读操作的并发性:允许多个用户并发查询库存,保证在读多写少的场景下库存查询的高效性。
  2. 保证写操作的独占性:避免并发写入的冲突,确保库存数据的一致性,满足商品库存管理系统的数据准确性需求。
  3. 降低读写冲突的等待时间:在写操作较少的情况下,通过读写锁分离有效降低读操作的等待时间,提升系统整体性能。

案例:商品库存管理

下面咱们来具体看一下案例实现,我们要实现一个商品库存管理,需求是这样滴:

  • 多个线程可以同时读取某商品的库存数量。
  • 只有一个线程可以更新库存,避免多个写操作造成数据不一致。

一、实现步骤

  1. 定义商品库存管理类 InventoryManager,使用 ReentrantReadWriteLock 进行读写锁分离。
  2. 创建 checkStock 方法进行库存读取操作,获取读锁。
  3. 创建 updateStock 方法进行库存更新操作,获取写锁。

以下是实现代码:

java 复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class InventoryManager {
    // 商品库存存储
    private final Map<String, Integer> inventory = new HashMap<>();
    // 读写锁
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    // 获取库存数量(读操作)
    public int checkStock(String productId) {
        readLock.lock();
        try {
            return inventory.getOrDefault(productId, 0);
        } finally {
            readLock.unlock();
        }
    }

    // 更新库存数量(写操作)
    public void updateStock(String productId, int quantity) {
        writeLock.lock();
        try {
            int currentStock = inventory.getOrDefault(productId, 0);
            inventory.put(productId, currentStock + quantity);
            System.out.println("Updated stock for product " + productId + ": " + (currentStock + quantity));
        } finally {
            writeLock.unlock();
        }
    }
}

二、测试案例

在测试案例中,模拟多个用户并发访问库存,读取库存的线程可以并发执行,而更新库存的线程会独占锁。

java 复制代码
public class InventoryManagerTest {
    public static void main(String[] args) {
        InventoryManager inventoryManager = new InventoryManager();
        
        // 初始化库存
        inventoryManager.updateStock("product_1", 100);
        
        // 模拟多个线程同时读取库存
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println("Stock for product_1: " + inventoryManager.checkStock("product_1"));
            }).start();
        }

        // 模拟一个线程更新库存
        new Thread(() -> {
            inventoryManager.updateStock("product_1", -10);
            System.out.println("Stock after selling 10 units for product_1: " + inventoryManager.checkStock("product_1"));
        }).start();
    }
}

三、代码分析

  • 读操作 (checkStock):

    • 使用 readLock 加锁,只需获取读锁,不会影响其他读取线程。
    • 多个读取线程可以同时进入 checkStock 方法,提升读取并发性。
  • 写操作 (updateStock):

    • 使用 writeLock 加锁,写操作会阻塞其他读写操作。
    • 确保写入操作是独占的,防止并发写操作导致的数据不一致问题。

四、运行结果示例

输出可能如下(顺序可能有所不同):

复制代码
Stock for product_1: 100
Stock for product_1: 100
Stock for product_1: 100
Stock for product_1: 100
Stock for product_1: 100
Updated stock for product product_1: 90
Stock after selling 10 units for product_1: 90

五、优缺点

  • 优点:读写锁分离允许多个读线程并发执行,大大提高了读操作的效率,适合读多写少的场景。
  • 缺点 :如果写操作频繁,写锁会阻塞读操作,可能会降低系统性能,但在写安全重要程度来看,牺牲点性能是完全可以忍受的。

到这里,你是不是可以感受到读写锁分离设计模式解决了大问题了呢,并发场景下我们必须要考虑这个问题。你学沸了吗,关注威哥爱编程,一起搞不寂寞。

相关推荐
KATA~1 分钟前
解决MyBatis-Plus枚举映射错误:No enum constant问题
java·数据库·mybatis
xyliiiiiL17 分钟前
一文总结常见项目排查
java·服务器·数据库
shaoing19 分钟前
MySQL 错误 报错:Table ‘performance_schema.session_variables’ Doesn’t Exist
java·开发语言·数据库
腥臭腐朽的日子熠熠生辉1 小时前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
ejinxian1 小时前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之1 小时前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码2 小时前
Spring Task 定时任务
java·前端·spring
俏布斯2 小时前
算法日常记录
java·算法·leetcode
27669582922 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿
爱的叹息2 小时前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring