分布式锁实现方式:基于Redis的分布式锁实现(Spring Boot + Redis)

发布于:2025-07-03 ⋅ 阅读:(24) ⋅ 点赞:(0)

Redis实现分布式锁的原理

Redis分布式锁基于其单线程执行命令的特性,通过原子操作实现多节点间的互斥访问。下面从原理、实现、问题及优化四个方面详细解析:

1.原子性与互斥性

Redis分布式锁的核心是原子性操作

  1. 获取锁:使用SET key value NX EX timeout命令

    • NX(Not eXists):仅当key不存在时设置成功
    • EX timeout:设置过期时间,防止死锁
    • 原子性:Redis单线程执行命令,确保多客户端并发请求时只有一个能成功
  2. 释放锁:先验证锁持有者再删除

    • 必须使用Lua脚本保证原子性,避免误删其他线程的锁
-- 释放锁的Lua脚本
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
2.分布式锁的实现步骤

1. 获取锁流程

  • 客户端生成唯一标识(如UUID)作为锁的值
  • 执行SET lock_key unique_id NX EX 10(10秒过期)
  • 返回OK表示获取锁成功,否则失败

2. 释放锁流程

  • 客户端携带锁的唯一标识调用Lua脚本
  • 脚本先检查锁的值是否与传入标识一致
  • 一致则删除锁,返回1;不一致返回0

示例

1. 添加依赖

pom.xml中添加Spring Data Redis依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置Redis连接

application.yml中配置Redis服务器信息:

spring:
  redis:
    host: localhost
    port: 6379
    password: yourpassword  # 如果有密码
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0
3. 创建分布式锁接口

定义锁的基本操作:

public interface RedisLock {
    /**
     * 尝试获取锁
     * @param lockKey 锁的键
     * @param requestId 请求标识(用于释放锁时校验)
     * @param expireTime 锁的过期时间
     * @param timeUnit 时间单位
     * @return 是否成功获取锁
     */
    boolean tryLock(String lockKey, String requestId, long expireTime, TimeUnit timeUnit);
    
    /**
     * 释放锁
     * @param lockKey 锁的键
     * @param requestId 请求标识
     * @return 是否成功释放锁
     */
    boolean releaseLock(String lockKey, String requestId);
}
4. 实现分布式锁(重点)

使用RedisTemplate实现锁操作,关键在于:

  • 获取锁:使用setIfAbsent原子操作
  • 释放锁:使用Lua脚本保证原子性
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

@Component
public class RedisLockImpl implements RedisLock {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 释放锁的Lua脚本:先验证锁的持有者,再删除锁
    private static final DefaultRedisScript<Long> RELEASE_LOCK_SCRIPT;
    
    static {
        RELEASE_LOCK_SCRIPT = new DefaultRedisScript<>();
        RELEASE_LOCK_SCRIPT.setScriptText(
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end"
        );
        RELEASE_LOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(String lockKey, String requestId, long expireTime, TimeUnit timeUnit) {
        // 核心方法:原子性地设置锁和过期时间
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, requestId, expireTime, timeUnit);
        return result != null && result;
    }

    @Override
    public boolean releaseLock(String lockKey, String requestId) {
        // 使用Lua脚本保证原子性
        Long result = redisTemplate.execute(
            RELEASE_LOCK_SCRIPT,
            Collections.singletonList(lockKey),
            requestId
        );
        return result != null && result == 1L;
    }
}
5. 使用分布式锁
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class OrderService {

    @Autowired
    private RedisLock redisLock;

    public void createOrder(String orderId) {
        String lockKey = "order-lock:" + orderId;
        String requestId = UUID.randomUUID().toString();
        boolean locked = false;

        try {
            // 尝试获取锁,设置过期时间为10秒
            locked = redisLock.tryLock(lockKey, requestId, 10, TimeUnit.SECONDS);
            
            if (locked) {
                // 获得锁成功,执行关键业务逻辑
                System.out.println("获取锁成功,开始处理订单: " + orderId);
                // 模拟业务处理
                Thread.sleep(2000);
            } else {
                // 获得锁失败,处理失败逻辑
                System.out.println("获取锁失败,稍后重试或执行其他策略");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 无论如何都尝试释放锁,确保不会死锁
            if (locked) {
                redisLock.releaseLock(lockKey, requestId);
            }
        }
    }
}

setIfAbsent方法

RedisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit) 是实现分布式锁的核心方法,它对应Redis的命令:

SET key value NX EX timeout

关键点

  1. 原子性:该方法会原子性地完成三个操作:

    • 检查key是否存在
    • 如果不存在,则设置key的值
    • 同时设置key的过期时间
  2. 防止死锁

    • 必须设置过期时间,确保即使持有锁的进程崩溃,锁也会自动释放
    • 过期时间不宜过短(避免业务未完成锁就过期)或过长(影响性能)
  3. 唯一标识

    • value使用唯一的requestId(如UUID),用于标识锁的持有者
    • 释放锁时必须验证requestId,防止误删其他线程的锁

释放锁的原子性问题

释放锁时不能简单地直接删除key,必须先验证锁的持有者:

// 错误示例(非原子操作,有竞态条件)
if (redis.get(lockKey).equals(requestId)) {
    redis.delete(lockKey);
}

// 正确方式:使用Lua脚本保证原子性
Long result = redisTemplate.execute(RELEASE_LOCK_SCRIPT, 
                                 Collections.singletonList(lockKey), 
                                 requestId);