一、下載
https://github.com/MicrosoftArchive/redis/releases
Redis支持32位和64位。這個需要根據你係統平臺的實際情況選擇,這裏下載 Redis-x64-xxx.zip壓縮包到 D 盤redis文件夾下。
解壓:
二、Redis臨時服務
1.打開cmd,進入到剛纔解壓到的目錄,啓動臨時服務:redis-server.exe redis.windows.conf
(備註:通過這個命令,會創建Redis臨時服務,不會在window服務列表出現Redis服務名稱和狀態,此窗口關閉,服務會自動關閉。)
2.打開另一個cmd窗口,客戶端調用:redis-cli.exe -h 127.0.0.1 -p 6379
(默認6379端口:6379在是手機按鍵上MERZ對應的號碼,而MERZ取自意大利歌女Alessia Merz的名字。MERZ長期以來被antirez及其朋友當作愚蠢的代名詞。Redis作者antirez同學在twitter上說將在下一篇博文中向大家解釋爲什麼他選擇6379作爲默認端口號......😯)
三、Redis自定義Windows服務安裝
1.進入Redis安裝包目錄,安裝服務
redis-server.exe --service-install redis.windows.conf --service-name redisserver1 --loglevel verbose
win+r -> services.msc,可以看到服務安裝成功
安裝服務:redis-server.exe --service-install redis.windows.conf --service-name redisserver1 --loglevel verbose
啓動服務:redis-server.exe --service-start --service-name redisserver1
停止服務:redis-server.exe --service-stop --service-name redisserver1
卸載服務:redis-server.exe --service-uninstall--service-name redisserver1
四、主從服務
1.將d盤下新建一個文件夾叫redis2,把redis文件夾的東西拷貝到redis2文件夾下,將redis-windows.conf配置文件中的ip 和端口號改一下,然後按照上面的步驟安裝一個服務叫redisserver2
2.使用redis桌面管理器(下載地址:https://redisdesktop.com/),鏈接兩個redis庫
3.設置密碼把redis.windows.conf文件中 #requirepass foobared 的#號去掉改爲自己的密碼即可
4.設置好保存後,若要使設置起作用,需要重啓redis服務
5.端口號和ip同理
6.重啓後需要輸入密碼
7.slaveof 127.0.0.1 6379 設置主從,6379是主庫,6380是從庫。(設置同步時,會將主庫所有數據一起同步過來。)
五、哨兵模式
1.在主服務器redis文件夾下新建文件:sentinel.conf
輸入:sentinel monitor host6379 127.0.0.1 6379 1
2.執行 redis-server.exe sentinel.conf --sentinel
六、StackExchange客戶端
using StackExchange.Redis; using System; namespace ConsoleApp1 { class Program { #region ConnectionMultiplexer是StackExchange.Redis的核心,它被整個應用程序共享和重用,應該設置爲單例 // redis config // 哨兵26379 主從服務器6379,6380 private static readonly ConfigurationOptions ConfigurationOptions = ConfigurationOptions.Parse("127.0.0.1:26379,127.0.0.1:6379,127.0.0.1:6380,password='',connectTimeout=60"); //the lock for singleton private static readonly object Locker = new object(); //singleton private static ConnectionMultiplexer _redisConn; //singleton public static ConnectionMultiplexer GetRedisConn() { if (_redisConn == null) { lock (Locker) { if (_redisConn == null || !_redisConn.IsConnected) { _redisConn = ConnectionMultiplexer.Connect(ConfigurationOptions); } } } return _redisConn; } #endregion static void Main(string[] args) { _redisConn = GetRedisConn(); IDatabase db = _redisConn.GetDatabase(); #region string var strKey = "hello"; var strValue = "world"; db.StringSet(strKey, strValue); #endregion #region hash 使用場景:微博個人信息 string hashKey = "myhash"; //hset db.HashSet(hashKey, "f1", "v1"); db.HashSet(hashKey, "f2", "v2"); HashEntry[] values1 = db.HashGetAll(hashKey); //hgetall Console.Write("hgetall " + hashKey + ", result is"); foreach (HashEntry hashEntry in values1) { Console.Write(" " + hashEntry.Name + " " + hashEntry.Value); } #endregion #region list 使用場景:微博粉絲 //list key string listKey = "myList"; //rpush db.ListRightPush(listKey, "a"); db.ListRightPush(listKey, "b"); db.ListRightPush(listKey, "c"); //lrange RedisValue[] values = db.ListRange(listKey, 0, -1); Console.Write("lrange " + listKey + " 0 -1, result is "); for (int i = 0; i < values.Length; i++) { Console.Write(values[i] + " "); } Console.WriteLine(); #endregion #region set 使用場景:隊列 //set key string setKey = "mySet"; //sadd db.SetAdd(setKey, "a"); db.SetAdd(setKey, "b"); db.SetAdd(setKey, "c"); //sismember bool isContains = db.SetContains(setKey, "a"); Console.WriteLine("set " + setKey + " contains a is " + isContains); #endregion #region sortedset 使用場景:隊列 string sortedSetKey = "myZset"; //sadd db.SortedSetAdd(sortedSetKey, "xiaoming", 85); db.SortedSetAdd(sortedSetKey, "xiaohong", 100); db.SortedSetAdd(sortedSetKey, "xiaofei", 62); db.SortedSetAdd(sortedSetKey, "xiaotang", 73); //zrevrangebyscore RedisValue[] names = db.SortedSetRangeByRank(sortedSetKey, 0, 2, Order.Ascending); Console.Write("zrevrangebyscore " + sortedSetKey + " 0 2, result is "); for (int i = 0; i < names.Length; i++) { Console.Write(names[i] + " "); } Console.WriteLine(); #endregion Console.ReadLine(); } } }
如果主服務器掛了,從服務器不能立刻變爲主庫,寫數據會失敗,因爲哨兵切換從庫爲主庫時默認30秒(sentinel.conf配置文件中配置)
哨兵臨時服務窗口不能關閉(上一步的操作)
如果關閉需要安裝爲windows服務守護進程:redis-server --service-install sentinel.conf --sentinel --service-name RedisSentinel --port 26379
sentinel配置說明:
# Example sentinel.conf # 哨兵sentinel實例運行的端口 默認26379 port 26379 # 哨兵sentinel的工作目錄 dir /tmp # 哨兵sentinel監控的redis主節點的 ip port # master-name 可以自己命名的主節點名字 只能由字母A-z、數字0-9 、這三個字符".-_"組成。 # quorum 當這些quorum個數sentinel哨兵認爲master主節點失聯 那麼這時 客觀上認爲主節點失聯了 # sentinel monitor <master-name> <ip> <redis-port> <quorum> sentinel monitor mymaster 127.0.0.1 6379 2 # 當在Redis實例中開啓了requirepass foobared 授權密碼 這樣所有連接Redis實例的客戶端都要提供密碼 # 設置哨兵sentinel 連接主從的密碼 注意必須爲主從設置一樣的驗證密碼 # sentinel auth-pass <master-name> <password> sentinel auth-pass mymaster MySUPER--secret-0123passw0rd # 指定多少毫秒之後 主節點沒有應答哨兵sentinel 此時 哨兵主觀上認爲主節點下線 默認30秒 # sentinel down-after-milliseconds <master-name> <milliseconds> sentinel down-after-milliseconds mymaster 30000 # 這個配置項指定了在發生failover主備切換時最多可以有多少個slave同時對新的master進行 同步, 這個數字越小,完成failover所需的時間就越長, 但是如果這個數字越大,就意味着越 多的slave因爲replication而不可用。 可以通過將這個值設爲 1 來保證每次只有一個slave 處於不能處理命令請求的狀態。 # sentinel parallel-syncs <master-name> <numslaves> sentinel parallel-syncs mymaster 1 # 故障轉移的超時時間 failover-timeout 可以用在以下這些方面: #1. 同一個sentinel對同一個master兩次failover之間的間隔時間。 #2. 當一個slave從一個錯誤的master那裏同步數據開始計算時間。直到slave被糾正爲向正確的master那裏同步數據時。 #3.當想要取消一個正在進行的failover所需要的時間。 #4.當進行failover時,配置所有slaves指向新的master所需的最大時間。不過,即使過了這個超時,slaves依然會被正確配置爲指向master,但是就不按parallel-syncs所配置的規則來了 # 默認三分鐘 # sentinel failover-timeout <master-name> <milliseconds> sentinel failover-timeout mymaster 180000 # SCRIPTS EXECUTION #配置當某一事件發生時所需要執行的腳本,可以通過腳本來通知管理員,例如當系統運行不正常時發郵件通知相關人員。 #對於腳本的運行結果有以下規則: #若腳本執行後返回1,那麼該腳本稍後將會被再次執行,重複次數目前默認爲10 #若腳本執行後返回2,或者比2更高的一個返回值,腳本將不會重複執行。 #如果腳本在執行過程中由於收到系統中斷信號被終止了,則同返回值爲1時的行爲相同。 #一個腳本的最大執行時間爲60s,如果超過這個時間,腳本將會被一個SIGKILL信號終止,之後重新執行。 #通知型腳本:當sentinel有任何警告級別的事件發生時(比如說redis實例的主觀失效和客觀失效等等),將會去調用這個腳本, 這時這個腳本應該通過郵件,SMS等方式去通知系統管理員關於系統不正常運行的信息。調用該腳本時,將傳給腳本兩個參數, 一個是事件的類型, 一個是事件的描述。 如果sentinel.conf配置文件中配置了這個腳本路徑,那麼必須保證這個腳本存在於這個路徑,並且是可執行的,否則sentinel無法正常啓動成功。 #通知腳本 # sentinel notification-script <master-name> <script-path> sentinel notification-script mymaster /var/redis/notify.sh # 客戶端重新配置主節點參數腳本 # 當一個master由於failover而發生改變時,這個腳本將會被調用,通知相關的客戶端關於master地址已經發生改變的信息。 # 以下參數將會在調用腳本時傳給腳本: # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port> # 目前<state>總是“failover”, # <role>是“leader”或者“observer”中的一個。 # 參數 from-ip, from-port, to-ip, to-port是用來和舊的master和新的master(即舊的slave)通信的 # 這個腳本應該是通用的,能被多次調用,不是針對性的。 # sentinel client-reconfig-script <master-name> <script-path> sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
redis.windows.conf配置文件說明:
daemonize yes #是否以後臺進程運行 pidfile /var/run/redis/redis-server.pid #pid文件位置 port 6379#監聽端口 bind 127.0.0.1 #綁定地址,如外網需要連接,設置0.0.0.0 timeout 300 #連接超時時間,單位秒 loglevel notice #日誌級別,分別有: # debug :適用於開發和測試 # verbose :更詳細信息 # notice :適用於生產環境 # warning :只記錄警告或錯誤信息 logfile /var/log/redis/redis-server.log #日誌文件位置 syslog-enabled no #是否將日誌輸出到系統日誌 databases 16#設置數據庫數量,默認數據庫爲0 ############### 快照方式 ############### save 900 1 #在900s(15m)之後,至少有1個key發生變化,則快照 save 300 10 #在300s(5m)之後,至少有10個key發生變化,則快照 save 60 10000 #在60s(1m)之後,至少有1000個key發生變化,則快照 rdbcompression yes #dump時是否壓縮數據 dir /var/lib/redis #數據庫(dump.rdb)文件存放目錄 ############### 主從複製 ############### slaveof <masterip> <masterport> #主從複製使用,用於本機redis作爲slave去連接主redis masterauth <master-password> #當master設置密碼認證,slave用此選項指定master認證密碼 slave-serve-stale-data yes #當slave與master之間的連接斷開或slave正在與master進行數據同步時,如果有slave請求,當設置爲yes時,slave仍然響應請求,此時可能有問題,如果設置no時,slave會返回"SYNC with master in progress"錯誤信息。但INFO和SLAVEOF命令除外。 ############### 安全 ############### requirepass foobared #配置redis連接認證密碼 ############### 限制 ############### maxclients 128#設置最大連接數,0爲不限制 maxmemory <bytes>#內存清理策略,如果達到此值,將採取以下動作: # volatile-lru :默認策略,只對設置過期時間的key進行LRU算法刪除 # allkeys-lru :刪除不經常使用的key # volatile-random :隨機刪除即將過期的key # allkeys-random :隨機刪除一個key # volatile-ttl :刪除即將過期的key # noeviction :不過期,寫操作返回報錯 maxmemory-policy volatile-lru#如果達到maxmemory值,採用此策略 maxmemory-samples 3 #默認隨機選擇3個key,從中淘汰最不經常用的 ############### 附加模式 ############### appendonly no #AOF持久化,是否記錄更新操作日誌,默認redis是異步(快照)把數據寫入本地磁盤 appendfilename appendonly.aof #指定更新日誌文件名 # AOF持久化三種同步策略: # appendfsync always #每次有數據發生變化時都會寫入appendonly.aof # appendfsync everysec #默認方式,每秒同步一次到appendonly.aof # appendfsync no #不同步,數據不會持久化 no-appendfsync-on-rewrite no #當AOF日誌文件即將增長到指定百分比時,redis通過調用BGREWRITEAOF是否自動重寫AOF日誌文件。 ############### 虛擬內存 ############### vm-enabled no #是否啓用虛擬內存機制,虛擬內存機將數據分頁存放,把很少訪問的頁放到swap上,內存佔用多,最好關閉虛擬內存 vm-swap-file /var/lib/redis/redis.swap #虛擬內存文件位置 vm-max-memory 0 #redis使用的最大內存上限,保護redis不會因過多使用物理內存影響性能 vm-page-size 32 #每個頁面的大小爲32字節 vm-pages 134217728 #設置swap文件中頁面數量 vm-max-threads 4 #訪問swap文件的線程數 ############### 高級配置 ############### hash-max-zipmap-entries 512 #哈希表中元素(條目)總個數不超過設定數量時,採用線性緊湊格式存儲來節省空間 hash-max-zipmap-value 64 #哈希表中每個value的長度不超過多少字節時,採用線性緊湊格式存儲來節省空間 list-max-ziplist-entries 512 #list數據類型多少節點以下會採用去指針的緊湊存儲格式 list-max-ziplist-value 64 #list數據類型節點值大小小於多少字節會採用緊湊存儲格式 set-max-intset-entries 512 #set數據類型內部數據如果全部是數值型,且包含多少節點以下會採用緊湊格式存儲 activerehashing yes #是否激活重置哈希
一主二從三哨兵
redis主從:是備份關係, 我們操作主庫,數據也會同步到從庫。 如果主庫機器壞了,從庫可以上。就好比你 D盤的片丟了,但是你移動硬盤裏邊備份有。
redis哨兵:哨兵保證的是HA,保證特殊情況故障自動切換,哨兵盯着你的“redis主從集羣”,如果主庫死了,它會告訴你新的老大是誰。
redis集羣:集羣保證的是高併發,因爲多了一些兄弟幫忙一起扛。同時集羣會導致數據的分散,整個redis集羣會分成一堆數據槽,即不同的key會放到不不同的槽中。
主從保證了數據備份,哨兵保證了HA 即故障時切換,集羣保證了高併發性。
七、集羣搭建(轉)
原文地址:https://blog.csdn.net/hao495430759/article/details/80540407
1.首先我們構建集羣節點目錄:
(集羣正常運作至少需要三個主節點,不過在剛開始試用集羣功能時, 強烈建議使用六個節點: 其中三個爲主節點, 而其餘三個則是各個主節點的從節點。主節點崩潰,從節點的Redis就會提升爲主節點,代替原來的主節點工作,崩潰的主Redis回覆工作後,會成爲從節點)
拷貝開始下載的redis解壓後的目錄,並修改文件名(比如按集羣下redis端口命名)如下:
6380,6381,6382,6383,6384,6385對應的就是後面個節點下啓動redis的端口。
在節點目錄下新建文件,輸入(舉例在6380文件夾下新建文件)
title redis-6380;
redis-server.exe redis.windows.conf
然後保存爲start.bat 下次啓動時直接執行該腳本即可;
接着分別打開各個文件下的 redis.windows.conf,分別修改如下配置(舉例修改6380文件下的redis.window.conf文件):
port 6380 //修改爲與當前文件夾名字一樣的端口號
appendonly yes //指定是否在每次更新操作後進行日誌記錄,Redis在默認情況下是異步的把數據寫入磁盤,如果不開啓,可能會在斷電時導致一段時間內的數據丟失。 yes表示:存儲方式,aof,將寫操作記錄保存到日誌中
cluster-enabled yes //開啓集羣模式
cluster-config-file nodes-6380.conf //保存節點配置,自動創建,自動更新(建議命名時加上端口號)
cluster-node-timeout 15000 //集羣超時時間,節點超過這個時間沒反應就斷定是宕機
注意:在修改配置文件這幾項配置時,配置項前面不能有空格,否則啓動時會報錯(參考下面)
其他文件節點 6381~6385也修改相應的節點配置信息和建立啓動腳本(略)。
2.下載Ruby並安裝:
下載地址:http://railsinstaller.org/en 這裏下載的是2.3.3版本:
下載完成後安裝,一步步點next知道安裝完成(安裝時勾選3個選項)
然後對ruby進行配置:
3.構建集羣腳本redis-trib.rb
可以打開 https://raw.githubusercontent.com/antirez/redis/unstable/src/redis-trib.rb 然後複製裏面的內容到本地並保存爲redis-trib.rb;
如下圖,與redis集羣節點保存在同一個文件夾下(比如我所有節點都存放在redis-cluster文件夾下)。
然後依次啓動所有集羣節點start.bat
然後cmd進入redis集羣節點目錄後,執行: (–replicas 1 表示爲集羣中的每個主節點創建一個從節點)
redis-trib.rb create --replicas 1 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385
將會出現下圖的輸出信息
上圖可看出 主節點爲6380,6381,6382 端口的三個地址,6384,6385,6383爲三個從節點
中途會詢問是否打印更多詳細信息,輸入yes即可,然後redis-trib 就會將這份配置應用到集羣當中,讓各個節點開始互相通訊
Redis集羣數據分配策略:
採用一種叫做哈希槽 (hash slot)的方式來分配數據,redis cluster 默認分配了 16384 個slot,三個節點分別承擔的slot 區間是:(上圖3個M:節點的slots描述)
節點6380覆蓋0-5460;
節點6381覆蓋5461-10922;
節點6382覆蓋10923-16383.
最後查看所有集羣節點,會看到:
集羣搭建並啓動成功。。。
4.測試集羣
進入任意一個集羣節點,cmd執行 redis-cli.exe -c -p 6381
寫入任意一個value,查詢
寫一個hash:
hset redis:test:hash Hash1 12345
可以看到集羣會用CRC16算法來取模得到所屬的slot,然後將這個key分到哈希槽區間的節點上CRC16(key) % 16384
所以,可以看到我們set的key計算之後被分配到了slot-162 上, 而slot-162處在節點6380上,因此redis自動redirect到了6380節點上。
八、訂閱與發佈
using StackExchange.Redis; using System; namespace ConsoleApp1 { class Program { static void Main(string[] args) { var redisconn = ConnectionMultiplexer.Connect(ConfigurationOptions.Parse("127.0.0.1:6379,password='',connectTimeout=60")); #region 訂閱 Console.WriteLine("請輸入您要訂閱哪個通道的信息?"); var channelKey = Console.ReadLine(); redisconn.GetSubscriber().Subscribe(channelKey, (chanel, msg) => { Console.WriteLine($"通道:{channelKey}接受到發佈的內容爲:{msg}"); }); Console.WriteLine("您訂閱的通道爲:<< " + channelKey + " >> ! 一切就緒,等待發布消息!勿動,一動就沒啦!!"); #endregion #region 發佈 Console.WriteLine("請輸入要發佈向哪個通道?"); var channel = Console.ReadLine(); Console.WriteLine("請輸入要發佈的消息內容."); var message = Console.ReadLine(); redisconn.GetSubscriber().Publish(channel, message); #endregion Console.ReadKey(); } } }
封裝:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Threading; namespace ConsoleApp1 { class Program { static void Main(string[] args) { #region 訂閱 var redis = new Redis(); redis.RedisSubMessageEvent += RedisSubMessageEvent; redis.Use(1).RedisSub("redis_20200105_pay"); #endregion #region 發佈 for (var i = 1; i < 20; i++) { Redis.Using(rd => { rd.Use(1).RedisPub<string>("redis_20200105_pay", "pay amt=" + i); }); Thread.Sleep(1000); } #endregion Console.ReadKey(); } private static void RedisSubMessageEvent(string msg) { Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} RedisSubMessageEvent: {msg}"); } } public class Redis : IDisposable { private static ConnectionMultiplexer redis = null; private static bool connected = false; private IDatabase db = null; private int current = 0; public static bool IsConnected { get { Open(); return redis.IsConnected; } } public static bool Test() { bool r = true; try { Redis.Using(rs => { rs.Use(0); }); } catch (Exception e) { r = false; } return r; } private static int Open() { if (connected) return 1; redis = ConnectionMultiplexer.Connect("127.0.0.1:6379,password='',abortConnect = false"); connected = true; return 1; } public static void Using(Action<Redis> a) { using (var red = new Redis()) { a(red); } } public Redis Use(int i) { Open(); current = i; db = redis.GetDatabase(i); return this; } public void Set(string key, string val, TimeSpan? ts = null) { db.StringSet(key, val, ts); } public string Get(string key) { return db.StringGet(key); } public void Remove(string key) { db.KeyDelete(key, CommandFlags.HighPriority); } public bool Exists(string key) { return db.KeyExists(key); } public void Dispose() { db = null; } #region Redis發佈訂閱 public delegate void RedisDeletegate(string str); public event RedisDeletegate RedisSubMessageEvent; /// <summary> /// 訂閱 /// </summary> /// <param name="subChannel"></param> public void RedisSub(string subChannel) { redis.GetSubscriber().Subscribe(subChannel, (channel, message) => { RedisSubMessageEvent?.Invoke(message); //觸發事件 }); } /// <summary> /// 發佈 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="channel"></param> /// <param name="msg"></param> /// <returns></returns> public long RedisPub<T>(string channel, T msg) { string s = JsonConvert.SerializeObject(msg); return redis.GetSubscriber().Publish(channel, JsonConvert.SerializeObject(msg)); } /// <summary> /// 取消訂閱 /// </summary> /// <param name="channel"></param> public void Unsubscribe(string channel) { redis.GetSubscriber().Unsubscribe(channel); } /// <summary> /// 取消全部訂閱 /// </summary> public void UnsubscribeAll() { redis.GetSubscriber().UnsubscribeAll(); } #endregion } }
將redis發佈訂閱模式用做消息隊列和rabbitmq的區別:
可靠性
redis :沒有相應的機制保證消息的可靠消費,如果發佈者發佈一條消息,而沒有對應的訂閱者的話,這條消息將丟失,不會存在內存中;
rabbitmq:具有消息消費確認機制,如果發佈一條消息,還沒有消費者消費該隊列,那麼這條消息將一直存放在隊列中,直到有消費者消費了該條消息,以此可以保證消息的可靠消費;
實時性
redis:實時性高,redis作爲高效的緩存服務器,所有數據都存在在服務器中,所以它具有更高的實時性
消費者負載均衡:
rabbitmq隊列可以被多個消費者同時監控消費,但是每一條消息只能被消費一次,由於rabbitmq的消費確認機制,因此它能夠根據消費者的消費能力而調整它的負載;
redis發佈訂閱模式,一個隊列可以被多個消費者同時訂閱,當有消息到達時,會將該消息依次發送給每個訂閱者;
持久性
redis:redis的持久化是針對於整個redis緩存的內容,它有RDB和AOF兩種持久化方式,可以將整個redis實例持久化到磁盤,以此來做數據備份,防止異常情況下導致數據丟失。
rabbitmq:隊列,消息都可以選擇性持久化,持久化粒度更小,更靈活;
隊列監控
rabbitmq實現了後臺監控平臺,可以在該平臺上看到所有創建的隊列的詳細情況,良好的後臺管理平臺可以方面我們更好的使用;
redis沒有所謂的監控平臺。
總結
redis: 輕量級,低延遲,高併發,低可靠性;
rabbitmq:重量級,高可靠,異步,不保證實時;
rabbitmq是一個專門的AMQP協議隊列,他的優勢就在於提供可靠的隊列服務,並且可做到異步,而redis主要是用於緩存的,redis的發佈訂閱模塊,可用於實現及時性,且可靠性低的功能。
九、Redis五種數據類型的使用場景
1、字符串(Strings)
緩存功能:
字符串最經典的使用場景,redis具有支撐高併發特性,所以緩存通常能起到加速讀寫和降低IO操作的作用。
計數器:
許多運用都會使用redis作爲計數的基礎工具,他可以實現快速計數、查詢緩存的功能,同時數據可以一步落地到其他的數據源。
如:視頻播放數系統就是使用redis作爲視頻播放數計數的基礎組件。
共享session:
出於負載均衡的考慮,分佈式服務會將用戶信息的訪問均衡到不同服務器上,
用戶刷新一次訪問可能會需要重新登錄,爲避免這個問題可以用redis將用戶session集中管理,
在這種模式下只要保證redis的高可用和擴展性的,每次獲取用戶更新或查詢登錄信息
都直接從redis中集中獲取。
限速:
出於安全考慮,每次進行登錄時讓用戶輸入手機驗證碼,爲了短信接口不被頻繁訪問,會限制用戶每分鐘獲取驗證碼的頻率。
database.StringSet("name", "蒼");//設置StringSet(key, value) string str = database.StringGet("name");//結果:蒼 database.StringSet("name_two", str, TimeSpan.FromSeconds(10));//設置時間,10s後過期。
//創建對象 Demo demo = new Demo() { Name = "蒼", Age = 18, Height = 1.83 }; string demojson = JsonConvert.SerializeObject(demo);//序列化 database.StringSet("model", demojson); string model = database.StringGet("model"); demo = JsonConvert.DeserializeObject<Demo>(model);//反序列化
2、哈希(Hashes)
存儲、讀取、修改用戶屬性
常用於存儲一個對象(用戶姓名、年齡、生日......)
使用string增加了序列號反序列化的開銷,key重複值太多內存的浪費
var user = new User { Name = "james", Age = 18 }; string json = JsonConvert.SerializeObject(user);//序列化 db.HashSet("user", "londo", json); db.HashSet("user", "polo", json); db.HashSet("user", "kobe", json); //獲取Model string hashcang = db.HashGet("user", "londo"); user = JsonConvert.DeserializeObject<User>(hashcang);//反序列化 //獲取List RedisValue[] values = db.HashValues("user");//獲取所有value IList<User> demolist = new List<User>(); foreach (var item in values) { User hashmodel = JsonConvert.DeserializeObject<User>(item); demolist.Add(hashmodel); }
3、列表(Lists)
1).最新消息排行等功能(比如朋友圈的時間線)
2).消息隊列
3).文章列表:
每個用戶都有屬於自己的文章列表,現在需要分頁展示文章列表,此時可以考慮使用列表,列表不但有序
同時支持按照索引範圍獲取元素
for (int i = 0; i < 6; i++) { db.ListRightPush("list", i);//從底部插入數據 } for (int i = 6; i < 10; i++) { db.ListLeftPush("list", i);//從頂部插入數據 } long length = db.ListLength("list");//長度 10 RedisValue rightPop = db.ListRightPop("list");//從底部拿出數據 即:刪除5 var popValue = rightPop.ToString();// 5 RedisValue leftpop = db.ListLeftPop("list");//從頂部拿出數據 即:刪除9 RedisValue[] list = db.ListRange("list");//列表數據 8 7 6 0 1 2 3 4 var s = list[0].ToString(); // 8
4、集合(Sets)
1).共同好友
2).利用唯一性,統計訪問網站的所有獨立ip
3).好友推薦時,根據tag求交集,大於某個閾值就可以推薦
標籤(tag):
集合類型比較典型的使用場景,如一個用戶對娛樂、體育比較感興趣,另一個可能對新聞感興趣,
這些興趣就是標籤,有了這些數據就可以得到同一標籤的人,以及用戶的共同愛好的標籤,
這些數據對於用戶體驗以及曾強用戶粘度比較重要。
db.SetAdd("TagAsNBA", "1張三"); db.SetAdd("TagAsNBA", "2李四"); db.SetAdd("TagAsNBA", "3王五"); RedisValue[] setList = db.SetMembers("TagAsNBA"); foreach (var item in setList) { Console.WriteLine(item); }
5、有序集合(Sorted sets)
1).排行榜
2).帶權重的消息隊列
排行榜:
有序集合經典使用場景。例如視頻網站需要對用戶上傳的視頻做排行榜,
榜單維護可能是多方面:按照時間、按照播放量、按照獲得的贊數等。
db.SortedSetAdd("RankingAsScore", "張三", 1); db.SortedSetAdd("RankingAsScore", "王五", 3); db.SortedSetAdd("RankingAsScore", "李四", 2); RedisValue[] sortedSetList = db.SortedSetRangeByRank("RankingAsScore"); foreach (RedisValue t in sortedSetList) { Console.WriteLine(t + " --- " + t.HasValue); }
測試源碼:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Threading; namespace ConsoleApp1 { class Program { public class User { public string Name { get; set; } public int Age { get; set; } } #region ConnectionMultiplexer是StackExchange.Redis的核心,它被整個應用程序共享和重用,應該設置爲單例 // redis config // 哨兵26379 主從服務器6379,6380,開啓哨兵模式,會自動鏈接 private static readonly ConfigurationOptions ConfigurationOptions = ConfigurationOptions.Parse("127.0.0.1:6379,password='',connectTimeout=60"); //the lock for singleton private static readonly object Locker = new object(); //singleton private static ConnectionMultiplexer _redisConn; //singleton public static ConnectionMultiplexer GetRedisConn() { if (_redisConn == null) { lock (Locker) { if (_redisConn == null || !_redisConn.IsConnected) { _redisConn = ConnectionMultiplexer.Connect(ConfigurationOptions); } } } return _redisConn; } #endregion static void Main(string[] args) { _redisConn = GetRedisConn(); IDatabase db = _redisConn.GetDatabase(1); #region sets 集合 db.SetAdd("TagAsNBA", "1張三"); db.SetAdd("TagAsNBA", "2李四"); db.SetAdd("TagAsNBA", "3王五"); RedisValue[] setList = db.SetMembers("TagAsNBA"); foreach (var item in setList) { Console.WriteLine(item); } #endregion #region SetSorted 有序集合 db.SortedSetAdd("RankingAsScore", "張三", 1); db.SortedSetAdd("RankingAsScore", "王五", 3); db.SortedSetAdd("RankingAsScore", "李四", 2); RedisValue[] sortedSetList = db.SortedSetRangeByRank("RankingAsScore"); foreach (RedisValue t in sortedSetList) { Console.WriteLine(t + " --- " + t.HasValue); } #endregion #region hash ////創建對象 //var user = new User //{ // Name = "james", // Age = 18 //}; //string json = JsonConvert.SerializeObject(user);//序列化 //db.HashSet("user", "londo", json); //db.HashSet("user", "polo", json); //db.HashSet("user", "kobe", json); ////獲取Model //string hashcang = db.HashGet("user", "londo"); //user = JsonConvert.DeserializeObject<User>(hashcang);//反序列化 ////獲取List //RedisValue[] values = db.HashValues("user");//獲取所有value //IList<User> demolist = new List<User>(); //foreach (var item in values) //{ // User hashmodel = JsonConvert.DeserializeObject<User>(item); // demolist.Add(hashmodel); //} #endregion #region list //for (int i = 0; i < 6; i++) //{ // db.ListRightPush("list", i);//從底部插入數據 //} //for (int i = 6; i < 10; i++) //{ // db.ListLeftPush("list", i);//從頂部插入數據 //} //long length = db.ListLength("list");//長度 10 //RedisValue rightPop = db.ListRightPop("list");//從底部拿出數據 即:刪除5 //var popValue = rightPop.ToString();// 5 //RedisValue leftpop = db.ListLeftPop("list");//從頂部拿出數據 即:刪除9 //RedisValue[] list = db.ListRange("list");//列表數據 8 7 6 0 1 2 3 4 //var s = list[0].ToString(); // 8 #endregion //var strKey = "hello"; //var strValue = "world"; //db.StringSet(strKey, strValue, TimeSpan.FromSeconds(500)); #region 訂閱 //var redis = new Redis(); //redis.RedisSubMessageEvent += RedisSubMessageEvent; //redis.Use(1).RedisSub("redis_20200105_pay"); #endregion #region 發佈 //for (var i = 1; i < 20; i++) //{ // Redis.Using(rd => { // rd.Use(1).RedisPub<string>("redis_20200105_pay", "pay amt=" + i); // }); // Thread.Sleep(200); //} #endregion Console.ReadKey(); } private static void RedisSubMessageEvent(string msg) { Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} RedisSubMessageEvent: {msg}"); } } public class Redis : IDisposable { private static ConnectionMultiplexer redis = null; private static bool connected = false; private IDatabase db = null; private int current = 0; public static bool IsConnected { get { Open(); return redis.IsConnected; } } public static bool Test() { bool r = true; try { Redis.Using(rs => { rs.Use(0); }); } catch (Exception e) { r = false; } return r; } private static int Open() { if (connected) return 1; redis = ConnectionMultiplexer.Connect("127.0.0.1:6379,password='',abortConnect = false"); connected = true; return 1; } public static void Using(Action<Redis> a) { using (var red = new Redis()) { a(red); } } public Redis Use(int i) { Open(); current = i; db = redis.GetDatabase(i); return this; } public void Set(string key, string val, TimeSpan? ts = null) { db.StringSet(key, val, ts); } public string Get(string key) { return db.StringGet(key); } public void Remove(string key) { db.KeyDelete(key, CommandFlags.HighPriority); } public bool Exists(string key) { return db.KeyExists(key); } public void Dispose() { db = null; } #region Redis發佈訂閱 public delegate void RedisDeletegate(string str); public event RedisDeletegate RedisSubMessageEvent; /// <summary> /// 訂閱 /// </summary> /// <param name="subChannel"></param> public void RedisSub(string subChannel) { redis.GetSubscriber().Subscribe(subChannel, (channel, message) => { RedisSubMessageEvent?.Invoke(message); //觸發事件 }); } /// <summary> /// 發佈 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="channel"></param> /// <param name="msg"></param> /// <returns></returns> public long RedisPub<T>(string channel, T msg) { string s = JsonConvert.SerializeObject(msg); return redis.GetSubscriber().Publish(channel, JsonConvert.SerializeObject(msg)); } /// <summary> /// 取消訂閱 /// </summary> /// <param name="channel"></param> public void Unsubscribe(string channel) { redis.GetSubscriber().Unsubscribe(channel); } /// <summary> /// 取消全部訂閱 /// </summary> public void UnsubscribeAll() { redis.GetSubscriber().UnsubscribeAll(); } #endregion } }
十、Redis分佈式鎖秒殺場景案例
第一問,有沒有用過分佈式鎖?
有,基於redis的分佈鎖
第二問,redis爲什麼可以做分佈式鎖?
Redis爲單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,且多客戶端對Redis的連接並不存在競爭關係。
代碼實現的,主要是針對某一筆數據的流水號加鎖,防止多個線程寫入這個數據。(具有互斥性)
源碼:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static void Main(string[] args) { const string productKey = "0002_RC";//秒殺key hotelId_RoomType Thread.Sleep(5000); // 2、創建20個請求來秒殺 for (var i = 0; i < 20; i++) { var thread = new Thread(() => { SkillProduct(productKey); }); thread.Start(); } Console.ReadKey(); } /// <summary> /// 秒殺方法 /// </summary> public static void SkillProduct(string productKey) { var redisLock = new RedisLock(); redisLock.Lock(Environment.MachineName); // 1、獲取商品庫存 var stockNum = redisLock.GetStockNum(productKey); // 2、判斷商品庫存是否爲空 if (stockNum == 0) { // 2.1 秒殺失敗消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:不好意思,秒殺已結束,商品編號:{stockNum}"); redisLock.UnLock(Environment.MachineName); return; } // 3、秒殺成功消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:恭喜你,秒殺成功,商品編號:{stockNum}"); // 4、扣減商品庫存 redisLock.SubStockNum(productKey); redisLock.UnLock(Environment.MachineName); } } /// <summary> /// redis分佈式鎖 /// 分佈式鎖四要素 /// 1、鎖名 /// 2、加鎖操作 /// 3、解鎖操作 /// 4、鎖超時時間 /// </summary> public class RedisLock { // 1、redis連接管理類 private readonly ConnectionMultiplexer _connectionMultiplexer = null; // 2、redis數據操作類 private readonly IDatabase _database; public RedisLock() { _connectionMultiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379"); _database = _connectionMultiplexer.GetDatabase(0); } /// <summary> /// 加鎖 /// 1、key : 鎖名 /// 2、value : 誰加了這把鎖 : 防止鎖被其線程釋放掉 /// 3、超時時間 :防止死鎖 /// </summary> public void Lock(string key) { //如果加鎖失敗,繼續獲取鎖,無限失敗 while (true) { //當前服務器id,可以用酒店ID var value = Environment.MachineName;//Thread.CurrentThread.ManagedThreadId // 1、key:鎖名,可以用酒店id或者商品id 2、value:誰加了這把鎖,防止鎖被其線程釋放掉 3、超時時間:防止死鎖 bool isSucceed = _database.LockTake(key, value, TimeSpan.FromSeconds(10)); if (isSucceed) { break; } // 休眠一下 Thread.Sleep(200); } } /// <summary> /// 解鎖 /// </summary> public void UnLock(string key) { // 1、解鎖 _database.LockRelease(key, Environment.MachineName); // 2、關閉資源 _connectionMultiplexer.Close(); } /// <summary> /// 獲取庫存 /// </summary> public int GetStockNum(string key) { var redisValue = _database.StringGet(key); var num = JsonConvert.DeserializeObject<int>(redisValue); return num; } /// <summary> /// 扣減庫存 /// </summary> /// <param name="key"></param> /// <returns></returns> public void SubStockNum(string key) { var redisValue = _database.StringGet(key); var num = JsonConvert.DeserializeObject<int>(redisValue); var value = num - 1; _database.StringSet(key, value); } } }
使用c#的Lock爲什麼不行?
引文Lock只能鎖線程不能鎖進程。
源碼:
using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static void Main(string[] args) { const string productKey = "0002_RC";//秒殺key hotelId_RoomType Thread.Sleep(5000); ////創建30個請求來秒殺 //for (var i = 0; i < 30; i++) //{ // var thread = new Thread(() => // { // SkillProduct(productKey); // }); // thread.Start(); //} //創建30個請求來秒殺 var clock = new CsharpLock(); for (var i = 0; i < 30; i++) { var thread = new Thread(() => { clock.SkillProduct(productKey); }); thread.Start(); } Console.ReadKey(); } /// <summary> /// 秒殺方法 /// </summary> public static void SkillProduct(string productKey) { var redisLock = new RedisLock(); redisLock.Lock(Environment.MachineName); // 1、獲取商品庫存 var stockNum = redisLock.GetStockNum(productKey); // 2、判斷商品庫存是否爲空 if (stockNum == 0) { // 2.1 秒殺失敗消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:不好意思,秒殺已結束,商品編號:{stockNum}"); redisLock.UnLock(Environment.MachineName); return; } // 3、秒殺成功消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:恭喜你,秒殺成功,商品編號:{stockNum}"); // 4、扣減商品庫存 redisLock.SubStockNum(productKey); redisLock.UnLock(Environment.MachineName); } } /// <summary> /// C#Lock鎖 /// </summary> public class CsharpLock { public readonly object LockObj = new object(); /// <summary> /// 秒殺方法 /// </summary> public void SkillProduct(string productKey) { var redisLock = new RedisLock(); lock (LockObj) { // 1、獲取商品庫存 var stockNum = redisLock.GetStockNum(productKey); // 2、判斷商品庫存是否爲空 if (stockNum == 0) { // 2.1 秒殺失敗消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:不好意思,秒殺已結束,商品編號:{stockNum}"); redisLock.UnLock(Environment.MachineName); return; } // 3、秒殺成功消息 Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:恭喜你,秒殺成功,商品編號:{stockNum}"); // 4、扣減商品庫存 redisLock.SubStockNum(productKey); } } } /// <summary> /// redis分佈式鎖 /// 分佈式鎖四要素 /// 1、鎖名 /// 2、加鎖操作 /// 3、解鎖操作 /// 4、鎖超時時間 /// </summary> public class RedisLock { // 1、redis連接管理類 private readonly ConnectionMultiplexer _connectionMultiplexer = null; // 2、redis數據操作類 private readonly IDatabase _database; public RedisLock() { _connectionMultiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379"); _database = _connectionMultiplexer.GetDatabase(0); } /// <summary> /// 加鎖 /// 1、key : 鎖名 /// 2、value : 誰加了這把鎖 : 防止鎖被其線程釋放掉 /// 3、超時時間 :防止死鎖 /// </summary> public void Lock(string key) { //如果加鎖失敗,繼續獲取鎖,無限失敗 while (true) { //當前服務器id,可以用酒店ID var value = Environment.MachineName;//Thread.CurrentThread.ManagedThreadId // 1、key:鎖名,可以用酒店id或者商品id 2、value:誰加了這把鎖,防止鎖被其線程釋放掉 3、超時時間:防止死鎖 bool isSucceed = _database.LockTake(key, value, TimeSpan.FromSeconds(10)); if (isSucceed) { break; } // 休眠一下 Thread.Sleep(200); } } /// <summary> /// 解鎖 /// </summary> public void UnLock(string key) { // 1、解鎖 _database.LockRelease(key, Environment.MachineName); // 2、關閉資源 _connectionMultiplexer.Close(); } /// <summary> /// 獲取庫存 /// </summary> public int GetStockNum(string key) { var redisValue = _database.StringGet(key); var num = JsonConvert.DeserializeObject<int>(redisValue); return num; } /// <summary> /// 扣減庫存 /// </summary> public void SubStockNum(string key) { var redisValue = _database.StringGet(key); var num = JsonConvert.DeserializeObject<int>(redisValue); var value = num - 1; _database.StringSet(key, value); } } }
Lock(this)有問題
源碼:
public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) { if (value.IsNull) throw new ArgumentNullException(nameof(value)); return StringSet(key, value, expiry, When.NotExists, flags); }
可以看到調用的是set方法,繼續看set方法可以看到
private Message GetStringSetMessage(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrExistsOrNotExists(when); if (value.IsNull) return Message.Create(Database, flags, RedisCommand.DEL, key); if (expiry == null || expiry.Value == TimeSpan.MaxValue) { // no expiry switch (when) { case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value); case When.NotExists: return Message.Create(Database, flags, RedisCommand.SETNX, key, value); case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX); } } long milliseconds = expiry.Value.Ticks / TimeSpan.TicksPerMillisecond; if ((milliseconds % 1000) == 0) { // a nice round number of seconds long seconds = milliseconds / 1000; switch (when) { case When.Always: return Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value); case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX); case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX); } } switch (when) { case When.Always: return Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value); case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX); case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX); } throw new NotSupportedException(); }
RedisCommand.SETNX:不執行set命令如果key存在
RedisCommand.SETEX:命令將覆蓋已有的值
XX
: 只在鍵已經存在時, 纔對鍵進行設置操作