当前位置:   article > 正文

详解 Redis 分布式锁的 5 种方案_redis分布式锁

redis分布式锁

终码一生 2023-10-12 21:32 发表于河南

以下文章来源于悟空聊架构 ,作者悟空聊架构

悟空聊架构.

用故事讲解分布式、架构。 《 JVM 性能调优实战》专栏作者, 《Spring Cloud 实战 PassJava》开源作者, 自主开发了 PMP 刷题小程序。

本地加锁的方式在分布式的场景下不适用,所以本文我们来探讨下如何引入分布式锁解决本地锁的问题。本篇所有代码和业务基于我的开源项目 PassJava。

本篇主要内容如下:

图片

一、本地锁的问题

首先我们来回顾下本地锁的问题:

目前题目微服务被拆分成了四个微服务。前端请求进来时,会被转发到不同的微服务。假如前端接收了 10 W 个请求,每个微服务接收 2.5 W 个请求,假如缓存失效了,每个微服务在访问数据库时加锁,通过锁(synchronzied 或 lock)来锁住自己的线程资源,从而防止缓存击穿

这是一种本地加锁的方式,在分布式情况下会带来数据不一致的问题:比如服务 A 获取数据后,更新缓存 key =100,服务 B 不受服务 A 的锁限制,并发去更新缓存 key = 99,最后的结果可能是 99 或 100,但这是一种未知的状态,与期望结果不一致。流程图如下所示:

图片

二、什么是分布式锁

基于上面本地锁的问题,我们需要一种支持分布式集群环境下的锁:查询 DB 时,只有一个线程能访问,其他线程都需要等待第一个线程释放锁资源后,才能继续执行。

生活中的案例:可以把锁看成房门外的一把,所有并发线程比作,他们都想进入房间,房间内只能有一个人进入。当有人进入后,将门反锁,其他人必须等待,直到进去的人出来。

图片

我们来看下分布式锁的基本原理,如下图所示:

图片

我们来分析下上图的分布式锁:

  • 1.前端将 10W 的高并发请求转发给四个题目微服务。

  • 2.每个微服务处理 2.5 W 个请求。

  • 3.每个处理请求的线程在执行业务之前,需要先抢占锁。可以理解为“占坑”。

  • 4.获取到锁的线程在执行完业务后,释放锁。可以理解为“释放坑位”。

  • 5.未获取到的线程需要等待锁释放。

  • 6.释放锁后,其他线程抢占锁。

  • 7.重复执行步骤 4、5、6。

大白话解释:所有请求的线程都去同一个地方“占坑”,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放“坑位”。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库,这篇讲的就是如何用 Redis 做“分布式坑位”

三、Redis 的 SETNX

Redis 作为一个公共可访问的地方,正好可以作为“占坑”的地方。

用 Redis 实现分布式锁的几种方案,我们都是用 SETNX 命令(设置 key 等于某 value)。只是高阶方案传的参数个数不一样,以及考虑了异常情况。

我们来看下这个命令,SETNXset If not exist的简写。意思就是当 key 不存在时,设置 key 的值,存在时,什么都不做。

在 Redis 命令行中是这样执行的:

set <key> <value> NX

我们可以进到 redis 容器中来试下 SETNX 命令。

先进入容器:

docker exec -it <容器 id> redid-cli

然后执行 SETNX 命令:将 wukong 这个 key 对应的 value 设置成 1111

set wukong 1111 NX

返回 OK,表示设置成功。重复执行该命令,返回 nil表示设置失败。

图片

四、青铜方案

我们先用 Redis 的 SETNX 命令来实现最简单的分布式锁。

3.1 青铜原理

我们来看下流程图:

图片

  • 多个并发线程都去 Redis 中申请锁,也就是执行 setnx 命令,假设线程 A 执行成功,说明当前线程 A 获得了。

  • 其他线程执行 setnx 命令都会是失败的,所以需要等待线程 A 释放锁。

  • 线程 A 执行完自己的业务后,删除锁。

  • 其他线程继续抢占锁,也就是执行 setnx 命令。因为线程 A 已经删除了锁,所以又有其他线程可以抢占到锁了。

代码示例如下,Java 中 setnx 命令对应的代码为 setIfAbsent

setIfAbsent 方法的第一个参数代表 key,第二个参数代表值。

  1. // 1.先抢占锁
  2. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock""123");
  3. if(lock) {
  4.   // 2.抢占成功,执行业务
  5.   List<TypeEntity> typeEntityListFromDb = getDataFromDB();
  6.   // 3.解锁
  7.   redisTemplate.delete("lock");
  8.   return typeEntityListFromDb;
  9. else {
  10.   // 4.休眠一段时间
  11.   sleep(100);
  12.   // 5.抢占失败,等待锁释放
  13.   return getTypeEntityListByRedisDistributedLock();
  14. }

一个小问题:那为什么需要休眠一段时间?

因为该程序存在递归调用,可能会导致栈空间溢出。

3.2 青铜方案的缺陷

青铜之所以叫青铜,是因为它是最初级的,肯定会带来很多问题。

设想一种家庭场景:晚上小空一个人开锁进入了房间,打开了电灯

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/696682
推荐阅读
相关标签