基于Java开发的Redis互斥锁写法(生产级规范)

基于Java开发的Redis互斥锁写法(生产级规范)

本文基于Java语言,结合RedisTemplate(Spring Boot集成Redis首选),整理生产环境中可用的Redis互斥锁写法,覆盖单节点简单场景、业务耗时不确定场景、分布式集群场景,严格遵循“高可用、防死锁、防误删、防并发冲突”原则,附完整代码、逻辑说明及生产级注意事项,适配Java项目实际开发需求。
前置依赖:Spring Boot项目中引入Redis依赖(Spring Data Redis),配置Redis连接信息(application.yml),注入RedisTemplate(或StringRedisTemplate),确保Redis连接正常。
<!-- Maven依赖示例 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- application.yml配置示例 -->
spring:
  redis:
    host: localhost
    port: 6379
    password: your_password
    database: 0
    lettuce:
      pool:
        max-active: 16  # 连接池最大连接数
        max-idle: 8     # 连接池最大空闲连接
        min-idle: 4     # 连接池最小空闲连接
        max-wait: 1000ms # 连接池最大等待时间 -->

一、基础版互斥锁(单节点Redis,简单并发场景)

1.1 核心逻辑

基于Redis的SET NX EX命令(原子操作)实现:仅当锁key不存在时,才设置key-value并指定过期时间,确保“判断不存在+设置值+设置过期时间”一步完成,避免并发漏洞;业务处理完成后,校验锁归属并手动释放,防止误删其他客户端的锁。
核心命令:SET lockKey lockValue NX EX expireSeconds(Redis 2.6.12+支持,原子性保证)。

1.2 代码实现(Spring Boot + RedisTemplate)

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 基础版Redis互斥锁(单节点,适用于简单并发场景)
 */
@Component
public class RedisBasicMutexLock {

    // 注入StringRedisTemplate(优先使用,避免序列化问题)
    private final StringRedisTemplate stringRedisTemplate;

    // 构造器注入(Spring 4.3+支持,无需@Autowired)
    public RedisBasicMutexLock(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取互斥锁
     * @param lockKey 锁的key(需区分业务资源,如"lock:product:1001")
     * @param expireSeconds 锁的过期时间(秒),必须大于业务处理时间
     * @return 锁的value(唯一标识,用于释放锁校验),null表示获取锁失败
     */
    public String acquireLock(String lockKey, long expireSeconds) {
        // 生成唯一value(UUID,避免误释放其他客户端的锁)
        String lockValue = UUID.randomUUID().toString();
        // 原子操作:SET NX EX,key不存在则设置,同时指定过期时间
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
        // 若获取锁成功,返回唯一value;失败返回null
        return Boolean.TRUE.equals(success) ? lockValue : null;
    }

    /**
     * 释放互斥锁(校验锁归属,避免误删)
     * @param lockKey 锁的key
     * @param lockValue 获取锁时返回的唯一value
     * @return true:释放成功;false:释放失败(锁不存在或归属不符)
     */
    public boolean releaseLock(String lockKey, String lockValue) {
        // 1. 获取当前锁的value
        String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
        // 2. 校验:锁存在且value与当前客户端的value一致,才释放
        if (lockValue != null && lockValue.equals(currentValue)) {
            // 手动删除锁
            stringRedisTemplate.delete(lockKey);
            return true;
        }
        return false;
    }
}

// 业务使用示例(Service层)
@Component
public class ProductService {

    private final RedisBasicMutexLock basicMutexLock;

    public ProductService(RedisBasicMutexLock basicMutexLock) {
        this.basicMutexLock = basicMutexLock;
    }

    // 模拟:商品库存扣减(需要互斥锁避免并发超卖)
    public boolean deductStock(Long productId) {
        String lockKey = "lock:product:stock:" + productId;
        String lockValue = null;
        try {
            // 尝试获取锁,过期时间设为10秒(业务处理预计5秒)
            lockValue = basicMutexLock.acquireLock(lockKey, 10);
            if (lockValue == null) {
                // 获取锁失败,返回失败(或重试)
                System.out.println("获取锁失败,当前有其他线程正在处理商品库存");
                return false;
            }
            // 执行业务逻辑:查询库存、扣减库存(模拟耗时操作)
            System.out.println("获取锁成功,开始扣减商品" + productId + "库存");
            Thread.sleep(5000); // 模拟业务耗时
            System.out.println("商品库存扣减完成");
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("业务处理异常");
            return false;
        } finally {
            // 无论业务是否成功,都释放锁(避免死锁)
            if (lockValue != null) {
                basicMutexLock.releaseLock(lockKey, lockValue);
                System.out.println("锁已释放");
            }
        }
    }
}

1.3 核心注意事项

  • 锁key命名规范:必须携带业务标识和资源ID(如“lock:product:stock:1001”),避免不同业务、不同资源共用一个key,导致锁冲突。
  • lockValue必须唯一:使用UUID生成,核心目的是避免“误释放锁”——若不校验value,客户端A获取的锁,可能被客户端B误删除(如A处理超时,锁未过期前B获取锁,A处理完后删除B的锁)。
  • 过期时间设置:必须大于业务处理的最大耗时(建议预留2-3倍冗余,如业务耗时5秒,过期时间设10-15秒),避免锁提前过期导致并发冲突。
  • finally块释放锁:无论业务是否正常执行、是否抛出异常,都必须在finally块中释放锁,防止客户端宕机或业务异常导致锁无法释放,引发死锁。
  • 适用场景:单节点Redis、并发量适中、业务处理时间固定的简单场景(如简单的库存扣减、缓存更新)。

二、进阶版互斥锁(带锁续期,解决业务耗时不确定问题)

2.1 核心优化点

基础版的痛点:若业务处理时间超过锁的过期时间,锁会自动失效,导致并发冲突。进阶版通过“后台线程续期”机制,在业务未处理完成时,自动延长锁的过期时间,避免锁提前失效;同时支持自动停止续期、优雅释放锁。
核心逻辑:获取锁成功后,启动一个守护线程(不阻塞主线程),每隔“过期时间的1/3”秒,校验锁是否仍归当前客户端所有,若归属于当前客户端,则延长锁的过期时间;业务处理完成后,停止续期线程并释放锁。

2.2 代码实现(带续期机制)

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 进阶版Redis互斥锁(带锁续期,解决业务耗时不确定问题)
 */
@Component
public class RedisRenewMutexLock {

    private final StringRedisTemplate stringRedisTemplate;

    public RedisRenewMutexLock(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取锁并启动续期线程
     * @param lockKey 锁的key
     * @param expireSeconds 锁的初始过期时间(秒)
     * @return 锁信息(包含lockKey、lockValue、续期开关),null表示获取锁失败
     */
    public LockInfo acquireLock(String lockKey, long expireSeconds) {
        String lockValue = UUID.randomUUID().toString();
        // 原子操作获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
        if (Boolean.FALSE.equals(success)) {
            return null;
        }
        // 初始化锁信息,设置续期开关(原子布尔值,保证线程安全)
        LockInfo lockInfo = new LockInfo(lockKey, lockValue, expireSeconds);
        // 启动续期线程(守护线程,主线程结束后自动退出)
        startRenewThread(lockInfo);
        return lockInfo;
    }

    /**
     * 启动续期线程
     * 续期间隔:过期时间的1/3(如过期10秒,每3秒续期一次)
     */
    private void startRenewThread(LockInfo lockInfo) {
        Thread renewThread = new Thread(() -> {
            while (!lockInfo.isStopRenew()) {
                try {
                    // 每隔过期时间的1/3秒,执行一次续期
                    Thread.sleep(lockInfo.getExpireSeconds() * 1000 / 3);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
                // 校验锁是否归当前客户端所有,若是则续期
                String currentValue = stringRedisTemplate.opsForValue().get(lockInfo.getLockKey());
                if (lockInfo.getLockValue().equals(currentValue)) {
                    // 续期:重置锁的过期时间
                    stringRedisTemplate.expire(lockInfo.getLockKey(), lockInfo.getExpireSeconds(), TimeUnit.SECONDS);
                    System.out.println("锁续期成功,key:" + lockInfo.getLockKey() + ",新过期时间:" + lockInfo.getExpireSeconds() + "秒");
                } else {
                    // 锁已失效或归属其他客户端,停止续期
                    lockInfo.setStopRenew(true);
                }
            }
        });
        // 设为守护线程,避免主线程结束后,续期线程仍占用资源
        renewThread.setDaemon(true);
        renewThread.start();
    }

    /**
     * 释放锁(停止续期,校验归属后删除)
     * @param lockInfo 获取锁时返回的锁信息
     * @return true:释放成功;false:释放失败
     */
    public boolean releaseLock(LockInfo lockInfo) {
        if (lockInfo == null) {
            return false;
        }
        // 1. 停止续期线程
        lockInfo.setStopRenew(true);
        // 2. 校验锁归属
        String currentValue = stringRedisTemplate.opsForValue().get(lockInfo.getLockKey());
        if (lockInfo.getLockValue().equals(currentValue)) {
            // 3. 删除锁
            stringRedisTemplate.delete(lockInfo.getLockKey());
            System.out.println("锁释放成功,key:" + lockInfo.getLockKey());
            return true;
        }
        System.out.println("锁已失效或归属其他客户端,无需释放,key:" + lockInfo.getLockKey());
        return false;
    }

    /**
     * 锁信息封装(存储锁的核心信息,线程安全)
     */
    public static class LockInfo {
        private final String lockKey;
        private final String lockValue;
        private final long expireSeconds;
        // 续期开关(原子布尔值,保证多线程操作安全)
        private final AtomicBoolean stopRenew = new AtomicBoolean(false);

        public LockInfo(String lockKey, String lockValue, long expireSeconds) {
            this.lockKey = lockKey;
            this.lockValue = lockValue;
            this.expireSeconds = expireSeconds;
        }

        // getter/setter
        public String getLockKey() { return lockKey; }
        public String getLockValue() { return lockValue; }
        public long getExpireSeconds() { return expireSeconds; }
        public boolean isStopRenew() { return stopRenew.get(); }
        public void setStopRenew(boolean stop) { stopRenew.set(stop); }
    }
}

// 业务使用示例
@Component
public class OrderService {

    private final RedisRenewMutexLock renewMutexLock;

    public OrderService(RedisRenewMutexLock renewMutexLock) {
        this.renewMutexLock = renewMutexLock;
    }

    // 模拟:订单创建(业务耗时不确定,需锁续期)
    public boolean createOrder(Long orderId) {
        String lockKey = "lock:order:create:" + orderId;
        RedisRenewMutexLock.LockInfo lockInfo = null;
        try {
            // 获取锁,初始过期时间10秒(业务耗时可能超过10秒)
            lockInfo = renewMutexLock.acquireLock(lockKey, 10);
            if (lockInfo == null) {
                System.out.println("获取锁失败,当前有其他线程正在创建订单" + orderId);
                return false;
            }
            // 模拟业务耗时(超过初始过期时间)
            System.out.println("获取锁成功,开始创建订单" + orderId);
            Thread.sleep(15000); // 耗时15秒,续期机制会生效
            System.out.println("订单" + orderId + "创建完成");
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("订单创建异常");
            return false;
        } finally {
            // 释放锁(自动停止续期)
            renewMutexLock.releaseLock(lockInfo);
        }
    }
}

2.3 核心注意事项

  • 续期线程设计:采用守护线程,避免主线程结束后,续期线程仍在运行,浪费系统资源;续期间隔设为“过期时间的1/3”,既避免频繁续期,又能确保锁不会提前过期。
  • 线程安全:使用AtomicBoolean作为续期开关,保证多线程环境下续期开关的操作安全,避免出现“续期线程无法停止”的问题。
  • 锁信息封装:将lockKey、lockValue、过期时间、续期开关封装为LockInfo类,便于管理和传递,提升代码可读性和可维护性。
  • 异常处理:续期线程中捕获InterruptedException,避免线程异常挂起;业务线程中也需处理中断异常,确保程序优雅退出。
  • 适用场景:单节点Redis、业务处理时间不确定(如复杂的订单处理、数据同步)、并发量中等的场景。

三、分布式互斥锁(Redis Cluster/主从架构,生产推荐)

3.1 核心痛点解决

单节点Redis的互斥锁,存在“主节点宕机,锁丢失”的风险(如主节点持有锁key,未同步到从节点,主从切换后,从节点无锁key,其他客户端可重新获取锁)。
分布式版基于Redlock算法(Redis官方推荐),核心逻辑:在Redis集群的多个主节点上获取锁,只有当超过半数主节点获取锁成功,才算整体获取锁成功;释放锁时,需在所有获取锁成功的节点上删除锁,确保集群环境下的锁安全性和高可用性。
Java实现:使用Redis官方推荐的Redisson框架(封装了Redlock算法,无需手动实现多节点校验,支持自动续期、分布式锁的各种高级特性,生产环境首选)。

3.2 代码实现(Redisson + Spring Boot)

<!-- Maven依赖(Redisson) -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version> <!-- 版本与Spring Boot版本适配,可自行调整 -->
</dependency>

<!-- application.yml配置(Redisson集群配置,适配Redis Cluster) -->
spring:
  redis:
    cluster:
      nodes:
        - localhost:6379
        - localhost:6380
        - localhost:6381
        - localhost:6382
        - localhost:6383
        - localhost:6384
    password: your_password

# Redisson配置(可选,默认配置可满足大部分场景)
redisson:
  lock:
    watchdog-timeout: 30000 # 看门狗续期超时时间(默认30秒)
  threads: 16 # 处理线程数
  netty-threads: 32 # netty线程数

// 分布式锁实现(基于Redisson)
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

/**
 * 分布式Redis互斥锁(基于Redisson,适配Redis Cluster/主从架构,生产级)
 */
@Component
public class RedisDistributedMutexLock {

    // 注入RedissonClient(Redisson自动配置,无需手动创建)
    private final RedissonClient redissonClient;

    public RedisDistributedMutexLock(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    /**
     * 获取分布式锁(自动续期,默认看门狗机制)
     * @param lockKey 锁的key(区分业务资源)
     * @param waitTime 获取锁的等待时间(秒):获取不到锁时,最多等待多久
     * @param leaseTime 锁的租赁时间(秒):若为-1,启用看门狗自动续期(默认30秒,可配置)
     * @return RLock:锁对象,用于释放锁;null:获取锁失败
     */
    public RLock acquireLock(String lockKey, long waitTime, long leaseTime) {
        // 获取分布式锁(公平锁/非公平锁,默认非公平锁;可指定为公平锁:redissonClient.getFairLock(lockKey))
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁:waitTime等待时间,leaseTime租赁时间,时间单位秒
            boolean success = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
            return success ? lock : null;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    /**
     * 释放分布式锁
     * @param lock 获取锁时返回的RLock对象
     */
    public void releaseLock(RLock lock) {
        if (lock != null && lock.isHeldByCurrentThread()) {
            // 仅释放当前线程持有的锁,避免误释放其他线程的锁
            lock.unlock();
            System.out.println("分布式锁释放成功,key:" + lock.getName());
        }
    }
}

// 业务使用示例(分布式场景:跨服务商品库存扣减)
@Component
public class DistributedProductService {

    private final RedisDistributedMutexLock distributedMutexLock;

    public DistributedProductService(RedisDistributedMutexLock distributedMutexLock) {
        this.distributedMutexLock = distributedMutexLock;
    }

    public boolean deductDistributedStock(Long productId) {
        String lockKey = "lock:distributed:product:stock:" + productId;
        RLock lock = null;
        try {
            // 获取锁:等待时间3秒(获取不到锁则超时),租赁时间-1(启用看门狗自动续期)
            lock = distributedMutexLock.acquireLock(lockKey, 3, -1);
            if (lock == null) {
                System.out.println("获取分布式锁失败,当前有其他服务正在处理商品" + productId + "库存");
                return false;
            }
            // 执行业务逻辑:跨服务查询库存、扣减库存(模拟分布式场景)
            System.out.println("获取分布式锁成功,开始跨服务扣减商品" + productId + "库存");
            Thread.sleep(8000); // 模拟业务耗时
            System.out.println("商品" + productId + "库存扣减完成(分布式场景)");
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("分布式库存扣减异常");
            return false;
        } finally {
            // 释放锁
            distributedMutexLock.releaseLock(lock);
        }
    }
}

3.3 核心说明

  • Redisson优势:封装了Redlock算法,自动处理多节点锁的获取、续期、释放,无需手动实现复杂逻辑;支持公平锁、非公平锁、可重入锁、读写锁等多种锁类型,适配不同分布式场景。
  • 看门狗机制:当leaseTime设为-1时,Redisson会启动看门狗线程,每隔10秒(默认)自动续期锁的过期时间(默认续期为30秒),确保业务未处理完成时,锁不会过期;业务处理完成后,手动释放锁,看门狗自动停止。
  • 集群要求:Redis集群需为Cluster架构(至少3个主节点),或主从+哨兵架构,确保单个节点宕机后,锁仍能正常工作(超过半数节点持有锁,即可保证锁有效)。
  • 锁的释放:必须使用获取锁时返回的RLock对象释放,Redisson会自动校验锁的归属,避免误释放;同时,isHeldByCurrentThread()方法可判断当前线程是否持有锁,进一步提升安全性。

3.4 核心注意事项

  • Redisson版本适配:需根据Spring Boot版本选择对应的Redisson版本,避免版本冲突(如Spring Boot 2.7.x,推荐Redisson 3.23.x)。
  • 等待时间(waitTime)设置:根据业务并发量合理设置,避免等待时间过长导致请求超时,或过短导致获取锁失败率过高(如并发量高,可设3-5秒)。
  • 租赁时间(leaseTime)设置:若业务处理时间固定,可设置具体的租赁时间(大于业务耗时);若时间不确定,设为-1,启用看门狗自动续期(推荐)。
  • 避免锁滥用:分布式锁会降低系统并发性能,仅在需要解决跨服务、跨节点并发冲突的场景使用(如分布式库存扣减、分布式订单创建)。
  • 容错性:Redisson支持失败重试机制,当某个Redis节点宕机时,会自动尝试其他节点获取锁,提升分布式锁的可用性。

四、Redis读数据库时的双重锁Java代码方案

4.1 核心场景与设计思路

核心场景:Redis缓存穿透、击穿场景下,读取数据库数据并缓存到Redis时,需通过“双重锁”(第一重Redis锁、第二重本地锁)避免高并发下多次穿透到数据库,同时兼顾性能与安全性。
设计思路(双重锁机制):
  1. 第一重锁(Redis分布式锁):拦截跨线程、跨服务的并发请求,确保同一时刻只有一个请求能进入“读数据库+缓存更新”流程,避免分布式场景下的并发穿透。
  2. 第二重锁(本地锁,synchronized):拦截同一进程内的多线程并发请求,减少Redis锁的竞争频率,提升性能(避免同一进程内多个线程频繁请求Redis获取锁)。
核心流程:先查Redis → 缓存未命中 → 加本地锁 → 再次查Redis(避免本地线程已缓存) → 仍未命中 → 加Redis锁 → 读数据库 → 更新Redis → 释放双锁。

4.2 生产级代码实现(Spring Boot + Redisson)

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;

/**
 * Redis读数据库时的双重锁实现(生产级,适配分布式场景)
 * 双重锁:本地锁(synchronized)+ Redis分布式锁
 */
@Component
public class RedisDoubleLockReadService {

    // 注入Redis和Redisson客户端
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    // 本地锁对象(全局唯一,确保同一进程内锁的唯一性)
    private final Object localLock = new Object();

    // 构造器注入
    public RedisDoubleLockReadService(StringRedisTemplate stringRedisTemplate, RedissonClient redissonClient) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.redissonClient = redissonClient;
    }

    /**
     * 双重锁读取数据(先查Redis,未命中则读库并缓存)
     * @param cacheKey Redis缓存key(如"cache:product:1001")
     * @param lockKey Redis分布式锁key(如"lock:read:product:1001")
     * @param productId 业务资源ID(如商品ID)
     * @return 读取到的数据(String类型,可根据业务封装为实体类)
     */
    public String readDataWithDoubleLock(String cacheKey, String lockKey, Long productId) {
        // 1. 第一重校验:查询Redis缓存,命中则直接返回
        String cacheData = stringRedisTemplate.opsForValue().get(cacheKey);
        if (StringUtils.hasText(cacheData)) {
            System.out.println("Redis缓存命中,直接返回数据");
            return cacheData;
        }

        // 2. 第二重校验:加本地锁(synchronized),拦截同一进程内多线程并发
        synchronized (localLock) {
            // 2.1 再次查询Redis(避免本地其他线程已缓存数据,减少Redis锁竞争)
            cacheData = stringRedisTemplate.opsForValue().get(cacheKey);
            if (StringUtils.hasText(cacheData)) {
                System.out.println("本地锁内Redis缓存命中,返回数据");
                return cacheData;
            }

            // 3. 第三重:加Redis分布式锁,拦截跨进程/跨服务并发
            RLock redisLock = redissonClient.getLock(lockKey);
            try {
                // 尝试获取Redis锁:等待3秒,租赁时间10秒(启用看门狗自动续期)
                boolean lockSuccess = redisLock.tryLock(3, -1, TimeUnit.SECONDS);
                if (!lockSuccess) {
                    // 获取锁失败,返回默认值或提示,避免阻塞
                    System.out.println("获取Redis分布式锁失败,无法读取数据库");
                    return "暂时无法获取数据,请稍后重试";
                }

                // 4. 锁获取成功,读取数据库(模拟业务逻辑)
                System.out.println("Redis锁获取成功,开始读取数据库数据");
                String dbData = readDataFromDb(productId); // 实际场景中调用DAO层方法

                // 5. 读取数据库成功后,更新Redis缓存(设置过期时间,避免缓存雪崩)
                if (StringUtils.hasText(dbData)) {
                    stringRedisTemplate.opsForValue().set(cacheKey, dbData, 300, TimeUnit.SECONDS);
                    System.out.println("数据库数据读取成功,已更新Redis缓存");
                }

                // 6. 返回数据库数据
                return dbData;
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("双重锁读取数据异常");
                return null;
            } finally {
                // 释放Redis分布式锁(仅当前线程持有锁时释放)
                if (redisLock != null && redisLock.isHeldByCurrentThread()) {
                    redisLock.unlock();
                    System.out.println("Redis分布式锁已释放");
                }
            }
        }
    }

    /**
     * 模拟从数据库读取数据(实际场景中替换为DAO层查询)
     * @param productId 商品ID
     * @return 数据库中的商品数据
     */
    private String readDataFromDb(Long productId) {
        // 模拟数据库查询耗时
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        // 模拟返回数据库数据(实际场景中封装为实体类,此处用String简化)
        return "商品ID:" + productId + ",商品名称:测试商品,库存:100";
    }
}

// 业务使用示例(Controller/Service层)
@Component
public class ProductReadService {

    private final RedisDoubleLockReadService doubleLockReadService;

    public ProductReadService(RedisDoubleLockReadService doubleLockReadService) {
        this.doubleLockReadService = doubleLockReadService;
    }

    // 读取商品详情(双重锁保护,避免缓存穿透)
    public String getProductDetail(Long productId) {
        // 定义缓存key和锁key(遵循命名规范)
        String cacheKey = "cache:product:detail:" + productId;
        String lockKey = "lock:read:product:detail:" + productId;
        // 调用双重锁读取方法
        return doubleLockReadService.readDataWithDoubleLock(cacheKey, lockKey, productId);
    }
}

4.3 核心注意事项

  • 本地锁设计:使用synchronized锁(锁全局唯一对象localLock),确保同一进程内所有线程竞争同一把锁,避免同一进程内多线程频繁请求Redis锁,提升性能。
  • 双重校验缓存:两次查询Redis(锁前、本地锁内),避免“缓存刚被其他线程更新”导致的无效数据库查询,进一步减少穿透。
  • Redis锁选型:使用Redisson分布式锁,支持自动续期、锁归属校验,避免死锁和误释放,适配分布式集群场景;若为单节点Redis,可替换为基础版Redis锁。
  • 锁粒度控制:锁key需绑定具体业务资源(如“lock:read:product:detail:1001”),避免全局锁,提升并发能力。
  • 异常处理:捕获InterruptedException,确保线程中断后能优雅退出;释放Redis锁时,必须校验锁归属,避免误释放其他线程的锁。
  • 缓存更新:读取数据库后,必须设置Redis缓存过期时间(如300秒),避免缓存雪崩;同时可结合业务场景,设置合理的过期时间冗余。
  • 适用场景:高并发读场景、缓存穿透/击穿风险较高的场景(如商品详情查询、用户信息查询),兼顾性能与数据一致性。

4.4 流程展示理解

下图借鉴:https://blog.csdn.net/weixin_63434398/article/details/156640252

 请求处理流程

用户请求


┌───────────────────────┐
│ 构建 fullShortUrl │
└───────────────────────┘

┌─────────────────────────┴─────────────────────────┐
│ 无锁区域 │
│ ┌─────────────┐ 命中 ┌──────────────┐ │
│ │ 查缓存 │────────────▶│ 直接跳转 │ │
│ └─────────────┘ └──────────────┘ │
│ │ 未命中 │
│ ▼ │
│ ┌─────────────┐ 不存在 ┌──────────────┐ │
│ │ 布隆过滤器 │────────────▶│ 返回 404 │ │
│ └─────────────┘ └──────────────┘ │
│ │ 可能存在 │
│ ▼ │
│ ┌─────────────┐ 存在 ┌──────────────┐ │
│ │ 空值缓存 │────────────▶│ 返回 404 │ │
│ └─────────────┘ └──────────────┘ │
│ │ 不存在 │
└────────┴──────────────────────────────────────────┘


┌───────────────────────┐
│ 获取分布式锁 │
│ lock.lock() │
└───────────────────────┘

┌────────┴──────────────────────────────────────────┐
│ 有锁区域 │
│ ┌─────────────┐ 命中 ┌──────────────┐ │
│ │ 再查缓存 │────────────▶│ 直接跳转 │ │
│ │ (双重判定) │ │ (别人加载的) │ │
│ └─────────────┘ └──────────────┘ │
│ │ 仍未命中 │
│ ▼ │
│ ┌─────────────┐ 存在 ┌──────────────┐ │
│ │ 再查空值 │────────────▶│ 返回 404 │ │
│ │ (双重判定) │ └──────────────┘ │
│ └─────────────┘ │
│ │ 仍不存在 │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 查询数据库 │ │
│ │ 路由表 → 短链接表 → 写入缓存 → 跳转 │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘


┌───────────────────────┐
│ 释放锁 │
│ lock.unlock() │
└───────────────────────┘

并发场景时序图

假设三个请求几乎同时到来,缓存为空

时间轴 ──────────────────────────────────────────────────────▶

请求A ─────┬──────────────────────────────────────────────────
│ 查缓存 → 未命中
│ 查布隆 → 可能存在
│ 查空值 → 不存在
│ 获取锁 ✓
│ 双重判定 → 仍未命中
│ 查数据库...
│ 写入缓存 ◀──────────────── 这时候缓存有值了
│ 释放锁
└──▶ 跳转成功

请求B ───────────┬────────────────────────────────────────────
│ 查缓存 → 未命中
│ 查布隆 → 可能存在
│ 查空值 → 不存在
│ 等待锁... ⏳
│ │
│ ▼ (A释放锁后)
│ 获取锁 ✓
│ 双重判定 → 命中!(A已写入)
│ 释放锁
└──▶ 直接跳转,没查库!

请求C ───────────────────────────────────────────────────┬────
│ 查缓存 → 命中!
└──▶ 直接跳转,没加锁!

五、生产级通用规范(所有写法必遵循)

  • 连接池配置:必须配置Redis连接池(lettuce或jedis),设置合理的最大连接数、空闲连接数、等待时间,避免频繁创建、关闭连接,提升性能和稳定性。
  • 锁key命名规范:统一格式为“lock:业务模块:资源标识”(如“lock:product:stock:1001”“lock:order:create:2024”),确保锁的唯一性,避免冲突。
  • 异常处理:所有锁的操作(获取、释放、续期)都需捕获异常(如Redis连接异常、中断异常),避免程序崩溃,同时记录日志,便于故障排查。
  • 锁的粒度:尽量缩小锁的粒度,避免使用全局锁(如“lock:product:all”),改为细粒度锁(如“lock:product:stock:1001”),提升系统并发能力。
  • 日志记录:在获取锁、释放锁、续期、获取锁失败等关键节点,记录详细日志(如锁key、客户端标识、操作时间),便于排查并发问题。
  • 避免长时间持有锁:业务逻辑尽量简洁,减少锁的持有时间(如将非核心逻辑移出锁的范围),避免影响系统并发性能。
  • 压测验证:上线前,对互斥锁进行压测,模拟高并发场景,验证锁的有效性、性能和稳定性,避免出现死锁、并发冲突等问题。

六、总结

1. 单节点简单场景:使用基础版(RedisTemplate + SET NX EX),代码简洁,满足简单并发需求;
2. 单节点业务耗时不确定场景:使用进阶版(带后台线程续期),避免锁提前过期,提升可靠性;
3. 分布式集群场景:使用Redisson分布式锁(推荐),基于Redlock算法,适配Redis Cluster/主从架构,支持自动续期、高可用,是生产环境首选方案;
4. Redis读数据库场景:使用“本地锁+synchronized+Redis分布式锁”双重锁方案,有效避免缓存穿透、击穿,兼顾性能与安全性。
Java开发中,优先使用Spring Data Redis(RedisTemplate)或Redisson框架,避免手动编写复杂的原子命令,确保锁的安全性和可维护性;同时严格遵循生产级规范,规避死锁、误删锁、并发冲突等问题。
posted @ 2026-03-23 22:21  ConfidentLiu  阅读(8)  评论(0)    收藏  举报