Redis实现分布式锁

发布于:2025-05-08 ⋅ 阅读:(10) ⋅ 点赞:(0)

分布式锁是分布式系统中解决资源竞争问题的重要机制。Redis凭借其高性能和原子性操作,成为实现分布式锁的热门选择。本文将详细介绍如何使用Java和Redis实现分布式锁,并重点讲解如何通过Lua脚本保证锁操作的原子性。

一、分布式锁的基本要求

一个可靠的分布式锁应满足以下条件:

  1. 互斥性:同一时刻只有一个客户端能持有锁

  2. 避免死锁:即使客户端崩溃,锁也能自动释放

  3. 容错性:只要大部分Redis节点正常运行,客户端就能获取和释放锁

  4. 释放锁的正确性:只能由锁的持有者释放锁

二、不使用Lua脚本的基础实现

1. 基础分布式锁实现(Java + StringRedisTemplate)

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

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

@Component
public class BasicRedisLock {

    private final StringRedisTemplate stringRedisTemplate;
    
    public BasicRedisLock(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取锁
     * @param lockKey 锁的key
     * @param expireTime 过期时间(秒)
     * @return 锁的value(用于释放锁时验证)
     */
    public String lock(String lockKey, long expireTime) {
        String value = UUID.randomUUID().toString();
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success) ? value : null;
    }

    /**
     * 释放锁
     * @param lockKey 锁的key
     * @param value 锁的value
     * @return 是否释放成功
     */
    public boolean unlock(String lockKey, String value) {
        // 1. 获取当前锁的值
        String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
        
        // 2. 验证是否是自己的锁
        if (value.equals(currentValue)) {
            // 3. 删除锁
            return stringRedisTemplate.delete(lockKey);
        }
        return false;
    }
}

2. 基础实现的使用示例

@RestController
public class OrderController {

    @Autowired
    private BasicRedisLock basicRedisLock;
    
    @PostMapping("/createOrder")
    public String createOrder() {
        String lockKey = "order_lock";
        String lockValue = null;
        
        try {
            // 尝试获取锁
            lockValue = basicRedisLock.lock(lockKey, 30);
            if (lockValue == null) {
                return "系统繁忙,请稍后再试";
            }
            
            // 执行业务逻辑
            return "订单创建成功";
        } finally {
            // 释放锁
            if (lockValue != null) {
                basicRedisLock.unlock(lockKey, lockValue);
            }
        }
    }
}

3. 基础实现的缺点

  1. 非原子性操作问题

    • 释放锁的操作分为"获取值"、"比较值"和"删除键"三步,不是原子操作

    • 在比较值和删除键之间,锁可能已过期并被其他客户端获取,导致误删别人的锁

  2. 网络延迟问题

    • 客户端A获取锁并执行时间过长,锁已自动释放

    • 客户端B获取了锁

    • 客户端A执行完任务后,仍会尝试释放锁,可能释放客户端B的锁

  3. 性能问题

    • 每次释放锁需要至少2次Redis操作(GET+DEL)

三、使用Lua脚本优化实现

1. Lua脚本实现的分布式锁

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

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

@Component
public class LuaRedisLock {

    private final StringRedisTemplate stringRedisTemplate;
    
    public LuaRedisLock(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // 获取锁(与基础实现相同)
    public String lock(String lockKey, long expireTime) {
        String value = UUID.randomUUID().toString();
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success) ? value : null;
    }

    // 使用Lua脚本释放锁
    public boolean unlock(String lockKey, String value) {
        // Lua脚本
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                          "    return redis.call('del', KEYS[1]) " +
                          "else " +
                          "    return 0 " +
                          "end";
        
        // 创建Redis脚本对象
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(luaScript);
        redisScript.setResultType(Long.class);
        
        // 执行脚本
        Long result = stringRedisTemplate.execute(
            redisScript, 
            Collections.singletonList(lockKey), 
            value
        );
        
        return result != null && result == 1;
    }
}

2. Lua脚本详解

Lua脚本语法说明
-- KEYS[1] 表示第一个键参数
-- ARGV[1] 表示第一个非键参数
-- redis.call() 用于执行Redis命令

-- 脚本逻辑:
if redis.call('get', KEYS[1]) == ARGV[1] then  -- 如果锁的值等于传入的值
    return redis.call('del', KEYS[1])          -- 则删除这个键
else
    return 0                                   -- 否则返回0表示失败
end
Lua脚本在Redis中的优势
  1. 原子性:整个脚本作为一个整体执行,执行期间不会被其他命令打断

  2. 减少网络开销:多个操作合并为一个脚本,减少客户端与Redis的交互次数

  3. 灵活性:可以编写复杂的逻辑来处理各种场景

3. Lua脚本实现的使用示例

@RestController
public class PaymentController {

    @Autowired
    private LuaRedisLock luaRedisLock;
    
    @PostMapping("/pay")
    public String payOrder(@RequestParam String orderId) {
        String lockKey = "pay_lock:" + orderId;
        String lockValue = null;
        
        try {
            // 尝试获取锁,设置30秒过期
            lockValue = luaRedisLock.lock(lockKey, 30);
            if (lockValue == null) {
                return "支付处理中,请勿重复提交";
            }
            
            // 执行业务逻辑
            processPayment(orderId);
            
            return "支付成功";
        } finally {
            // 释放锁
            if (lockValue != null) {
                luaRedisLock.unlock(lockKey, lockValue);
            }
        }
    }
    
    private void processPayment(String orderId) {
        // 支付处理逻辑
    }
}

四、两种实现的对比

特性 基础实现 Lua脚本实现
原子性 非原子操作 原子操作
安全性 可能误删别人的锁 不会误删别人的锁
网络开销 至少2次Redis操作 1次Redis操作
实现复杂度 简单 需要了解Lua脚本
性能 较低 较高
适用场景 对安全性要求不高的简单场景 对安全性和性能有要求的场景

五、Lua脚本的更多用法

1. 带重试的获取锁脚本

public String lockWithRetry(String lockKey, long expireTime, long waitTime) {
    String value = UUID.randomUUID().toString();
    
    String luaScript = "local wait = tonumber(ARGV[3]) * 1000 " +
                      "local endTime = redis.call('time')[1] * 1000 + wait " +
                      "while redis.call('time')[1] * 1000 < endTime do " +
                      "    if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
                      "        redis.call('expire', KEYS[1], ARGV[2]) " +
                      "        return ARGV[1] " +
                      "    end " +
                      "    -- 短暂休眠 " +
                      "    redis.call('echo', 'waiting...') " +
                      "end " +
                      "return nil";
    
    DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptText(luaScript);
    redisScript.setResultType(String.class);
    
    return stringRedisTemplate.execute(
        redisScript,
        Collections.singletonList(lockKey),
        value, String.valueOf(expireTime), String.valueOf(waitTime)
    );
}

2. 锁续期脚本

public boolean renewLock(String lockKey, String value, long expireTime) {
    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                      "    return redis.call('expire', KEYS[1], ARGV[2]) " +
                      "else " +
                      "    return 0 " +
                      "end";
    
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptText(luaScript);
    redisScript.setResultType(Long.class);
    
    Long result = stringRedisTemplate.execute(
        redisScript,
        Collections.singletonList(lockKey),
        value, String.valueOf(expireTime)
    );
    
    return result != null && result == 1;
}

六、总结

  1. 基础实现简单但存在安全隐患,适合对一致性要求不高的场景

  2. Lua脚本实现通过原子操作解决了安全问题,是生产环境推荐的做法

  3. Lua脚本在Redis中执行是原子的,适合实现复杂的多步操作

  4. 实际应用中还可以结合Redisson等成熟框架,它们提供了更完善的分布式锁实现


网站公告

今日签到

点亮在社区的每一天
去签到