在實際生產環境中,處理訂單的并發請求時,我們經常會遇到重復扣費的問題。本文將通過一個具體的代碼示例,分析在使用同步鎖時仍然導致重復扣費的原因,并提供一個基于Redis分布式鎖的解決方案。
背景:這個案例出現在商家在小程序端接單重復扣費,PC端也能接單,并且PC端和小程序端不是一套代碼,但是接單的代碼幾乎一致
一、問題描述
在以下代碼中,OrderServiceImpl 類使用了 Java 的同步鎖來保證對訂單狀態變更的操作是線程安全的:
public class OrderServiceImpl {public Operating orderStateChange(OrderStateReq orderStateReq) {synchronized (OrderServiceImpl.class) {//訂單idInteger orderId = orderStateReq.getOrderId();//根據訂單id查看訂單是否滿足扣費 不滿足則拋異常 滿足則扣費}}
}
二、問題分析
盡管我們在 orderStateChange 方法中使用了同步鎖,但仍然可能導致重復扣費的問題,原因有以下幾點:
鎖粒度過大:synchronized (OrderServiceImpl.class) 鎖定的是整個類,這樣雖然可以避免多個線程同時進入臨界區,但在分布式環境下,這種鎖機制無法跨JVM工作。
鎖的范圍有限:Java 的 synchronized 鎖僅在單個 JVM 中有效,如果你的應用程序部署在多臺服務器上,每個服務器上的 JVM 都會有自己的鎖,這就無法避免分布式環境下的并發問題。
業務邏輯不完善:即使在單機環境中,鎖住整個類也會導致性能瓶頸,因為所有訂單處理請求都必須排隊進入同步塊,無法充分利用多線程的優勢。
三、解決方案
為了解決上述問題,我們可以使用 Redis 分布式鎖。Redis 分布式鎖的特點是可以跨多個 JVM 保證唯一性,從而避免分布式環境下的并發問題。
1. 引入 Redis 依賴
首先,在你的項目中引入 Redis 相關依賴(以 Spring Boot 為例):
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.2</version></dependency>
2. 實現 Redis 分布式鎖
然后,我們實現一個簡單的 Redis 分布式鎖機制。可以使用 Redisson 庫,這個庫封裝了 Redis 鎖的實現,使用起來非常方便。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;public class RedisLockUtil {private static RedissonClient redissonClient;// 有指定庫和密碼也需要賦值static {Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379");config.useSingleServer().setPassword("redisPassword");config.useSingleServer().setDatabase("database");redissonClient = Redisson.create(config);}public static RLock getLock(String lockKey) {return redissonClient.getLock(lockKey);}
}
3. 使用 Redis 分布式鎖
在 OrderServiceImpl 中使用 Redis 分布式鎖來實現訂單狀態變更操作:
import org.redisson.api.RLock;public class OrderServiceImpl {public Operating orderStateChange(OrderStateReq orderStateReq) {String lockKey = "orderLock:" + orderStateReq.getOrderId();RLock lock = RedisLockUtil.getLock(lockKey);try {// 嘗試加鎖,等待時間為10秒,鎖超時時間為30秒if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {try {//訂單idInteger orderId = orderStateReq.getOrderId();//根據訂單id查看訂單是否滿足扣費 不滿足則拋異常 滿足則扣費} finally {lock.unlock();}} else {// 獲取鎖失敗,處理邏輯throw new RuntimeException("獲取鎖失敗,請稍后再試");}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("線程中斷", e);}}
}
在使用了分布式鎖后上線一周內讓DB再查看已經沒有了重復扣費現象
四、總結
通過以上步驟,我們可以解決同步鎖在分布式環境下無法避免重復扣費的問題。使用 Redis 分布式鎖,不僅能在多臺服務器上保證鎖的唯一性,還能提高系統的并發處理能力,避免性能瓶頸。
希望本文對你在解決分布式系統中的并發問題有所幫助,如果有任何問題或建議,歡迎交流討論。