SpringBoot 集成 Redis 分布式锁实战:从手动实现到注解式优雅落地

在分布式系统中,并发操作共享资源(如库存扣减、订单创建)时,单机锁(synchronized/ReentrantLock)无法跨服务实例生效,而 Redis 分布式锁是解决该问题的主流方案。本文基于前文 SpringBoot+Redis 缓存实践,不仅实现生产级 Redis 分布式锁核心逻辑 ,还封装了注解式分布式锁(@DLock),让业务代码与锁逻辑解耦,兼顾性能与开发优雅性。

一、核心设计思路

本文实现的分布式锁包含两大核心部分:

  1. 底层核心实现 :基于 Redis SET NX PX原子命令 + Lua 脚本,保证锁的互斥性、安全性;
  2. 上层优雅封装 :通过自定义注解@DLock+AOP 切面,实现 "注解即加锁",无需手动编写加锁 / 释放锁代码;
  3. 复用缓存配置:直接复用缓存实践中定义的序列化规则,保证全局 Redis 操作的一致性。

二、环境准备

复用前文SpringBoot 集成 Redis 缓存实践

  • 依赖spring-boot-starter-aop(AOP 切面依赖);

三、核心代码实现

1. 分布式锁通用接口

定义标准化接口,隔离锁的底层实现,便于后续扩展(如 Zookeeper 锁):

java 复制代码
package com.demo.redis.lock;

/**
 * 分布式锁通用接口
 */
public interface DistributedLock {

    /**
     * 阻塞式获取锁(获取不到则一直等待)
     * @param lockKey    锁的唯一标识
     * @param expireTime 锁过期时间(毫秒)
     * @return 是否获取成功
     */
    boolean lock(String lockKey, long expireTime);

    /**
     * 释放锁
     * @param lockKey 锁的唯一标识
     * @return 是否释放成功
     */
    boolean unlock(String lockKey);

    /**
     * 带超时的非阻塞获取锁
     * @param lockKey    锁的唯一标识
     * @param expireTime 锁过期时间(毫秒)
     * @param waitTime   最大等待时间(毫秒,-1表示无限等待)
     * @return 是否获取成功
     */
    boolean tryLock(String lockKey, long expireTime, long waitTime);
}

2. Redis 分布式锁核心实现(生产级)

解决线程安全、多锁兼容、异常处理等核心问题,是分布式锁的底层核心:

java 复制代码
package com.demo.redis.lock.impl;

import cn.hutool.core.collection.CollectionUtil;
import com.demo.redis.config.RedisConfigure;
import com.demo.redis.lock.DistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Redis分布式锁实现类
 * 核心特性:
 * 1. SET NX PX 原子加锁 + Lua脚本原子释放锁
 * 2. ThreadLocal<Map>存储锁标识,支持同一线程持有多把锁
 * 3. 复用缓存配置的序列化规则,保证全局一致性
 */
@Slf4j
@Component
public class RedisDistributedLock implements DistributedLock {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    // 线程本地存储:key=锁标识,value=唯一UUID(避免线程间锁标识冲突)
    private ThreadLocal<Map<String, String>> lockFlag = new ThreadLocal<>();

    // 释放锁Lua脚本:保证"判断锁归属+删除锁"原子性,防止误删其他客户端锁
    private static final String UNLOCK_LUA_SCRIPT =
            "if redis.call("get",KEYS[1]) == ARGV[1] " +
                    "then " +
                    "    return redis.call("del",KEYS[1]) " +
                    "else " +
                    "    return 0 " +
                    "end ";

    /**
     * 阻塞式获取锁:复用tryLock逻辑,无限等待
     */
    @Override
    public boolean lock(String lockKey, long expireTime) {
        return tryLock(lockKey, expireTime, -1);
    }

    /**
     * 释放锁:核心保证仅持有者可释放
     */
    @Override
    public boolean unlock(String lockKey) {
        // 1. 前置校验:当前线程未持有该锁,直接返回失败
        Map<String, String> map = lockFlag.get();
        if (CollectionUtil.isEmpty(map) || !map.containsKey(lockKey)) {
            log.warn("释放锁失败:当前线程未持有锁,lockKey={}", lockKey);
            return false;
        }

        try {
            // 2. 执行Lua脚本释放锁(原子操作)
            Boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                byte[] scriptByte = RedisSerializer.string().serialize(UNLOCK_LUA_SCRIPT);
                return connection.eval(
                        scriptByte,
                        ReturnType.BOOLEAN,
                        1,
                        RedisConfigure.KEY_SERIALIZER.serialize(lockKey),  // 复用缓存的key序列化器
                        RedisConfigure.VALUE_SERIALIZER.serialize(map.get(lockKey))  // 复用缓存的value序列化器
                );
            });

            if (!Boolean.TRUE.equals(success)) {
                log.warn("释放锁失败:锁已过期或被其他客户端持有,lockKey={}", lockKey);
            }
            return Boolean.TRUE.equals(success);
        } catch (Exception e) {
            log.error("释放锁异常,lockKey={}", lockKey, e);
        } finally {
            // 3. 清理线程本地存储,避免内存泄漏
            map.remove(lockKey);
            if (map.isEmpty()) {
                lockFlag.remove(); // 无锁时清空ThreadLocal
            }
        }
        return false;
    }

    /**
     * 带超时的非阻塞获取锁:核心原子加锁逻辑
     */
    @Override
    public boolean tryLock(String lockKey, long expireTime, long waitTime) {
        // 1. 参数校验:过期时间必须>0,防止死锁
        if (expireTime <= 0) {
            throw new IllegalArgumentException("锁过期时间必须大于0");
        }

        long startTime = System.currentTimeMillis();
        String uuid = UUID.randomUUID().toString(); // 每个锁的唯一标识

        // 2. 循环重试获取锁
        while (true) {
            try {
                // 3. 执行Redis原子加锁命令(SET NX PX)
                Boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                    // 初始化线程本地存储
                    Map<String, String> map = lockFlag.get();
                    if (map == null) {
                        map = new HashMap<>();
                    }
                    map.put(lockKey, uuid);
                    lockFlag.set(map);

                    // 核心:Redis底层原子操作(NX=仅当key不存在时设置,PX=设置过期时间)
                    return connection.set(
                            RedisConfigure.KEY_SERIALIZER.serialize(lockKey),
                            RedisConfigure.VALUE_SERIALIZER.serialize(uuid),
                            Expiration.from(expireTime, TimeUnit.MILLISECONDS),
                            RedisStringCommands.SetOption.ifAbsent()
                    );
                });

                // 4. 加锁成功则返回
                if (Boolean.TRUE.equals(success)) {
                    log.info("获取锁成功,lockKey={}, uuid={}", lockKey, uuid);
                    return true;
                }

                // 5. 校验是否超时,超时则放弃
                if (waitTime != -1 && (System.currentTimeMillis() - startTime) > waitTime) {
                    log.info("获取锁超时,lockKey={}, waitTime={}ms", lockKey, waitTime);
                    return false;
                }
            } catch (Exception e) {
                log.error("获取锁异常,lockKey={}", lockKey, e);
            }

            // 6. 未超时则短暂休眠后重试(避免CPU空转)
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态
                return false;
            }
        }
    }
}

3. 自定义分布式锁注解(@DLock)

通过注解简化加锁逻辑,支持自定义锁前缀、锁标识、过期时间:

java 复制代码
package com.demo.redis.lock;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 分布式锁注解
 * 作用于方法上,实现"注解即加锁"
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DLock {

    /**
     * 锁前缀(用于区分业务)
     */
    String prefix() default "";

    /**
     * 锁标识的SpEL表达式(支持方法参数解析)
     * 示例:[#productId, #user.id]
     */
    String[] keys() default {};

    /**
     * 锁过期时间(毫秒)
     * 默认使用切面中的默认值
     */
    long lockExpire() default 0;
}

4. 分布式锁切面(AOP)

解析@DLock注解,自动完成加锁 / 释放锁逻辑,是注解式锁的核心:

java 复制代码
package com.demo.redis.lock;

import cn.hutool.core.util.StrUtil;
import com.demo.redis.utils.SpelUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 分布式锁切面
 * 解析@DLock注解,自动加锁/释放锁
 */
@Aspect
@Component
@EnableAspectJAutoProxy
@Slf4j
public class DLockAspect {

    public static final String SPLICE_STR = ":";
    /**
     * 默认锁过期时间(ms)
     */
    private static final long DEFAULT_LOCK_EXPIRE_MS = 2000L;

    @Autowired
    private DistributedLock distributedLock;

    /**
     * 切点:匹配所有标注@DLock的方法
     */
    @Pointcut("@annotation(dLock)")
    public void pointCut(DLock dLock) {
    }

    /**
     * 环绕通知:执行加锁→执行业务→释放锁
     */
    @Around("pointCut(dLock)")
    public Object invoke(ProceedingJoinPoint joinPoint, DLock dLock) throws Throwable {
        long startTime = System.currentTimeMillis();
        // 1. 构建锁标识
        String lockKey = dLock.prefix();
        final String[] keys = dLock.keys();
        if (keys.length > 0) {
            String[] paramValues = new String[keys.length];
            for (int i = 0; i < keys.length; i++) {
                // 解析SpEL表达式,获取方法参数值
                paramValues[i] = SpelUtils.parse(keys[i], joinPoint);
            }
            log.info("分布式锁参数解析结果:{}", Arrays.toString(paramValues));
            lockKey += String.join(SPLICE_STR, paramValues);
        }
        // 兜底:未配置锁标识时,使用类名+方法名
        if (StrUtil.isBlank(lockKey)) {
            lockKey = joinPoint.getTarget().getClass().getSimpleName() + SPLICE_STR + joinPoint.getSignature().getName();
        }
        lockKey = lockKey.toLowerCase(); // 统一小写,避免大小写冲突

        try {
            // 2. 获取锁(使用注解配置的过期时间,无则用默认值)
            long lockExpire = dLock.lockExpire() > 0 ? dLock.lockExpire() : DEFAULT_LOCK_EXPIRE_MS;
            boolean lockSuccess = distributedLock.lock(lockKey, lockExpire);
            if (lockSuccess) {
                log.info("获取分布式锁成功,lockKey={},耗时(ms):{}", lockKey, System.currentTimeMillis() - startTime);
                // 3. 执行业务方法
                return joinPoint.proceed();
            } else {
                throw new RuntimeException("获取分布式锁失败:" + lockKey);
            }
        } finally {
            // 4. 释放锁(finally保证必释放)
            try {
                distributedLock.unlock(lockKey);
                log.info("释放分布式锁成功,lockKey={}", lockKey);
            } catch (Exception e) {
                log.error("释放分布式锁异常,lockKey={}", lockKey, e);
            }
        }
    }
}

5. SpEL 表达式解析工具

用于解析@DLock注解中的keys参数,支持从方法参数中提取锁标识:

java 复制代码
package com.demo.redis.utils;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;

/**
 * SpEL表达式解析工具
 * 解析@DLock注解中的keys参数
 */
@Slf4j
public class SpelUtils {

    /**
     * SpEL表达式解析器
     */
    private static final ExpressionParser PARSER = new SpelExpressionParser();
    /**
     * 方法参数名发现器
     */
    private static final ParameterNameDiscoverer NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

    /**
     * 解析SpEL表达式,获取实际参数值
     * @param eps       SpEL表达式(如#productId)
     * @param joinPoint 切面连接点
     * @return 解析后的参数值
     */
    public static String parse(String eps, ProceedingJoinPoint joinPoint) {
        try {
            // 1. 获取被注解方法
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            // 2. 获取方法参数名
            String[] paramNames = NAME_DISCOVERER.getParameterNames(method);
            // 3. 解析SpEL表达式
            Expression expression = PARSER.parseExpression(eps);
            // 4. 构建表达式上下文(绑定方法参数)
            EvaluationContext context = new StandardEvaluationContext();
            Object[] args = joinPoint.getArgs();
            for (int i = 0; i < args.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
            // 5. 计算表达式值
            final Object value = expression.getValue(context);
            log.info("解析SpEL表达式:{} → {}", eps, value);
            return value == null ? "" : value.toString();
        } catch (Exception e) {
            log.error("解析SpEL表达式失败,表达式:{}", eps, e);
        }
        return "";
    }
}

四、实战场景:库存扣减

以电商核心场景 "库存扣减" 为例,分别演示手动加锁注解式加锁两种方式,验证分布式锁有效性。

1. 库存业务接口

java 复制代码
package com.demo.redis.service;

/**
 * 库存业务接口
 */
public interface IStockService {
    /**
     * 手动加锁扣减库存
     * @param productId 商品ID
     * @param deductNum 扣减数量
     * @return 是否扣减成功
     */
    boolean deductStock(Long productId, Integer deductNum);

    /**
     * 注解式加锁扣减库存
     * @param productId 商品ID
     * @param deductNum 扣减数量
     * @return 是否扣减成功
     */
    boolean deductStockV2(Long productId, Integer deductNum);
}

2. 库存业务实现类

java 复制代码
package com.demo.redis.service.impl;

import com.demo.redis.lock.DLock;
import com.demo.redis.lock.DistributedLock;
import com.demo.redis.service.IStockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * 库存业务实现:分布式锁解决并发扣减问题
 */
@Slf4j
@Service
public class StockServiceImpl implements IStockService {

    // 模拟库存存储(实际为数据库/Redis)
    private volatile Long stock = 100L;

    @Resource
    private DistributedLock distributedLock;

    /**
     * 方式1:手动加锁扣减库存
     */
    @Override
    public boolean deductStock(Long productId, Integer deductNum) {
        // 1. 定义锁标识(按商品粒度加锁,提升并发性能)
        String lockKey = "stock:lock:" + productId;
        // 2. 锁过期时间(30秒,需大于业务执行时间)
        long expireTime = TimeUnit.SECONDS.toMillis(30);

        try {
            // 3. 获取分布式锁
            boolean lockSuccess = distributedLock.lock(lockKey, expireTime);
            if (!lockSuccess) {
                log.error("库存扣减失败:获取锁超时,productId={}", productId);
                return false;
            }

            // 4. 核心业务:扣减库存(互斥区,仅一个线程执行)
            if (stock >= deductNum) {
                log.info("扣减库存前:{},扣减数量:{}", stock, deductNum);
                stock -= deductNum;
                log.info("扣减库存后:{}", stock);
                return true;
            } else {
                log.error("库存不足:productId={},剩余库存={},扣减数量={}", productId, stock, deductNum);
                return false;
            }
        } finally {
            // 5. 释放锁:finally块保证锁必释放,防止死锁
            distributedLock.unlock(lockKey);
        }
    }

    /**
     * 方式2:注解式加锁扣减库存(优雅版)
     * @DLock注解说明:
     * - prefix:锁前缀,区分业务
     * - keys:锁标识(SpEL表达式,提取productId)
     * - lockExpire:锁过期时间(30秒)
     */
    @DLock(prefix = "stock:lock:", keys = "#productId", lockExpire = 30_000L)
    @Override
    public boolean deductStockV2(Long productId, Integer deductNum) {
        // 核心业务:无需编写任何加锁/释放锁代码
        if (stock >= deductNum) {
            log.info("扣减库存前:{},扣减数量:{}", stock, deductNum);
            stock -= deductNum;
            log.info("扣减库存后:{}", stock);
            return true;
        } else {
            log.error("库存不足:productId={},剩余库存={},扣减数量={}", productId, stock, deductNum);
            return false;
        }
    }

    // 供测试获取库存
    public Long getStock() {
        return stock;
    }
}

五、测试验证

编写并发测试用例,模拟 100 个线程并发扣减库存,验证两种加锁方式的有效性:

java 复制代码
package com.demo.redis;

import com.demo.redis.service.IStockService;
import com.demo.redis.service.impl.StockServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
@SpringBootTest(classes = DemoRedisApplication.class)
public class StockServiceTest {

    @Autowired
    private IStockService stockService;

    // 并发线程数
    private static final int THREAD_COUNT = 100;
    // 每个线程扣减数量
    private static final int DEDUCT_NUM = 1;

    /**
     * 测试手动加锁扣减库存
     */
    @Test
    public void testDeductStock() throws InterruptedException {
        // 1. 创建线程池(模拟分布式环境多实例并发)
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);

        // 2. 模拟并发扣减
        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                try {
                    stockService.deductStock(1L, DEDUCT_NUM);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        // 3. 等待所有线程执行完成
        countDownLatch.await();
        executorService.shutdown();

        // 4. 验证库存结果(预期:100 - 100*1 = 0)
        Long finalStock = ((StockServiceImpl) stockService).getStock();
        log.info("手动加锁-并发扣减完成,最终库存:{}", finalStock);
    }

    /**
     * 测试注解式加锁扣减库存
     */
    @Test
    public void testDeductStockV2() throws InterruptedException {
        // 1. 重置库存(避免前一个测试影响)
        ((StockServiceImpl) stockService).stock = 100L;
        
        // 2. 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);

        // 3. 模拟并发扣减
        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                try {
                    stockService.deductStockV2(1L, DEDUCT_NUM);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        // 4. 等待所有线程执行完成
        countDownLatch.await();
        executorService.shutdown();

        // 5. 验证库存结果
        Long finalStock = ((StockServiceImpl) stockService).getStock();
        log.info("注解式加锁-并发扣减完成,最终库存:{}", finalStock);
    }
}

六、生产级考虑问题

业务执行时间超过锁过期时间,锁提前释放导致并发问题、同一线程多次获取同一把锁,往往续期锁、可重入锁等方面扩展必不可少。

相关推荐
张柏慈1 天前
Java性能优化:实战技巧与案例解析
java
天“码”行空1 天前
简化Lambda——方法引用
java·开发语言
带刺的坐椅1 天前
MCP 进化:让静态 Tool 进化为具备“上下文感知”的远程 Skills
java·ai·llm·agent·solon·mcp·tool-call·skills
预立科技1 天前
Redis 中 Lua 与 Pipeline 的相同点,区别,使用场景
redis·pipeline·lua
java1234_小锋1 天前
Java线程之间是如何通信的?
java·开发语言
张张努力变强1 天前
C++ Date日期类的设计与实现全解析
java·开发语言·c++·算法
曲幽1 天前
FastAPI多进程部署:定时任务重复执行?手把手教你用锁搞定
redis·python·fastapi·web·lock·works
while(1){yan}1 天前
Spring事务
java·数据库·spring boot·后端·java-ee·mybatis
毕设源码-赖学姐1 天前
【开题答辩全过程】以 高校社团管理平台为例,包含答辩的问题和答案
java
余瑜鱼鱼鱼1 天前
线程和进程的区别和联系
java·开发语言·jvm