在实际生产环境中,处理订单的并发请求时,我们经常会遇到重复扣费的问题。本文将通过一个具体的代码示例,分析在使用同步锁时仍然导致重复扣费的原因,并提供一个基于Redis分布式锁的解决方案。
背景:这个案例出现在商家在小程序端接单重复扣费,PC端也能接单,并且PC端和小程序端不是一套代码,但是接单的代码几乎一致
一、问题描述
在以下代码中,OrderServiceImpl 类使用了 Java 的同步锁来保证对订单状态变更的操作是线程安全的:
java
public class OrderServiceImpl {
public Operating orderStateChange(OrderStateReq orderStateReq) {
synchronized (OrderServiceImpl.class) {
//订单id
Integer orderId = orderStateReq.getOrderId();
//根据订单id查看订单是否满足扣费 不满足则抛异常 满足则扣费
}
}
}
二、问题分析
尽管我们在 orderStateChange 方法中使用了同步锁,但仍然可能导致重复扣费的问题,原因有以下几点:
锁粒度过大:synchronized (OrderServiceImpl.class) 锁定的是整个类,这样虽然可以避免多个线程同时进入临界区,但在分布式环境下,这种锁机制无法跨JVM工作。
锁的范围有限:Java 的 synchronized 锁仅在单个 JVM 中有效,如果你的应用程序部署在多台服务器上,每个服务器上的 JVM 都会有自己的锁,这就无法避免分布式环境下的并发问题。
业务逻辑不完善:即使在单机环境中,锁住整个类也会导致性能瓶颈,因为所有订单处理请求都必须排队进入同步块,无法充分利用多线程的优势。
三、解决方案
为了解决上述问题,我们可以使用 Redis 分布式锁。Redis 分布式锁的特点是可以跨多个 JVM 保证唯一性,从而避免分布式环境下的并发问题。
1. 引入 Redis 依赖
首先,在你的项目中引入 Redis 相关依赖(以 Spring Boot 为例):
java
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.2</version>
</dependency>
2. 实现 Redis 分布式锁
然后,我们实现一个简单的 Redis 分布式锁机制。可以使用 Redisson 库,这个库封装了 Redis 锁的实现,使用起来非常方便。
java
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 分布式锁来实现订单状态变更操作:
java
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 {
//订单id
Integer orderId = orderStateReq.getOrderId();
//根据订单id查看订单是否满足扣费 不满足则抛异常 满足则扣费
} finally {
lock.unlock();
}
} else {
// 获取锁失败,处理逻辑
throw new RuntimeException("获取锁失败,请稍后再试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("线程中断", e);
}
}
}
在使用了分布式锁后上线一周内让DB再查看已经没有了重复扣费现象
四、总结
通过以上步骤,我们可以解决同步锁在分布式环境下无法避免重复扣费的问题。使用 Redis 分布式锁,不仅能在多台服务器上保证锁的唯一性,还能提高系统的并发处理能力,避免性能瓶颈。
希望本文对你在解决分布式系统中的并发问题有所帮助,如果有任何问题或建议,欢迎交流讨论。