分布式锁-Redis解决方案和Redisson解决方案

本文探讨了分布式锁在打车应用中的具体实现,通过Redis和Redisson两种方式解决多司机抢单问题。作者详细介绍了使用synchronized和自定义Redis锁的原理、问题及解决方案,以及Redisson工具的引入和其优势。

文章目录


系列文章:
分布式锁-数据库mysql解决方案
分布式锁-Redis解决方案和Redisson解决方案
分布式锁-Redis红锁解决方案
Zookeeper-06-Zookeeper锁原理演进及Redis锁优缺点终极选型指南
分布式锁深度精讲:从底层理论原理到主流方案精准选型


前言

在分布式微服务架构遍地开花的当下,单机部署的业务场景早已成为过去式。集群化部署、多实例负载均衡是后端服务的标配,但随之而来的多进程、多服务并发争抢共享资源问题,成了开发绕不开的核心痛点。

就像我们日常使用的打车软件,乘客下单后,多位司机同时争抢同一笔订单,订单资源唯一,只能允许一位司机抢单成功。如果没有合理的锁机制控制,就会出现多司机同时接单、订单数据错乱、库存对账异常等严重生产问题。

本地线程锁在集群环境下完全失效,Redis分布式锁凭借高性能、高可用、轻量级的特性,成为互联网企业生产开发中解决分布式并发互斥问题的首选方案。

本文将带你循序渐进学习Redis分布式锁:先厘清核心基础概念与锁的核心区别,再从原生Redis命令手写分布式锁,一步步迭代修复各类生产致命bug,最后重点讲解生产环境标配Redisson框架的核心原理、配置与实战用法,包含编程式锁和企业常用的声明式注解锁,文末深度剖析Redis分布式锁优缺点,零基础也能轻松看懂、上手落地。

一、核心基础认知:搞懂锁与分布式锁的本质区别

1.1 什么是分布式锁?

简单来说,分布式锁就是面向分布式集群环境的互斥锁

在单机单服务场景下,多线程并发操作共享资源,我们用普通锁就能保证线程安全;但在分布式架构中,同一个业务服务会部署在多台不同服务器上,本质是多进程跨主机并发操作共享资源,普通本地锁无法跨机器生效,此时就需要分布式锁实现多客户端互斥访问临界资源,保障并发场景下的数据一致性。

1.2 本地锁、分布式锁、事务核心三者核心区别

很多新手容易混淆普通锁、分布式锁、数据库事务的使用场景,一张明细拆解帮你彻底分清,精准适配不同业务需求:

  • 本地线程锁(synchronized/Lock):核心解决单进程内多线程并发安全问题。仅作用于当前单机服务JVM内部,只能锁住本机多线程资源,跨服务器、跨进程完全失效,仅适用于单体架构简单并发场景。

  • Redis分布式锁:核心解决分布式集群多进程跨主机并发互斥问题。针对多台服务器部署的同款业务服务,保证同一时间只有一个服务实例能操作共享临界资源,是微服务集群并发控制的核心方案。

  • 数据库事务:核心解决单次业务会话内多数据库操作的数据一致性问题,保证事务内所有SQL要么全部成功提交,要么全部回滚,不处理并发争抢问题,仅管控数据操作原子性。

  • 分布式事务:核心解决多服务联动跨库数据一致性问题,比如下单服务、库存服务、支付服务跨服务联动,保证所有关联服务操作整体成功或整体回滚,和分布式锁的并发互斥核心诉求完全不同。

二、实战业务场景:打车司机抢单核心需求(只学习,生产不用)

2.1 业务核心需求

乘客在打车平台发起出行订单,订单生成后状态为待抢单,平台多位在线司机可同时发起抢单请求。核心规则:一笔订单仅允许一位司机抢单成功,抢单后订单状态变更为已接单,其他司机抢单直接失败

该服务为集群多实例部署,多个司机的抢单请求会被负载均衡分发到不同服务器,本地锁完全无效,必须使用分布式锁实现并发互斥。

2.2 基础接口层代码(Controller)

编写抢单请求接收接口,接收订单ID和司机ID,调用抢单核心业务逻辑,代码简洁适配后续锁逻辑对接:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 打车抢单控制层
 * @author 技术教学博客
 */
@RestController
public class GrabOrderController {

    // 注入抢单核心业务服务
    @Resource
    private GrabOrderService grabService;

    /**
     * 司机抢单接口
     * @param orderId 待抢订单ID
     * @param driverId 抢单司机ID
     * @return 抢单结果提示
     */
    @GetMapping("/grab/order/{orderId}")
    public String grabOrder(@PathVariable("orderId") int orderId, Integer driverId){
        System.out.println("收到抢单请求:订单号"+orderId+",抢单司机ID:"+driverId);
        // 调用抢单核心逻辑(含锁控制)
        grabService.grabOrder(orderId,driverId);
        return "抢单请求处理完成";
    }
}

2.3 本地锁synchronized实测:集群环境彻底失效

很多新手第一时间会想到用JDK自带的synchronized本地锁实现互斥,但synchronized仅锁当前JVM进程,集群多服务器部署下完全无效。不同服务器的服务实例各自加锁,互不影响,依旧会出现多司机同时抢单成功的问题。

本地锁失效演示代码:

import org.springframework.stereotype.Service;
import javax.annotation.Resource;

/**
 * 本地锁抢单实现(集群环境失效演示)
 */
@Service
public class GrabOrderLocalLockImpl implements GrabOrderService {

    @Resource
    private OrderService orderService;

    @Override
    public String grabOrder(int orderId, int driverId) {
        // 以订单ID作为锁对象
        String lockKey = orderId + "";
        // 本地同步锁,仅单机生效
        synchronized (lockKey.intern()) {
            try {
                System.out.println("司机:"+driverId+" 开始执行抢单核心逻辑");
                // 调用订单数据库操作业务逻辑
                boolean grabResult = orderService.grab(orderId, driverId);
                if(grabResult) {
                    System.out.println("司机:"+driverId+" 抢单成功!");
                }else {
                    System.out.println("司机:"+driverId+" 抢单失败,订单已被抢占");
                }
            } finally {
                // 本地锁自动释放,无需手动操作
            }
        }
        return null;
    }
}

2.4 订单核心业务底层代码(公共基础逻辑)

该层为订单数据库查询、状态修改的基础业务代码,所有锁方案都会复用该逻辑,核心判断订单是否为待抢单状态,修改订单归属司机及订单状态,无需关注并发控制,仅做基础业务处理:

import org.springframework.stereotype.Service;
import javax.annotation.Resource;

/**
 * 订单核心业务实现层
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private TblOrderDao mapper; // 订单数据库DAO层

    @Override
    public boolean grab(int orderId, int driverid) {
        // 根据订单ID查询订单详情
        TblOrder order = mapper.selectByPrimaryKey(orderId);
        // 模拟业务处理耗时,放大并发冲突场景
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        // 判断订单状态:0=待抢单,1=已接单
        if (order.getOrderStatus().intValue() == 0){
            // 修改订单为已接单状态,绑定抢单司机ID
            order.setOrderStatus(1);
            order.setDriverId(driverid);
            // 更新订单数据到数据库
            mapper.updateByPrimaryKeySelective(order);
            return true;
        }
        // 订单已被抢占,返回抢单失败
        return false;
    }
}

三、手写Redis原生分布式锁:一步步迭代修复所有生产bug(只学习,生产不用)

明白了本地锁失效的核心问题后,我们基于Redis原生命令手写分布式锁,从最简版本开始,逐个修复宕机死锁、原子性失效、误删他人锁、锁超时业务未执行完四大核心生产问题,彻底吃透分布式锁底层原理。

3.1 手写Redis锁四大核心痛点及优化方向

  1. 问题1:服务宕机导致死锁:加锁成功后服务突然宕机、重启,锁无法手动释放,其他服务永久抢不到锁。优化:给锁Key设置过期时间,自动释放锁

  2. 问题2:加锁和设置过期时间非原子操作:先加锁、再设过期时间,两步操作中间服务宕机,依旧会死锁。优化:使用Redis setIfAbsent原子命令,一步完成加锁+设过期时间

  3. 问题3:误删除其他线程的锁:锁自动过期释放后,新线程抢到锁,旧线程业务执行完手动删锁,误删掉新线程的锁。优化:锁Value存入当前线程唯一标识,删锁前校验归属权,只删自己的锁

  4. 问题4:锁提前过期,业务未执行完毕:业务执行耗时超过锁过期时间,锁自动释放,出现并发争抢。优化:新增守护线程看门狗,定时给锁续约续命,业务不结束锁不失效

3.2 迭代版手写Redis锁代码(含过期时间+锁归属权校验)

整合前三大优化点,实现原子加锁、自动过期、防误删锁核心能力,是手写锁基础可用版本:

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * 手写Redis原生分布式锁(基础优化版)
 * 解决:死锁、原子加锁、误删他人锁问题
 */
@Service
public class GrabRedisLockImpl implements GrabOrderService {

    // Redis操作模板
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 订单核心业务服务
    @Resource
    private OrderService orderService;

    @Override
    public String grabOrder(int orderId , int driverId){
        // 1. 定义分布式锁Key:按订单维度加锁,不同订单互不影响
        String lockKey = "order_grab_lock_" + orderId;
        // 2. 定义锁Value:当前抢单司机ID,作为锁唯一归属标识
        String lockValue = String.valueOf(driverId);

        // 3. 原子操作:加锁+设置10秒过期时间,一步完成杜绝宕机死锁
        boolean lockSuccess = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey.intern(), lockValue, 10L, TimeUnit.SECONDS);

        // 加锁失败,直接返回抢单失败
        if(!lockSuccess) {
            System.out.println("司机:"+driverId+" 抢单失败,锁已被占用");
            return null;
        }

        try {
            // 模拟业务执行耗时
            TimeUnit.SECONDS.sleep(3);
            // 执行核心抢单业务逻辑
            System.out.println("司机:"+driverId+" 执行抢单核心业务逻辑");
            boolean grabResult = orderService.grab(orderId, driverId);
            if(grabResult) {
                System.out.println("司机:"+driverId+" 抢单成功!");
            }else {
                System.out.println("司机:"+driverId+" 抢单失败,订单已被抢占");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 4. 释放锁:先校验锁归属权,只删除自己加的锁,防止误删
            String currentLockValue = stringRedisTemplate.opsForValue().get(lockKey.intern());
            if(lockValue.equals(currentLockValue)) {
                stringRedisTemplate.delete(lockKey.intern());
                System.out.println("司机:"+driverId+" 成功释放自己的抢单锁");
            }
        }
        return null;
    }
}

3.3 终极手写版:新增看门狗锁续约续命机制

基础优化版仍存在锁提前过期的问题,我们手动实现看门狗守护线程,后台异步线程定时给锁续约,业务没执行完就持续延长锁过期时间,彻底解决锁超时并发问题。

第一步:锁续约守护线程服务类

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * Redis锁续约看门狗服务(手动续命线程)
 */
@Service
public class RedisLockRenewService {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 异步定时给锁续约
     * @param lockKey 锁Key
     * @param lockValue 锁归属Value
     * @param expireTime 锁初始过期时间
     */
    @Async // 开启异步线程执行续约,不阻塞主业务流程
    public void renewLock(String lockKey, String lockValue, int expireTime) {
        System.out.println("看门狗启动,开始为锁"+lockKey+" 持续续约续命");
        // 循环校验:锁存在且是自己的锁,就持续续约
        String currentValue = redisTemplate.opsForValue().get(lockKey);
        while (StringUtils.isNotBlank(currentValue) && currentValue.equals(lockValue)){
            // 每n/3秒续约一次,默认10秒过期,每3秒续约一次
            int sleepTime = expireTime / 3;
            try {
                Thread.sleep(sleepTime * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
            // 重置锁过期时间,完成续命
            redisTemplate.expire(lockKey,expireTime, TimeUnit.SECONDS);
            System.out.println("锁"+lockKey+" 续约成功,过期时间已刷新");
            // 重新获取锁最新状态,判断是否继续续约
            currentValue = redisTemplate.opsForValue().get(lockKey);
        }
        System.out.println("业务执行结束,锁"+lockKey+" 停止续约");
    }
}

第二步:整合续约机制的完整手写锁业务代码

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * 手写Redis分布式锁(终极完整版:含续约看门狗)
 */
@Service
public class GrabRedisLockFinalImpl implements GrabOrderService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private OrderService orderService;

    // 注入锁续约看门狗服务
    @Resource
    private RedisLockRenewService redisLockRenewService;

    @Override
    public String grabOrder(int orderId , int driverId){
        String lockKey = "order_grab_lock_" + orderId;
        String lockValue = String.valueOf(driverId);
        // 锁初始过期时间10秒
        int lockExpireTime = 10;

        // 原子加锁:加锁同时设置过期时间,杜绝宕机死锁
        boolean lockSuccess = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey.intern(), lockValue, lockExpireTime, TimeUnit.SECONDS);

        // 加锁成功,启动异步看门狗续约线程,持续给锁续命
        if(lockSuccess) {
            redisLockRenewService.renewLock(lockKey.intern(), lockValue, lockExpireTime);
            System.out.println("司机:"+driverId+" 加锁成功,看门狗续约线程已启动");
        }else {
            System.out.println("司机:"+driverId+" 抢单失败,锁已被其他司机占用");
            return null;
        }

        try {
            // 模拟长耗时业务操作,远超锁初始过期时间,测试续约效果
            TimeUnit.SECONDS.sleep(30);
            System.out.println("司机:"+driverId+" 执行抢单核心业务逻辑");
            // 调用订单底层抢单业务
            boolean grabResult = orderService.grab(orderId, driverId);
            if(grabResult) {
                System.out.println("司机:"+driverId+" 抢单成功!订单已绑定当前司机");
            }else {
                System.out.println("司机:"+driverId+" 抢单失败,订单已被提前抢占");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁前置校验:只能删除自己持有锁,杜绝误删其他线程锁
            String currentLockValue = stringRedisTemplate.opsForValue().get(lockKey.intern());
            if(lockValue.equals(currentLockValue)) {
                stringRedisTemplate.delete(lockKey.intern());
                System.out.println("司机:"+driverId+" 业务执行完毕,成功释放抢单锁");
            }
        }
        return null;
    }
}

四、生产级首选:Redisson分布式锁实战落地(重点)

手写Redis锁虽然能通过不断迭代修复各类并发问题,实现基础可用,但在真实生产环境中仍存在明显短板。手动编写的续约逻辑健壮性差、不支持锁可重入、无法适配Redis集群、没有锁超时重试、异常容错机制简陋,一旦面对高并发、多实例、复杂业务场景,极易出现隐藏线上bug。

Redisson是Redis官方推荐、企业生产标配的分布式锁框架,彻底封装了Redis分布式锁所有底层复杂逻辑,把原子加解锁、自动看门狗续约、锁可重入、公平锁/读写锁/红锁适配、集群容错等一系列核心能力全部封装成熟,开发者无需关注底层原理,只需简单配置即可快速落地使用。

  • Redisson 的加锁机制:线程1去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。线程2也去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

  • Redisson 的还有一个重要的续约机制,只要客户端一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间。
    在这里插入图片描述

4.1 Redisson核心核心优势机制

  • 全程Lua脚本原子保障:所有加锁、解锁、锁续约、锁释放操作底层全部基于Lua脚本执行,单脚本命令原子性生效,杜绝并发错乱、中间态宕机问题,无需开发者手动保证原子操作。

  • 内置自动看门狗续生机能:业务线程加锁成功后,Redisson自动后台启动看门狗守护线程,默认每隔10秒检测锁状态,只要当前业务线程还持有锁、业务未执行完毕,就自动刷新锁过期时间,无需手动编写续约线程,彻底解决锁提前过期问题。

  • 天然支持锁可重入:底层记录线程加锁次数,同一业务线程可多次重复加锁,对应多次解锁,避免业务嵌套加锁逻辑导致的死锁问题,适配复杂业务嵌套场景。

  • 智能阻塞抢锁重试:加锁失败不会直接返回失败,线程自动循环自旋重试抢锁,适配多线程正常并发争抢场景,无需开发者手动编写自旋重试逻辑。

  • 多类型锁按需适配:内置可重入锁、公平锁、读写锁、联锁、红锁等多种锁模式,既能满足普通抢单互斥场景,也能适配缓存读写分离、多Redis节点强一致等复杂特殊业务。

4.2 第一步:项目引入Redisson Maven依赖

SpringBoot项目直接引入核心依赖,无需额外配置,适配绝大多数SpringBoot版本,兼容性稳定:

<!-- Redisson分布式锁核心依赖,生产稳定常用版本 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.8</version>
</dependency>

4.3 第二步:Redisson客户端配置类(SpringBoot整合)

编写配置类,初始化RedissonClient全局核心客户端Bean,统一配置Redis单机连接地址、数据库编号,后续所有业务锁全部复用该客户端,无需重复创建:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Redisson分布式锁全局配置类
 * 生产环境可扩展配置Redis集群、哨兵、密码认证等信息
 */
@Configuration
public class RedissonConfig {

    /**
     * 初始化Redisson核心客户端Bean,全局单例使用
     */
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 配置单机Redis连接信息,生产环境替换为真实Redis地址
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379")
              .setDatabase(0);
        // 创建并返回Redisson客户端实例
        return Redisson.create(config);
    }
}

4.4 第三步:编程式Redisson锁实战代码(基础手写调用)

业务代码手动编写加锁、解锁逻辑,直观易懂,适合简单短流程业务,新手快速上手调试,底层Redisson自动封装原子操作和看门狗续约:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

/**
 * 生产级Redisson分布式锁抢单实战实现(编程式基础用法)
 */
@Service
public class GrabRedissonLockImpl implements GrabOrderService {

    // 注入全局Redisson客户端
    @Resource
    private RedissonClient redissonClient;

    // 注入订单核心基础业务服务
    @Resource
    private OrderService orderService;

    @Override
    public String grabOrder(int orderId , int driverId){
        // 1. 按订单维度定义分布式锁Key,不同订单互不干扰,精准锁临界资源
        String lockKey = "order_grab_lock_" + orderId;
        // 2. 获取Redisson可重入锁实例
        RLock rLock = redissonClient.getLock(lockKey.intern());

        try {
            // 3. 核心加锁:默认30秒过期,自动看门狗持续续约,失败自动自旋重试
            rLock.lock();
            System.out.println("司机:"+driverId+" 获取Redisson分布式锁成功,开始执行抢单流程");

            // 执行核心抢单业务逻辑,无需关心锁过期、续约问题
            boolean grabResult = orderService.grab(orderId, driverId);
            if(grabResult) {
                System.out.println("司机:"+driverId+" 抢单成功!订单已正式绑定当前司机");
            }else {
                System.out.println("司机:"+driverId+" 抢单失败,订单已被其他司机抢先接单");
            }
        } finally {
            // 4. 安全释放锁:先判断锁是否存在、是否为当前线程持有,避免非法解锁报错
            if(rLock.isLocked() && rLock.isHeldByCurrentThread()) {
                rLock.unlock();
                System.out.println("司机:"+driverId+" 业务执行完毕,安全释放分布式锁");
            }
        }
        return null;
    }
}

4.5 第四步:生产极简必备!Redisson声明式注解锁(AOP实现)

编程式锁需要在每个业务方法中重复编写加锁、解锁模板代码,代码冗余、维护性差。企业生产开发主流推荐自定义注解+AOP切面声明式锁,核心优势:加锁解锁零侵入业务代码、统一锁规则管控、全局锁逻辑统一维护、支持SpEL动态拼接锁Key,适配所有复杂业务场景。

核心实现三步走:自定义分布式锁注解、AOP切面统一拦截加解锁、业务方法直接注解标注使用,无需任何重复锁相关代码。

4.5.1 第一步:自定义RedisLock核心注解

定义注解参数,支持动态锁Key、抢锁等待时间、锁过期时间,leaseTime=-1默认开启Redisson看门狗自动续约机制:

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

/**
 * Redisson声明式分布式锁自定义注解
 * 支持SpEL表达式动态拼接锁Key、自定义等待时间、续约时间
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {

    /**
     * 分布式锁Key,支持SpEL表达式动态取值
     */
    String key();

    /**
     * 抢锁最大等待时间,默认3秒
     */
    long waitTime() default 3;

    /**
     * 锁过期释放时间,默认-1
     * -1 = 启用Redisson看门狗自动续约续命
     */
    long leaseTime() default -1;
}

4.5.2 第二步:AOP切面统一锁处理核心类

通过Spring AOP环绕通知拦截标注@RedisLock注解的业务方法,自动解析SpEL动态锁Key、尝试抢锁、加锁失败抛异常、业务执行后自动释放锁,全程无需业务层干预:

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.core.annotation.Order;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.concurrent.TimeUnit;

/**
 * Redis分布式锁AOP切面拦截类
 * 统一实现声明式加锁、解锁、锁续约逻辑
 */
@Aspect
@Component
@Order(1)
@Slf4j
@RequiredArgsConstructor
public class RedisLockAspect {

    private final RedissonClient redissonClient;

    // SpEL表达式解析器,用于动态解析锁Key
    private static final ExpressionParser SPEL_PARSER = new SpelExpressionParser();

    /**
     * 环绕通知:拦截所有加了@RedisLock注解的方法
     */
    @Around("@annotation(redisLock)")
    public Object around(ProceedingJoinPoint pjp, RedisLock redisLock) throws Throwable {
        // 1. 解析SpEL表达式,动态生成业务锁Key
        EvaluationContext context = new StandardEvaluationContext();
        // 注入方法参数,供SpEL表达式取值使用
        Object[] args = pjp.getArgs();
        String[] parameterNames = org.springframework.core.LocalVariableTableParameterNameDiscoverer.INSTANCE.getParameterNames(pjp.getSignature().getMethod());
        if (parameterNames != null && args != null) {
            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], args[i]);
            }
        }
        // 解析最终锁Key
        String lockKey = SPEL_PARSER.parseExpression(redisLock.key()).getValue(context, String.class);
        log.info("声明式分布式锁开始抢锁,锁Key:{}", lockKey);

        // 2. 获取Redisson可重入锁实例
        RLock lock = redissonClient.getLock(lockKey);

        // 3. 尝试抢锁:自定义等待时间、过期时间,-1自动开启看门狗续约
        boolean locked = lock.tryLock(redisLock.waitTime(), redisLock.leaseTime(), TimeUnit.SECONDS);
        // 加锁失败,直接抛出业务异常,拒绝执行并发业务
        if (!locked) {
            throw new RuntimeException("获取分布式锁失败,订单正在被其他司机抢占,锁Key:" + lockKey);
        }

        // 4. 加锁成功,执行目标业务方法
        try {
            return pjp.proceed();
        } finally {
            // 5. 业务执行完毕,仅当前持有锁线程可解锁,防止误删锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.info("声明式分布式锁释放成功,锁Key:{}", lockKey);
            }
        }
    }
}

4.5.3 第三步:业务层极简使用(零锁代码侵入)

业务方法仅需添加一行@RedisLock注解,通过SpEL动态绑定订单ID生成锁Key,无需手动加锁、解锁、处理续约,极简开发、统一管控:

import org.springframework.stereotype.Service;
import javax.annotation.Resource;

/**
 * Redisson声明式注解锁抢单业务实现(生产极简版)
 * 零锁代码侵入,专注核心业务逻辑开发
 */
@Service
public class GrabRedissonAnnotationLockImpl implements GrabOrderService {

    @Resource
    private OrderService orderService;

    /**
     * 司机抢单核心方法,仅需添加注解自动实现分布式锁控制
     * key:SpEL表达式动态按订单ID生成唯一锁
     * waitTime:抢锁最大等待2秒
     * leaseTime:-1 开启Redisson自动看门狗续约
     */
    @Override
    @RedisLock(key = "'order:grab:lock:' + #orderId", waitTime = 2, leaseTime = -1)
    public String grabOrder(int orderId , int driverId){
        System.out.println("司机:"+driverId+" 声明式锁加锁成功,执行抢单核心业务");
        // 仅需编写纯抢单业务,无需任何锁相关操作
        boolean grabResult = orderService.grab(orderId, driverId);
        if(grabResult) {
            System.out.println("司机:"+driverId+" 抢单成功!订单已绑定");
        }else {
            System.out.println("司机:"+driverId+" 抢单失败,订单已被抢占");
        }
        return null;
    }
}

五、Redis分布式锁整体优缺点深度分析

5.1 核心优点

  • 高性能高并发适配性强:Redis基于内存读写操作,加锁、解锁、续约逻辑均为毫秒级响应,性能远超数据库悲观锁、乐观锁,完美适配打车抢单、电商秒杀、库存扣减等高并发核心业务场景,吞吐量支撑能力极强。

  • 轻量级低成本易落地:无需额外搭建专属中间件,依托项目现有Redis环境即可快速接入,代码开发量少、配置简单,运维成本极低,新手开发也能快速上手落地。

  • 集群适配性好,稳定性高:配合Redisson框架使用,自动实现看门狗续约、锁容错、异常重试、可重入等生产级能力,完美适配微服务集群多实例部署架构,满足绝大多数互联网公司业务生产要求。

  • 灵活性高,适配多场景:支持自定义锁过期时间、公平锁/非公平锁切换、锁等待策略配置,配合声明式注解可统一管控全局锁策略,适配不同业务并发优先级。

5.2 不可规避核心缺点

  • 主从异步复制存在锁失效风险:Redis主从集群数据同步为异步模式,客户端在主节点加锁成功后,锁数据还未同步到从节点,主节点突发宕机,从节点自动晋升为新主节点,其他客户端可再次加锁,出现多个客户端同时持有同一把锁的问题,导致并发数据错乱。

  • 非强一致性锁方案:Redis分布式锁属于最终一致性锁,无法实现分布式强数据一致性,对于银行转账、金融交易、资金对账等绝对零误差、零并发冲突的核心强一致场景,无法满足业务要求。

  • 看门狗续约少量消耗资源:Redisson看门狗后台守护线程持续运行,针对超长耗时业务,续约线程会持续占用少量服务器及Redis资源,极端超高并发场景需合理优化锁过期时间及续约频率。

六、全文核心总结

  1. 本地synchronized线程锁仅适用于单机单体架构,微服务集群多实例部署环境下本地锁完全失效,必须使用分布式锁解决跨进程、跨机器并发争抢共享资源问题。

  2. 手写Redis原生分布式锁适合学习底层原理,必须依次修复死锁、原子性失效、误删锁、锁超时四大核心bug,才能基础可用,但生产环境手写逻辑容错差、隐患多,不建议线上使用。

  3. Redisson锁分两种用法:编程式适合简单临时业务,生产长期项目一律首选声明式注解锁(AOP+自定义注解),业务零侵入、统一维护、极简开发。

  4. Redis分布式锁高性能、轻量级,适配绝大多数高并发普通业务;若业务需要金融级强数据一致性,建议放弃Redis锁,选用Zookeeper分布式锁或Redisson红锁方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苍煜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值