使用锁的目的
多个外部线程同时来竞争使用同一资源时,会彼此影响,导致混乱
锁的目的,将资源的使用做排它性处理,使同一时间,仅一个线程能访问资源
并不是所有的资源,都无法同时服务多个线程 ------ 比如,无状态的资源
无成员变量/成员变量不存在变化的类---- 就是无状态类 ----- 这种类是线程安全的
有状态的对象,也不一定是不安全的
---如果状态变化是原子的(即没有中间变迁过程,变化不需要时间,没有中间态) ---- 那么它一样是线程安全的
锁的本质
锁要解决的问题是 ------- 资源数据会不一致
锁要达成的目标是 ------- 让资源使用起来,像原子性一样
锁达成目标的手段 ------- 让使用者访问资源时,只能排队,一个一个地去访问资源
在单机应用里,JVM可以通过以下工具,可协调资源像原子性一样操作
1、sychronized ------ java语言天生支持
2、lock ---- jdk有接口标准
分布式环境下,如何协调资源达到原子性的操作?
1、sychronized / lock 这些java天然的实现,无法跨JVM发挥作用
2、只得去寻求分布式环境里,大家都公认的服务来做见证人,以协调资源
3、常见的公证人 ------》 mysql/zk/file/redis
4、目标 ----- 通过公证人发出信号,来协调分布式的访问者,排队访问资源
5、条件 ----- 任何一个能够提供【是/否】信号量的事物,都可以来做公证人
6、陷阱 ----- 发出锁信号量的动作,本身必须是原子性的
|
实现思路 |
优点 |
缺点 |
利用mysql的实现方案 |
利用数据库自身提供的锁机制实现,要求数据库支持行级锁; |
实现简单,稳定可靠 |
性能差,无法适应高并发场景; 容易出现死锁的情况; 无法优雅的实现阻塞式锁; |
利用redis的实现方案 |
基于Redis的setnx命令实现,并通过lua脚本保证解锁时对缓存操作序列的原子性; |
性能好
|
实现相对较复杂 无法优雅的实现阻塞式锁; |
利用zookeeper的实现方案 |
基于zk的节点特性以及watch机制实现; |
性能好,稳定可靠性,,能较好的实现阻塞式锁; |
实现相对复杂 |
mysql来充当公证人,利用的是一条sql语句执行的成功/失败,是原子的,流程如下:
关于redis分布式锁的基础知识
缓存有效期 :redis中的数据,不一定都是持久化的;给定key设置的生存时间,当key过期时,它会被自动删除;
SETNX 命令 :SETNX key value,将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
lua脚本:轻量小巧的脚本语言,用于支持redis操作序列的原子性;
redis加解锁的正确姿势 来自于redis作者antirez的总结归纳
加锁
通过setnx向特定的key写入一个随机值,并同时设置失效时间,写值成功既加锁成功;
@Override
//阻塞式加锁,使用setNx命令返回OK的加锁成功,并生产随机值
public boolean tryLock() {
//产生随机值,标识本次锁编号
String uuid = UUID.randomUUID().toString();
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
/**
* key:我们使用key来当锁
* uuid:唯一标识,这个锁是我加的,属于我
* NX:设入模式【SET_IF_NOT_EXIST】--仅当key不存在时,本语句的值才设入
* PX:给key加有效期
* 1000:有效时间为 1 秒
*/
String ret = jedis.set(KEY, uuid,"NX","PX",1000);
//设值成功--抢到了锁
if("OK".equals(ret)){
local.set(uuid);//抢锁成功,把锁标识号记录入本线程--- Threadlocal
return true;
}
//key值里面有了,我的uuid未能设入进去,抢锁失败
return false;
}
为了防止线程宕机,造成锁死在那里挡道,需要给锁认定一个有效期限,
------此期限的自动失效解锁,与线程的主动解锁之间,会存在冲突,reids的解锁流程必须考虑这一点:
//错误解锁方式
public void unlockWrong() {
//获取redis的原始连接
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
String uuid = jedis.get(KEY);//现在锁还是自己的
//uuid与我的相等,证明这是我当初加上的锁
if (null != uuid && uuid.equals(local.get())){//现在锁还是自己的
//锁失效了
//删锁
jedis.del(KEY);
}
}
上图的解锁逻辑虽然是正确的,但因为整个动作不是原子的,因为不安全。需要改为lua脚本来执行
//正确解锁方式
public void unlock() {
//读取lua脚本
String script = FileUtils.getScript("unlock.lua");
//获取redis的原始连接
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
//通过原始连接连接redis执行lua脚本
jedis.eval(script, Arrays.asList(KEY), Arrays.asList(local.get()));
}
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
来源:CSDN
作者:日薪灬越亿
链接:https://blog.csdn.net/a1173537204/article/details/103652965