Redis分布式锁-golang实现

分布式事务锁通常用在多台机器上运行的程序需要进行状态同步的场景下,例如转账业务、分布式的扫描限速场景等;

如果是一个进程里面的共享资源,比如一个全局变量,也就通过代码内的锁进行上锁操作

目前微服务,分布式计算等盛行,所以一个项目可能会在很多云服务器或容器(docker)上,每个进程都是系统级别的隔离,很多时候资源都是在其他机子上,这个时候如果很多进程需要更改资源,就需要涉及到锁

但是,这个锁在每个进程的代码上是无法实现的,需要借助一个第三方的服务,比如redis,ETCD等,提供一个锁的服务,每个进程就可以通过获取这个锁,从而获取对资源的操作权

1. 定义

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

通过以上对比可以发现单纯的使用redis自带的setNx方法在遇到多个server使用一个redis时,因没有校验机制会出现serverA加的锁被serverB释放的情况,所以setNx多用于校验单个server是否重复提交请求,而分布式锁需要参考github.com/bsm/redislock的实现

2. 原理

首先借助于redis的setnx命令来操作,setnx本身针对key赋值的时候会判断redis中是否存在这个key,如果有返回-1, 如果没有,他会直接set键值。那他跟直接set键值有啥区别? setnx是原子操作,而set不能保证原子性。

3. setnx/setex

1)有多个协程同时抢到锁,但只有一个协程拿到锁
2)有过期时间,超过过期时间锁自动释放

  • SETNX : SETNX设置 key对应的值为 string类型的 value。 如果key 已经存在,返回 0,nx 是not exist 的意思。
  • SETEX : SETEX设置key 对应的值为 string 类型的 value,并指定此键值对应的有效期
# setnx : 例如我们添加一个{hello : world} 的键值对
redis 127.0.0.1:6379> get hello
"world"
# 由于原来 name 有一个对应的值,所以本次的修改不生效,且返回码是 0。
redis 127.0.0.1:6379> setnx hello world
(integer) 0
redis 127.0.0.1:6379> get hello
"world"

# setex : 例如我们添加一个haircolor= red 的键值对,并指定它的有效期是10 秒
redis 127.0.0.1:6379>setex haircolor 10 red
OK
redis 127.0.0.1:6379> get haircolor
"red"
redis 127.0.0.1:6379> get haircolor
(nil)

4. redislock

通过以上对比可以发现单纯的使用redis自带的setNx方法在遇到多个server使用一个redis时,因没有校验机制会出现serverA加的锁被serverB释放的情况,所以setNx多用于校验单个server是否重复提交请求,而分布式锁需要参考github.com/bsm/redislock的实现

  1. 有多个协程同时抢到锁,但只有一个协程拿到锁
  2. 有过期时间,超过过期时间锁自动释放
  3. 通过Lua脚本判断准备释放的锁中的值是否为自己设置的
package cache

import (
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/bsm/redislock"
)

var locker *redislock.Client

func initRedisLock() *redislock.Client {
    client := redis.NewClient(&redis.Options{
        Network: "tcp",
        Addr:    "127.0.0.1:6379",
    })
    defer client.Close()
    // 根据client创建锁
    if locker == nil {
        locker = redislock.New(client)
    }
    return locker
}

func verifyTTL(ctx context.Context, lock *redislock.Lock) error {
    if lock == nil {
        return errors.New("nil lock")
    }
    // 校验TTL
    if ttl, err := lock.TTL(ctx); err != nil {
        return err
    } else if ttl > 500*time.Millisecond {
        return nil
    }
    // 延长锁的使用时间为新的ttl
    if err := lock.Refresh(ctx, 500*time.Millisecond, nil); err != nil {
        return err
    }
    return nil
}

func getLocker(ctx context.Context, key string) (*redislock.Lock, error) {
    // // redislock v0.3.0,RetryCount阻塞后重试次数,RetryBackoff阻塞后重试时间间隔
    //     option := redislock.Options{
    //         RetryCount:   1,
    //         RetryBackoff: 100*time.Millisecond,
    //         Metadata:     "",
    //         Context:      nil,
    //     }
    // redislock 最新版,Retry every 100ms, for up-to 3x
    option := redislock.Options{
        RetryStrategy: redislock.LimitRetry(redislock.LinearBackoff(200*time.Millisecond), 5),
    }
    // 第一个参数指定key,相同的key之间是互斥关系,第二个是获取锁的最大时间,第三个是option可选项redislock.Options里面支持配置参数
    lock, err := locker.Obtain(ctx, key, 2*time.Second, &option)
    if err != nil {
        if err == redislock.ErrNotObtained {
            fmt.Println("could not obtain lock!")
        }
        return nil, err
    }
    return lock, nil
}

func main()  {
    ctx := context.Background()
    InitRedisLock()
    locker,err := getLocker(context.Background(),"redisKey")
    if err != nil {
        return
    }
    err = verifyTTL(ctx,locker)
    defer locker.Release(ctx)

    //TODO: Work
}

5. redis分布式锁的库

发表评论

邮箱地址不会被公开。 必填项已用*标注