读写锁分离设计模式详解

大家好,我是 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

五、优缺点

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

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

相关推荐
星河梦瑾41 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富44 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲1 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇1 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
Yvemil71 小时前
《开启微服务之旅:Spring Boot 从入门到实践》(三)
java
Anna。。1 小时前
Java入门2-idea 第五章:IO流(java.io包中)
java·开发语言·intellij-idea
思忖小下1 小时前
梳理你的思路(从OOP到架构设计)_简介设计模式
设计模式·架构·eit
.生产的驴2 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
爱上语文2 小时前
宠物管理系统:Dao层
java·开发语言·宠物