分布式锁笔记

1,分布式锁的目的

   在后台开发过程中,通常是为了在同一时间内,保证是有一个客户端能访问共享资源。如果是单机服务,通常使用互斥锁即可保证共享资源在同一时间只被一个客户端访问。这样保证了数据资源的一致性。但是实际生产开发中,不可能使用仅仅使用单机。避免单点故障,都是使用集群分布式的模式进行部署模式开发的。同时,也产生了新的问题,在集群分布式中,如何才能保证统一时间,共享资源只被一个客户端访问?解决该问题的方法就是分布式锁。

2.分布式锁机中模型

 a.利用数据的分布式事务来实现,具体模型如下:

   使用数据库实现分布式锁的构想是,当要访问共享资源时,向数据库中插入一条数据,表示已经获取了该资源的锁,释放锁的时候,将该条记录从数据路中删除即可。

   for instance:创建一张锁表信息:


Drop resource_lock if exists;
CREATE TABLE `resource_lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `resource_lock_key` varchar(64) NOT NULL COMMENT '锁定的资源方法名',
  `resource_lock_val` tinyint NOT NULL COMMENT '资源锁value',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, COMMENT '更新时间'
  `PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_resource_lock` (`resource_lock_key`) USING BTREE CMMENT '资源锁key唯一存在'
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

加锁:

insert into resource_lock(resource_lock_key,resource_lock_val)value("lock","method_lock");

 解锁:先查找出来锁,与当前的获取锁的客户端比较,相同之后,在进行删除,达到解锁。


begin;
result = select resource_lock_val from resource_lock where key = "lock";
if result.value =="method_lock":
delete from resource_lock where key = "lock";
commit;

以上情况是在理想状态下是没有问题的,当时实际情况是有如下问题所在。

缺陷:

a.客户端A访问一个资源,向数据表中插入一条数据,但是客户端A挂掉了,但是锁仍旧在数据表中,客户端B需要访问资源的时候,是无法获取到锁的。如下图:

解决方法:需要给该锁设置一个定时时效性,启动一个线程轮训数据表,检查该锁超过一定的时效性,就删除掉该记录,这样就可避免a情况的发生。另外也可在锁表中添加当前获取锁客户端信息字段,这样再一次需要获取锁,只需要判断客户端的字段跟当前的客户端是否一致,如果是,直接获取锁即可。

模拟两个客户端加锁解锁过程:

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"strings"
	"sync"
	"time"
)

var SqlDB *sql.DB

var wg sync.WaitGroup

func clientFirst() {
	defer wg.Done()
	for {
		fmt.Println("client first  add lock...")
		sqlstr := &strings.Builder{}
		sqlstr.WriteString("insert into resource_lock(resource_lock_key,resource_lock_val)value(?,?)")
		sqlargs := make([]interface{}, 0)
		sqlargs = append(sqlargs, "lock", "method_lock")

		_, err := SqlDB.Exec(sqlstr.String(), sqlargs...)
		if err != nil {
			fmt.Println("client first add lock failed", err.Error())
			continue //
		}
		fmt.Println("client first process things")

		// 解锁
		sqlstr.Reset()
		sqlstr.WriteString("delete from resource_lock where resource_lock_key = 'lock'")
		_, err = SqlDB.Exec(sqlstr.String())
		if err != nil {
			fmt.Println("first delete lock failed ...", err.Error())
		} else {
			fmt.Println("client first  delete lock...")
		}

		time.Sleep(time.Millisecond * 15)
	}

}

func clientSecond() {
	defer wg.Done()
	for {
		fmt.Println("client second add lock...")
		sqlstr := &strings.Builder{}
		sqlstr.WriteString("insert into resource_lock(resource_lock_key,resource_lock_val)value(?,?)")
		sqlargs := make([]interface{}, 0)
		sqlargs = append(sqlargs, "lock", "method_lock")
		_, err := SqlDB.Exec(sqlstr.String(), sqlargs...)
		if err != nil {
			fmt.Println("client second add lock failed", err.Error())
			time.Sleep(time.Millisecond * 3)
			continue //
		}
		fmt.Println("client second process things")

		// 解锁
		sqlstr.Reset()
		sqlstr.WriteString("delete from resource_lock where resource_lock_key = 'lock'")
		_, err = SqlDB.Exec(sqlstr.String())
		if err != nil {
			fmt.Println("client second failed to delete lock", err.Error())
		} else {
			fmt.Println("client second  delete lock...")
		}

		time.Sleep(time.Millisecond * 5)
	}

}

func mointer() {
	defer wg.Done()

	curtime := ""
	tmp := "2006-01-02 15:04:05"
	for {
		sqlstr := "select update_time from resource_lock where resource_lock_key = 'lock'"
		var time_str string
		rows := SqlDB.QueryRow(sqlstr)
		err := rows.Scan(&time_str)

		if err != nil || err == sql.ErrNoRows {
			time.Sleep(time.Second * 1)
			continue
		}
		if len(curtime) == 0 {
			curtime = time_str
		}

		res, _ := time.ParseInLocation(tmp, time_str, time.Local)
		cur_res, _ := time.ParseInLocation(tmp, curtime, time.Local)
		tm := res.Unix() - cur_res.Unix()
		if tm >= 10 {
			strstr := "delete from resource_lock where resource_lock = 'lock'"
			fmt.Println("mointer delete unexpired lock")
			SqlDB.Exec(strstr)
			curtime = "" // 置空
		}

		time.Sleep(time.Millisecond * 5)

	}
}
func main() {
	var err error
	wg.Add(4)

	SqlDB, err = sql.Open("mysql", "root:passwd@tcp(localhost:port)/sql_db?charset=utf8")
	if err != nil {
		fmt.Println("connect failed", err.Error())
	}
	fmt.Println("success to connect mysql db !")

	go clientFirst()
	go clientSecond()

	go mointer()

	go func() {
		defer wg.Done()
		wg.Wait()
	}()

	// 主进程阻塞
	select {}
}

b.基于缓存模型的分布式锁

在缓存中redis当中,使用的是setnx命令进行实现的分布式锁,都是原子操作处理的,实现思想跟数据库一样,需要加锁的时候,在缓存中添加一条数据,释放锁的时候,删除掉该数据即可。如下所示:

加锁:如下代码利用uuid作为value值。


func redis_lock(lock_name string, acquire_time int, time_out time.Duration) (string, error) {
 
    // """获取一个分布式锁"""
    identifier := uuid.NewV1().String()
    end := time.Now().Second() + acquire_time
    lock := "string:lock:" + lock_name
    for {
 
        if t := time.Now().Second(); t <= end {
            if client.SetNX(lock, identifier, time_out).Val() {
                return identifier, nil
            } else {
                // 获取生命周期时间,并设置过期时间
                client.Expire(lock, client.TTL(lock).Val())
            }
        } else {
            break
        }
        time.Sleep(time.Second)
    }
 
    return "", nil
}
 
如上代码,获取锁时,尝试获取锁,并且给予时间内尝试获取锁,超时就表示获取失败。并在失败时,检查之前的,并设置该锁的有效时间长,避免该锁一直存在,导致其他客户端无法获取锁。

 解锁:


func redis_unlock(lock_name string, identify string) {
    lock := "string:lock:" + lock_name
    for {
        val, err := client.Get(lock).Result()
        fmt.Println("unlock key-var", lock, "-", val)
        if err == nil {
            if val == identify {
                client.Del(lock)
                return
            }
        } else {
            break
        }
    }
}
如上代码中,表示的是解锁。

但是如上也是有缺陷的:

a.A获取了锁,但是A的业务逻辑进入睡眠了,但是锁的有效期到了,redis中自动清理了该锁,B需要来加锁,也成功了获取了锁,但是,此刻A从业务中睡眠中醒来,在释放锁的时候,将该锁匙放掉了,这样导致了问题的产生。如下图:

所以使用时,避免业务休眠时间大于锁的有效期。从而避免出现如上图问题。

b.redis中使用get和del也要进行原子操作,使用lua脚本实现原子操作处理,如上代码修改如下:


const (
    script     = `if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`
)
 
func redis_unlock(lock_name string, identify string) {
    lock := "string:lock:" + lock_name
    var count int = 0
    for {
        // 利用lua脚本来保证redis的get和del操作的原子性
        if val, err := client.Eval(script, []string{lock}, identify).Result(); err != nil {
            if !strings.Contains(err.Error(), "Noscript") {
                fmt.Println("no script !!!")
            }
 
            // 尝试五次,放弃,所周期时间到了,也会释放该锁
            if count < 5 {
                break
            }
            count++
 
        } else {
            fmt.Println("unlock key-var", lock, "-", val)
            client.ScriptLoad(script)
            break
        }
 
    }
}

如上代码,保证了原子操作,get&del,用于释放锁。

c.redis中实现了如下方式实现分布式锁:

RedLock算法

这个算法基于N个完全独立的Redis节点(通常情况下N可以设置成5),客户端可以分成以下5个步骤来完成获取锁的操作

  1. 获取当前时间(毫秒数),记录当前的时间为 T1
  2. 按顺序依次向N个Redis节点执行 获取锁 的操作。这个获取操作跟前面基于单Redis节点的 获取锁 的过程相同,包含随机字符串 randomValue ,也包含过期时间(比如 PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个 获取锁 的操作还有一个超时时间(T2),它要远小于锁的有效时间(T3几十毫秒量级,T2 << T3)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用获取锁成功的当前时间(T5 = T4 - T1,获取锁消耗的时间)。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time,也就是T3时间,并且T5 < T3),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
  4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间,即(T6 = T2 - T5),表示的是成功获取锁之后锁的有效时间。
  5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间T2),那么客户端应该立即向所有Redis节点发起 释放锁 的操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值