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() ;}}
}