前言
傳統(tǒng)的鎖(比如編程語言里的lock)厕氨,都是對(duì)多線程進(jìn)行控制进每,但是對(duì)多進(jìn)程、或者多客戶端就無能為力了命斧,為此田晚,就誕生了分布式鎖。
通常国葬,redis會(huì)被用作緩存功能贤徒,但是芹壕,它也可以有其他的一些用途。本文接奈,就是利用redis實(shí)現(xiàn)分布式鎖功能踢涌。當(dāng)然,實(shí)現(xiàn)分布式鎖還有很多種其他方式鲫趁,比如:
- 基于數(shù)據(jù)庫樂觀鎖
- 基于ZooKeeper的分布式鎖
不管哪種方式斯嚎,他的基本原理是不變的:用一個(gè)狀態(tài)值表示鎖,對(duì)鎖的占用和釋放通過狀態(tài)值來標(biāo)識(shí)挨厚。
基本原理
redis為單進(jìn)程單線程模式堡僻,采用隊(duì)列模式將并發(fā)訪問變成串行訪問,且多客戶端對(duì)redis的連接并不存在競爭關(guān)系疫剃。
關(guān)鍵命令
> SETNX key value
# SETNX: SET if Not eXists
# 將key的值設(shè)置為value
# 當(dāng)key存在時(shí)钉疫,不做任何操作,返回 0
# 當(dāng)key不存在時(shí)巢价,進(jìn)行設(shè)置操作牲阁,返回 1
> GET key
# 返回key的值
> GETSET key value
# 返回key的舊值,同時(shí)將key的值設(shè)置成value
實(shí)現(xiàn)的基本思想
key為任意值壤躲,value代表過期時(shí)間(unix time = now + expire)
實(shí)現(xiàn)Lock
函數(shù)城菊,此函數(shù)進(jìn)行一次加鎖嘗試。
首先調(diào)用SETNX
設(shè)置碉克,如果成功凌唬,則成功獲得鎖。否則漏麦,用GET
獲取key值客税,和當(dāng)前時(shí)間比較,如果對(duì)比發(fā)現(xiàn)過期撕贞,放棄加鎖更耻。否則進(jìn)行下一步。用GETSET
獲取key值捏膨,如果發(fā)現(xiàn)未過期秧均,則加鎖成功。 如果過期号涯,則表示有其他客戶端已經(jīng)先于本客戶端設(shè)置熬北,放棄加鎖。
Talk is cheap, show me the code.
下面诚隙,就用一個(gè)redis單實(shí)例實(shí)現(xiàn)分布式鎖。如果是多客戶端使用起胰,確本糜郑客戶端時(shí)間一致巫延。
/*
expire: N秒后鎖失效,允許其他客戶端競爭
*/
func Lock(conn redis.Conn, key string, expire int) bool {
var now int64 = time.Now().Unix()
r1, err := conn.Do("SETNX", key, now+int64(expire))
if err != nil {
return false
}
v1, err := redis.Int(r1, err)
if err != nil {
return false
}
if v1 == 1 {
return true
}
/*此時(shí)key存在地消,查看對(duì)應(yīng)的值*/
r, err := conn.Do("GET", key)
if err != nil {
return false
}
v2, err := redis.Int64(r, err)
if err != nil {
return false
}
if now < v2 {
/*值未過期炉峰,表示其他客戶端占用資源,放棄鎖*/
return false
} else {
/*獲取舊值脉执,設(shè)置新值*/
r, err := conn.Do("GETSET", key, now+int64(expire))
if err != nil {
return false
}
v3, err := redis.Int64(r, err)
if err != nil {
return false
}
if now >= v3 {
return true
}
/*
else情況:表示其他redis客戶端搶先一步設(shè)置成功疼阔,此時(shí)放棄鎖
return false
*/
}
return false
}
/*
釋放鎖
*/
func Unlock(conn redis.Conn, key string) bool {
var _, err = conn.Do("DEL", key)
if err != nil {
return false
}
return true
}
/*
嘗試加鎖
*/
func TryLock(conn redis.Conn, key string, expire int, timeout int) bool {
var b = Lock(conn, key, expire)
if b {
return b
}
if timeout == 0 {
return false
}
var ticker = time.NewTicker(time.Duration(timeout) * time.Second)
defer ticker.Stop()
for {
select {
case <-time.After(100 * time.Millisecond):
if Lock(conn, key, expire) {
/*成功lock后返回,否則一直持續(xù)到超時(shí)*/
return true
}
}
select {
case <-ticker.C:
return false
default:
//DO NOTHING
}
}
}