分布式锁
synchronized只能保证单个JVM内部的线程互斥,不能保证集群模式下的多个JVM的线程互斥。
分布式锁原理
每个JVM内部都有自己的锁监视器,但是跨JVM,就会有多个锁监视器,就会有多个线程获取到锁,不能实现多JVM进程之间的互斥。
我们不能使用JVM内部的锁监视器,我们必须让多个JVM去使用同一个锁监视器,所以肯定是一个独立于JVM内部的,多个JVM都可以看到的监视器。
过程
特性
多进程可见
多个JVM都可以看到,比如Redis,MySQL等。JVM外部的基本都可以实现。
互斥
只能有一个人拿到锁
高可用
大多数情况下,获取锁都是成功的,而不是频繁失败
高并发/高性能
加锁本身就会影响性能,会变成串行执行,如果加锁本身也很慢,就不行了。
安全性
异常情况下,比如,获取锁完毕之后,锁无法释放,服务宕机了。
死锁问题等等。
功能性特性
比如是否可重入,阻塞还是非阻塞的,公平还是非公平锁
不同的分布式锁区别
MySQL
- 互斥:通过事务的互斥锁来实现,事务提交锁释放,异常事务回滚
- 高可用:依赖MySQL本身的高可用
- 高性能:受限于MySQL的性能
- 安全性:通过事务获取锁,断开链接的时候,锁会自动释放
Redis
- 互斥:通过setnx互斥命令来实现互斥
- 高可用:Redis本身可以实现主从和集群模式,可用性高
- 高性能:较高
- 安全性:服务出现故障,锁无法释放,死锁,可以利用key的过期机制来实现
Zookeeper
- 互斥:利用内部节点的唯一性和有序性来实现,每个节点的id都是自增的,删除节点,另外一个节点就说最小的了
- 高可用:支持集群
- 高性能:保证强一致性,主从之间数据同步会消耗一定时间
- 安全性:创建的是临时节点,服务宕机,锁会自动释放
Redis实现分布式锁
分布式锁需要实现两个最基本的方法
获取锁
互斥
确保只能有一个线程执行成功。通过redis的setnx命令来实现,同时执行时,只有1个能执行成功,实现互斥。
#获取锁
setnx key value
- 添加锁的过期时间,避免服务宕机引起死锁。过期时间需要注意,业务还没处理完但是锁过期的问题
#设置过期时间
expire key 10
为了避免出现,setnx后,expire之前,服务宕机的问题,我们将两条命令合并为一条,保证原子性
#添加锁 nx是互斥,ex是过期时间
set key value ex 10 nx
#或者
set key value nx ex 10
非/阻塞式获取锁
获取锁成功返回ok,失败返回nil,如果失败了,有两种解决方案,jdk中,有两种方案:一直阻塞式等待,另一种,获取锁失败即刻返回。
非阻塞式获取锁,尝试一次,成功返回true,失败返回false!
释放锁
手动释放
手动删除即可
#释放锁
del key
超时释放
获取锁时,添加一个超时时间,避免出现服务宕机,锁无法被释放
流程
分布式锁初级版
执行流程
分布式锁代码
接口
/**
* 分布式锁
*
* @author zhangzengxiu
* @date 2023/10/9
*/
public interface ILock {
/**
* 尝试去获取锁
*
* @param timeoutSc 过期时间,过期锁自动释放
* @return 获取成功返回true,失败返回false
*/
boolean tryLock(long timeoutSc);
/**
* 释放锁
*/
void unlock();
}
实现
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author zhangzengxiu
* @date 2023/10/9
*/
public class SimpleRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
/**
* 锁统一前缀
*/
public static final String KEY_PRE = "lock:";
/**
* 业务名称
*/
private String name;
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeoutSc) {
//获取线程标识
long threadId = Thread.currentThread().getId();
String key = KEY_PRE + name;
String value = String.valueOf(threadId);
Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(key,