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

问题发现

在工作中,运维对于安装这个服务存在阻碍,需要安装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

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

总结

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

以上。

感兴趣关注:小唐云原生

相关推荐
coderWangbuer几秒前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql
攸攸太上6 分钟前
JMeter学习
java·后端·学习·jmeter·微服务
Kenny.志9 分钟前
2、Spring Boot 3.x 集成 Feign
java·spring boot·后端
sky丶Mamba26 分钟前
Spring Boot中获取application.yml中属性的几种方式
java·spring boot·后端
千里码aicood1 小时前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin334455662 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
数字扫地僧2 小时前
HBase与Hive、Spark的集成应用案例
后端
架构师吕师傅2 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
bug菌3 小时前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee