Redis 实现分布式锁
普通锁
当多个线程访问同一个共享资源
时,需要某种机制来保证只有满足某个条件(获取锁成功)的线程才能访问到资源,而不满足条件(获取锁失败)的线程只能等待,在下一轮竞争中来获取锁才能访问资源。所以锁是一种用来解决多个执行线程或访问共享资源时出现错误或数据不一致问题的工具。
实现方式:
-
使用
synchronized
关键字。synchronized
是一个隐式锁,因为其解锁和锁定的操作是由 JVM 通过对象的 monitor 监视器锁自动完成的,我们也无法插手整个上锁和解锁的过程。 -
使用
java.util.concurrent.locks.Lock
的实现类,例如java.util.concurrent.locks.ReentrantLock
。这种方式的话就是显示锁了,需要手动上锁和解锁。 -
使用
CAS
(Compare And Swap) 无锁机制,这个的话实际上属于乐观锁机制,核心概念就是比较并交换。在执行CAS
操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
分布式锁
分布式锁应该具备哪些条件:
互斥性: 同一时刻只能有一个线程持有锁
可重入性: 同一节点上的同一个线程如果获取了锁之后能够再次获取锁
锁超时:和J.U.C中的锁一样支持锁超时,防止死锁
高性能和高可用: 加锁和解锁需要高效,同时也需要保证高可用,防止分布式锁失效
具备阻塞和非阻塞性:能够及时从阻塞状态中被唤醒
首先在分布式、微服务架构中,我们的项目通常是以下图模式来部署的,当然网关
、nginx
也都是存在集群的。
上图可以看到,变量A
存在三个服务器内存中(这个变量A
主要体现是在一个类中的一个成员变量,是一个有状态的对象)。假设我们使用的是普通锁,由于三台服务器之间变量互不可见,当多个请求调用时,可能就会出现操作三个不同内存区域的数据,读取的数据也可能会与设值得不一致。
基于以上问题,就出现了所谓的分布式锁(也不知道这个名字的作者是谁)的概念。其实简单点来说,就是在请求我们的接口时加上一个额外的步骤,只是这个步骤需要一个中间的媒介,比如像Redis
、ZooKeeper
甚至使用Mysql
。这个中间媒介的作用就是相当于一个标记,标记了当前是谁获取到了这个锁。整体的过程就是请求来了,先调用判断中间媒介是否存在,不存在则尝试获取,获取成功或者失败后则和普通锁处理逻辑一样。
三种实现方式:
实现方式这一块上面也说了,主要需要一个中间的媒介。
- 基于
Redis
,当一个客户端向缓存中写成功一个key-value时,其他的客户端不能在写入相同的key - 基于数据库,向数据库插入一条数据(比如用id主键,或者唯一索引)等达到其他的客户端无法再插入相同的数据
- 基于
Zookeeper
,原理就是利用了Zookeeper
的临时节点来实现。当客户端向zk写入节点时,如果写入成功,其他的客户端就无法写入成功,可以理解为互斥。
Redis实现
唯一性
使用 Redis 来实现锁的唯一性话主要是使用SETNX(SET if Not eXists)
命令,这个命令的话就是当指定的 key
不存在时,为 key
设置指定的值。设置成功则返回 1
。 设置失败则返回 0
。
当多个请求来时只有一个能通过SETNX(SET if Not eXists)
设置成功,但是如果这个请求报错了,死循环了或者整个项目直接宕掉了,后面所有的线程就不能设置成功了,那么就涉及防止死锁的问题。