在融资平台微服务演进中关于分布式锁一些总结

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 EXISTexpire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

 

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件。

使用实现分布式性能好,实现起来较为方便。

使用实现分布式的缺点时时间来控制的失效时间并不是十分的靠


(三)使用zookeeper实现

原理:

  1. 每一个客端在需要获取锁资源的候,要首先到locker节点下创建顺序节点node_n

  2. 然后立刻获取locker下面所有的子节点

  3. 要注意的是,同一时间可能有多个客端争抢资源,那么locker下的node节点的数量就可能大于1,由于点后面有一串数字,先建的点的数字小于后面建的点的数字,把点的按后面的数字由小到大的排序,那么排在第一位的,一定是最先建的节点,这点代表最先争抢资源的那个客户端

  4. 候需要判断本客户端刚刚创建的点是不是最小的点,如果是,则认为自己已经获得了锁资源,如果不是,则说明在之前已有其他的客户端发起了争抢锁的操作,于是我需要等待它释放锁,也就是等待它把那个节点删除。那么我可以通过监听比自己小一点的点的除事件,来知道那个客端是否已经释放锁资源。如果是已经删除了,我再次获取locker节点下的所有子节点,然后再把后面的走一遍,直到获取了锁。

可以直接使用zookeeper第三方Curator客户端,这个客端中封装了一个可重入的锁服务。

使用Zookeeper实现分布式锁的优点:有效的解决单点问题,不可重入问题,非阻塞问题以及无法放的问题。实现起来较为简单。

使用Zookeeper实现分布式锁的缺点:性能上不如使用缓存实现分布式锁。需要ZK的原理有所了解



种方案的比

上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

从理解的易程度角度(从低到高)

数据 > 缓存 > Zookeeper

实现的复性角度(从低到高)

Zookeeper >= 缓存 > 数据

从性能角度(从高到低)

缓存 >Zookeeper >= 数据

从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据


以上是关于在融资平台微服务演进中关于分布式锁一些总结的主要内容,如果未能解决你的问题,请参考以下文章

关于 分布式和微服务 的一些总结

关于 分布式和微服务 的一些总结

关于 分布式和微服务 的一些总结

宜信微服务任务调度平台建设实践|分享实录

图解微服务架构演进

宜信开源|分布式任务调度平台SIA-TASK的架构设计与运行流程