redis下分布式锁的简单实现
一、微服务和分布式的概念
1.什么是微服务
微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成,系统的哥哥微服务可被独立部署,各个微服务之间是松耦合的,每个微服务仅关注于一件任务并很好的完成该任务。
2.什么是分布式
一个业务分拆多个子业务,部署到不同的服务器上。
微服务式架构设计方式,分布式是系统部署方式
1.分布式:分散压力
2.微服务:分散能力
3.什么是集群
同一个业务,部署在多个服务器上。
简单来说,分布式是以缩短单个任务的执行时间来提升效率的,而集群则是通过提高单位时间内执行的任务数来提升效率。
二、微服务架构需要解决的四大问题
1.客户端如何访问这么多的服务?
通过api网关解决客户端访问,客户端访问服务器时先经过api网关,网关再将请求分发到对应的服务。
网关:又称网间连接器、协议转换器。网关的功能是实现网络之间的相互连接。网络不仅可以让广域网间相互连接,也可以让局域网之间相互连接。网络在计算机和设备之间起转换的作用,相当于多个翻译器,可以使用不同的协议、语言、数据在不同的系统之间进行转换。
2.服务与服务之间如何通信
同步通信
HTTP:Apache HTTP Client
RPC:DUbbo支持Java
异步通信:
消息队列:RabbitMQ、Kafka
3.这么多服务,如何管理
服务治理
服务注册与发现
1.基于客户端的服务注册与发现:Apache Zookeeper。
2基于服务端的服务注册与发现:Netfix Eureka。
4.服务挂了,怎么办
重试机制;服务熔断;服务降级;服务限流。
三、分布式锁
1.什么是分布式协调技术
分布式协调技术主要用来解决分布式环境当中多个进程之间的同步控制,让他们有序的取访问某个临界资源,防止造成“脏数据”的后果。
图中的五个商品就是临界资源,我们要是服务能够有序的访问临界资源。对于单台服务器来说,就不会存在这个问题,我们可以使用同步代码块的方式来保证多线程下的安全。
分布式协调的本质就是分布式锁。而Zookeeper就是基于分布式锁实现的框架
2.什么是分布式锁
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度,而这个分布式协调技术的核心就是实现分布式锁。
成员变量A存在JVM1、JVM2、JVM3三个JVM内存中。
成员变量A同时都会在JVM分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的。
不同时发来,三个请求分别操作三个不同的JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的。
注意:该成员变量A是一个有状态的对象
3.分布式锁应该具备哪些条件
1.在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程获取。
2.高可用的获取锁与释放锁;
3.高性能的获取锁与释放锁;
4.具备可重入性(当一个线程获取锁对象之后,这个线程可以再次获取本对象上的锁)。
5.具有锁失效机制,防止死锁的发生。
6.具备非阻塞特性,及没有获取到锁将直接返回获取锁失败。
4.分布式锁实现的几种方式
1.Mencahed:利用Memcached的add命令,次命令是原子性操作,只有在key不存在的情况下,才能add成功,也就意味着线程得到了锁。
2.redis:利用redis的setnx命令,次命令同样是原子性操作,只有在key不存在的情况下,才能set成功。
3.Zookeeper:利用Zookeeper的特殊临界节点,来实现分布式锁和等待队列,Zookeeper设计的初衷,就是为了实现分布式锁服务的。
4.CHubby:Google公司实现的粗粒度分布式锁服务,底层利用了Paxos一致性算法。
四、redis分布式锁的实现
1.实现原则
1.互斥性:任意时刻,只能有一个客户端持有锁。
2.不发生死锁:即使有客户端在持有锁期间没有主动释放锁,应该设置过期设置,让其在一定时间内自动失效(解锁)。
3.容错性:只要大部分的redis节点正常运行,客户端就可以加锁解锁。
4.自己加锁自己解锁:加锁和解锁必须是同一个客户端,客户端不能把别人加的锁给解了。
2.分布式锁实现的三个核心要素
setnx命令:(set if not exists)命令在指定的key不存在时,为key设置指定的值。设置成功返回1,失败返回0。可以利用这个特点来做分布式锁。
加锁:
最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名。value可以设置为1,加锁的代码:
setnx(miaosha_goodsID,1);
1.当一个线程执行setnx返回1,说明key原本不存在,该线程成功获得了锁。
2.当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
解锁:
释放锁最简单的方式是执行del指令
del(miaosha_goodsID);
锁超时
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式的释放锁,这块资源将会被永远锁住(死锁),别的线程将无法访问这块资源。所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx不支持超时参数,所以需要额外的指令。
expire(miaosha_goodsID,30);
综合代码
if(setnx(miaosha_goodsID,1) == 1){
expire(miaosha_goodsID,30)
try{
do something....
}finally{
del(miaosha_goodsID);
}
}
3.存在的问题以及解决办法?
1.setnx和expire这两条命令,不具有原子性
出现错误场景:当某线程执行setnx,成功得到了锁,setnx刚执行成功,还未来得及执行expire指令,这个线程就挂掉了。这样一来,这把锁就没有设置过期时间,变成死锁,别的线程再也无法获得锁。
redis在2.8之后给set命令加了好多参数,我们可以使用set命令来解决这个问题。
set(key,value,NX,PX,time);
key:我们使用key来当锁
value:传的一般是requestID,有可能会疑惑,有key作为锁不就够了吗?其实这是为了保证可靠性,保证解锁的时候有依据,保证自己加的锁自己解。
NX:意思是SET IF NOT EXIST,即当key不存在时,进行set操作,若key已经存在,不做任何操作。
PX:意思是要给key加一个过期的设置,具体时间由第五个参数来定。
time:代表key的过期时间。
2.del导致误删(A线程删除了B线程加的锁)
比如A线程成功得到了锁,并且设置的超时时间是30秒,如果某些原因导致A线程执行的很慢,过了30秒都没执行完,这时候过期自动释放,线程B得到了锁,随后A执行完了任务,线程A接着执行del来释放锁。但这时候线程B还没有执行完,线程A实际上删除的是线程B加的锁。
可以在del释放锁之前加一个判断,验证当前的锁是不是自己加的锁,可以在再加锁的时候把当前线程ID当作value,并在删除之前验证key对应的value是不是自己线程的ID。
加锁
String threadID = Thread.currentThread().getId();
set(key,threadID,NX,PX,30)
解锁
if(threadId.equals(redis.get(key))){
del(key);
}
3.判断和释放两个不是原子操作?
我们可以使用Lua脚本和redis中的eval()方法来解决这个问题。
public class RedisTool{
private static final long RELEASE_SUCCESS = 1L;
/**
释放分布式锁
@param jedis redis客户端
@param lockKey 锁
@param requestId 请求标识
@return 是否释放成功
**/
public static boolean releaseDistrbutedLock(Jedis jedis,String lockKey,String requestId){
String script = "if redis.call('get',KEYS[1] == ARGV[1]) then return redis.call('del',KEYS[1]) else reutrn 0 end";
Object result = jedis.eval(script,Collection.singletonList(lockKey),Collections.singletonList(requestId));
//我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARG[1]赋值为RequestId。eval()方法是将Lua代码交给Redis服务端执行。
if(RELEASE_SUCCESS.equals(result)){
return true;
}
return false;
}
}
那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁),那么为什么要使用Lua语言来实现呢?因为要确保上述操作的原子性。
redis官方对于eval的解释:在eval执行Lua代码的时候,Lua代码将被当成一个命令取执行,并且直到eval命令执行完成,redis才会执行其他命令。