摘要:
1. 单原子性指令
2.Lua脚本原子性执行多指令
3. Redis实现分布式锁

  • 3.1 单Redis实例保证锁的可靠性和高效性
    加锁:
    A) SET NX指令作为加锁的单原子性指令
    B) 设置锁自动释放的有效时间,看门狗
    C) 加锁释放锁操作对应唯一的客户端

    释放锁:
    A) 使用Lua脚本判断是否是加锁的同一个客户端
  • 3.2 Redis master集群保证锁的可靠性+可用性
  • 3.2.1 Redlock算法:N/2 +1 条件以及重计算锁有效时间机制
ps: 所有并发问题的产生都是因为对数据的修改在底层分为三步:读数据,修改数据,写回数据;

当一个线程执行这三步操作期间,其他线程同时对同一个数据执行以上三步操作,那么最终就会导致期望结果与实际结果不一致。(从Redis读数据是单线程的,但是在客户端请求Redis时以及在客户端程序中修改数据,修改好的数据写回Redis时这三个时间点,Redis可能被其他线程访问了同一个数据

一、Redis如何保证并发访问数据可靠性

  • INCR,DECR单原子性指令,类似于Java里面的AtomicInteger(底层依赖操作系统的原子性操作),但只能适用于对数据的简单增减i++,i–;
  • Lua脚本复合指令,将一段复杂的数据修改操作写到Lua脚本文件中,然后以一个Lua脚本文件为单位,原子性执行,执行脚本文件时不会有其他并发操作;
  • 分布式锁

二、Redis作为分布式锁(单Redis节点)

锁调度其实就是判断锁状态0/1,为0加锁成功,为1加锁失败;

注意:加锁释放锁的过程分为三步“读取锁状态、修改锁状态值,写回锁状态值”,对应上述一个操作由三个单位操作组成,所以加锁过程本身也存在并发问题。

A) Java中加锁:通过CAS进行,CAS底层使用了Unsafe类,其方法都是本地方法,依赖操作系统的总线窥探机制以及缓存一致性通信机制实现对数据修改的原子性;

B) Redis中加锁:Redis对数据的修改操作是单线程的,但是读取锁状态,以及写回锁状态的操作不是单线程的,所以存在并发问题,可以考虑使用以上单原子性操作指令或者Lua脚本保证锁状态修改的原子性,通常用SETNX以及DEL单原子性指令完成Redis加锁释放锁(相当于Java中的lock(), unlock()),但是删除锁时一般要包括以下判断逻辑(是否和加锁的是同一个对象),所以释放锁时一般要执行Lua脚本。

注:SETNX设置的键值对的value要和对应的客户端绑定,保证加锁和释放锁的是同一个客户端

因为客户端宕机会导致缺少释放锁的操作(相当于没有通过finally释放锁),其他客户端就无法获取该锁了,所以会设置一个自动释放锁的时间,超时后该锁被自动释放(finally操作,一般会设置一个看门狗判断在当前客户端如果正常执行没有宕机时,若剩余时间少于锁的1/3,自动延长锁的有效时间,如果看门狗判断当前客户端意外宕机则不会延长锁有效期),但是客户端一加锁后运行超时,锁被自动释放,之后客户端二获取锁开始运行,在客户端二还没结束前,客户端一运行结束执行正常的释放锁操作,那么就会导致客户端二的锁被释放,造成客户端二对数据的修改并发问题—所以以上情况的解决方法是保证加锁和释放锁的对象是同一个

请添加图片描述

一般Redis会给每一个连接的客户端分配一个唯一标识NX,在SET时加入该唯一标识,并且设置过期时间PX,使用指令如下:

SET lock unique_value NX PX 10000;

三、 Redis作为分布式锁(Redis集群,都是master而非主从集群)—Redlock算法

客户端向多个Redis实例申请加锁,超过半数Redis实例完成加锁且锁有效时间足够则客户端完成加锁

  1. 获取当前时间;
  2. 客户端依次向每个Redis加锁,每次请求加锁会有超时时间,超过一定时间后客户端放弃和当前Redis实例请求锁,转而向下一个Redis实例请求加锁;
  3. 客户端完成和超过半数Redis实例的加锁操作,计算当前锁的有效时间还剩余多少(在第一个Redis上设置的有效时间 - 加完后续所有锁浪费的时间),如果剩余时间为负或者过少,说明与第一个Redis建立的锁快要过期了,直接释放所有加上的锁,重新向Redis集群发起加锁请求。

参考文章:

https://time.geekbang.org/column/article/301092

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐