基于 Redis 的分布式锁
介绍
在分布式系统中,多个服务实例可能会竞争访问共享资源,如何保证同一时刻只有一个实例能操作某个资源,避免数据不一致?分布式锁就是解决这类问题的重要手段。
本文将介绍如何基于 Redis 实现分布式锁,并用 Spring Boot 写一个实用示例。
为什么选择 Redis 分布式锁?
- 高性能:Redis 是内存数据库,读写速度快,适合做锁的管理。
- 支持多客户端:Redis 本身支持多客户端并发访问。
- 简单易用:使用
SET NX
命令即可实现原子加锁操作。 - 广泛应用:很多互联网公司用 Redis 实现分布式锁。
Redis 分布式锁原理简述
典型实现基于 Redis 的分布式锁,关键是利用 Redis 的 SET key value NX PX timeout
命令:
NX
表示“仅当 key 不存在时才设置”——保证原子性加锁。PX timeout
设置锁的过期时间,避免死锁。
加锁步骤:
- 客户端尝试执行
SET lock_key unique_value NX PX 10000
(10秒过期) - 如果返回 OK,则加锁成功;否则说明锁被占用,加锁失败。
- 业务执行完成后,通过唯一的
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 分布式锁是分布式系统中保证数据一致性和业务安全的重要手段,正确设计与使用能够有效提升系统稳定性和性能。