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

相关推荐
2503_92841156几秒前
11.11 Express-generator和文件上传和身份认证
数据库·node.js·express
长沙红胖子Qt12 分钟前
关于 mariadb开源数据库忘记密码 的解决方法
数据库·mariadb
二进制的Liao33 分钟前
【编程】脚本编写入门:从零到一的自动化之旅
数据库·python·算法·自动化·bash
影子240134 分钟前
oralce创建种子表,使用存储过程生成最大值sql,考虑并发,不考虑并发的脚本,plsql调试存储过程,java调用存储过程示例代码
java·数据库·sql
武子康40 分钟前
Java-172 Neo4j 访问方式实战:嵌入式 vs 服务器(含 Java 示例与踩坑)
java·服务器·数据库·sql·spring·nosql·neo4j
ruleslol1 小时前
SpringBoot18-redis的配置
spring boot·redis
昂子的博客1 小时前
Redis缓存 更新策略 双写一致 缓存穿透 击穿 雪崩 解决方案... 一篇文章带你学透
java·数据库·redis·后端·spring·缓存
xixixi777771 小时前
了解一下APM工具——就像给软件系统装的“全身CT”,能实时透视从用户点击到后端数据库的每个环节性能,精准定位哪里慢、为什么慢
数据库·安全·数据采集·apm·日志监控
无心水2 小时前
【分布式利器:Kafka】1、Kafka 入门:Broker、Topic、分区 3 张图讲透(附实操命令)
分布式·kafka·topic·isr·broker·分区·生产者消费者模式java实现
q***9942 小时前
PON架构(全光网络)
网络·数据库·架构