当前位置: 首页 > news >正文

Redis实现分布式锁

分布式锁是分布式系统中解决资源竞争问题的重要机制。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 {@Autowiredprivate 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])          -- 则删除这个键
elsereturn 0                                   -- 否则返回0表示失败
end
Lua脚本在Redis中的优势
  1. 原子性:整个脚本作为一个整体执行,执行期间不会被其他命令打断

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

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

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

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

 

相关文章:

  • 2025五一杯A题五一杯数学建模思路代码文章教学:支路车流量推测问题
  • python类私有变量
  • 【2025最新面经】暑期实习常问知识点
  • 简单句练习--语法基础
  • Python Cookbook-6.12 检查一个实例的状态变化
  • C++11新的特性
  • ZYNQB笔记(十六):AXI DMA 环路测试
  • 开发板型号 ESP32-DevKitC-32模块型号 ESP32-WROOM-32 和主控芯片 ESP32-D0WDQ6-V3
  • 【AI论文】仅通过一个训练样本对大型语言模型进行推理的强化学习
  • RocketMQ常见面试题一
  • Python PyTorch库【机器学习框架】全面深入讲解与实践 入选【全站综合热榜第四】【领域内容榜第一】
  • dify+ollama+知识库 部署
  • 【树莓派Pico FreeRTOS】-FreeRTOS-SMP移植
  • 第十六届 -- 蓝桥杯Web开发大学组省赛个人复盘
  • [FPGA 官方 IP] Binary Counter
  • 编程题python常用技巧-持续
  • 第 11 届蓝桥杯 C++ 青少组中 / 高级组省赛 2020 年真题,选择题详细解释
  • 【笔记】深度学习模型训练的 GPU 内存优化之旅③:内存交换篇
  • 如何降低LabVIEW开发费用
  • 自动剪辑批量混剪视频过原创软件工具视频帧级处理技术实践批量截图解析
  • 安徽六安特色产品将“组团”入沪,借力五五购物节开拓市场
  • 5月人文社科中文原创好书榜|巫蛊:中国文化的历史暗流
  • “网约摩托”在部分县城上线:起步价五六元,专家建议纳入监管
  • 解放日报:浦东夯实“热带雨林”式科创生态
  • 澎湃读报丨央媒头版集中刊发社论,庆祝“五一”国际劳动节
  • 神十九都带回了哪些实验样品?果蝇等生命类样品已交付科学家