分布式锁的核心思想,就是使用外部的一块共享的区域,来完成锁的实现。
一、使用mysql数据库实现(基本不用)
1、使用数据库悲观锁
可以使用select ... for update
来实现分布式锁。
例如:建一个lock表,获取锁就是插入一条数据,移除锁就是删除掉这条数据,使用mysql的for update
来保证原子性。
2、使用数据库乐观锁
增加一个version字段,每次更新修改,都会自增加一。
例如:为id是1的用户余额加10
select version,balance from account where user_id ='1';
进行更新时,where条件附带上版本号:
update account set balance = balance+10 ,version = version+1 where version
= #{version} and user_id ='1';
如果更新失败,则循环上面两步
二、使用redis实现(常用)
1、使用setnx+expire命令实现
setnx是set if not exists 的缩写,也就是只有不存在的时候才设置rediskey 设置成功时返回 1 , 设置失败时返回 0
if(jedis.setnx(key,lock_value) == 1){ //setnx加锁
expire(key,100); //设置过期时间
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}
缺点:
- 加锁操作和设置超时时间是分开的。假设在执行完
setnx
加锁后,正要执行expire
设置过期时间时,出现问题,则锁永远无法释放。所以需要使用lua脚本来使setnx+expire成为原子操作 - 如果超时时间过短,会出现业务还未结束,锁就被释放的情况。
2、Redisson框架(常用)
Redisson是一个知名的、优秀的redis客户端类库,封装了大量的基于redis的复杂的一些操作
redission原理图:
当线程加锁成功后,会启动一个后台线程,会每隔10秒检查一下,还持有锁,那么就会不断的延长锁key的30秒生存时间。因此,Redisson就是使用watch dog
解决了锁过期释放,业务没执行完问题。
Redisson底层大量使用lua脚本来保证原子性操作,如下面的尝试加锁代码:
简单使用方法:
1、引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
2、配置redisson
@Bean(name = "redisClient")
public RedissonClient redisClient() {
Config config = new Config();
config.useSingleServer()
.setPassword("123456")
.setTimeout(1000000)
.setAddress("redis://127.0.0.1:6379");
;
return Redisson.create(config);
}
3、业务中使用
//1、获取锁
RLock rLock = redisClient.getLock("lock_key");
try{
//2、加锁
rLock.lock();
//业务代码……
}finally{
//3、释放锁
rLock.unlock();
}
3、RedLock
redis主从复制架构的问题:
- 客户端 A 从 master 获取到锁
- 在 master 将锁同步到 slave 之前,master 宕掉了。
- slave 节点被晋级为 master 节点
- 客户端 B 取得了同一个资源被客户端 A 已经获取到的另外一个锁。
安全失效!
正式为了解决上面的问题,才有了基于redis实现的分布式锁——RedLock
它能够保证以下特性:
- 互斥性:在任何时候,只能有一个客户端能够持有锁;
- 避免死锁:当客户端拿到锁后,即使发生了网络分区或者客户端宕机,也不会发生死锁;(利用key的存活时间)
- 容错性:只要多数节点的redis实例正常运行,就能够对外提供服务,加锁或者释放锁;
而非redLock是无法满足互斥性的。
三、使用zookeeper实现
Zookeeper的节点Znode有四种类型:
-
持久节点:默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在。
-
持久顺序节点:所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号,持久节点顺序节点就是有顺序的持久节点。
-
临时节点:和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
-
临时顺序节点:有顺序的临时节点(用于实现分布式锁)。
zookeeper分布式锁原理步骤:
- zookeeper首先创建一个/lock节点
- 当有节点获取锁时,先为这个节点创建临时节点,例如lock-702564158761685-000001,序列号按创建顺序递增。
- zookeeper会检查 lock-702564158761685-000001 是否/lock下的最小节点,如果是该节点得到锁,否则监听 lock-702564158761685-000001 前一个节点状态
-
当前一个节点的释放锁时会通知(唤醒)后一个节点,(这样只监听前一个节点的方式,避免了线程“惊群效应”)。
简单使用方法:
1、引入依赖
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
</dependency>
2、创建zk客户端CuratorFramework
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(CONNECT_ADDR)
.retryPolicy(retryPolicy)
.connectionTimeoutMs(CONNECTION_TIMEOUT)
.sessionTimeoutMs(SESSION_TIMEOUT)
.build();
client.start();
3、业务中使用
//1、创建临时顺序节点
InterProcessMutex mutex = new InterProcessMutex(client, "/locks/" + workNumber)
try{
//2、加锁
mutex.acquire();
//业务代码
} finally {
//3、解锁
mutex.release();
}
扩展:curator recipes 中的各种锁:
- InterProcessMutex:可重入、独占锁
- InterProcessSemaphoreMutex:不可重入、独占锁
- InterProcessReadWriteLock:读写锁
- InterProcessSemaphoreV2 : 共享信号量
- InterProcessMultiLock:多重共享锁 (将多个锁作为单个实体管理的容器)