Golang可重入锁的实现-《GO开发知识笔记》

admin 2025-11-04 01:11:21 编程 来源:ZONE.CI 全球网 0 阅读模式
  • 为什么需要可重入锁
  • 具体实现
  • 参考文章

    项目中遇到了可重入锁的需求和实现,具体记录下。

    为什么需要可重入锁

    我们平时说的分布式锁,一般指的是在不同服务器上的多个线程中,只有一个线程能抢到一个锁,从而执行一个任务。而我们使用锁就是保证一个任务只能由一个线程来完成。所以我们一般是使用这样的三段式逻辑:

    1. Lock();
    2. DoJob();
    3. Unlock();

    但是由于我们的系统都是分布式的,这个锁一般不会只放在某个进程中,我们会借用第三方存储,比如 Redis 来做这种分布式锁。但是一旦借助了第三方存储,我们就必须面对这个问题:Unlock是否能保证一定运行呢?这个问题,我们面对的除了程序的bug之外,还有网络的不稳定,进程被杀死,服务器被down机等。我们是无法保证Unlock一定被运行的。那么我们就一般在Lock的时候为这个锁加一个超时时间作为兜底。

    1. LockByExpire(duration);
    2. DoJob();
    3. Unlock();

    这个超时时间是为了一旦出现异常情况导致Unlock没有被运行,这个锁在duration时间内也会被自动释放。这个在redis中我们一般就是使用set ex 来进行锁超时的设定。但是有这个超时时间我们又遇上了问题,超时时间设置多久合适呢?当然要设置的比 DoJob 消耗的时间更长,否则的话,在任务还没结束的时候,锁就被释放了,还是有可能导致并发任务的存在。但是实际上,同样由于网络超时问题,系统运行状况问题等,我们是无法准确知道DoJob这个函数要执行多久的。那么这时候怎么办呢?有两个办法:第一个方法,我们可以对DoJob做一个超时设置。让DoJob最多只能执行n秒,那么我的分布式锁的超时时长设置比n秒长就可以了。为一个任务设置超时时间在很多语言是可以做到的。比如golang 中的 TimeoutContext。而第二种方法,就是我们先为锁设置一个比较小的超时时长,然后不断续期这个锁。对一个锁的不断需求,也可以理解为重新开始加锁,这种可以不断续期的锁,就叫做可重入锁。除了主线程之外,可重入锁必然有一个另外的线程(或者携程)可以对这个锁进行续期,我们叫这个额外的程序叫做watchDog(看门狗)。

    具体实现

    在Golang中,语言级别天生支持协程,所以这种可重入锁就非常容易实现:

    1. // DistributeLockRedis 基于redis的分布式可重入锁,自动续租
    2. type DistributeLockRedis struct {
    3. key string // 锁的key
    4. expire int64 // 锁超时时间
    5. status bool // 上锁成功标识
    6. cancelFun context.CancelFunc // 用于取消自动续租携程
    7. redis redis.Client // redis句柄
    8. }
    9. // 创建可
    10. func NewDistributeLockRedis(key string, expire int64) *DistributeLockRedis {
    11. return &DistributeLockRedis{
    12. key : key,
    13. expire : expire,
    14. }
    15. }
    16. // TryLock 上锁
    17. func (dl *DistributeLockRedis) TryLock() (err error) {
    18. if err = dl.lock(); err != nil {
    19. return err
    20. }
    21. ctx, cancelFun := context.WithCancel(context.Background())
    22. dl.cancelFun = cancelFun
    23. dl.startWatchDog(ctx) // 创建守护协程,自动对锁进行续期
    24. dl.status = true
    25. return nil
    26. }
    27. // competition 竞争锁
    28. func (dl *DistributeLockRedis) lock() error {
    29. if res, err := redis.String(dl.redis.Do(context.Background(), "SET", dl.key, 1, "NX", "EX", dl.expire)); err != nil {
    30. return err
    31. }
    32. return nil
    33. }
    34. // guard 创建守护协程,自动续期
    35. func (dl *DistributeLockRedis) startWatchDog(ctx context.Context) {
    36. safeGo(func() error {
    37. for {
    38. select {
    39. // Unlock通知结束
    40. case <-ctx.Done():
    41. return nil
    42. default:
    43. // 否则只要开始了,就自动重入(续租锁)
    44. if dl.status {
    45. if res, err := redis.Int(dl.redis.Do(context.Background(), "EXPIRE", dl.key, dl.expire)); err != nil {
    46. return nil
    47. }
    48. // 续租时间为 expire/2 秒
    49. time.Sleep(time.Duration(dl.expire/2) * time.Second)
    50. }
    51. }
    52. }
    53. })
    54. }
    55. // Unlock 释放锁
    56. func (dl *DistributeLockRedis) Unlock() (err error) {
    57. // 这个重入锁必须取消,放在第一个地方执行
    58. if dl.cancelFun != nil {
    59. dl.cancelFun() // 释放成功,取消重入锁
    60. }
    61. var res int
    62. if dl.status {
    63. if res, err = redis.Int(dl.redis.Do(context.Background(), "Del", dl.key)); err != nil {
    64. return fmt.Errorf("释放锁失败")
    65. }
    66. if res == 1 {
    67. dl.status = false
    68. return nil
    69. }
    70. }
    71. return fmt.Errorf("释放锁失败")
    72. }

    这段代码的逻辑基本上都以注释的形式来写了。其中主要就在startWatchDog,对锁进行重新续期

    1. ctx, cancelFun := context.WithCancel(context.Background())
    2. dl.cancelFun = cancelFun
    3. dl.startWatchDog(ctx) // 创建守护协程,自动对锁进行续期
    4. dl.status = true

    首先创建一个cancelContext,它的context函数cancelFunc是给Unlock进行调用的。然后启动一个goroutine进程来循环续期。这个新启动的goroutine在主goroutine处理结束,调用Unlock的时候,才会结束,否则会在 过期时间/2 的时候,调用一次redis的expire命令来进行续期。至于外部,在使用的时候如下

    1. func Foo() error {
    2. key := foo
    3. // 创建可重入的分布式锁
    4. dl := NewDistributeLockRedis(key, 10)
    5. // 争抢锁
    6. err := dl.TryLock()
    7. if err != nil {
    8. // 没有抢到锁
    9. return err
    10. }
    11. // 抢到锁的记得释放锁
    12. defer func() {
    13. dl.Unlock()
    14. }
    15. // 做真正的任务
    16. DoJob()
    17. }

    参考文章

    如果还想了解更多,以下的参考文章值得阅读。redissonhttps://github.com/redisson/redisson滴滴 曾奇:谈谈我所认识的分布式锁http://blog.itpub.net/69908606/viewspace-2644366/Redis 分布式锁|从青铜到钻石的五种演进方案https://my.oschina.net/u/4499317/blog/5039486分布式锁中的王者方案 - Redissonhttps://xie.infoq.cn/article/d8e897f768eb1a358a0fd6300#:~:text=Redisson%20%E6%98%AF%E4%B8%80%E4%B8%AA%E5%9C%A8Redis,In%2DMemory%20Data%20Grid%EF%BC%89%E3%80%82redisson中的看门狗机制总结https://www.cnblogs.com/jelly12345/p/14699492.htmlRedis分布式锁如何自动续期https://blog.csdn.net/yangbindxj/article/details/123189395到底什么是重入锁,拜托,一次搞清楚!https://zhuanlan.zhihu.com/p/71018541

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  0