当前位置: 首页 > 科技观察

分布式锁的封装也是很有讲究的

时间:2023-03-16 00:19:06 科技观察

分布式锁通常有多种选择,例如基于Redis、基于Zookeeper、基于数据库等方案。Redis是用来缓存数据,在项目中使用的,所以更多的人使用Redis来做分布式锁。如果使用Redis作为锁,可以直接使用开源方案,比如redisson。最常见的用法如下:RLocklock=redisson.getLock("anyLock");lock.lock();run();lock.unlock();获取锁对象,调用lock()加锁,执行业务逻辑,调用unlock()释放锁。虽然框架提供的使用方法很简洁,但是我们还是需要对锁进行包裹。打包的目的是提高可扩展性和易用性。抽象接口如果我们直接使用redisson的原生API进行加锁,那么很多地方都会出现RLock相关的代码。突然有一天,由于某些原因,需要更换锁。这个时候变化的范围会比较大。凡是用到RLock的地方都要改。如下图所示:很多服务都使用了RLock.lock()方法。当我们需要更换锁时,所有涉及到的类和方法都要修改。修改的点显示在红色部分。所以我们需要做一个抽象层。我们可以定义一个DistributedLock接口来提供锁相关的能力,并提供多种实现,方便替换和扩展。如下图所示:每个Service都使用DistributedLock接口来加锁。当我们需要更换锁的实现时,不需要改变使用的地方,只需要更换DistributedLock的实现即可。自动释放自动释放是指加锁后需要在业务逻辑执行完毕后自动关闭锁。按照之前Redisson的方法,我们需要手动调用unlock()来释放持有的锁。当然Redisson也提供了超时释放的功能。一般情况下,业务执行完必须释放锁,才能继续处理下一次请求同一个锁。手动释放资源最常见的问题就是忘记释放,所以在JDK7中引入了try-with-resources来自动释放资源,相信大家都不陌生。所以在封装的时候,我们尽量不让用户手动发布,减少出错的概率。对于有结果的,我们可以使用Supplier来传递你的逻辑,对于没有返回结果的,我们可以使用Runnable来传递你的逻辑。/***Lock*@paramkeylockKey*@paramwaitTime尝试加锁,等待时间(ms)*@paramleaseTime加锁后无效时间(ms)*@paramsuccess加锁成功执行逻辑*@paramfail加锁失败执行逻辑*@return*/Tlock(Stringkey,intwaitTime,intleaseTime,供应商成功,供应商失败);使用:Stringresult=distributedLock.lock("1001",1000,()->{System.out.println("进来...");try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}return"success";},()->{System.out.println("Failedtoaddlock...");return"fail";});容灾需要注意的另一个问题是锁的可用性。万一对应的Redis有问题,去这个时候Locking肯定会失败。如果什么都不做,会影响正常的业务运行,造成业务不可用。除了实现Redis锁,我们还可以实现其他的锁,比如数据库锁。当Redis锁不可用时,它会降级为数据库锁。虽然性能受到影响,但不会影响业务。在加锁过程中,如果数据库锁不再可用(题外话:全部不可用的可能性很小),还不如让业务操作失败。因为我们使用加锁的场景,肯定是为了防止并发场景带来的问题。如果没有锁,你会异常消费,让业务继续运??行,不加锁可能会出现业务问题。当然监控也是非常有必要的,比如Redis,数据库监控。发生故障时,有人员及时介入。监控系统Redis、数据库、Zookeeper必须监控承载分布式实现的中间件。另一种监控是对相应锁动作的更细粒度的监控。比如加锁时间、释放锁时间、锁中业务执行时间、锁的并发度、执行次数、锁失败次数等。这些数据指标非常重要,可以帮助你及时发现问题。例如,10秒内数百次锁定失败已降级为数据库锁定。这时候你收到告警,一看就知道Redis有问题,及时解决。监视方法是可选的。每个公司都不一样。可以将数据暴露给Prometheus抓取,也可以集成Cat做好埋点工作。只要能监控报警就好了。