定位线上同步锁仍然重复扣费的Bug定位及Redis分布式锁解决方案

在实际生产环境中,处理订单的并发请求时,我们经常会遇到重复扣费的问题。本文将通过一个具体的代码示例,分析在使用同步锁时仍然导致重复扣费的原因,并提供一个基于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 分布式锁,不仅能在多台服务器上保证锁的唯一性,还能提高系统的并发处理能力,避免性能瓶颈。

希望本文对你在解决分布式系统中的并发问题有所帮助,如果有任何问题或建议,欢迎交流讨论。

相关推荐
神仙别闹4 分钟前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭28 分钟前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
Data跳动39 分钟前
Spark内存都消耗在哪里了?
大数据·分布式·spark
暮湫44 分钟前
泛型(2)
java
超爱吃士力架1 小时前
邀请逻辑
java·linux·后端
南宫生1 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石1 小时前
12/21java基础
java
李小白661 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp1 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
Code apprenticeship2 小时前
怎么利用Redis实现延时队列?
数据库·redis·缓存