什么是分布式事务锁

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

定义:

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

原理

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

1.setnx

设置 key对应的值为 string类型的 value。 如果key 已经存在,返回 0,nx 是not exist 的意思。

例如我们添加一个{hello : world} 的键值对,可以这样做:

redis 127.0.0.1:6379> get hello 
"world"
redis 127.0.0.1:6379> setnx hello world
(integer) 0
redis 127.0.0.1:6379> get hello 
"world"
redis 127.0.0.1:6379>


由于原来 name 有一个对应的值,所以本次的修改不生效,且返回码是 0。

2.setex

设置key 对应的值为 string 类型的 value,并指定此键值对应的有效期。

例如我们添加一个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)


实现

package main

import (
	"fmt"
	"github.com/gomodule/redigo/redis"
	"sync"
	"time"
)

func getLock(redisAddr, lockKey string, ex uint, retry int) error {
	if retry <= 0 {
		retry = 10
	}
	conn, err := redis.DialTimeout("tcp", redisAddr, time.Minute, time.Minute, time.Minute)
	if err != nil {
		fmt.Println("conn to redis failed, err:%v", err)
		return err
	}
	defer conn.Close()
	ts := time.Now() // as random value
	for i := 1; i <= retry; i++ {
		if i > 1 { // sleep if not first time
			time.Sleep(time.Second)
		}
		v, err := conn.Do("SET", lockKey, ts, "EX", retry, "NX")
		if err == nil {
			if v == nil {
				fmt.Println("get lock failed, retry times:", i)
			} else {
				fmt.Println("get lock success")
				break
			}
		} else {
			fmt.Println("get lock failed with err:", err)
		}
		if i >= retry {
			err = fmt.Errorf("get lock failed with max retry times.")
			return err
		}
	}
	return nil
}

func unLock(redisAddr, lockKey string) error {
	conn, err := redis.DialTimeout("tcp", redisAddr, time.Minute, time.Minute, time.Minute)
	if err != nil {
		fmt.Println("conn to redis failed, err:%v", err)
		return err
	}
	defer conn.Close()
	v, err := redis.Bool(conn.Do("DEL", lockKey))
	if err == nil {
		if v {
			fmt.Println("unLock success")
		} else {
			fmt.Println("unLock failed")
			return fmt.Errorf("unLock failed")
		}
	} else {
		fmt.Println("unLock failed, err:", err)
		return err
	}
	return nil
}

const (
	RedisAddr = "127.0.0.1:6379"
)

func main() {
	var wg sync.WaitGroup

	key := "lock_demo"

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			time.Sleep(time.Second)
			// getLock
			err := getLock(RedisAddr, key, 10, 10)
			if err != nil {
				fmt.Println(fmt.Sprintf("worker[%d] get lock failed:%v", id, err))
				return
			}
			// sleep for random
			//for j := 0; j < 5; j++ {
			time.Sleep(time.Second)
			fmt.Println(fmt.Sprintf("worker[%d] hold lock for %ds", id, 1))
			//}
			// unLock
			err = unLock(RedisAddr, key)
			if err != nil {
				fmt.Println(fmt.Sprintf("worker[%d] unlock failed:%v", id, err))
			}
			fmt.Println(fmt.Sprintf("worker[%d] done", id))
		}(i)
	}

	wg.Wait()
	fmt.Println("demo is done!")
}



参考

2.《advanced-go-programming-book》