【经验项】分布式锁到底要怎么写

问题发现

在工作中,运维对于安装这个服务存在阻碍,需要安装redis集群模式。我对此产生了疑问?
这个服务为什么需要使用到redis集群? 这个服务一直都是单例程序,有状态 信息无法横向扩展,这里就有个冲突点:使用了redis集群,但状态信息存在内存中而影响到无法横向扩展。 而进行代码review后,发现redis集群的用途只是为了加分布式锁,但是!!程序又不是分布式的。
释怀:优秀的设计往往让人摸不着头脑。

那么就有人问了,这跟主题有什么关系? 这个代码的分布式锁写的有大问题。

代码分析

这里将程序中的代码思想复现一波:

go 复制代码
 c, err := redis.Dial("tcp", ":6379", redis.DialPassword("123456"))
 if err != nil {
  panic(err)
 }
 defer c.Close()
 timeoutMs := 60 * 1000  // 60s
    // 执行SetNX 命令, 如果key存在 isset为0, key不存在 isset 为1
 isset, err := redis.Int(c.Do("SETNX", key, value))
 if err != nil {
  fmt.Errorf("TryLock SETNX failed, err[%s]", err)
  return false, err
 }
    // 如果插入成功
 if isset == 1 {
  if timeoutMs > 0 {
            // 设置超时时间
   c.Do("PEXPIRE", key, timeoutMs)
  }
  return true, nil
 }
    // 如果key存在, 超时时间存在
 if timeoutMs > 0 {
  // if key exists but ttl not set, reset ttl
  if ttl, err := redis.Int64(c.Do("PTTL", key)); err == nil && ttl == -1 {
   c.Do("PEXPIRE", key, timeoutMs)
  }
 }
    return false, nil

粗心的读者能够发现,这个代码问题太多了,主要问题:

  • 锁非原子操作,在高并发场景下,如果SetNX过程中程序挂掉了,那么这个key也就是永久有效。
  • 为了解决#1的错误,让所有对这个key操作的进程对其进行续期操作。也就是说,你没加上TTL,我好心给你加上。注意是key存在,但是没有TTL的时候才会有效。

如果出现这个场景,也就意味着1分钟内,抢占进程1分钟内无法进行正确的执行。那么这个问题,目前还没有暴雷的原因:

  • 当前这个程序从分布式进程的设计,被改成单进程模式,根本没有其他进程回去抢占,只有协程进行抢占,而同一时间内,不会超过10个左右的协程,并发度不高。
  • 当前业务形态决定,由于该程序为后台程序, 能够允许1分钟以上的容错时间。
  • redis采用集群的模式,提高了容错性。

精彩,到目前为止,我竟然觉得这个代码没有一点问题了。

(又不是又能用.罗老师.jpg)

那么,比较可靠的分布式锁,该如何实现? 复习一下。

分布式锁

锁(Lock):是一种用于同步并发访问共享资源的 机制。它通过确保在同一时刻只有一个线程或进程能够访问特定资源,从而避免数据竞争和不一致性 。锁通常用于多线程编程和分布式系统中。

锁的底层逻辑:Key的设置/删除。

分布式锁应用在分布式系统中,多进程并发访问共享资源的情况下,通常情况下,使用第三方中间件存储锁对象,如:zookeeper,redis,etcd等。

在编写分布式锁时,一般采取以下几个步骤:

  • 加锁:

    • 原子性操作:设置key,value,其中value为客户端唯一标识符,并同步设置超时时间;
    • 续期操作。
    • redis命令: SET KEY VALUE NX [EX (second) | PX (milliseconds)]
  • 释放锁:

    • 判断锁的值是否正确;
    • 删除Key。

接下来分析,为什么这么做? 以下使用redis进行举例。

加锁

加锁分为:获得锁 和 没有获得锁。
没有获取锁的情况

根据业务的情况,可采用的方案:

  1. 阻塞直到获取到锁。

  2. 非阻塞锁:

    1. 直接跳过后续流程返回;
    2. 监听锁事件,释放后重新尝试获取;

获得锁的情况

为什么需要设置超时时间?

举例如下:

如果,在图中processA获取锁成功后,执行后续流程失败,没有正常释放锁。那么processB以及其他进程将会一直无法执行。所以processA需要对锁进行超时时间设置

那么问题来,设置时间多长?

简单的话,根据业务需求,可以通过业务监控来进行设置,如最长业务处理时间*2。

如果对于处理时间未知的情况,就需要另一个手段:续约 ,在执行过程中,TTL 快过期时候进行重置过期时间操作。

释放锁

释放锁不就很简单把删除就行了吗? NoNoNo, 设想一下,在程序执行过程中,出现异常情况,processA执行过程中,processB 也获取到了锁,processB执行的非常快,把processA的锁也释放了咋办。

所以在设置key的时候,需要给定value的唯一标识。释放锁的时候判断现在的锁是不是自己之前加的。

当然,这种情况如果中间件为集群的情况下,出现的话过于极端的。

以上都是redis单实例的情况下,如果是redis主从同步的情况下,考虑的可能更加多了,总而言之不够可靠。

其他方案

etcd分布式锁,etcd使用raft共识算法进行数据同步,大多数情况下,etcd的安装都是3个节点以上,可用性较高,并且原生提供了事务,租约(lease),watch等机制。

使用可以参考:github.com/etcd-io/etc...

如果程序只是想作为高可用,在k8s中可以直接使用client-go,进行leader election操作,比如 kube-scheduler, kube-controller,起了多份的情况下,只有一个节点会运行,可以使用:

sql 复制代码
 kubectl get leases -n kube-system

可以查看具体是哪个程序抢到了主。

总结

分布式锁用好的话,知识点还是比较多的,毕竟引入了第三方组件进行控制,一定要做好故障分析。

以上。

感兴趣关注:小唐云原生

相关推荐
无籽西瓜a16 小时前
【西瓜带你学设计模式 | 第十一期 - 模板方法模式】模板方法模式 —— 流程骨架与钩子实现、优缺点与适用场景
java·后端·设计模式·软件工程·模板方法模式
牛奔16 小时前
g:Go 版本管理器安装与使用指南
开发语言·后端·golang
chenglin01616 小时前
Semantic Kernel 内核详解
后端·python·flask
青柠代码录18 小时前
【SpringCloud】Nacos 组件:服务注册与发现
后端
2401_895521341 天前
SpringBoot Maven快速上手
spring boot·后端·maven
disgare1 天前
关于 spring 工程中添加 traceID 实践
java·后端·spring
ictI CABL1 天前
Spring Boot与MyBatis
spring boot·后端·mybatis
小江的记录本1 天前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
yhole1 天前
springboot三层架构详细讲解
spring boot·后端·架构
香香甜甜的辣椒炒肉1 天前
Spring(1)基本概念+开发的基本步骤
java·后端·spring