Redis实现分布式锁
Redis实现的分布式锁
分布式锁的基本原理
在集群环境下我们部署了多个tomcat,每个tomcat都有属于自己的jvm,在不同tomcat中synchronized锁的对象是自身jvm中的对象,所以synchronized锁就会失效,这个时候我们就需要分布式锁去解决这个问题
分布式锁就是足分布式系统或集群模式下多进程可见并且互斥的锁。分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
分布式锁应该满足的条件
可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思
互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
高可用:程序不易崩溃,时时刻刻都保证较高的可用性
高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能
安全性:安全也是程序中必不可少的一环
Redis实现的分布式锁
Redis实现分布式锁
redis的setNx可以保证只有一个线程可以创建key,如果已经存在key就不会再覆盖,利用这个特性我们可以实现分布式锁,只有当setNx设置锁成功的时候才代表线程竞争锁成功,否则竞争锁失败,为了防止死锁我们通常会为锁设置一个有效期
Redis分布式锁误删问题
线程竞争锁成功后在锁到期后还没完成业务,其他线程在锁超时释放后竞争到锁,这时候原线程完成业务,将后一天线程的锁删除
解决方法:value设置为线程id,每个线程只能删除自己线程id的锁,但是还有极端情况,当线程判断锁是自己后锁过期,其他线程竞争成功后锁被删除,所以我们使用lua脚本处理redis保证原子性操作
1 | -- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示 |
基于setnx实现的分布式锁存在的问题
重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。
**超时释放:**我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患
主从一致性: 如果Regis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。
这个时候我们可以使用redission解决
Redission
使用
引入依赖
1 | <dependency> |
配置类
1 | @Configuration |
使用
1 | @Resource |
可重入性
redission使用hash结构表示锁,大key表示锁是否存在,小key表示当前这把锁被哪个线程持有
锁重试和WatchDog机制
通过追溯tryLock的源码可以看到,redission尝试获取锁也是使用lua脚本进行调用
阅读上述代码,我们发现,redission在获取锁失败后,会订阅锁释放的信号,并且在等待时间超过限制后主动释放并且取消订阅,如果第二次还是没有成功获取锁,那么将进入死循环,直到成功获取锁对象或等待时间超时
而为了防止线程阻塞导致的超时释放,通过追踪源码我们发现在线程成功竞争到锁后会提交一个回调函数
在回调函数中,每隔watchDog默认锁释放时间的三分之一,就会刷新锁的释放时间,只要线程没有宕机,那么锁就会一直刷新
主从一致性
为了提高redis的可用性,我们会搭建集群,如果主机没来得及将锁同步给从机就宕机了,那么下一个主节点选出来后,就会造成数据不一致的情况,为了解决这个问题redission提出了MultiLock锁,不再使用主从,只有全部节点的锁都获取成功才算获取锁成功