文章目录
传统方式实现分布式锁弊端
传统方法的问题:先 SET 再 EXPIRE
在许多使用 Redis 的 Spring Boot 应用中,开发者可能会选择先使用 SET 命令设置一个键,然后使用 EXPIRE 命令为这个键设置过期时间。这种方法简单直观,但存在一些潜在的问题:
- 非原子操作:SET 和 EXPIRE 两个命令分开执行,它们之间可能会由于各种原因(比如网络延迟或程序崩溃)造成执行中断,这样可能会出现键被设置了但没有设置过期时间的情况,导致锁永久存在。
- 竞态条件:在高并发的环境中,如果在 SET 和 EXPIRE 之间有其他进程也尝试设置相同的键,可能会造成预期外的行为。例如,一个进程可能会覆盖了另一个进程的锁,但使用了原来的过期时间,这可能会导致锁提前被释放。
Lua 脚本和分布式锁的关系
什么是 Lua 脚本?
Lua 是一种轻量级的脚本语言,以其简洁和易嵌入的特性广受欢迎。它通常被用于游戏开发和嵌入到应用程序中,提供灵活的扩展和自定义功能。在 Redis 中,Lua 脚本可以执行原子操作
,这是实现分布式锁的关键。
为什么选择 Lua 脚本来实现分布式锁?
使用 Lua 脚本实现分布式锁具有多个优势:
● 原子性:Redis 可以保证 Lua 脚本的原子性执行,这意味着在脚本执行期间不会有其他命令插入执行。
● 效率:通过减少网络往返次数,Lua 脚本的执行效率非常高。
● 简洁性:使用 Lua 脚本可以用较少的代码实现复杂的逻辑。
分布式锁的实现原理
分布式锁主要用于控制分布式环境中多个节点对共享资源的访问。实现原理包括:
● 锁的创建:如果锁资源可用(即不存在),则创建锁。
● 锁的维持:通过定时更新锁的有效时间来维持锁的持有状态。
● 锁的释放:完成资源访问后,释放锁,使其它节点可以访问资源。
基于redis+Lua 脚本的分布式锁的实现
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
获取分布式锁
@Service
public class RedisLockService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 尝试获取一个分布式锁。
*
* @param key 锁的唯一标识符。
* @param expireTime 锁的过期时间,单位是秒。
* @return 如果成功获取锁,返回 true,否则返回 false。
*/
public boolean acquireLock(String key, Long expireTime) {
//value设置一个随机值
String value = UUID.randomUUID().toString().replace("-", "");
// Lua 脚本,用于检查并设置锁
// 如果 key 不存在,则设置 key 并设置其过期时间,然后返回 1
// 如果 key 存在,直接返回 0,表示锁已经被其他客户端持有
String luaScript =
"if redis.call('exists', KEYS[1]) == 0 then " +
" redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2]); " +
" return 1; " +
"else " +
" return 0; " +
"end;";
// 创建 RedisScript 对象,用于执行 Lua 脚本
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript); // 设置脚本
redisScript.setResultType(Long.class); // 设置脚本返回类型为 Long
// 执行 Lua 脚本
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(expireTime));
// 判断是否成功获取锁
return result != null && result == 1;
}
}
测试
@RestController
@RequestMapping("/lock")
public class RedisLockController {
@Autowired
private RedisLockService redisLockService;
@GetMapping("/acquireLock")
public String acquireLock(@RequestParam String key, @RequestParam Long expireTime) {
boolean isLocked = redisLockService.acquireLock(key, expireTime);
return isLocked ? "Lock acquired successfully." : "Failed to acquire lock.";
}
}
访问 http://127.0.0.1:8080/lock/acquireLock?key=user:test11&expireTime=60
成功
失败