百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

手把手教你在 Spring Boot3 里纯编码实现自定义分布式锁

mhr18 2025-08-06 21:37 3 浏览 0 评论

为什么要自己实现分布式锁?

你是不是早就受够了引入各种第三方依赖时的繁琐?尤其是分布式锁这块,每次集成 Redisson 或者 Zookeeper,都得额外维护一堆配置,有时候还会因为版本兼容问题头疼半天。

其实在很多中小型项目里,分布式场景没那么复杂,用那些成熟框架有点 “杀鸡用牛刀” 的意思。自己实现的话,不仅能精准控制锁的逻辑,还能避开开源工具里那些用不上的冗余功能,让代码更轻量。就像上周有个读者留言说,他们项目里 Redis 版本比较老,用 Redisson 总出问题,最后还是靠自己写的简易锁解决了问题。

分布式锁的核心原则

在动手之前,咱们得先明确分布式锁必须满足的几个核心原则,这是保证锁可靠的基础:

  • 互斥性:同一时间只能有一个线程拿到锁,这是分布式锁的基本要求。
  • 防死锁:就算持有锁的线程意外挂了,锁也得能自动释放,避免永远阻塞其他线程。
  • 容错性:万一 Redis 节点出点小问题,锁的逻辑不能跟着崩掉,要能尽量保证服务可用。

核心实现步骤(纯编码版)

准备工作:配置 Redis 环境

因为咱们要基于 Redis 实现分布式锁,所以先得在 Spring Boot3 里配置好 Redis 连接。

配置 RedisTemplate

新建 Redis 配置类,注入 StringRedisTemplate,记得设置序列化方式:

@Configuration
public class RedisConfig {

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        // 设置key和value的序列化方式(避免乱码)
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

配置文件信息

在 application.yml 里填写 Redis 连接参数:

spring:
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8  # 最大连接数
        max-idle: 8    # 最大空闲连接
        min-idle: 2    # 最小空闲连接

定义锁接口:明确核心功能

先抽象出分布式锁的基本操作,方便后续实现和扩展:

public interface DistributedLock {

    /**
     * 立即获取锁(不等待)
     * @param lockKey 锁的key
     * @param expireTime 过期时间(防止死锁)
     * @param timeUnit 时间单位
     * @return 锁的标识(UUID),获取失败返回null
     */
    String acquireLock(String lockKey, long expireTime, TimeUnit timeUnit);

    /**
     * 释放锁
     * @param lockKey 锁的key
     * @param lockValue 锁的标识(获取锁时返回的UUID)
     * @return 是否释放成功
     */
    boolean releaseLock(String lockKey, String lockValue);

    /**
     * 尝试获取锁(带等待时间)
     * @param lockKey 锁的key
     * @param waitTime 最大等待时间
     * @param expireTime 锁过期时间
     * @param timeUnit 时间单位
     * @return 锁的标识,超时未获取返回null
     * @throws InterruptedException 线程中断异常
     */
    String tryAcquireLock(String lockKey, long waitTime, long expireTime, TimeUnit timeUnit) throws InterruptedException;
}

实现 Redis 锁:核心逻辑编码

这部分是关键,咱们用 Redis 原生 API 实现锁的获取和释放,全程不依赖第三方工具。

核心实现类

@Component
public class RedisDistributedLock implements DistributedLock {

    private final StringRedisTemplate stringRedisTemplate;

    // 释放锁的Lua脚本(保证判断和删除的原子性)
    private static final String RELEASE_LOCK_LUA_SCRIPT = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";

    // 构造器注入Redis模板
    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public String acquireLock(String lockKey, long expireTime, TimeUnit timeUnit) {
        // 生成唯一标识(避免释放别人的锁)
        String lockValue = UUID.randomUUID().toString();
        // 用setIfAbsent实现原子性加锁(等价于Redis的SET NX EX命令)
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, expireTime, timeUnit);
        return success != null && success ? lockValue : null;
    }

    @Override
    public boolean releaseLock(String lockKey, String lockValue) {
        // 执行Lua脚本释放锁
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(RELEASE_LOCK_LUA_SCRIPT);
        redisScript.setResultType(Long.class);
        // 执行脚本:KEYS[1]是lockKey,ARGV[1]是lockValue
        Long result = stringRedisTemplate.execute(
            redisScript, 
            Collections.singletonList(lockKey), 
            lockValue
        );
        return result != null && result > 0;
    }

    @Override
    public String tryAcquireLock(String lockKey, long waitTime, long expireTime, TimeUnit timeUnit) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        long waitMillis = timeUnit.toMillis(waitTime);
        String lockValue = UUID.randomUUID().toString();

        while (true) {
            // 尝试获取锁
            Boolean success = stringRedisTemplate.opsForValue()
                    .setIfAbsent(lockKey, lockValue, expireTime, timeUnit);
            if (success != null && success) {
                return lockValue; // 获取成功
            }

            // 检查是否超时
            long elapsedTime = System.currentTimeMillis() - startTime;
            if (elapsedTime >= waitMillis) {
                return null; // 超时未获取
            }

            // 短暂休眠后重试(避免频繁请求Redis)
            Thread.sleep(100);
        }
    }
}

关键逻辑说明

  • 获取锁:用setIfAbsent方法,同时设置过期时间,保证 “加锁 + 过期” 原子性。
  • 释放锁:通过 Lua 脚本先判断锁的归属(避免误删别人的锁),再删除,保证操作原子性。
  • 重试机制:tryAcquireLock方法中,超过等待时间则放弃,避免无限阻塞。

注解 + AOP:简化业务使用

为了让业务代码更简洁,咱们用注解 + AOP 实现自动加锁 / 释放锁。

定义锁注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLockAnnotation {

    /**
     * 锁的key(支持SpEL表达式,比如"order:{#orderId}")
     */
    String lockKey();

    /**
     * 锁过期时间(默认5秒)
     */
    long expireTime() default 5;

    /**
     * 时间单位(默认秒)
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 最大等待时间(默认0,即不等待)
     */
    long waitTime() default 0;
}

实现 AOP 切面

@Aspect
@Component
@Slf4j
public class DistributedLockAspect {

    private final DistributedLock distributedLock;

    public DistributedLockAspect(DistributedLock distributedLock) {
        this.distributedLock = distributedLock;
    }

    // 拦截带有@DistributedLockAnnotation的方法
    @Around("@annotation(distributedLockAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint, DistributedLockAnnotation distributedLockAnnotation) throws Throwable {
        // 解析锁的key(支持SpEL表达式)
        String lockKey = parseLockKey(joinPoint, distributedLockAnnotation.lockKey());
        long expireTime = distributedLockAnnotation.expireTime();
        TimeUnit timeUnit = distributedLockAnnotation.timeUnit();
        long waitTime = distributedLockAnnotation.waitTime();

        // 获取锁
        String lockValue = waitTime <= 0 
            ? distributedLock.acquireLock(lockKey, expireTime, timeUnit)
            : distributedLock.tryAcquireLock(lockKey, waitTime, expireTime, timeUnit);

        if (lockValue == null) {
            log.warn("获取分布式锁失败,lockKey: {}", lockKey);
            throw new RuntimeException("获取分布式锁失败");
        }

        try {
            // 执行目标方法(业务逻辑)
            return joinPoint.proceed();
        } finally {
            // 释放锁(必须在finally中执行)
            boolean releaseSuccess = distributedLock.releaseLock(lockKey, lockValue);
            if (!releaseSuccess) {
                log.warn("释放分布式锁失败,lockKey: {}, lockValue: {}", lockKey, lockValue);
            }
        }
    }

    // 解析SpEL表达式,生成最终的lockKey
    private String parseLockKey(ProceedingJoinPoint joinPoint, String lockKey) {
        if (!lockKey.contains("#")) {
            return lockKey; // 不包含SpEL表达式,直接返回
        }

        // 解析方法参数和SpEL表达式
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();

        SpelExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();

        // 填充方法参数到SpEL上下文
        DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
        String[] parameterNames = discoverer.getParameterNames(method);
        if (parameterNames != null) {
            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], args[i]);
            }
        }

        // 执行SpEL表达式,获取最终的lockKey
        Expression expression = parser.parseExpression(lockKey);
        return expression.getValue(context, String.class);
    }
}

业务使用示例

在需要加锁的方法上添加注解即可,比如订单创建场景:

@Service
public class OrderService {

    @DistributedLockAnnotation(
        lockKey = "order:{#orderId}",  // 锁的key,通过SpEL表达式动态生成
        expireTime = 10,               // 锁10秒后过期
        waitTime = 3                   // 最多等待3秒
    )
    public void createOrder(String orderId) {
        // 业务逻辑:比如扣减库存、创建订单记录等
        log.info("创建订单,orderId: {}", orderId);
        // 模拟业务处理时间
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

测试验证:确保锁的可靠性

光写代码还不够,必须通过测试验证锁的功能是否正常。

编写测试类

@SpringBootTest
public class DistributedLockTest {

    @Autowired
    private DistributedLock distributedLock;

    @Autowired
    private OrderService orderService;

    private final String lockKey = "test:lock";
    private int count = 0; // 用于验证互斥性的计数器

    /**
     * 测试锁的互斥性:多线程抢锁,最终count应等于线程数
     */
    @Test
    public void testDistributedLock() throws InterruptedException {
        int threadCount = 100; // 100个线程抢锁
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < threadCount; i++) {
            final int index = i;
            executorService.submit(() -> {
                try {
                    // 尝试获取锁,最多等5秒,锁过期3秒
                    String lockValue = distributedLock.tryAcquireLock(lockKey, 5, 3, TimeUnit.SECONDS);
                    if (lockValue != null) {
                        count++; // 临界区操作
                        System.out.println("线程" + index + "获取锁成功,count: " + count);
                        Thread.sleep(100); // 模拟业务处理
                        distributedLock.releaseLock(lockKey, lockValue);
                    } else {
                        System.out.println("线程" + index + "获取锁失败");
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();
        executorService.shutdown();
        System.out.println("最终count: " + count); // 若锁有效,count应等于100
    }

    /**
     * 测试注解式锁的使用
     */
    @Test
    public void testAnnotationLock() throws InterruptedException {
        int threadCount = 10;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < threadCount; i++) {
            final int index = i;
            executorService.submit(() -> {
                try {
                    orderService.createOrder("ORDER_" + index);
                } catch (Exception e) {
                    System.out.println("线程" + index + "执行失败: " + e.getMessage());
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();
        executorService.shutdown();
    }
}

测试注意事项

  • 执行测试时,建议用redis-cli monitor命令监控 Redis 操作,确认锁的获取和释放是否符合预期。
  • 重点观察testDistributedLock的最终 count 值,若等于线程数,说明锁的互斥性有效。
  • 测试注解功能时,查看日志是否有序输出,避免出现并发问题。

进阶优化:让锁更完善

实际使用中,还可以根据需求添加以下功能:

看门狗机制(锁续期)

如果业务执行时间可能超过锁的过期时间,可以添加定时续期

// 在获取锁后启动定时任务,每隔一段时间(如过期时间的1/3)延长锁的过期时间
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

// 续期任务示例
private void startRenewTask(String lockKey, String lockValue, long expireTime, TimeUnit timeUnit) {
    long period = timeUnit.toMillis(expireTime) / 3; // 每1/3过期时间续期一次
    scheduler.scheduleAtFixedRate(() -> {
        // 延长锁的过期时间
        stringRedisTemplate.expire(lockKey, expireTime, timeUnit);
    }, 0, period, TimeUnit.MILLISECONDS);
}

可重入锁支持

通过记录线程持有锁的次数,实现可重入性(同一线程可多次获取同一把锁):

// 用ThreadLocal记录当前线程持有的锁和次数
private ThreadLocal<Map<String, Integer>> lockCount = ThreadLocal.withInitial(HashMap::new);

Redis 集群适配

生产环境建议用 Redis 集群,避免单点故障,配置时只需修改 Redis 连接信息即可兼容。

总结

优势:轻量可控、无第三方依赖、按需定制功能。

注意事项

  1. 必须保证锁的过期时间大于业务最大执行时间(或启用续期)。
  2. 释放锁时必须校验锁的归属(通过 UUID),避免误删。
  3. 高并发场景下,建议合理设置重试间隔,避免 Redis 压力过大。

这套方案已经能满足大部分中小型项目的需求,你可以根据自己的业务场景调整细节。如果还有其他疑问,或者想补充某个功能,欢迎在评论区交流!

相关推荐

订单超时自动取消业务的 N 种实现方案,从原理到落地全解析

在分布式系统架构中,订单超时自动取消机制是保障业务一致性的关键组件。某电商平台曾因超时处理机制缺陷导致日均3000+订单库存锁定异常,直接损失超50万元/天。本文将从技术原理、实现细节、...

使用Spring Boot 3开发时,如何选择合适的分布式技术?

作为互联网大厂的后端开发人员,当你满怀期待地用上SpringBoot3,准备在项目中大显身手时,却发现一个棘手的问题摆在面前:面对众多分布式技术,究竟该如何选择,才能让SpringBoot...

数据库内存爆满怎么办?99%的程序员都踩过这个坑!

你的数据库是不是又双叒叕内存爆满了?!服务器监控一片红色警告,老板在群里@所有人,运维同事的电话打爆了手机...这种场景是不是特别熟悉?别慌!作为一个在数据库优化这条路上摸爬滚打了10年的老司机,今天...

springboot利用Redisson 实现缓存与数据库双写不一致问题

使用了Redisson来操作Redis分布式锁,主要功能是从缓存和数据库中获取商品信息,以下是针对并发时更新缓存和数据库带来不一致问题的解决方案1.基于读写锁和删除缓存策略在并发更新场景下,...

外贸独立站数据库炸了?对象缓存让你起死回生

上周黑五,一个客户眼睁睁看着服务器CPU飙到100%——每次页面加载要查87次数据库。这让我想起2024年Pantheon的测试:Redis缓存能把WooCommerce查询速度提升20倍。跨境电商最...

手把手教你在 Spring Boot3 里纯编码实现自定义分布式锁

为什么要自己实现分布式锁?你是不是早就受够了引入各种第三方依赖时的繁琐?尤其是分布式锁这块,每次集成Redisson或者Zookeeper,都得额外维护一堆配置,有时候还会因为版本兼容问题头疼半...

如何设计一个支持百万级实时数据推送的WebSocket集群架构?

面试解答:要设计一个支持百万级实时数据推送的WebSocket集群架构,需从**连接管理、负载均衡、水平扩展、容灾恢复**四个维度切入:连接层设计-**长连接优化**:采用Netty或Und...

Redis数据结构总结——面试最常问到的知识点

Redis作为主流的nosql存储,面试时经常会问到。其主要场景是用作缓存,分布式锁,分布式session,消息队列,发布订阅等等。其存储结构主要有String,List,Set,Hash,Sort...

skynet服务的缺陷 lua死循环

服务端高级架构—云风的skynet这边有一个关于云风skynet的视频推荐给大家观看点击就可以观看了!skynet是一套多人在线游戏的轻量级服务端框架,使用C+Lua开发。skynet的显著优点是,...

七年Java开发的一路辛酸史:分享面试京东、阿里、美团后的心得

前言我觉得有一个能够找一份大厂的offer的想法,这是很正常的,这并不是我们的饭后谈资而是每个技术人的追求。像阿里、腾讯、美团、字节跳动、京东等等的技术氛围与技术规范度还是要明显优于一些创业型公司...

mysql mogodb es redis数据库之间的区别

1.MySQL应用场景概念:关系型数据库,基于关系模型,使用表和行存储数据。优点:支持ACID事务,数据具有很高的一致性和完整性。缺点:垂直扩展能力有限,需要分库分表等方式扩展。对于复杂的查询和大量的...

redis,memcached,nginx网络组件

1.理解阻塞io,非阻塞io,同步io,异步io的区别2.理解BIO和AIO的区别io多路复用只负责io检测,不负责io操作阻塞io中的write,能写多少是多少,只要写成功就返回,譬如准备写500字...

SpringBoot+Vue+Redis实现验证码功能

一个小时只允许发三次验证码。一次验证码有效期二分钟。SpringBoot整合Redis...

AWS MemoryDB 可观测最佳实践

AWSMemoryDB介绍AmazonMemoryDB是一种完全托管的、内存中数据存储服务,专为需要极低延迟和高吞吐量的应用程序而设计。它与Redis和Memcached相似,但具有更...

从0构建大型AI推荐系统:实时化引擎从工具到生态的演进

在AI浪潮席卷各行各业的今天,推荐系统正从幕后走向前台,成为用户体验的核心驱动力。本文将带你深入探索一个大型AI推荐系统从零起步的全过程,揭示实时化引擎如何从单一工具演进为复杂生态的关键路径。无论你是...

取消回复欢迎 发表评论: