分布式实战-分布式锁mysql与redis实现(二)

拥有回忆 提交于 2019-12-22 16:02:56

使用锁的目的

多个外部线程同时来竞争使用同一资源时,会彼此影响,导致混乱

锁的目的,将资源的使用做排它性处理,使同一时间,仅一个线程能访问资源

并不是所有的资源,都无法同时服务多个线程 ------ 比如,无状态的资源

无成员变量/成员变量不存在变化的类---- 就是无状态类 ----- 这种类是线程安全的

有状态的对象,也不一定是不安全的

     ---如果状态变化是原子的(即没有中间变迁过程,变化不需要时间,没有中间态) ---- 那么它一样是线程安全的

锁的本质

锁要解决的问题是 ------- 资源数据会不一致

锁要达成的目标是 ------- 让资源使用起来,像原子性一样

锁达成目标的手段 ------- 让使用者访问资源时,只能排队,一个一个地去访问资源

在单机应用里,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

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!