欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 名人名企 > Spring Boot + Lua 手写分布式锁(支持自动续期 / 可重入)

Spring Boot + Lua 手写分布式锁(支持自动续期 / 可重入)

2025/3/9 0:40:47 来源:https://blog.csdn.net/weixin_46619605/article/details/146090696  浏览:    关键词:Spring Boot + Lua 手写分布式锁(支持自动续期 / 可重入)

1、简介

在分布式系统环境中,多个服务或节点可能并发地访问和修改同一资源,这种情况极易导致数据不一致或死锁问题。为解决这一问题,分布式锁机制应运而生。相较于直接使用现成的分布式锁解决方案,通过自己动手实践,我们能够更深刻地理解其内部的运作机制与核心原理。
通过Spring Boot集成Redis,并使用Lua脚本,我们可以实现一个支持自动续期和可重入的分布式锁。Lua脚本的原子性执行确保了获取和释放锁的操作是不可分割的,从而避免了竞态条件。自动续期功能则通过守护线程或看门狗机制实现,确保锁在业务处理过程中不会因过期而被其他客户端获取。

2、实战案例

2.1 环境准备

这里我们只需要引入一个redis相关的依赖即可

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

2.2 定义加锁/解锁LUA脚本

加锁Lua脚本

local key = KEYS[1]
local lockId = ARGV[1]
local expireTime = ARGV[2]
-- 判断锁是否存在
if (redis.call('exists', key) == 0) thenredis.call('hset', key, lockId, 1)redis.call('pexpire', key, expireTime)return 1
end
-- 判断是否是当前线程持有锁
if (redis.call('hexists', key, lockId) == 1) then-- 如果当前线程已经获取锁了,则进行累加计数器redis.call('hincrby', key, lockId, 1)-- 重置过期时间redis.call('pexpire', key, expireTime)return 1
end
return 0

释放锁Lua脚本

local key = KEYS[1]
local lockId = ARGV[1]
-- 当前线程未获取锁
if (redis.call('hexists', key, lockId) == 0) thenreturn nil
end
local count = redis.call('hincrby', key, lockId, -1)
if (count > 0) thenredis.call('pexpire', key, ARGV[2])return 0
elseredis.call('del', key)return 1
end

锁续期Lua脚本

local key = KEYS[1]
local lockId = ARGV[1]
local expireTime = ARGV[2]
-- 判断是否是当前线程持有锁
if (redis.call('hexists', key, lockId) == 1) then-- 重置过期时间redis.call('pexpire', key, expireTime)return 1
end
return 0

接下来配置配置上面3个脚本

2.3 Lua脚本配置

@Configuration
public class RedisLockConfig {@BeanDefaultRedisScript<Long> lockScript() {DefaultRedisScript<Long> script = new DefaultRedisScript<>();script.setLocation(new ClassPathResource("lua/lock.lua"));script.setResultType(Long.class);return script;}@BeanDefaultRedisScript<Long> unlockScript() {DefaultRedisScript<Long> script = new DefaultRedisScript<>();script.setLocation(new ClassPathResource("lua/unlock.lua"));script.setResultType(Long.class);return script;}@BeanDefaultRedisScript<Long> renewalScript() {DefaultRedisScript<Long> script = new DefaultRedisScript<>();script.setLocation(new ClassPathResource("lua/renewal.lua"));script.setResultType(Long.class);return script;}
}

通过如上的定义方便我们在分布式锁的实现中使用。

2.4 分布式锁实现

@Component
public class RedisDistributedLock {/**默认锁前缀*/private static final String LOCK_PREFIX = "pack:lock:";/**默认锁超时时间*/private static final long LOCK_EXPIRE_TIME = 30 * 1000 ;/**记录锁key与ID*/private static final ThreadLocal<Map<String, String>> lockHolder = ThreadLocal.withInitial(HashMap::new);private final StringRedisTemplate redisTemplate;private final DefaultRedisScript<Long> lockScript;private final DefaultRedisScript<Long> unlockScript;private final DefaultRedisScript<Long> renewalScript ;private final String id ;private final String lockKey ;private final ScheduledExecutorService scheduler ;public RedisDistributedLock(StringRedisTemplate redisTemplate,DefaultRedisScript<Long> lockScript,DefaultRedisScript<Long> unlockScript,DefaultRedisScript<Long> renewalScript,String lockKey) {this.redisTemplate = redisTemplate;this.lockScript = lockScript;this.unlockScript = unlockScript;this.renewalScript = renewalScript ;this.id = UUID.randomUUID().toString().replaceAll("-", "") ;this.lockKey = lockKey ;this.scheduler = Executors.newSingleThreadScheduledExecutor() ;}public void lock() {/**TODO*/}public void unlock() {/**TODO*/}private String generateLockId() {return this.id + ":" + Thread.currentThread().threadId();}
}

接下来,我们具体来实现里面的逻辑。

加锁操作

public void lock() {final long expireMillis = LOCK_EXPIRE_TIME ; String actualKey = LOCK_PREFIX + lockKey;try {while (true) {/**生成唯一的锁ID*/String lockId = generateLockId();Long result = redisTemplate.execute(lockScript, List.of(actualKey), lockId,String.valueOf(expireMillis));if (result != null && result == 1) {/**成功获取分布式锁*/lockHolder.get().put(actualKey, lockId);startRenewalThread(actualKey, lockId, expireMillis);return; }// 为了解决惊群效应;但是这里是有明显缺陷的,这导致始终会再这里等待一定的时间;我们应该采用发布/订阅消息的方式来实现TimeUnit.MILLISECONDS.sleep((long)(Math.random() * 50) + 50) ;}} catch (Exception e) {System.err.println("加锁失败: " + e.getMessage()) ;throw new RuntimeException(e) ;}
}
/**根据UUID+当前线程id生成唯一id*/
private String generateLockId() {return this.id + ":" + Thread.currentThread().threadId();
}
/**锁续期*/
private void startRenewalThread(String key, String lockId, long expireTime) {long delay = expireTime / 3;scheduler.scheduleAtFixedRate(() -> {Long ret = this.redisTemplate.execute(renewalScript, List.of(key), lockId, String.valueOf(expireTime)) ;if (ret == null || ret == 0) {scheduler.shutdownNow();}}, delay, delay, TimeUnit.MILLISECONDS);
}

释放锁操作

public void unlock() {final long expireMillis = LOCK_EXPIRE_TIME ; String actualKey = LOCK_PREFIX + lockKey;String lockId = lockHolder.get().get(actualKey);if (lockId == null) {return;}Long ret = redisTemplate.execute(unlockScript, List.of(actualKey), lockId,String.valueOf(expireMillis)) ;if (ret != null && ret == 1) {lockHolder.get().remove(actualKey) ;stopRenewalThread() ;}
}
private void stopRenewalThread() {if (scheduler != null && !scheduler.isShutdown()) {scheduler.shutdownNow();}
}

以上就是分布式锁的所有代码了;下面定义获取锁的工具类。

@Component
public class PackLock {private final StringRedisTemplate stringRedisTemplate ;private final DefaultRedisScript<Long> lockScript;private final DefaultRedisScript<Long> unlockScript;private final DefaultRedisScript<Long> renewalScript;public PackLock(StringRedisTemplate stringRedisTemplate,DefaultRedisScript<Long> lockScript,DefaultRedisScript<Long> unlockScript,DefaultRedisScript<Long> renewalScript) {this.stringRedisTemplate = stringRedisTemplate;this.lockScript = lockScript ;this.unlockScript = unlockScript ;this.renewalScript = renewalScript ;}/**每次都需要创建新的锁对象*/public RedisDistributedLock getLock(String lockKey) {RedisDistributedLock lock = new RedisDistributedLock(stringRedisTemplate, this.lockScript, this.unlockScript, renewalScript, lockKey) ;return lock ;}
}

2.5 锁的使用

@Service
public class ProductService {private int count = 20 ;private final PackLock packLock ;public ProductService(PackLock packLock) {this.packLock = packLock;}public void calc() {if (count <= 0) {return ;}RedisDistributedLock lock = this.packLock.getLock("xxx") ;lock.lock() ;try {if (count > 0) {count-- ;}} finally {lock.unlock() ;}}
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词