在融资平台微服务演进中关于分布式锁一些总结
Posted 消费金融架构
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在融资平台微服务演进中关于分布式锁一些总结相关的知识,希望对你有一定的参考价值。
编辑:阿兵
现在基本所有应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法满足一致性(Consitency)、可用性(Availability)、分区容错性(Partitiontolerance),最多只能满足两项”。所以很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大数的场景中,都是需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接收的范围内即可。
在很多场景中,为了保证数据的最终一致性,需要很多技术方案的支持。比如分布式事务,分布式锁等。有时候,我们需要保证一个方法内在同一时间内只能被同一个线程执行。在单机环境中,java提供了很多并发处理相关的API,是这些API在分布式场景中就无能为力了。也就是说单纯的java api并不能提供分布式锁的能力,所以针对分布式锁的实现,常见的方案:
1.基于数据库乐观锁实现;
2.基于redis实现;
3.基于zookeeper实现;
实现一个分布式锁的锁具备的要求:
1.互斥性:在任意时刻,只有一个客户端能持有锁。
2.不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3.具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
(一)基于数据库的实现
A)基于数据表来实现。
直接创建一个张锁表,然后通过操作该表中的数据来实现。当我们要锁住某个资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
当我们要锁住某个方法时,执行SQL:
insertinto methodLock(method_name,desc) values (‘method_name’,‘desc’);
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
当方法执行完毕之后,想要释放锁的话,需要执行SQL:
deletefrom methodLock where method_name ='method_name'
上面这种简单的实现有以下几个问题:
1、这把锁依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解决操作失败,就会导致记录一直在数据库中,其他线程无法在获得锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁的操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据库表中数据已经存在了。
当然,我们也可以有其它方式解决上面的问题:
1、数据库是单点?那就搞两个数据库,数据库之前双向同步,一旦挂掉快速切换到备库上。
2、没有失效时间?可以做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、非阻塞?可以写一个while循环,直到insert成功再返回成功。
4、非重入?可以在数据库表中加一个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库中可以查到的话,就直接把锁分配给它即可。
B)基于数据库的乐观锁
使用数据库实现分布式锁的优点:实现比较简单。
使用数据库实现分布式锁的缺点:可扩展性不好,数据库锁实现只能是非阻塞锁,即应该为tryLock,是尝试获得锁,如果无法获得则会返回失败。该锁机制没有过期时间。
(二)基于redis的实现
因为我们的redis是单机部署的,所以通过jedis插件来实现。
可以看到,我们加锁就一行代码:jedis.set(String key, Stringvalue, String nxxx, String expx, int time),这个set()方法一共有五个形参:
· 第一个为key,我们使用key来当锁,因为key是唯一的。
· 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
· 第三个为nxxx,这个参数我们填的是NX,意思是SETIF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
· 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
· 第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:
1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
2. 已有锁存在,不做任何操作。
错误示例1
比较常见的错误示例就是使用jedis.setnx()
和jedis.expire()
组合实现加锁,代码如下:
setnx()方法作用就是SET IFNOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。
如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件。
使用缓存实现分布式锁的优点:性能好,实现起来较为方便。
使用缓存实现分布式锁的缺点:通过超时时间来控制锁的失效时间并不是十分的靠谱。
(三)使用zookeeper实现
原理:
每一个客户端在需要获取锁资源的时候,要首先到locker节点下创建顺序节点node_n
然后立刻获取locker下面所有的子节点
要注意的是,同一时间可能有多个客户端争抢资源,那么locker下的node节点的数量就可能大于1,由于顺序节点后面有一串数字,先创建的节点的数字小于后面创建的节点的数字,把这些节点的按后面的数字由小到大的排序,那么排在第一位的,一定是最先创建的节点,这个节点代表最先争抢资源的那个客户端
这个时候需要判断本客户端刚刚创建的节点是不是最小的节点,如果是,则认为自己已经获得了锁资源,如果不是,则说明在这之前已经有其他的客户端发起了争抢锁的操作,于是我们需要等待它释放锁,也就是等待它把那个节点删除。那么我们可以通过监听比自己小一点的节点的删除事件,来知道那个客户端是否已经释放锁资源。如果是已经删除了,我们再次获取locker节点下的所有子节点,然后再把后面的走一遍,直到获取了锁。
可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
使用Zookeeper实现分布式锁的优点:有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
使用Zookeeper实现分布式锁的缺点:性能上不如使用缓存实现分布式锁。需要对ZK的原理有所了解
三种方案的比较
上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 >Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库
以上是关于在融资平台微服务演进中关于分布式锁一些总结的主要内容,如果未能解决你的问题,请参考以下文章