是谁,又被分布式锁给锁住了?(上)

大家好,我是徒手敲代码。

今天来介绍一下分布式锁。首先思考下这些问题:

  • 为什么需要分布式锁?
  • 基于 Redis 如何实现分布式锁?
  • 单纯使用setNx命令来加锁,会存在什么问题?
  • 经常听到的RedLock,是百分百完美的方案吗?
  • 除了 Redis,还有其他的解决方案吗?

因为篇幅问题,本文主要解决前三个问题。

为什么需要分布式锁

一个新技术的诞生,往往是现有的技术,无法满足业务需求。

与分布式锁相对应的,是单机锁。当我们在同一台机器,多个线程同时操作一个共享资源,那么通常会采用加锁的方式,只有获取到锁的线程,才能对共享资源进行访问,否则就要等待锁的释放,如下图:

如果业务需要拓展,单台服务器已经无法满足需求,单机系统需要拆分成多个微服务,分别部署在多台服务器上,如果还是使用单机锁的话,就会出现下图这种情况:

尽管每个服务各自都加了锁,但是这个锁只是 JVM 层面的,从数据库的角度看,还是同一时间有多个线程过来竞争。显然是会有问题的。

这个时候,就需要引入分布式锁了。想要实现分布式锁,需要引入一个外部系统,所有线程都要在这个外部系统上申请锁,只有申请到锁的线程,才能访问到共享资源。

而这个外部系统,可以是 MySQL、Redis、Zookeeper,接下来以Redis作为主线,来讲解分布式锁的实现。

Redis最简单的实现

利用 Redis 最简单的方式,就是使用setNx这个命令,含义是 set if not exists,这个key不存在,才设置它的值,否则直接返回。

比如:客户端1申请加锁,加锁成功:

bash 复制代码
127.0.0.1:6379> setnx lock 666
(integer) 1

客户端2申请加锁,加锁失败:

bash 复制代码
127.0.0.1:6379> setnx lock 666
(integer) 0

加锁之后,可以去修改共享资源数据,处理完成之后解锁:

bash 复制代码
127.0.0.1:6379> del lock
(integer) 1

存在问题

简单的背后,往往要付出一些代价。正所谓easy come, easy go

死锁问题

第一,命令没有设置过期时间,那么如果获取到锁的客户端,在释放锁之前挂了,那么这个锁将变成死锁;针对这种情况,可以给锁设置一个过期时间,即使服务挂了,锁也会自动释放。

过期时间长短的问题

既然有了过期时间,那么这个时间设置多久呢?

假设业务处理的时间,预估最多不会超过10s,我们将过期时间设置成10s

如果过期时间太短,那么A线程还没有处理完业务,B线程就进来了,两个线程同时操作共享资源,会导致数据出现不一致问题;在A线程操作完成的时候,释放锁,此时释放的是B线程的锁。

而且在处理的过程中,情况可能最多不止10s,还有各种异常情况,比如:程序内部异常、网络超时等,预估的时间很难准确。

如果过期时间太长,会导致系统资源长时间被无意义地占用,大量请求需要排队等待,系统的性能会下降。

如何解决呢?

解决过期时间的问题

在加锁的时候,同样先设置一个过期时间,然后再开一个守护线程(称为看门狗线程),这个线程负责定时检测锁的过期时间,如果锁快过期了,但是业务处理还没有完成,那么就要为这个锁的持有线程续命一波,延长它的过期时间,在Java当中,可以直接使用Redisson这个库,内部已经封装好了接口。

解决锁被其他线程释放的问题

这个问题的关键在于,释放锁的时候,都是无脑操作,没有判断释放的锁是不是自己的。

那么我们就要加上这个判断。在客户端加锁的时候,设置一个唯一的标识,比如线程 id。解锁的时候,先判断一下锁是否还归自己所有。解锁过程可以写成lua脚本让 Redis 执行:

vbnet 复制代码
if redis.call("GET",KEYS[1]) == ARGV[1]
then
    return redis.call("DEL",KEYS[1])
else
    return 0
end

注意,单机部署的 Redis 可以这么玩,但是主从架构部署的 Redis 就不行了,下篇文章继续来分析。

今天的分享到这里结束了。

关注公众号"徒手敲代码",免费领取腾讯大佬推荐的Java电子书!

相关推荐
.生产的驴15 分钟前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
景天科技苑24 分钟前
【Rust】Rust中的枚举与模式匹配,原理解析与应用实战
开发语言·后端·rust·match·enum·枚举与模式匹配·rust枚举与模式匹配
AnsenZhu27 分钟前
2025年Redis分片存储性能优化指南
数据库·redis·性能优化·分片
追逐时光者1 小时前
MongoDB从入门到实战之Docker快速安装MongoDB
后端·mongodb
方圆想当图灵1 小时前
深入理解 AOP:使用 AspectJ 实现对 Maven 依赖中 Jar 包类的织入
后端·maven
豌豆花下猫1 小时前
Python 潮流周刊#99:如何在生产环境中运行 Python?(摘要)
后端·python·ai
嘻嘻嘻嘻嘻嘻ys2 小时前
《Spring Boot 3 + Java 17:响应式云原生架构深度实践与范式革新》
前端·后端
异常君2 小时前
线程池隐患解析:为何阿里巴巴拒绝 Executors
java·后端·代码规范
mazhimazhi2 小时前
GC垃圾收集时,居然还有用户线程在奔跑
后端·面试
Python私教2 小时前
基于 Requests 与 Ollama 的本地大模型交互全栈实践指南
后端