其實網上正確地使用Redis的SETNX實現鎖機制 和 高並發1-Redis分布式鎖setnx,setex連用 說的都對,只是現在的redis做了很多優化比如現在的Set 指令如下
set key value [EX seconds] [PX milliseconds] [NX|XX] EX seconds:設置失效時長,單位秒 PX milliseconds:設置失效時長,單位毫秒 NX:key不存在時設置value,成功返回OK,失敗返回(nil) XX:key存在時設置value,成功返回OK,失敗返回(nil) 案例:設置name=p7+,失效時長100s,不存在時設置 1.1.1.1:6379> set name gavin ex 100 nx OK 1.1.1.1:6379> get name "gavin" 1.1.1.1:6379> ttl name (integer) 94
從上面可以看出,多個命令放在同一個redis
連接中並且redis
是單線程的,因此上面的操作可以看成setnx
和expire
的結合體,是原子性的。
所以設置的時候不用lua腳本了,大致邏輯如下:
$rs = $redis->set($key, $random, ex $time nx); if ($rs) { //處理更新緩存邏輯 // ...... //先判斷隨機數,是同一個則刪除鎖 if ($redis->get($key) == $random) { $redis->del($key); } }
解決了那些問題:
1.緩存雪崩: 例如某個查詢數據庫的接口因為請求量比較大所以加了緩存,並設定緩存過期后刷新。當並發量比較大並且緩存過期的瞬間,大量並發請求會直接查詢數據庫導致雪崩。如果使用鎖機制來控制只有一個請求去更新緩存就能避免雪崩的問題。這里的參數nx 是setNX,是set if not exists 的縮寫,也就是只有不存在的時候才設置, 設置成功時返回 1 , 設置失敗時返回 0 。可以利用它來實現鎖的效果,
2.key的過期時間,參數ex 是setex(set expire value)。 如果更新緩存的時候因為某些原因意外退出了,那么這個鎖需要自動刪除。【果果過期時間來完成】,由於這里是1條命令, 所以不需要用Multi/Exec 來保證原子性。
3.如果一個請求更新緩存的時間比鎖的有效期還要長,導致在緩存更新過程中鎖就失效了,此時另一個請求就會獲取到鎖,但前一個請求在緩存更新完畢的時候,直接刪除鎖的話就會出現誤刪其它請求創建的鎖的情況。所以要避免這種問題,刪除key的時候判斷一下value是否是當前value,是的話刪除,否則不執行刪除,LUA如下:
local lockKey = KEYS[1] local lockValue = ARGV[1] local result_1 = redis.call('get', lockKey) if result_1 == lockValue then local result_2= redis.call('del', lockKey) return result_2 else return 0 end
在C# 的demo, 需要安裝 相應的包, 我這里用的是 StackExchange.Redis,首先封裝RedisLock.cs
using StackExchange.Redis; using System; using System.Collections.Generic; using System.Text; namespace ConsoleApp { public class RedisLock { IDatabase db; ConnectionMultiplexer connection; string deleScript = $@" local lockKey = KEYS[1] local lockValue = ARGV[1] local result_1 = redis.call('get', lockKey) if result_1 == lockValue then local result_2= redis.call('del', lockKey) return result_2 else return 0 end"; public RedisLock(string connStr, int database = 0) { connection = ConnectionMultiplexer.Connect(connStr); db = connection.GetDatabase(database); } //加鎖 public bool Lock(string key, string value, int timeOut) { var result = db.Execute("set", key, value, "NX", "EX", timeOut); if (result.ToString().Contains("OK")) { return true; } return false; } //解鎖 public bool UnLock(string key, string value) { var keys = new List<RedisKey> { key }; var values = new List<RedisValue> { value }; var result = db.ScriptEvaluate(deleScript, keys.ToArray(), values.ToArray()); return Convert.ToInt32(result.ToString()) == 1; } public void Close() { if (connection.IsConnected) { connection.Close(true); } } } }
使用很簡單:
static void Main(string[] args) { string redisConn = "localhost:6379"; string key = "Name"; string value = "gavin"; int timeOut = 100; RedisLock rl = new RedisLock(redisConn, 0); if (rl.Lock(key, value, timeOut)) { Console.WriteLine("Hello World!"); if (!rl.UnLock(key, value)) { Console.WriteLine("UnLock failed"); } else { rl.Close(); Console.WriteLine("UnLock Okay"); } } else { rl.Close(); } Console.ReadKey(); }
go的demo, 這里用 "github.com/go-redis/redis"插件,封裝redisLock.go
package utils import ( "time" "github.com/go-redis/redis" ) type RedisLock struct { rc *redis.Client } func NewRedisLock(addr, password string, db int) *RedisLock { rdb := redis.NewClient(&redis.Options{ Addr: addr, Password: password, // no password set DB: db, // use default DB }) return &RedisLock{rc: rdb} } func (rl *RedisLock) Lock(key, value string, timeOut int) (bool, error) { set, err := rl.rc.SetNX(key, value, time.Duration(timeOut)*time.Second).Result() return set, err } func (rl *RedisLock) Unlock(key, value string) (bool, error) { ret, error := rl.rc.Get(key).Result() if error == nil { if value == ret { ressult, er := rl.rc.Del(key).Result() return ressult == 1, er } else { return false, error } } return false, error } func (rl *RedisLock) Close() error { return rl.rc.Close() }
調用如下:
package main import ( "fmt" "main/utils" ) func main() { key := "name" val := "gavin" timeOut := 100 rl := utils.NewRedisLock("localhost:6379", "", 1) if ret, _ := rl.Lock(key, val, timeOut); ret { fmt.Println("Lock okay") /// if result, _ := rl.Unlock(key, val); result { fmt.Println("Unlock okay") } } rl.Close() }
Python實現, 創建redislock.py文件: 需要安裝RedisPy庫
from redis import StrictRedis from redis.connection import ConnectionPool class RedisLock(object): def __init__(self,addr): #addr = 'redis://:foobared@localhost:6379/0' redis://[:password]@host:port/db #r =redis.Redis(host="123.516.74.190",port=6379,password="6666666666") pool = ConnectionPool.from_url(addr) redis = StrictRedis(connection_pool=pool) self.redis=redis def Lock(self,key,value,timeOut): return self.redis.set(key,value,ex=timeOut,nx=True) def UnLock(self,key,value): ret=False valByte=self.redis.get(key) val = bytes.decode(valByte) if val==value: self.redis.delete(key) ret=True return ret
調用:
import redisLock key="name" value="gavin" timeOut=100 rs=redisLock.RedisLock("redis://@127.0.0.1:6379/2") if rs.Lock(key,value,timeOut): print("Lock Ok") if rs.UnLock(key,value): print("Unlock Ok") print("done")