一个简单的redis分布式锁实现

127 阅读3分钟

背景

本人是一名小前端,有幸写了一会Go的代码。之前做后台需求遇到一个场景:在凌晨0点从数据库捞数据上报一份数据到产品日志平台上。一开始没有做处理,导致数据上报了多份到产品日志平台。

image.png

单机上报方案

实际上进行上报的机器只需要一台,其他机器并不需要上报。可以通过ip判断的方法,固定一台机器上报,其他机器不进行上报

package main

import (
  "net"
  "fmt"
  "os"
)

func GetIntranetIp() string {
  ip := ""
  addrs, err := net.InterfaceAddrs()

  if err != nil {
    fmt.Println(err)
    os.Exit(1)
	}

  for _, address := range addrs {

    // 检查ip地址判断是否回环地址
    if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
      if ipnet.IP.To4() != nil {
        ip = ipnet.IP.String()
      }
    }
  }
  return ip
}

func doTask() {
  fmt.Println("hello task")
}

func main() {
  ip := GetIntranetIp()
  if ip == "10.64.65.49" {
    doTask()
  }
}

该方法的缺点是换了机器后需要修改代码中的ip,如果上报的机器发生异常,上报功能就会瘫痪,健壮性十分差。

分布式锁方案

分布式锁使用redis的 SETNX 的功能来实现, SETNX 的作用是: 只在键 key 不存在的情况下,将键 key 的值设置为 value,若键 key 已经存在,则 SETNX 命令不做任何动作。

利用这个功能可以实现分布式锁功能:当触发条件时,所有机器都来抢锁,只有第一个 SETNX 成功的机器才能成功抢锁,其他机器 SETNX 都不会成功。

为了保证功能的健壮性,考虑到抢锁的成功的机器执行任务可能会失败,执行任务失败后会解锁。其他抢锁失败的机器会在10分钟后重试抢锁,抢锁成功后继续执行任务。

SETNX 可以设置有效时间,有效时间内其他机器抢锁会失败,有效时间过期后才能继续抢锁。保证每天都能至少有一次抢锁的机会。

流程图:

redis锁代码:

package main

import (
  "fmt"
  "xxx/redis"
  "xxx/utils"
  "xxx/cat"
  "math/rand"
  "time"
)

//redis分布式锁
type RedisLock struct {
  LockKey string       //抢锁解锁用的key
  Timeout int64        //锁超时释放时间(单位秒)
  cas     int64        //加锁成功时获取的随机数,用于解锁,避免被未抢到锁的client解锁
  r       *redis.Redis //存储使用的redis
}

//获取随机数
func (lock *RedisLock) getRandNum() int64 {
  ip := utils.GetLocalIP()
  s := rand.NewSource(int64(ip) + time.Now().UnixNano())
  r := rand.New(s)
  return r.Int63()
}

//加锁
func (lock *RedisLock) Lock(ctx *cat.Context) error {
  randValue := lock.getRandNum()
  data, err := lock.getRedis().Do(ctx, "SET", lock.LockKey, randValue, "NX", "EX", lock.Timeout)
  if err != nil {
    // redis错误
    lock.cas = 0
    return fmt.Errorf("lock key:%s error:%v", lock.LockKey, err)
  }

  if data == nil {
    // 抢锁失败返回
    lock.cas = -1
    return fmt.Errorf("lock key fail")
  }

  lock.cas = randValue
  return nil
}

//解锁
func (lock *RedisLock) UnLock(ctx *cat.Context) error {
  // lua脚本保证GET,判断,DEL三步操作为原子性
  luaStr := `
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
  `
  _, err := redis.Int64(lock.getRedis().Do(ctx, "EVAL", luaStr, 1, lock.LockKey, lock.cas))
  if err != nil {
    ctx.Error("unlock key:%s cas:%d err:%v", lock.LockKey, lock.cas, err)
    return err
  }

  return nil
}

func (lock *RedisLock) getRedis() *redis.Redis {
  return redis.New("red")
}

业务代码:

package main

import (
  "xxx/cat"
  "time"
  "fmt"
)

const expireKeySecond = 0.5 * 24 * 60 * 60    // 半天过期
const retryDuration = time.Minute * 10


func init() {
  //每天0点0分0秒触发
  specDb := "0 0 0 * * ?"
  cat.StartTask(specDb, time.Minute, LockReportRelation)
}


func getLockKey() string {
  now := time.Now()
  return fmt.Sprintf("qq_video_matchmaker_apprentice_relationship_lock_%d_%d_%d", now.Year(), now.Month(), now.Day())
}

func LockReportRelation(ctx *cat.Context) {
  lockKey := getLockKey()
  lock := &RedisLock{
    LockKey: lockKey, //作为锁的key
    Timeout: expireKeySecond,
  }

  // 第一次加锁
  err := lock.Lock(ctx)

  // 第一次没有抢到锁,则重试
  if err != nil {
    retryReportRelation(ctx, lockKey)

    return
  }

  err = doTask()

  // 如果任务失败,则解锁,并重试
  if err != nil {
    err := lock.UnLock(ctx)
    if err != nil {
      ctx.Error("LockReportRelation unlock fail, error is %v", err)
    }
    retryReportRelation(ctx, lockKey)
  }
}


func retryReportRelation(ctx *cat.Context, lockKey string) {
  retryDuration := retryDuration
  t := time.NewTimer(retryDuration)

  for i := 0; i < 3; i++ {
    <-t.C

    retryLock := &RedisLock{
      LockKey: lockKey, //作为锁的key
      Timeout: expireKeySecond,
    }

    err := retryLock.Lock(ctx)

    if err == nil {
      err = doTask()

      // 如果任务还是失败,则解锁
      if err != nil {
        err := retryLock.UnLock(ctx)
        if err != nil {
          ctx.Error("LockReportRelation retry unlock fail, error is %v", err)
        }
      }
    }

    t.Reset(retryDuration)
  }

  t.Stop()
}

func doTask() error {
  fmt.Println("do task")
  return nil
}