其实网上正确地使用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")