RedisLockRegistry分布式锁实现原理
一、使用
1、pom.xml导入依赖
2、配置文件
注册RedisLockRegistry,设置过期时间5s
@Configuration public class RedisLockRegistryConfiguration { // 用户操作锁key public static final String CACHE_USER_LOCK_KEY = "forlan:user:lock"; @Bean(name = "RedisLockRegistryExpireFiveSecond") public RedisLockRegistry redisLockRegistry(RedisConnectionFactory connectionFactory) { return new RedisLockRegistry(connectionFactory, CACHE_USER_LOCK_KEY, 5000L); } }
3、具体使用
public class ForlanTest { private static final Logger logger = LoggerFactory.getLogger(ForlanTest.class); @Autowired private RedisLockRegistry redisLockRegistry; public void test() { String lockKey = "key_id"; Lock loginLock = redisLockRegistry.obtain(lockKey.intern()); boolean getLock = false; try { // tryLock()底层调用this.tryLock(0L, TimeUnit.MILLISECONDS) getLock = loginLock.tryLock(5, TimeUnit.SECONDS); if (getLock) { //获得锁执行业务 } } catch (Exception e) { logger.error("异常信息...", e); } finally { if (getLock) { //释放锁 loginLock.unlock(); } } } }
二、原理
1、obtain方法
主要是根据lockKey去查locks这个map中是否已经存在这个key
如果存在就返回内部类RedisLock
如果不存在就创建一个RedisLock,以lockKey为key,RedisLock为value放入map中
注意:每个分布式应用自己都会创建一个RedisLockRegistry实例,同一个应用的多个线程共享RedisLock类
2、tryLock方法
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { long now = System.currentTimeMillis(); // 尝试拿取本地锁,拿不到直接返回失败false if (!this.localLock.tryLock(time, unit)) { return false; } else { try { long expire = now + TimeUnit.MILLISECONDS.convert(time, unit); boolean acquired; // 当前时间还没过期并且还未获得redis锁,睡眠100ms继续重试 while(!(acquired = this.obtainLock()) && System.currentTimeMillis() < expire) { Thread.sleep(100L); } if (!acquired) { this.localLock.unlock(); } return acquired; } catch (Exception var9) { this.localLock.unlock(); this.rethrowAsLockException(var9); return false; } } }
主要过程
先获得本地锁,拿不到直接返回失败
当前时间还没过期并且还没拿到redis锁,睡眠100ms继续重试
如果拿到redis锁,结束循环,返回成功
如果超时了还没拿到,释放锁,返回失败
拿redis锁的过程
private boolean obtainLock() { boolean success = (Boolean)RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript, Collections.singletonList(this.lockKey), new Object[]{RedisLockRegistry.this.clientId, String.valueOf(RedisLockRegistry.this.expireAfter)}); if (success) { this.lockedAt = System.currentTimeMillis(); } return success; }
通过obtainLock方法,执行lua脚本来获取
redisTemplate.execute()参数说明:
- 第一个参数(RedisLockRegistry.this.obtainLockScript)表示要执行的lua脚本,脚本内容如下:
为什么要用本地锁
- 为了可重入
- 为了减轻redis服务器的压力
为什么要用lua脚本
- 保证原子性
- 减少网络开销
- 替代redis的事务功能
如果业务执行太久,超过过期时间,会有什么问题?
分情况说的,单实例和多实例;
- 如果是单个实例情况下,就算key过期了,其它任务也拿不到锁,因为有本地锁ReentrantLock的约束;
- 如果是多实例的情况下,就会有问题;
3、unlock方法
三、总结
- RedisLockRegistry通过本地锁(ReentrantLock)和Redis锁,双重锁实现;
- 使用ReentrantLock可重入锁;
- RedisLockRegistry对锁无续期操作;
- 只适用于单实例的情况下,key过期,还能通过本地锁保证;多实例下无法通过本地锁保证,会有问题;