Redis实现分布式锁

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

一、分布式锁的基本要求

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

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

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

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

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

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

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

java 复制代码
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. 基础实现的使用示例

java 复制代码
@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脚本实现的分布式锁

java 复制代码
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脚本语法说明
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脚本实现的使用示例

java 复制代码
@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. 带重试的获取锁脚本

java 复制代码
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. 锁续期脚本

java 复制代码
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等成熟框架,它们提供了更完善的分布式锁实现

相关推荐
脆皮瞎几秒前
phpstorm用php连接数据库报错
数据库·php·phpstorm
没有感情的robot2 分钟前
redis 有序集合zrange和zrangebyscore的区别
数据库·redis
爱玩电脑的L28 分钟前
MYSQL-设计表
数据库·mysql
背帆42 分钟前
数据转储(go)
数据库·golang
南玖yy1 小时前
解锁 C++26 的未来:从语言标准演进到实战突破
开发语言·数据库·c++·人工智能·c++23·c++基础语法
geekmice1 小时前
通过SpringBoot+H2数据库+Mybatis实现DAO单元测试
数据库·spring boot·mybatis
老年DBA2 小时前
解决 Oracle EXPDP 無法鎖定 NFS 相關錯誤: ORA-27086 & flock: No locks available
数据库·oracle
菜是一种态度2 小时前
PostgreSQL数据库操作SQL
数据库·sql·postgresql
樱花树下的猫老师5 小时前
Win下的Kafka安装配置
分布式·kafka
海绵波波1078 小时前
DeepSeek谈《凤凰项目 一个IT运维的传奇故事》
运维·服务器·数据库