目录
什么是CAP理论 , 哪些技术用到AP,哪些用到CP?
什么是Base理论?
基于数据库的分布式锁(基于主键id和唯一索引)
1基于主键实现分布式锁
2基于唯一索引实现分布式锁
基于Redis的分布式锁
基于Redis实现分布式锁执行流程:
Redission实现分布式锁【封装了基于Redis的分布式锁】
什么是Redission?
基于Zookeeper的分布式锁
实现方案:临时顺序目录节点+监听机制
在项目中可以使用curator,这个是Apache封装好的基于zookeeper的分布式锁方案。
总结
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性(可用性),系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
理论指的是,在一个分布式系统中,一致性(Consistency),可用性(Avaliability),分区容错性(Partition Tolerance),三个要素最多只能同时实现两点,不能同时兼顾。
分布式容错:指的是分布式系统某个节点或网路分区出现故障的时候,不会影响整体的使用。
可用性:所有的节点都保持高可用性。【但是不会保证数据的一致性】。这里的高可用表示的是节点进行访问不会出现延迟,如果节点由于等待数据同步而进行了堵塞,那么该节点·就不满足高可用性。
一致性:在分布式系统中,读取数据应该是最新的数据【强一致性】,进行写操作后再去进行获取应该获取到最新的值,数据的一致性。
分区容错性是分布式系统的核心关键,如果一个节点或网络分区出现故障,整体就挂掉了,这就不叫作分布式系统。
满足CP,也就是满足一致性和容错性,舍弃可用性,如果系统要保证数据地强一致性(必须等待数据同步才进行响应)就可以考虑。常见的如Redis,Nacos,ZooKeeper
满足AP,也就是满足可用性和容错性,舍弃一致性,如果系统允许数据的弱一致性(最终一致性即可,获取的数据不是最新的,但最终是一致的)可以考虑。常见的如MySQL,Eureka,RocketMQ
Base理论指的是基本可用(Basic Available),软状态(Soft),最终一致性(eventually coonsistent)。它是基于的AP【可用性和分区容错性】的扩展,在分布式系统中某个节点或网络分区出现故障时,系统的整体不受影响,正常使用。允许在一段时间内数据不一致,但要保证数据的最终一致性。
基本可用(Basic Available)
响应时间上的损失:正常情况下,处理用户请求需要0.5s返回结果,但是由于系统出现故障,处理用户请求的时间变成3s。【但是还是可以使用--基本可用】
系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的非核心功能无法使用。
软状态(Soft)
指的是允许系统的数据存在中间状态,并认为中间状态并不会影响系统的整体的可用性,比如正在支付中,数据正在同步中.....
最终一致性(eventually coonsistent)
最终一致性指的是数据经过一段时间后,所有的节点数据最终都是一致性的。比如订单的"支付中"状态,最终会变为“支付成功”或者"支付失败" ,使订单状态与实际交易结果达成一致,但需要一定时间的延迟等待。
=================================单机环境===============================
首先在通过下面的图片认识一下单机环境,才能更好的理解分布式环境和基于Redis实现分布式锁;
在单机环境下,就是只有当前JVM虚拟机A中的多个线程对同一个Object(共享资源)进行操作,可以采用同步互斥(就是只有同一时间只有一个线程对共享资源进行事务操作)的方式,我们常用的方式就是采用加锁的方式(synchronized和lock)解决,虽然不能保证数据的强一致性,但是能够保证数据的最终一致性。
那么这时候就会出现问题了,如果两个虚拟机A和B(可以理解为两个应用)同时对一个共享资源(数据库中的一张表或者数据库中的一行数据进行操作),那么这时候就不能保证同步互斥了,加锁的方式就局限在解决一个虚拟机中的线程安全问题了。
这时候就出现了分布式锁,用来解决分布式系统之间访问共享数据,共享数据在同一时间只能被某单个进程中的某单个线程操作。
============================分布式环境===================================
分布式锁到底是什么呢?
就是在分布式环境中,保证多个进程的共享资源(可以是数据库中的一张表,一行数据)在同一时间只能被某个进程中的某个线程访问时,所采用的加锁方式。
分布式锁的三种方式
1基于数据库的分布式锁又可以分为1基于主键实现分布式锁和2基于唯一索引实现分布式锁。
其实原理一致,都是采用一个唯一的标识进行判断是否加锁。
原理:通过主键或者唯一索性两者都是唯一的特性,如果多个服务器同时请求到数据库,数据库只会允许同一时间只有一个服务器的请求在对数据库进行操作,其他服务器的请求就需要进行阻塞等待或者进行自旋。如何实现的呢?可以理解为同一时间只有一个请求能够拿到锁,当方式执行完成过后,对锁进行释放过后,其他请求就可以拿到锁再对数据库进行操作,这样就避免了数据不安全问题。
再解释一下,这个锁其实就是通过主键id或者唯一索引设定的分布式锁,比如服务器1和服务器2的请求都要对主键id为1的这行数据进行修改,这时候都会去数据库对这行数据进行加锁,但是只会有一个服务器的请求加锁成功(因为主键id是唯一的,不可能两个都能加锁上),另一个服务器就会进入阻塞等待或者自旋,等待锁的释放,然后再进行对这行数据进行操作。
拓展解释:
阻塞:线程等待锁释放的一种方式
自旋:自旋包括了递归自旋,while自旋。意思就是不断地去尝试获取锁,只有获取锁才会停止自旋过程,没有拿到就会一直尝试获取锁。
原理跟基于主键实现分布式索引一样
下面就来介绍基于Redis的分布式锁,直接上图;
虚拟机A和虚拟机B两个虚拟机都想对可变的共享资源(广义的概念,可以是数据库的某一张表和数据库中的某一行数据 ),就会出现线程安全问题,就需要基于锁模型实现同步互斥的手段,保证只有一个虚拟机中的线程进而实现这个线程的相对安全。现在虚拟机要对共享资源上锁,锁对象是Redis,操作的对象就是这个共享资源(假如数据库的一行数据)。
1虚拟机实例A根据Hash算法选择Redis节点(Redis采用的是集群部署,每个服务器都是一个节点),执行Lua脚本加锁(就是通过setnx方法,即set一个key(主键id)到Redis当中,判断Redis中是否有当前key,没有,就返回1,表示加锁成功,有就返回0,表示加锁失败),并且设置锁的过期时间。当虚拟机A对共享资源上锁成功过后,就拥有了对共享数据的操作权限,然后就可以对共享数据的操作处理,执行事务处理。
问题:在从Redis获取锁的过程和进行设置锁的过期时间过程中出现宕机,就会出现锁一辈子不会被释放?出现死锁问题?
这时候就需要保证获取锁和设置锁的过期时间两行代码的原子性,就是要么同时成功,要么同时失败,如何实现呢?
这时候只要保证将两行代码变成一行代码即可。
原本的setnx和expire是两行代码
if(jedis.setnx(lock_stock,1) == 1){ //获取锁
//=========在这里出现宕机了=====死锁问题出现了=========
expire(lock_stock,5) //设置锁超时
try {
业务代码
} finally {
jedis.del(lock_stock) //释放锁
}
}
通过set变成一行代码过后,解决了死锁问题。
if(set(lock_stock,1,"NX","EX",5) == 1){ //获取锁并设置超时
try {
业务代码
} finally {
del(lock_stock) //释放锁
}
}
2这时候如果虚拟机B也需要对共享资源进行操作,也去执行lua脚本进行加锁(就是采用setnx的方式--通过set一个key到redis,判断redis中是否已经存储了这个key(行数据的主键id)),如果查询到redis中没有,就会返回1表示加锁成功,如果有就会返回0表示加锁失败 ,这就能保证共享资源同一时间不会被多个虚拟机同时操作。
3当虚拟机A执行完对自己已经加锁的共享资源执行操作完成之后,必须要执行DEL释放锁,不然其他虚拟机包括虚拟机A都不能再对当前共享资源进行加锁操作,
问题:虚拟机A能保证会执行完成过后一定执行DEL释放锁吗? 答案是:不一定的
4当虚拟机A执行对共享资源事务操作完成之后,在执行DEL释放锁之前,代码出现问题,抛出异常就会出现这种问题,虚拟机A就永远不能执行DEL释放锁了,就会导致后续上锁都会失败。
问题:所以就出现上面这个执行流程,如何解决呢?
5这时候起初指执行lua脚本加锁的时候,存储一个过期时间,当不能主动进行DEL释放锁时,到达Redis设置的过期时间,锁就会过期。
这个时候又会出现另一个问题? 就是虚拟机A在设置的过期时间以内还没有执行完对共享资源的操作,锁就过期了,如何解决呢?
6这时候就会执行最后一个流程,后台守护线程(类似于Redission内部提供一个监控锁的看门狗),来定期的检查锁是否存在,如果存在,延长key的过期时间,还需要判断事务是否还在正常执行,如果是异常已经抛出异常,就不用进行后台守护线程了,然后等待锁自动过期。
说这么多?其实就是明白其执行原理,而实际开发过程中,大佬们已经使用Redission封装好了上面的具体实现细节。
简单理解为就是操作Redis的一个工具包,让我们使用Redis更加简单,让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson实现分布式锁
Redisson官方文档对分布式锁的解释总结下来有两点
1Redisson加锁自动有过期时间30s,监控锁的看门狗发现业务没执行完,会自动进行锁的续期(重回30s),这样做的好处是防止在程序还没有执行结束,锁自动过期被删除问题
2当业务执行完成不再给锁续期,即使没有手动释放锁,锁的过期时间到了也会自动释放锁。
// 注入
@Autowired private RedissonClient redissonClient; // 加锁 RLock lock = redissonClient.getLock("lock:" + preOrderKey); // 进行加锁【锁有自动过期时间,且看门狗会进行自动续期】 lock.lock();
什么是Zookeep?
ZooKeeper是一个分布式的协调服务,Zookeeper是基于CP,注重数据的一致性,若主机挂掉则Zookeeper不会对外进行提供服务了,需要选择一个新的Leader出来才能提供服务,不保证高可用性。简单来说zookeeper=文件系统+监听通知机制。
Zookeeper数据模型
Zookeeper会维护一个具有层次关系的树状的数据结构,它非常类似于一个标准的文件系统,如下图所示:同一个目录下不能有相同名称的目录节点
每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。
有四种类型的znode:
PERSISTENT-持久化目录节点
客户端与zookeeper断开连接后,该节点依旧存在
PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点
客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
EPHEMERAL-临时目录节点
客户端与zookeeper断开连接后,该节点被删除
EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点
客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
监听通知机制
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。
=======================================================================
1基于数据库实现:通常基于主键,或者唯一索引来实现分布式锁,但是性能比较差,一般不建议使用
2基于Redis实现分布式锁:可以使用setnx来加锁 ,但是需要设置锁的过期时间来防止死锁,所以要结合expire使用.为了保证setnx和expire两个命令的原子性,可以使用set命令组合【将setnx和expire结合成一行代码】。
总之自己封装Redis的分布式锁是很麻烦的,我们可以使用Redissoin来实现分布式锁,Redissoin已经封装好了。
基于zookeeper : 使用临时顺序节点+监听实现,线程进来都去创建临时顺序节点,第一个节点的创建线程获取到锁,后面的节点监听自己的上一个节点的删除事件,如果第一个节点被删除,释放锁第二个节点就成为第一个节点,获取到锁。
在项目中可以使用curator,这个是Apache封装好的基于zookeeper的分布式锁方案。