SpringBoot + Redis 实现分布式锁实战

举报
JiHua 发表于 2026/10/18 10:43:54 2026/10/18
【摘要】 在分布式系统中,多实例并发操作共享资源极易引发超卖、数据不一致问题。本文基于SpringBoot整合Redis,手把手实现Redis分布式锁,讲解SETNX+EX、Lua脚本两种实现方案,附带完整可运行代码,同时分析锁失效、死锁、锁误释放等常见坑点,给出生产环境优化建议,适合后端开发快速上手分布式锁基础实战。

前言

单体应用下,我们可以直接使用 synchronized 或者 ReentrantLock 解决并发争抢资源的问题。但随着服务集群化部署,多个服务实例运行在不同JVM进程,本地锁只能锁住当前实例,无法跨实例生效。

举个典型场景:电商库存扣减,服务部署3个实例,大量请求同时过来,本地锁会失效,出现库存超卖。分布式锁就是用来解决跨进程、跨服务并发资源竞争的方案。Redis因为高性能、操作原子性,是实现分布式锁最常用方案之一。

本文基于 SpringBoot 2.7 + RedisTemplate,从零实现Redis分布式锁,同时分析各种踩坑点。

一、环境依赖

引入maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

application.yml 配置redis连接

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0

二、简易版:SETNX + 过期时间(不推荐生产)

SETNX:只有key不存在时才设置值,返回成功代表抢到锁。同时必须设置过期时间,防止服务宕机导致锁一直存在,造成死锁。

⚠️ 坑点:setnx 和 expire 如果分成两条命令,中间服务崩溃,锁不会自动过期,发生死锁。必须保证原子性。

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Component
public class RedisDistLock {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取锁
     * @param lockKey 锁名称
     * @param lockValue 唯一标识(UUID),防止释放别人的锁
     * @param expireTime 过期时间,单位秒
     * @return 是否获取成功
     */
    public boolean tryLock(String lockKey, String lockValue, long expireTime){
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        // set key value nx ex 原子命令
        Boolean success = ops.setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁:判断value一致才删除,防止锁过期释放了别人的锁
     */
    public boolean unLock(String lockKey, String lockValue){
        Object val = redisTemplate.opsForValue().get(lockKey);
        if(lockValue.equals(val)){
            return Boolean.TRUE.equals(redisTemplate.delete(lockKey));
        }
        return false;
    }
}

问题:get + delete 两条命令,非原子。极端情况下:查询value相等之后,锁刚好过期,别的线程拿到锁,此时执行delete,就会删掉其他线程的锁。
解决方案:使用Lua脚本保证释放锁原子性

三、生产可用:Lua脚本实现分布式锁

Lua脚本在Redis中是原子执行,整个脚本执行期间不会被其他命令打断。获取锁依旧使用setIfAbsent原子命令,释放锁交给Lua脚本。

@Component
public class RedisLuaDistLock {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    // Lua脚本:判断value相等才删除key
    private static final String UNLOCK_SCRIPT = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
            """;

    public boolean tryLock(String lockKey, String lockValue, long expireTime){
        Boolean ok = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(ok);
    }

    public boolean unLock(String lockKey, String lockValue){
        Long result = redisTemplate.execute(
                new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
                Collections.singletonList(lockKey),
                lockValue
        );
        return result != null && result == 1;
    }
}

四、测试Controller

@RestController
@RequestMapping("/stock")
public class StockController {

    @Resource
    private RedisLuaDistLock redisLock;

    // 模拟库存
    private int stock = 10;

    @GetMapping("/deduct")
    public String deductStock(){
        String lockKey = "lock:stock";
        String lockValue = UUID.randomUUID().toString();
        boolean getLock = redisLock.tryLock(lockKey, lockValue, 10);
        if(!getLock){
            return "请求繁忙,请稍后重试";
        }
        try {
            if(stock > 0){
                stock --;
                return "扣减成功,剩余库存:" + stock;
            }else {
                return "库存不足";
            }
        }finally {
            // 必须finally释放锁,保证锁一定释放
            redisLock.unLock(lockKey, lockValue);
        }
    }
}

五、分布式锁核心问题分析

  1. 锁超时问题
    业务执行时间超过锁过期时间,锁提前释放,其他线程抢到锁。解决方案:锁续期(看门狗),Redisson内部已经封装好了看门狗机制,生产优先考虑Redisson。
  2. 锁误删除
    每个锁value使用唯一UUID,只能释放自己加的锁,不能直接delete key。
  3. Redis主从切换锁丢失
    主节点加锁成功,但还没同步到从节点,主宕机,从晋升为主,锁丢失。高一致性场景,推荐RedLock,或者数据库方案。

六、生产选型建议

简单项目:自研Lua版本分布式锁可以临时使用。
企业级项目:不要手写轮子,直接使用 Redisson,内置可重入锁、看门狗自动续期、锁重试、RedLock等能力,成熟稳定。

七、总结

Redis分布式锁核心要点:

  1. 加锁:SET key value NX EX 原子操作;
  2. 解锁:Lua脚本保证原子校验+删除;
  3. 锁value必须唯一,防止误删;
  4. 手写锁适合学习理解原理,真实业务优先Redisson。

分布式锁是后端面试高频考点,通过本篇实战代码,可以快速理解分布式并发的核心难点,同时掌握Redis原子命令与Lua脚本在项目中的落地方式。


【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。