使用Redis的SET實現鎖機制【C# 和Go實現】

其實網上正確地使用Redis的SETNX實現鎖機制 和 高併發1-Redis分佈式鎖setnx,setex連用 說的都對,只是如今的redis作了不少優化好比如今的Set 指令以下html

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是單線程的,所以上面的操做能夠當作setnxexpire的結合體,是原子性的。git

因此設置的時候不用lua腳本了,大體邏輯以下:github

$rs = $redis->set($key, $random, ex $time nx);
if ($rs) {
     //處理更新緩存邏輯
    // ......
    //先判斷隨機數,是同一個則刪除鎖
    if ($redis->get($key) == $random) {
        $redis->del($key);
    }
}

解決了那些問題:redis

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.csdom

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()
}
相關文章
相關標籤/搜索