分布式锁实践中的一些坑及优化手段

By | 2021年3月10日

分布式锁实践中的一些坑及优化手段

记录使用分布式锁时网上其他文章没有谈及的性能和优化问题,切勿生搬硬套网上代码。

本文仅讨论使用Redis作为分布式锁,不考虑Redis其他一些问题,如是否为单机节点,怎么做主从集群或者哨兵,与Zookeeper和其他分布式共识算法实现的分布式中间件有什么区别,我选择redis主要还是考虑它自身的性能和编码的复杂度,至于CP还是AP的选型暂时不作为考量标准。

当微服务由单机部署变为分布式集群部署,在业务中涉及的一些数据库操作或者其他可能存在并发问题的地方,都有可能因为代码层面考虑不周或存在漏洞,导致数据丢失更新,数据不一致的问题发生,我也是在工作中遇到这个问题。例如A请求查询数据库中可用次数为100,此时B请求也查询数据库中可用次数为100,A请求进行-1之后在数据库中修改为99,而B请求也-1之后修改数据库中可用次数为99,这时就发生了数据丢失更新的问题,给公司造成了损失,当并发量更大,B请求出现一定延时,可能会发生其他请求已经修改了十多次,而B请求又将可用次数改为99,如此日积月累会造成巨大的损失。

在根据一些技术文档提供的解决方案中,我选择了Redis来实现分布式锁,大致架构如下

实现大致原理:

  1. 当请求过来时,通过lua脚本,使用空间名称key和请求ID作为value,在redis中存入一个过期时间为10s的Str
  2. 请求处理完毕后主动释放自己加的锁,如果节点失败也可以通过过期时间自动删除,不会出现“死锁”
  3. 当多个请求同时发生,先竞争到锁的先执行,没有抢到锁的则生成一个随机等待时间,等待结束后再发起竞争,这样可以防止大量请求同时竞争造成羊群效应

BA058F4A-4CF9-4669-8814-B1A1BE2E190F

问题初现

如上架构,确实可以直接解决数据丢失更新的问题,这样意味着所有节点上的所有请求在操作的时候都会去加锁,读与读,读与写直接加锁,如此一把大锁悬在空中,勒住了所有请求,系统性能直线性下降,并发越大竞争越大。

并发三组线程请求三个不同的接口,线程数100,Ramp为1秒,循环2次,发送等待锁次数57720

image-20210105165133512

并发四组线程请求四个不同的接口,线程数100,Ramp为1秒,循环2次,发送等待锁次数74180

image-20210105165555885

cpu占用率

image-20210105165649385

根据测试可以得出以下分析

  1. 加锁可以解决数据丢失的问题
  2. 加锁会导致并发量急剧下降,最大为7/s
  3. 加锁导致大量锁竞争,至高并发时,单个请求需要竞争近百次

优化一

目前的锁很明显是悲观锁,请求上来就会先加锁,可以在优化为乐观锁,先去判断当前是否有请求冲突发生,如果没有竞争,则直接操作,如果有则进行加锁,类似于CAS这样的机制,可以少去首次加锁的开销。

优化二

细化锁粒度,参考Innodb的行锁设计,这个需要根据具体的业务场景来考量,在我们的业务中,同一用户的同一请求可以根据索引或键值ID来确定,然后用这类主键ID作为锁空间命名,可以极大程度的细化锁,不会让不同用户的不同请求发生冲突。

优化三

缩小随机等待时间区间,当竞争锁失败后,随机等待多久再去发起竞争呢,等待时间太长没有必要,等待时间过短的话会加剧竞争失败的发生。最初我的值是50-200ms,后来进过jmeter一系列压测,确定在10-50ms这个区间竞争数少,等待时间也短。

优化四

从SQL的角度来看,能否将这样类似的计费等操作进行合并,不要在代码中取出数据再进行增减,这样可以直接利用Mysql中Innodb的行锁,减少业务层对请求的阻塞。

升级优化的方案有很多,还可以使用第三方分布式锁的框架,提供一些可重入锁的支持等,具体场景具体优化,但网上提供的解决办法不能生搬硬套,否则在带来代码侵入的同时还无法提高性能。

发表评论

电子邮件地址不会被公开。 必填项已用*标注