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

基于 Redis 的分布式锁

介绍

在分布式系统中,多个服务实例可能会竞争访问共享资源,如何保证同一时刻只有一个实例能操作某个资源,避免数据不一致?分布式锁就是解决这类问题的重要手段。

本文将介绍如何基于 Redis 实现分布式锁,并用 Spring Boot 写一个实用示例。


为什么选择 Redis 分布式锁?

  • 高性能:Redis 是内存数据库,读写速度快,适合做锁的管理。
  • 支持多客户端:Redis 本身支持多客户端并发访问。
  • 简单易用:使用 SET NX 命令即可实现原子加锁操作。
  • 广泛应用:很多互联网公司用 Redis 实现分布式锁。

Redis 分布式锁原理简述

典型实现基于 Redis 的分布式锁,关键是利用 Redis 的 SET key value NX PX timeout 命令:

  • NX 表示“仅当 key 不存在时才设置”——保证原子性加锁。
  • PX timeout 设置锁的过期时间,避免死锁。

加锁步骤:

  1. 客户端尝试执行 SET lock_key unique_value NX PX 10000(10秒过期)
  2. 如果返回 OK,则加锁成功;否则说明锁被占用,加锁失败。
  3. 业务执行完成后,通过唯一的 unique_value 来验证自己是锁的持有者,调用 Lua 脚本原子释放锁。

Spring Boot 集成 Redis 分布式锁示例


1. 添加依赖

使用 Spring Boot Starter Redis:

<!-- pom.xml -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. Redis 配置(application.properties)

spring.redis.host=localhost
spring.redis.port=6379
# 如果有密码请配置:
# spring.redis.password=yourpassword

3. 编写 Redis 分布式锁工具类

这里我们用 StringRedisTemplate 实现加锁和解锁功能。

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Component
public class RedisDistributedLock {@Resourceprivate StringRedisTemplate redisTemplate;// Lua 脚本:校验锁的唯一标识后删除private static final String UNLOCK_LUA;static {UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) else return 0 end";}/*** 尝试获取锁* @param lockKey 锁的 key* @param expireMillis 过期时间,单位毫秒* @return 锁的唯一标识,成功返回UUID,失败返回null*/public String tryLock(String lockKey, long expireMillis) {String uuid = UUID.randomUUID().toString();Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, expireMillis, TimeUnit.MILLISECONDS);if (Boolean.TRUE.equals(success)) {return uuid;}return null;}/*** 释放锁* @param lockKey 锁的 key* @param uuid 获取锁时的唯一标识* @return 是否成功释放*/public boolean unlock(String lockKey, String uuid) {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(UNLOCK_LUA);redisScript.setResultType(Long.class);Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), uuid);return Long.valueOf(1L).equals(result);}
}

4. 使用示例

在业务代码中这样使用:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class SampleService {@Autowiredprivate RedisDistributedLock redisDistributedLock;private static final String LOCK_KEY = "lock:sample_task";public void doTaskWithLock() {String lockValue = redisDistributedLock.tryLock(LOCK_KEY, 10000);if (lockValue == null) {System.out.println("获取锁失败,任务已被其他实例执行");return;}try {System.out.println("获取锁成功,执行任务...");// 业务逻辑,确保任务幂等或合理超时Thread.sleep(5000);  // 模拟任务执行} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {boolean unlocked = redisDistributedLock.unlock(LOCK_KEY, lockValue);System.out.println("释放锁结果: " + unlocked);}}
}

5. Controller 调用测试

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class SampleController {@Autowiredprivate SampleService sampleService;@GetMapping("/lock-test")public String testLock() {sampleService.doTaskWithLock();return "任务触发完成";}
}

注意点和扩展

锁释放时能否用synchronized替代 Lua 脚本

Synchronized 是 JVM 内的线程锁,只能保证单机内多个线程间的同步,无法跨进程或跨机器生效。而分布式锁需要在多个服务实例间保证互斥访问,必须跨机器同步。

Redis 分布式锁释放时,通过 Lua 脚本在 Redis 端原子执行“校验锁持有者身份并释放锁”的操作,避免了竞态条件和误删锁的问题。

Java 层的 synchronized 不能替代 Redis 端 Lua 脚本的原子性操作,因为它无法控制分布式环境下的并发行为。


锁的过期时间如何合理设置

锁的过期时间是防止死锁的重要机制,但设置不合理会带来风险:

  • 过短风险:业务操作未完成,锁就自动过期释放,导致多个实例同时持锁,出现数据竞争和不一致。
  • 过长风险:业务异常或服务宕机时,锁长时间不释放,造成资源长时间被占用,影响系统并发性能。

如何合理设置?

  • 根据业务操作的最长执行时间估算,留出足够的安全缓冲(比如操作最大耗时的1.5倍~2倍)。
  • 在业务执行时,最好能动态续期锁,避免长时间操作被误释放。

为什么释放锁时一定要用唯一标识验证

释放锁时,必须确认当前请求是锁的持有者,否则可能误删其他客户端的锁,导致分布式锁失效。

  • 多个客户端可能会同时竞争同一个锁,只有获得锁的客户端才有权释放它。
  • 如果释放时不验证唯一标识(如 UUID),可能会误删别的客户端刚获得的锁,导致多个客户端同时持锁,出现并发冲突。
  • 唯一标识保证了锁的安全释放,实现“先验证身份,再释放”的原子操作。

考虑锁的重入

在某些场景下,业务逻辑可能会多次调用加锁方法,如果分布式锁不支持重入,会导致自己持有的锁被误释放或重复加锁失败。

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Component
public class RedisReentrantLock {@Resourceprivate StringRedisTemplate redisTemplate;// Lua脚本:尝试加锁(支持重入)private static final String LOCK_SCRIPT ="local key = KEYS[1] " +"local uuid = ARGV[1] " +"local expire = tonumber(ARGV[2]) " +"local val = redis.call('get', key) " +"if not val then " + "  redis.call('set', key, uuid..':1', 'px', expire) " +  // 初次加锁,count=1"  return 1 " +"else " +"  local idx = string.find(val, ':') " +"  local lockId = string.sub(val, 1, idx-1) " +"  local count = tonumber(string.sub(val, idx+1)) " +"  if lockId == uuid then " +   // 重入,count+1,续期"    count = count + 1 " +"    redis.call('set', key, uuid..':'..count, 'px', expire) " +"    return 1 " +"  else " +"    return 0 " +   // 已被他人持有"  end " +"end";// Lua脚本:解锁,递减计数,计数为0才释放private static final String UNLOCK_SCRIPT ="local key = KEYS[1] " +"local uuid = ARGV[1] " +"local val = redis.call('get', key) " +"if not val then return 0 end " +"local idx = string.find(val, ':') " +"local lockId = string.sub(val, 1, idx-1) " +"local count = tonumber(string.sub(val, idx+1)) " +"if lockId == uuid then " +"  count = count - 1 " +"  if count == 0 then " +"    redis.call('del', key) " +"    return 1 " +"  else " +"    redis.call('set', key, uuid..':'..count) " +"    return 1 " +"  end " +"else " +"  return 0 " +"end";private static final long DEFAULT_EXPIRE_MILLIS = 10000;public String lock(String lockKey) {String uuid = UUID.randomUUID().toString();Boolean success = tryLock(lockKey, uuid, DEFAULT_EXPIRE_MILLIS);return Boolean.TRUE.equals(success) ? uuid : null;}private Boolean tryLock(String lockKey, String uuid, long expireMillis) {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(LOCK_SCRIPT);redisScript.setResultType(Long.class);Long result = redisTemplate.execute(redisScript,Collections.singletonList(lockKey),uuid, String.valueOf(expireMillis));return Long.valueOf(1).equals(result);}public boolean unlock(String lockKey, String uuid) {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(UNLOCK_SCRIPT);redisScript.setResultType(Long.class);Long result = redisTemplate.execute(redisScript,Collections.singletonList(lockKey), uuid);return Long.valueOf(1).equals(result);}
}

RedLock vs Redisson

RedLock 是由 Redis 作者 Antirez 提出的分布式锁算法,旨在通过在多个独立 Redis 实例上同时加锁,确保加锁操作的安全和可靠。其核心是获得多数实例(即超过半数,如5个实例中的3个)的锁后,才算加锁成功,释放锁时也需要同步释放所有实例的锁,从而解决单点故障带来的锁安全和容错问题。

Redisson 是一个基于 Redis 的 Java 客户端和工具库,内置了包括 RedLock 在内的多种分布式锁实现,提供了简单易用的接口,帮助开发者快速集成分布式锁功能。除了分布式锁,Redisson 还提供丰富的分布式数据结构和工具,大幅简化了复杂分布式应用的开发难度。

下面是一个基于 Redisson 的简单分布式锁使用示例:

1. 添加依赖

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.19.8</version> <!-- 请根据需要选择最新版本 -->
</dependency>

2. 配置文件(application.yml)

spring:redis:host: localhostport: 6379

3. Redisson 配置(可选,Spring Boot 自动配置一般够用)

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");return Redisson.create(config);}
}

4. 使用分布式锁示例

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;@Service
public class MyService {@Resourceprivate RedissonClient redissonClient;public void doBusiness() {RLock lock = redissonClient.getLock("myLock");boolean locked = false;try {// 尝试加锁,最多等待3秒,锁过期时间10秒locked = lock.tryLock(3, 10, TimeUnit.SECONDS);if (locked) {System.out.println("获取锁成功,执行业务逻辑");// 模拟业务执行Thread.sleep(5000);} else {System.out.println("获取锁失败,稍后重试");}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {if (locked) {lock.unlock();System.out.println("释放锁");}}}
}

细节说明

  • trylock 支持重入锁,默认是可重入的。
  • 设置了等待时间和锁过期时间,防止死锁。
  • Redisson 自动处理锁的续期,不用额外写续期逻辑。

Redis 分布式锁总结


Redis 分布式锁是一种利用 Redis 原子操作实现的跨进程、跨机器的锁机制,广泛用于分布式系统中保证共享资源的互斥访问。其核心优势在于简单高效,基于 Redis 的单线程特性,可以快速完成加锁和解锁操作。

实现 Redis 分布式锁时,需注意以下关键点:

  • 唯一标识与安全释放:加锁时生成唯一标识,释放锁时通过 Lua 脚本校验标识,确保只有锁的持有者能解锁,避免误删他人锁。
  • 合理的过期时间:设置合理的锁过期时间防止死锁,但过期时间不宜过短,否则可能导致锁失效引发并发问题。
  • 重入锁支持:如业务需要支持重入,应设计锁的计数机制,防止同一客户端重复加锁导致误释放。
  • 高可用性与容错:单机 Redis 存在单点故障风险,RedLock 算法通过多个独立 Redis 实例实现多数节点加锁,提高锁的可靠性和容错性。
  • 成熟工具库:Redisson 等开源框架封装了复杂的分布式锁细节,提供简洁易用的接口,推荐在实际项目中使用。

总之,Redis 分布式锁是分布式系统中保证数据一致性和业务安全的重要手段,正确设计与使用能够有效提升系统稳定性和性能。

http://www.dtcms.com/a/406607.html

相关文章:

  • 郑州视频网站建设wordpress登录去不了后台
  • 淮安做网站杨凯企业建站官网运营
  • django 使用绑定多个数据库实现数据的同步
  • 面试复习题---Android技术专家3
  • 云手机与人工智能之间的关系
  • 做网站对电脑要求高吗荆州市住房和城乡建设厅官方网站
  • Python 高效实现 PDF 转 Word:告别手动复制粘贴
  • 9.9元奶茶项目:matlab+FPGA的cordic算法(向量模式)计算相位角
  • 广州越秀公司网站建设电子商务基础网站建设与维护单项选择题
  • 网站技术培训学校世界知名网站
  • 专业做消防工程师的正规网站做网站百度推广多少钱
  • mysql的 启动 与 客户端连接
  • 【星海出品】rabbitMQ队列处理深入研究
  • Mysql常见八股文
  • 社交类网站开发需求怎么做个人网页
  • 解析01背包
  • WitTkChart:基于Python tkinter Canvas的开源图表可视化库
  • NIST公布后量子加密标准的第五种算法HQC
  • NetCore+Web客户端实现gRPC实时推送
  • Bugku-想蹭网先解开密码
  • 房屋管理系统开发流程
  • 新浪云怎么做淘宝客网站科技与狠活是什么意思
  • Vue3 v-slot 详解与示例
  • Agno 架构介绍:高性 Multi-agent 系统框架深度解析
  • 哪里有南宁网站建设天河区建设和水务局网站
  • Cadence Allegro 电子设计 快问快答--03.OrCAD颜色在哪里设置?
  • 自己做的网站怎么上排行榜设计广告专业制作
  • 网站页面下沉的特效代码山西seo推广系统
  • S7-200 SMART GET/PUT 指令深度解析:从参数到故障排查(S7 协议的客户端 - 服务器通信)下
  • 做国外网站翻译中国小说赚钱外贸推广软件有哪些