SpringBoot中基于Redis实现分布式全局锁解决并发竞争

木白
木白
发布时间:2026-10-16| 7人浏览过
原创

本文演示如何在SpringBoot项目中,通过使用Jedis客户端操作Redis,实现分布式的同步锁。重点解决多进程并行处理共享资源(如库存)时产生的并发竞争和数据错误问题,涵盖从依赖引入、工具类编写到业务逻辑整合的完整流程。

模拟无锁状态下的资源竞争

在微服务或分布式系统中,同一个服务启动多个实例时,它们通常是无状态的,而状态数据存储在数据库或缓存中。当多个线程同时访问并修改同一份内存中的共享变量时,若缺乏同步机制,就会导致数据错乱。

以下代码模拟了一个简单的库存扣减场景:初始库存为900,启动1000个线程尝试扣减。由于没有加锁,`goodsCount--` 操作并非原子性,会出现“超卖”或资源浪费的情况。

public class CommonConsumerService {
    //库存个数
    static int goodsCount = 900;
    //卖出个数
    static int saleCount = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {Thread.sleep(2);} catch (InterruptedException e) {}
                if (goodsCount > 0) {
                    goodsCount--;
                    System.out.println("剩余库存:" + goodsCount + " 卖出个数" + ++saleCount);
                }
            }).start();
        }
        Thread.sleep(3000);
    }
}

运行上述代码,最终结果往往显示剩余库存为0,但卖出数量不足900(例如899)。这意味着有部分资源被“吞掉”,即多个线程同时判断 `goodsCount > 0` 为真,然后都执行了减一操作,导致部分计数未实际消耗却减少了库存值。

使用Redis实现分布式锁

为了在多进程环境下保证资源共享的安全性,可以借助Redis的单线程特性和原子性命令来实现分布式锁。核心思路是:获取锁时使用 `SET key value NX PX timeout`,确保只有第一个请求能将键存入并设置过期时间;释放锁时使用Lua脚本,判断Value是否匹配再删除,防止误删他人的锁。

第1步:引入Redis依赖

在SpringBoot项目中,需要引入 `spring-boot-starter-data-redis` 以支持Redis连接。如果使用原生Jedis进行底层操作,还需确保项目中有对应的Jedis客户端依赖。

compile "org.springframework.boot:spring-boot-starter-data-redis"
// 注意:若直接使用Jedis API,需额外引入jedis客户端依赖
// compile "redis.clients:jedis:x.x.x"

第2步:封装Redis加锁工具类

创建一个工具类,封装获取锁和释放锁的逻辑。获取锁时,使用 `SETNX` (SET IF NOT EXIST) 确保键不存在时才设置,并配合 `PX` (毫秒过期时间) 防止死锁。释放锁时,必须校验当前持有锁的请求ID(requestId)是否与Key关联的值一致,以避免解锁错误。

package com.kingboy.common.utils;
import redis.clients.jedis.Jedis;
import java.util.Collections;
/**
 * Redis分布式锁工具类
 */
public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;
    
    /**
     * 尝试获取分布式锁
     * @param jedis      Redis客户端
     * @param lockKey    锁的唯一标识
     * @param requestId  请求标识(用于后续校验是否为自己持有的锁)
     * @param expireTime 超时时间(毫秒)
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // SET key value NX PX time: 仅当key不存在时设置,并指定过期时间
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
    
    /**
     * 释放分布式锁
     * @param jedis     Redis客户端
     * @param lockKey   锁的唯一标识
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        // 使用Lua脚本保证删除操作的原子性:先get比对,再del
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

第3步:改造业务逻辑应用锁

将之前的无锁代码改为使用Redis锁。在每次进入临界区前,循环尝试获取锁,直到获取成功。处理完业务后,务必释放锁并关闭Jedis连接。

public class RedisLockConsumerService {
    //库存个数
    static int goodsCount = 900;
    //卖出个数
    static int saleCount = 0;
    
    @SneakyThrows
    public static void main(String[] args) {
        // 创建JedisPool连接池,请根据实际环境修改IP和密码
        JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "192.168.0.130", 6379, 1000);
        
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {Thread.sleep(2);} catch (InterruptedException e) {}
                
                Jedis jedis = null;
                boolean lock = false;
                try {
                    jedis = jedisPool.getResource();
                    // 循环尝试获取锁
                    while (!lock) {
                        // requestId使用线程名,expireTime设置为10秒
                        lock = RedisTool.tryGetDistributedLock(jedis, "goodsCount", 
                                            Thread.currentThread().getName(), 10000);
                    }
                    
                    // 获得锁后执行业务逻辑
                    if (goodsCount > 0) {
                        goodsCount--;
                        System.out.println("剩余库存:" + goodsCount + " 卖出个数" + ++saleCount);
                    }
                } finally {
                    // 无论成功与否,都要尝试释放锁
                    if (jedis != null) {
                        RedisTool.releaseDistributedLock(jedis, "goodsCount", Thread.currentThread().getName());
                        jedis.close();
                    }
                }
            }).start();
        }
        Thread.sleep(3000);
        jedisPool.close();
    }
}

执行改造后的程序,输出结果显示剩余库存正确归零,且卖出数量恰好为900。这证明了Redis分布式锁有效地解决了多线程/多进程间的资源竞争问题,保证了数据的原子性和一致性。

简要总结

通过上述示例,我们看到了在没有锁的情况下多线程操作共享变量的风险,以及如何利用Redis的原子性命令(SET NX PX)和Lua脚本构建一个简单的分布式锁机制。在实际生产环境中,建议直接使用经过充分测试的开源库(如Redission),以处理锁续期、看门狗等更复杂的场景。

以上就是SpringBoot中使用Redis作为全局锁示例过程的详细内容,更多关于SpringBoot Redis全局锁的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。