大家好,我是 V 哥。商城系统中,用户在浏览商品详情页时可以查看库存数量,这是读操作,频率较高。当用户下单成功时,系统会更新库存数量,这是写操作,但相对较少。这是一个再常见不过的应用场景了,在这种场景下,读写锁分离设计模式就是最好的武器。
V 哥推荐:2024 最适合入门的 JAVA 课程
读写锁分离设计模式
是一种多线程设计模式,适合在有读多写少的场景中使用。它通过读写操作的分离,提升了系统的并发性和性能。在这个模式中,读操作是共享的,可以同时被多个线程执行,而写操作需要独占锁,避免并发写入带来的数据不一致问题。
读写锁分离设计模式的原理
咱们再来把读写锁分离的原理先明确一下:
-
读写锁(ReadWriteLock):
- 读写锁提供了两种锁:读锁和写锁。
- 读锁是共享的,可以允许多个线程同时持有,多个线程可以并发读取数据。
- 写锁是独占的,只有一个线程能获取写锁。写操作会阻塞所有的读写操作,确保数据一致性。
-
适用场景:
- 读操作远多于写操作,数据写入不频繁的场景,如缓存、配置读取、数据分析等。
- 通过读写分离,可以避免读操作阻塞,从而提高系统的吞吐量和并发性。
特别的爱给特别的你,满足才是硬道理
咱们就拿在电商平台的商品库存管理系统来说,库存数据需要满足如下业务需求:
-
高并发访问:商城有大量用户会同时访问商品详情页,查询库存数量。访问这些商品库存的用户数是巨大的,因此读取库存的操作需要具备高并发能力,保证每个用户能够快速查询到最新的库存信息。
-
实时更新库存:当用户成功下单,库存需要相应减少。此外,可能会有后台系统进行库存更新操作,比如在补货时增加库存。因此,写操作虽然较少,但必须做到线程安全,确保更新数据的准确性,避免因并发写入导致库存错误。
-
数据一致性要求:为了确保库存数据的一致性,写操作必须是独占的,也就是说,只有在没有任何读或写操作时,系统才能进行写操作,从而避免多个线程同时写入导致的库存数据错误。同时,也要保证在进行写操作时,读线程不能获取到库存的中间状态,确保用户获取的是准确的库存信息。
-
读多写少:在实际业务中,库存查询的频率远高于更新库存的频率,绝大部分用户操作仅涉及查询商品是否有库存,而少部分用户操作涉及到更新库存,比如完成下单、取消订单、或者后台管理员进行库存调整。
-
性能要求:库存查询的响应速度直接影响到用户体验,特别是在大型促销活动中,大量用户同时访问某些热门商品的库存数据,因此必须保证高并发读取的性能。而写操作因为较少,不会频繁发生,能容忍一定的等待时间。
具体操作场景
回到功能业务,通常要实现的具体功能场景是这样的:
-
库存查询:用户在浏览商品详情页时,都会查看商品库存。一个页面可能会展示多个商品的库存,因此多个用户并发查询不同商品的库存时,系统需要支持多个读线程同时读取数据,而不会互相干扰。
-
库存更新:当用户下单购买商品时,系统需要减少相应的库存数量。这个写操作必须是独占的,以避免并发写入导致库存数量的不一致。更新操作还会在订单取消、订单失效等情况下发生。此外,后台的库存补货也是一种写操作,更新后的库存应能被用户实时查询到。
-
促销场景:在大促活动中(如双11、黑五促销),某些商品会有大量用户访问库存。如果不进行读写锁分离,频繁的写锁会阻塞所有的读线程,导致用户体验下降。因此,系统应支持多个用户同时进行读取操作,同时保障写入的独占性,以平衡高并发和数据一致性的需求。
实施目标
有了这样的场景,采用读写锁分离设计模式来优化库存管理,可以达到以下目标:
- 提升读操作的并发性:允许多个用户并发查询库存,保证在读多写少的场景下库存查询的高效性。
- 保证写操作的独占性:避免并发写入的冲突,确保库存数据的一致性,满足商品库存管理系统的数据准确性需求。
- 降低读写冲突的等待时间:在写操作较少的情况下,通过读写锁分离有效降低读操作的等待时间,提升系统整体性能。
案例:商品库存管理
下面咱们来具体看一下案例实现,我们要实现一个商品库存管理,需求是这样滴:
- 多个线程可以同时读取某商品的库存数量。
- 只有一个线程可以更新库存,避免多个写操作造成数据不一致。
一、实现步骤
- 定义商品库存管理类
InventoryManager
,使用ReentrantReadWriteLock
进行读写锁分离。 - 创建
checkStock
方法进行库存读取操作,获取读锁。 - 创建
updateStock
方法进行库存更新操作,获取写锁。
以下是实现代码:
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();}}
}
二、测试案例
在测试案例中,模拟多个用户并发访问库存,读取库存的线程可以并发执行,而更新库存的线程会独占锁。
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
五、优缺点
- 优点:读写锁分离允许多个读线程并发执行,大大提高了读操作的效率,适合读多写少的场景。
- 缺点:如果写操作频繁,写锁会阻塞读操作,可能会降低系统性能,但在
写安全
重要程度来看,牺牲点性能是完全可以忍受的。
到这里,你是不是可以感受到读写锁分离设计模式解决了大问题了呢,并发场景下我们必须要考虑这个问题。你学沸了吗,关注威哥爱编程,一起搞不寂寞。