<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      Redis筆記

      Redis入門

      概述

      Redis是什么?

      Redis(Remote Dictionary Server ),即遠(yuǎn)程字典服務(wù),是一個開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫,并提供多種語言的API。

      與memcached一樣,為了保證效率,數(shù)據(jù)都是緩存在內(nèi)存中。區(qū)別的是redis會周期性的把更新的數(shù)據(jù)寫入磁盤或者把修改操作寫入追加的記錄文件,并且在此基礎(chǔ)上實現(xiàn)了master-slave(主從)同步。

      Redis能該干什么?

      1. 內(nèi)存存儲、持久化,內(nèi)存是斷電即失的,所以需要持久化(RDB、AOF)
      2. 高效率、用于高速緩沖
      3. 發(fā)布訂閱系統(tǒng)
      4. 地圖信息分析
      5. 計時器、計數(shù)器(eg:瀏覽量)

      核心特性

      • 多樣的數(shù)據(jù)類型
      • 持久化
      • 集群
      • 事務(wù)

      基礎(chǔ)知識

      Redis是一個字典結(jié)構(gòu)的存儲服務(wù)器,一個Redis實例提供了多個用來存儲數(shù)據(jù)的字典,客戶端可以指定將這數(shù)據(jù)存儲在哪個字典中,這與在一個關(guān)系型數(shù)據(jù)庫實例(以MySQL為例)中可以創(chuàng)建多個數(shù)據(jù)庫類似,可以將其中的每個字典都理解成一個獨立的數(shù)據(jù)庫。

      16個數(shù)據(jù)庫分別為:DB 0~DB 15,默認(rèn)使用DB 0 ,可以使用select n切換到DB n,dbsize可以查看當(dāng)前數(shù)據(jù)庫的大小,與key數(shù)量相關(guān)。

      127.0.0.1:6379> config get databases # 命令行查看數(shù)據(jù)庫數(shù)量databases
      1) "databases"
      2) "16"
      
      127.0.0.1:6379> select 8 # 切換數(shù)據(jù)庫 DB 8
      OK
      127.0.0.1:6379[8]> dbsize # 查看數(shù)據(jù)庫大小
      (integer) 0
      
      # 不同數(shù)據(jù)庫之間 數(shù)據(jù)是不能互通的,并且dbsize 是根據(jù)庫中key的個數(shù)。
      127.0.0.1:6379> set name sakura 
      OK
      127.0.0.1:6379> SELECT 8
      OK
      127.0.0.1:6379[8]> get name # db8中并不能獲取db0中的鍵值對。
      (nil)
      127.0.0.1:6379[8]> DBSIZE
      (integer) 0
      127.0.0.1:6379[8]> SELECT 0
      OK
      127.0.0.1:6379> keys *
      1) "counter:__rand_int__"
      2) "mylist"
      3) "name"
      4) "key:__rand_int__"
      5) "myset:__rand_int__"
      127.0.0.1:6379> DBSIZE # size和key個數(shù)相關(guān)
      (integer) 5
      

      keys * :查看當(dāng)前數(shù)據(jù)庫中所有的key。

      flushdb:清空當(dāng)前數(shù)據(jù)庫中的鍵值對。

      flushall:清空所有數(shù)據(jù)庫的鍵值對。

      keys *這個命令需要慎重使用,如果數(shù)據(jù)庫中的鍵過多可能會造成卡頓,生產(chǎn)環(huán)境中應(yīng)該使用dbsize

      保存中文

      redis-cli --raw

      五大數(shù)據(jù)類型

      Redis是一個開源(BSD許可),內(nèi)存存儲的數(shù)據(jù)結(jié)構(gòu)服務(wù)器,可用作數(shù)據(jù)庫,高速緩存和消息隊列代理。它支持字符串哈希表列表集合有序集合位圖hyperloglogs等數(shù)據(jù)類型。內(nèi)置復(fù)制、Lua腳本、LRU收回、事務(wù)以及不同級別磁盤持久化功能,同時通過Redis Sentinel提供高可用,通過Redis Cluster提供自動分區(qū)

      Redis-key

      在redis中無論什么數(shù)據(jù)類型,在數(shù)據(jù)庫中都是以key-value形式保存,通過進行對Redis-key的操作,來完成對數(shù)據(jù)庫中數(shù)據(jù)的操作。

      下面學(xué)習(xí)的命令:

      • exists key:判斷鍵是否存在
      • del key:刪除鍵值對
      • move key db:將鍵值對移動到指定數(shù)據(jù)庫
      • expire key second:設(shè)置鍵值對的過期時間
      • type key:查看value的數(shù)據(jù)類型
      127.0.0.1:6379> keys * # 查看當(dāng)前數(shù)據(jù)庫所有key
      (empty list or set)
      127.0.0.1:6379> set name qinjiang # set key
      OK
      127.0.0.1:6379> set age 20
      OK
      127.0.0.1:6379> keys *
      1) "age"
      2) "name"
      127.0.0.1:6379> move age 1 # 將鍵值對移動到指定數(shù)據(jù)庫
      (integer) 1
      127.0.0.1:6379> EXISTS age # 判斷鍵是否存在
      (integer) 0 # 不存在
      127.0.0.1:6379> EXISTS name
      (integer) 1 # 存在
      127.0.0.1:6379> SELECT 1
      OK
      127.0.0.1:6379[1]> keys *
      1) "age"
      127.0.0.1:6379[1]> del age # 刪除鍵值對
      (integer) 1 # 刪除個數(shù)
      
      
      127.0.0.1:6379> set age 20
      OK
      127.0.0.1:6379> EXPIRE age 15 # 設(shè)置鍵值對的過期時間
      
      (integer) 1 # 設(shè)置成功 開始計數(shù)
      127.0.0.1:6379> ttl age # 查看key的過期剩余時間
      (integer) 13
      127.0.0.1:6379> ttl age
      (integer) 11
      127.0.0.1:6379> ttl age
      (integer) 9
      127.0.0.1:6379> ttl age
      (integer) -2 # -2 表示key過期,-1表示key未設(shè)置過期時間
      
      127.0.0.1:6379> get age # 過期的key 會被自動delete
      (nil)
      127.0.0.1:6379> keys *
      1) "name"
      
      127.0.0.1:6379> type name # 查看value的數(shù)據(jù)類型
      string
      

      關(guān)于TTL命令

      Redis的key,通過TTL命令返回key的過期時間,一般來說有3種:

      1. 當(dāng)前key沒有設(shè)置過期時間,所以會返回-1.
      2. 當(dāng)前key有設(shè)置過期時間,而且key已經(jīng)過期,所以會返回-2.
      3. 當(dāng)前key有設(shè)置過期時間,且key還沒有過期,故會返回key的正常剩余時間.

      關(guān)于重命名RENAMERENAMENX

      • RENAME key newkey修改 key 的名稱
      • RENAMENX key newkey僅當(dāng) newkey 不存在時,將 key 改名為 newkey 。

      更多命令學(xué)習(xí):Redis命令大全

      實際上每種數(shù)據(jù)結(jié)構(gòu)都有自己底層的內(nèi)部編碼實現(xiàn),而且是多種實現(xiàn),這樣Redis會在合適的場景選擇合適的內(nèi)部編碼

      Redis這樣設(shè)計有兩個好處:

      • 可以改進內(nèi)部編碼,而對外的數(shù)據(jù)結(jié)構(gòu)和命令沒有影響,這樣一旦開發(fā)出更優(yōu)秀的內(nèi)部編碼,無需改動外部數(shù)據(jù)結(jié)構(gòu)和命令
      • 多種內(nèi)部編碼實現(xiàn)可以在不同場景下發(fā)揮各自的優(yōu)勢

      String(字符串)

      普通的set、get直接略過。

      應(yīng)用場景

      1、緩存功能:String字符串是最常用的數(shù)據(jù)類型,不僅僅是redis,各個語言都是最基本類型,因此,利用redis作為緩存,配合其它數(shù)據(jù)庫作為存儲層,利用redis支持高并發(fā)的特點,可以大大加快系統(tǒng)的讀寫速度、以及降低后端數(shù)據(jù)庫的壓力。

      2、計數(shù)器:許多系統(tǒng)都會使用redis作為系統(tǒng)的實時計數(shù)器,可以快速實現(xiàn)計數(shù)和查詢的功能。而且最終的數(shù)據(jù)結(jié)果可以按照特定的時間落地到數(shù)據(jù)庫或者其它存儲介質(zhì)當(dāng)中進行永久保存。

      3、統(tǒng)計多單位的數(shù)量:eg,uid:gongming count:0 根據(jù)不同的uid更新count數(shù)量。

      4、共享用戶session:用戶重新刷新一次界面,可能需要訪問一下數(shù)據(jù)進行重新登錄,或者訪問頁面緩存cookie,這兩種方式做有一定弊端,1)每次都重新登錄效率低下 2)cookie保存在客戶端,有安全隱患。這時可以利用redis將用戶的session集中管理,在這種模式只需要保證redis的高可用,每次用戶session的更新和獲取都可以快速完成。大大提高效率。

      命令 描述 示例
      APPEND key value 向指定的key的value后追加字符串 127.0.0.1:6379> set msg hello OK 127.0.0.1:6379> append msg " world" (integer) 11 127.0.0.1:6379> get msg “hello world”
      DECR/INCR key 將指定key的value數(shù)值進行+1/-1(僅對于數(shù)字) 127.0.0.1:6379> set age 20 OK 127.0.0.1:6379> incr age (integer) 21 127.0.0.1:6379> decr age (integer) 20
      INCRBY/DECRBY key n 按指定的步長對數(shù)值進行加減 127.0.0.1:6379> INCRBY age 5 (integer) 25 127.0.0.1:6379> DECRBY age 10 (integer) 15
      INCRBYFLOAT key n 為數(shù)值加上浮點型數(shù)值 127.0.0.1:6379> INCRBYFLOAT age 5.2 “20.2”
      STRLEN key 獲取key保存值的字符串長度 127.0.0.1:6379> get msg “hello world” 127.0.0.1:6379> STRLEN msg (integer) 11
      GETRANGE key start end 按起止位置獲取字符串(閉區(qū)間,起止位置都取) 127.0.0.1:6379> get msg “hello world” 127.0.0.1:6379> GETRANGE msg 3 9 “l(fā)o worl”
      SETRANGE key offset value 用指定的value 替換key中 offset開始的值 127.0.0.1:6379> SETRANGE msg 2 hello (integer) 7 127.0.0.1:6379> get msg “tehello”
      GETSET key value 將給定 key 的值設(shè)為 value ,并返回 key 的舊值(old value)。 127.0.0.1:6379> GETSET msg test “hello world”
      SETNX key value 僅當(dāng)key不存在時進行set 127.0.0.1:6379> SETNX msg test (integer) 0 127.0.0.1:6379> SETNX name sakura (integer) 1
      SETEX key seconds value set 鍵值對并設(shè)置過期時間 127.0.0.1:6379> setex name 10 root OK 127.0.0.1:6379> get name (nil)
      MSET key1 value1 [key2 value2..] 批量set鍵值對 127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 OK
      MSETNX key1 value1 [key2 value2..] 批量設(shè)置鍵值對,僅當(dāng)參數(shù)中所有的key都不存在時執(zhí)行 127.0.0.1:6379> MSETNX k1 v1 k4 v4 (integer) 0
      MGET key1 [key2..] 批量獲取多個key保存的值 127.0.0.1:6379> MGET k1 k2 k3 1) “v1” 2) “v2” 3) “v3”
      PSETEX key milliseconds value 和 SETEX 命令相似,但它以毫秒為單位設(shè)置 key 的生存時間,
      getset key value 如果不存在值,則返回nil,如果存在值,獲取原來的值,并設(shè)置新的值

      String類似的使用場景:value除了是字符串還可以是數(shù)字,用途舉例:

      • 計數(shù)器
      • 統(tǒng)計多單位的數(shù)量:uid:123666:follow 0
      • 粉絲數(shù)
      • 對象存儲緩存

      List(列表)

      Redis列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊),一個列表最多可以包含 232 - 1 個元素 (4294967295, 每個列表超過40億個元素)。

      應(yīng)用場景

      1、消息隊列:reids的鏈表結(jié)構(gòu),可以輕松實現(xiàn)阻塞隊列,可以使用左進右出的命令組成來完成隊列的設(shè)計。比如:數(shù)據(jù)的生產(chǎn)者可以通過Lpush命令從左邊插入數(shù)據(jù),多個數(shù)據(jù)消費者,可以使用BRpop命令阻塞的“搶”列表尾部的數(shù)據(jù)。

      2、文章列表或者數(shù)據(jù)分頁展示的應(yīng)用。比如,我們常用的博客網(wǎng)站的文章列表,當(dāng)用戶量越來越多時,而且每一個用戶都有自己的文章列表,而且當(dāng)文章多時,都需要分頁展示,這時可以考慮使用redis的列表,列表不但有序同時還支持按照范圍內(nèi)獲取元素,可以完美解決分頁查詢功能。大大提高查詢效率。

      命令 描述
      LPUSH/RPUSH key value1[value2..] 從左邊/右邊向列表中PUSH值(一個或者多個)。
      LRANGE key start end 獲取list 起止元素(索引從左往右 遞增)
      LPUSHX/RPUSHX key value 向已存在的列名中push值(一個或者多個)
      `LINSERT key BEFORE AFTER pivot value`
      LLEN key 查看列表長度
      LINDEX key index 通過索引獲取列表元素
      LSET key index value 通過索引為元素設(shè)值
      LPOP/RPOP key 從最左邊/最右邊移除值 并返回
      RPOPLPUSH source destination 將列表的尾部(右)最后一個值彈出,并返回,然后加到另一個列表的頭部
      LTRIM key start end 通過下標(biāo)截取指定范圍內(nèi)的列表
      LREM key count value List中是允許value重復(fù)的 count > 0:從頭部開始搜索 然后刪除指定的value 至多刪除count個 count < 0:從尾部開始搜索… count = 0:刪除列表中所有的指定value。
      BLPOP/BRPOP key1[key2] timout 移出并獲取列表的第一個/最后一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發(fā)現(xiàn)可彈出元素為止。
      BRPOPLPUSH source destination timeout RPOPLPUSH功能相同,如果列表沒有元素會阻塞列表直到等待超時或發(fā)現(xiàn)可彈出元素為止。
      ---------------------------LPUSH---RPUSH---LRANGE--------------------------------
      
      127.0.0.1:6379> LPUSH mylist k1 # LPUSH mylist=>{1}
      (integer) 1
      127.0.0.1:6379> LPUSH mylist k2 # LPUSH mylist=>{2,1}
      (integer) 2
      127.0.0.1:6379> RPUSH mylist k3 # RPUSH mylist=>{2,1,3}
      (integer) 3
      127.0.0.1:6379> get mylist # 普通的get是無法獲取list值的
      (error) WRONGTYPE Operation against a key holding the wrong kind of value
      127.0.0.1:6379> LRANGE mylist 0 4 # LRANGE 獲取起止位置范圍內(nèi)的元素
      1) "k2"
      2) "k1"
      3) "k3"
      127.0.0.1:6379> LRANGE mylist 0 2
      1) "k2"
      2) "k1"
      3) "k3"
      127.0.0.1:6379> LRANGE mylist 0 1
      1) "k2"
      2) "k1"
      127.0.0.1:6379> LRANGE mylist 0 -1 # 獲取全部元素
      1) "k2"
      2) "k1"
      3) "k3"
      
      ---------------------------LPUSHX---RPUSHX-----------------------------------
      
      127.0.0.1:6379> LPUSHX list v1 # list不存在 LPUSHX失敗
      (integer) 0
      127.0.0.1:6379> LPUSHX list v1 v2  
      (integer) 0
      127.0.0.1:6379> LPUSHX mylist k4 k5 # 向mylist中 左邊 PUSH k4 k5
      (integer) 5
      127.0.0.1:6379> LRANGE mylist 0 -1
      1) "k5"
      2) "k4"
      3) "k2"
      4) "k1"
      5) "k3"
      
      ---------------------------LINSERT--LLEN--LINDEX--LSET----------------------------
      
      127.0.0.1:6379> LINSERT mylist after k2 ins_key1 # 在k2元素后 插入ins_key1
      (integer) 6
      127.0.0.1:6379> LRANGE mylist 0 -1
      1) "k5"
      2) "k4"
      3) "k2"
      4) "ins_key1"
      5) "k1"
      6) "k3"
      127.0.0.1:6379> LLEN mylist # 查看mylist的長度
      (integer) 6
      127.0.0.1:6379> LINDEX mylist 3 # 獲取下標(biāo)為3的元素
      "ins_key1"
      127.0.0.1:6379> LINDEX mylist 0
      "k5"
      127.0.0.1:6379> LSET mylist 3 k6 # 將下標(biāo)3的元素 set值為k6
      OK
      127.0.0.1:6379> LRANGE mylist 0 -1
      1) "k5"
      2) "k4"
      3) "k2"
      4) "k6"
      5) "k1"
      6) "k3"
      
      ---------------------------LPOP--RPOP--------------------------
      
      127.0.0.1:6379> LPOP mylist # 左側(cè)(頭部)彈出
      "k5"
      127.0.0.1:6379> RPOP mylist # 右側(cè)(尾部)彈出
      "k3"
      
      ---------------------------RPOPLPUSH--------------------------
      
      127.0.0.1:6379> LRANGE mylist 0 -1
      1) "k4"
      2) "k2"
      3) "k6"
      4) "k1"
      127.0.0.1:6379> RPOPLPUSH mylist newlist # 將mylist的最后一個值(k1)彈出,加入到newlist的頭部
      "k1"
      127.0.0.1:6379> LRANGE newlist 0 -1
      1) "k1"
      127.0.0.1:6379> LRANGE mylist 0 -1
      1) "k4"
      2) "k2"
      3) "k6"
      
      ---------------------------LTRIM--------------------------
      
      127.0.0.1:6379> LTRIM mylist 0 1 # 截取mylist中的 0~1部分
      OK
      127.0.0.1:6379> LRANGE mylist 0 -1
      1) "k4"
      2) "k2"
      
      # 初始 mylist: k2,k2,k2,k2,k2,k2,k4,k2,k2,k2,k2
      ---------------------------LREM--------------------------
      
      127.0.0.1:6379> LREM mylist 3 k2 # 從頭部開始搜索 至多刪除3個 k2
      (integer) 3
      # 刪除后:mylist: k2,k2,k2,k4,k2,k2,k2,k2
      
      127.0.0.1:6379> LREM mylist -2 k2 #從尾部開始搜索 至多刪除2個 k2
      (integer) 2
      # 刪除后:mylist: k2,k2,k2,k4,k2,k2
      
      
      ---------------------------BLPOP--BRPOP--------------------------
      
      mylist: k2,k2,k2,k4,k2,k2
      newlist: k1
      
      127.0.0.1:6379> BLPOP newlist mylist 30 # 從newlist中彈出第一個值,mylist作為候選
      1) "newlist" # 彈出
      2) "k1"
      127.0.0.1:6379> BLPOP newlist mylist 30
      1) "mylist" # 由于newlist空了 從mylist中彈出
      2) "k2"
      127.0.0.1:6379> BLPOP newlist 30
      (30.10s) # 超時了
      
      127.0.0.1:6379> BLPOP newlist 30 # 我們連接另一個客戶端向newlist中push了test, 阻塞被解決。
      1) "newlist"
      2) "test"
      (12.54s)
      

      小結(jié):

      • list實際上是一個鏈表,before Node after , left, right 都可以插入值
      • 如果key不存在,則創(chuàng)建新的鏈表
      • 如果key存在,新增內(nèi)容
      • 如果移除了所有值,空鏈表,也代表不存在
      • 在兩邊插入或者改動值,效率最高!修改中間元素,效率相對較低

      應(yīng)用:

      • 消息排隊
      • 消息隊列(Lpush Rpop)
      • 棧(Lpush Lpop)

      Set(集合)

      Redis的Set是string類型的無序集合。集合成員是唯一的,這就意味著集合中不能出現(xiàn)重復(fù)的數(shù)據(jù)。Redis中集合是通過哈希表實現(xiàn)的,所以添加,刪除,查找的復(fù)雜度都是O(1),集合中最大的成員數(shù)為 232 - 1 (4294967295, 每個集合可存儲40多億個成員)。

      應(yīng)用場景

      1、標(biāo)簽:比如我們博客網(wǎng)站常常使用到的興趣標(biāo)簽,把一個個有著相同愛好,關(guān)注類似內(nèi)容的用戶利用一個標(biāo)簽把他們進行歸并。

      2、共同好友功能,共同喜好,或者可以引申到二度好友之類的擴展應(yīng)用。

      3、統(tǒng)計網(wǎng)站的獨立IP。利用set集合當(dāng)中元素不唯一性,可以快速實時統(tǒng)計訪問網(wǎng)站的獨立IP。

      數(shù)據(jù)結(jié)構(gòu)

      set的底層結(jié)構(gòu)相對復(fù)雜寫,使用了intset和hashtable兩種數(shù)據(jù)結(jié)構(gòu)存儲,intset可以理解為數(shù)組。

      命令 描述
      SADD key member1[member2..] 向集合中無序增加一個/多個成員
      SCARD key 獲取集合的成員數(shù)
      SMEMBERS key 返回集合中所有的成員
      SISMEMBER key member 查詢member元素是否是集合的成員,結(jié)果是無序的
      SRANDMEMBER key [count] 隨機返回集合中count個成員,count缺省值為1
      SPOP key [count] 隨機移除并返回集合中count個成員,count缺省值為1
      SMOVE source destination member 將source集合的成員member移動到destination集合
      SREM key member1[member2..] 移除集合中一個/多個成員
      SDIFF key1[key2..] 返回所有集合的差集 key1- key2 - …
      SDIFFSTORE destination key1[key2..] 在SDIFF的基礎(chǔ)上,將結(jié)果保存到集合中(覆蓋)。不能保存到其他類型key噢!
      SINTER key1 [key2..] 返回所有集合的交集
      SINTERSTORE destination key1[key2..] 在SINTER的基礎(chǔ)上,存儲結(jié)果到集合中。覆蓋
      SUNION key1 [key2..] 返回所有集合的并集
      SUNIONSTORE destination key1 [key2..] 在SUNION的基礎(chǔ)上,存儲結(jié)果到及和張。覆蓋
      SSCAN KEY [MATCH pattern] [COUNT count] 在大量數(shù)據(jù)環(huán)境下,使用此命令遍歷集合中元素,每次遍歷部分
      ---------------SADD--SCARD--SMEMBERS--SISMEMBER--------------------
      
      127.0.0.1:6379> SADD myset m1 m2 m3 m4 # 向myset中增加成員 m1~m4
      (integer) 4
      127.0.0.1:6379> SCARD myset # 獲取集合的成員數(shù)目
      (integer) 4
      127.0.0.1:6379> smembers myset # 獲取集合中所有成員
      1) "m4"
      2) "m3"
      3) "m2"
      4) "m1"
      127.0.0.1:6379> SISMEMBER myset m5 # 查詢m5是否是myset的成員
      (integer) 0 # 不是,返回0
      127.0.0.1:6379> SISMEMBER myset m2
      (integer) 1 # 是,返回1
      127.0.0.1:6379> SISMEMBER myset m3
      (integer) 1
      
      ---------------------SRANDMEMBER--SPOP----------------------------------
      
      127.0.0.1:6379> SRANDMEMBER myset 3 # 隨機返回3個成員
      1) "m2"
      2) "m3"
      3) "m4"
      127.0.0.1:6379> SRANDMEMBER myset # 隨機返回1個成員
      "m3"
      127.0.0.1:6379> SPOP myset 2 # 隨機移除并返回2個成員
      1) "m1"
      2) "m4"
      # 將set還原到{m1,m2,m3,m4}
      
      ---------------------SMOVE--SREM----------------------------------------
      
      127.0.0.1:6379> SMOVE myset newset m3 # 將myset中m3成員移動到newset集合
      (integer) 1
      127.0.0.1:6379> SMEMBERS myset
      1) "m4"
      2) "m2"
      3) "m1"
      127.0.0.1:6379> SMEMBERS newset
      1) "m3"
      127.0.0.1:6379> SREM newset m3 # 從newset中移除m3元素
      (integer) 1
      127.0.0.1:6379> SMEMBERS newset
      (empty list or set)
      
      # 下面開始是多集合操作,多集合操作中若只有一個參數(shù)默認(rèn)和自身進行運算
      # setx=>{m1,m2,m4,m6}, sety=>{m2,m5,m6}, setz=>{m1,m3,m6}
      
      -----------------------------SDIFF------------------------------------
      
      127.0.0.1:6379> SDIFF setx sety setz # 等價于setx-sety-setz
      1) "m4"
      127.0.0.1:6379> SDIFF setx sety # setx - sety
      1) "m4"
      2) "m1"
      127.0.0.1:6379> SDIFF sety setx # sety - setx
      1) "m5"
      
      
      -------------------------SINTER---------------------------------------
      # 共同關(guān)注(交集)
      
      127.0.0.1:6379> SINTER setx sety setz # 求 setx、sety、setx的交集
      1) "m6"
      127.0.0.1:6379> SINTER setx sety # 求setx sety的交集
      1) "m2"
      2) "m6"
      
      -------------------------SUNION---------------------------------------
      
      127.0.0.1:6379> SUNION setx sety setz # setx sety setz的并集
      1) "m4"
      2) "m6"
      3) "m3"
      4) "m2"
      5) "m1"
      6) "m5"
      127.0.0.1:6379> SUNION setx sety # setx sety 并集
      1) "m4"
      2) "m6"
      3) "m2"
      4) "m1"
      5) "m5"
      

      Hash(哈希)

      Redis hash 是一個string類型的field和value的映射表,hash特別適合用于存儲對象,Set就是一種簡化的Hash,只變動key,而value使用默認(rèn)值填充。可以將一個Hash表作為一個對象進行存儲,表中存放對象的信息。

      應(yīng)用場景

      1、由于hash數(shù)據(jù)類型的key-value的特性,用來存儲關(guān)系型數(shù)據(jù)庫中表記錄,是redis中哈希類型最常用的場景。一條記錄作為一個key-value,把每列屬性值對應(yīng)成field-value存儲在哈希表當(dāng)中,然后通過key值來區(qū)分表當(dāng)中的主鍵。

      2、經(jīng)常被用來存儲用戶相關(guān)信息。優(yōu)化用戶信息的獲取,不需要重復(fù)從數(shù)據(jù)庫當(dāng)中讀取,提高系統(tǒng)性能。

      命令 描述
      HSET key field value 將哈希表 key 中的字段 field 的值設(shè)為 value 。重復(fù)設(shè)置同一個field會覆蓋,返回0
      HMSET key field1 value1 [field2 value2..] 同時將多個 field-value (域-值)對設(shè)置到哈希表 key 中。
      HSETNX key field value 只有在字段 field 不存在時,設(shè)置哈希表字段的值。
      HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。
      HGET key field value 獲取存儲在哈希表中指定字段的值
      HMGET key field1 [field2..] 獲取所有給定字段的值
      HGETALL key 獲取在哈希表key 的所有字段和值
      HKEYS key 獲取哈希表key中所有的字段
      HLEN key 獲取哈希表中字段的數(shù)量
      HVALS key 獲取哈希表中所有值
      HDEL key field1 [field2..] 刪除哈希表key中一個/多個field字段
      HINCRBY key field n 為哈希表 key 中的指定字段的整數(shù)值加上增量n,并返回增量后結(jié)果 一樣只適用于整數(shù)型字段
      HINCRBYFLOAT key field n 為哈希表 key 中的指定字段的浮點數(shù)值加上增量 n。
      HSCAN key cursor [MATCH pattern] [COUNT count] 迭代哈希表中的鍵值對。
      ------------------------HSET--HMSET--HSETNX----------------
      127.0.0.1:6379> HSET studentx name sakura # 將studentx哈希表作為一個對象,設(shè)置name為sakura
      (integer) 1
      127.0.0.1:6379> HSET studentx name gyc # 重復(fù)設(shè)置field進行覆蓋,并返回0
      (integer) 0
      127.0.0.1:6379> HSET studentx age 20 # 設(shè)置studentx的age為20
      (integer) 1
      127.0.0.1:6379> HMSET studentx sex 1 tel 15623667886 # 設(shè)置sex為1,tel為15623667886
      OK
      127.0.0.1:6379> HSETNX studentx name gyc # HSETNX 設(shè)置已存在的field
      (integer) 0 # 失敗
      127.0.0.1:6379> HSETNX studentx email 12345@qq.com
      (integer) 1 # 成功
      
      ----------------------HEXISTS--------------------------------
      127.0.0.1:6379> HEXISTS studentx name # name字段在studentx中是否存在
      (integer) 1 # 存在
      127.0.0.1:6379> HEXISTS studentx addr
      (integer) 0 # 不存在
      
      -------------------HGET--HMGET--HGETALL-----------
      127.0.0.1:6379> HGET studentx name # 獲取studentx中name字段的value
      "gyc"
      127.0.0.1:6379> HMGET studentx name age tel # 獲取studentx中name、age、tel字段的value
      1) "gyc"
      2) "20"
      3) "15623667886"
      127.0.0.1:6379> HGETALL studentx # 獲取studentx中所有的field及其value
       1) "name"
       2) "gyc"
       3) "age"
       4) "20"
       5) "sex"
       6) "1"
       7) "tel"
       8) "15623667886"
       9) "email"
      10) "12345@qq.com"
      
      
      --------------------HKEYS--HLEN--HVALS--------------
      127.0.0.1:6379> HKEYS studentx # 查看studentx中所有的field
      1) "name"
      2) "age"
      3) "sex"
      4) "tel"
      5) "email"
      127.0.0.1:6379> HLEN studentx # 查看studentx中的字段數(shù)量
      (integer) 5
      127.0.0.1:6379> HVALS studentx # 查看studentx中所有的value
      1) "gyc"
      2) "20"
      3) "1"
      4) "15623667886"
      5) "12345@qq.com"
      
      -------------------------HDEL--------------------------
      127.0.0.1:6379> HDEL studentx sex tel # 刪除studentx 中的sex、tel字段
      (integer) 2
      127.0.0.1:6379> HKEYS studentx
      1) "name"
      2) "age"
      3) "email"
      
      -------------HINCRBY--HINCRBYFLOAT------------------------
      127.0.0.1:6379> HINCRBY studentx age 1 # studentx的age字段數(shù)值+1
      (integer) 21
      127.0.0.1:6379> HINCRBY studentx name 1 # 非整數(shù)字型字段不可用
      (error) ERR hash value is not an integer
      127.0.0.1:6379> HINCRBYFLOAT studentx weight 0.6 # weight字段增加0.6
      "90.8"
      

      Hash變更的數(shù)據(jù)user name age,尤其是用戶信息之類的,經(jīng)常變動的信息!Hash更適合于對象的存儲,Sring更加適合字符串存儲!

      Zset(有序集合)

      不同的是每個元素都會關(guān)聯(lián)一個double類型的分?jǐn)?shù)(score)。redis正是通過分?jǐn)?shù)來為集合中的成員進行從小到大的排序。score相同:按字典順序排序,有序集合的成員是唯一的,但分?jǐn)?shù)(score)卻可以重復(fù)。

      應(yīng)用場景

      1、 排行榜:有序集合經(jīng)典使用場景。例如視頻網(wǎng)站需要對用戶上傳的視頻做排行榜,榜單維護可能是多方面:按照時間、按照播放量、按照獲得的贊數(shù)等。

      2、用Sorted Sets來做帶權(quán)重的隊列,比如普通消息的score為1,重要消息的score為2,然后工作線程可以選擇按score的倒序來獲取工作任務(wù)。讓重要的任務(wù)優(yōu)先執(zhí)行。

      命令 描述
      ZADD key score member1 [score2 member2] 向有序集合添加一個或多個成員,或者更新已存在成員的分?jǐn)?shù)
      ZCARD key 獲取有序集合的成員數(shù)
      ZCOUNT key min max 計算在有序集合中指定區(qū)間score的成員數(shù)
      ZINCRBY key n member 有序集合中對指定成員的分?jǐn)?shù)加上增量 n
      ZSCORE key member 返回有序集中,成員的分?jǐn)?shù)值
      ZRANK key member 返回有序集合中指定成員的索引
      ZRANGE key start end 通過索引區(qū)間返回有序集合成指定區(qū)間內(nèi)的成員
      ZRANGEBYLEX key min max 通過字典區(qū)間返回有序集合的成員
      ZRANGEBYSCORE key min max 通過分?jǐn)?shù)返回有序集合指定區(qū)間內(nèi)的成員-inf 和 +inf分別表示最小最大值,只支持開區(qū)間()
      ZLEXCOUNT key min max 在有序集合中計算指定字典區(qū)間內(nèi)成員數(shù)量
      ZREM key member1 [member2..] 移除有序集合中一個/多個成員
      ZREMRANGEBYLEX key min max 移除有序集合中給定的字典區(qū)間的所有成員
      ZREMRANGEBYRANK key start stop 移除有序集合中給定的排名區(qū)間的所有成員
      ZREMRANGEBYSCORE key min max 移除有序集合中給定的分?jǐn)?shù)區(qū)間的所有成員
      ZREVRANGE key start end 返回有序集中指定區(qū)間內(nèi)的成員,通過索引,分?jǐn)?shù)從高到底
      ZREVRANGEBYSCORRE key max min 返回有序集中指定分?jǐn)?shù)區(qū)間內(nèi)的成員,分?jǐn)?shù)從高到低排序
      ZREVRANGEBYLEX key max min 返回有序集中指定字典區(qū)間內(nèi)的成員,按字典順序倒序
      ZREVRANK key member 返回有序集合中指定成員的排名,有序集成員按分?jǐn)?shù)值遞減(從大到小)排序
      ZINTERSTORE destination numkeys key1 [key2 ..] 計算給定的一個或多個有序集的交集并將結(jié)果集存儲在新的有序集合 key 中,numkeys:表示參與運算的集合數(shù),將score相加作為結(jié)果的score
      ZUNIONSTORE destination numkeys key1 [key2..] 計算給定的一個或多個有序集的交集并將結(jié)果集存儲在新的有序集合 key 中
      ZSCAN key cursor [MATCH pattern\] [COUNT count] 迭代有序集合中的元素(包括元素成員和元素分值)
      -------------------ZADD--ZCARD--ZCOUNT--------------
      127.0.0.1:6379> ZADD myzset 1 m1 2 m2 3 m3 # 向有序集合myzset中添加成員m1 score=1 以及成員m2 score=2..
      (integer) 2
      127.0.0.1:6379> ZCARD myzset # 獲取有序集合的成員數(shù)
      (integer) 2
      127.0.0.1:6379> ZCOUNT myzset 0 1 # 獲取score在 [0,1]區(qū)間的成員數(shù)量
      (integer) 1
      127.0.0.1:6379> ZCOUNT myzset 0 2
      (integer) 2
      
      ----------------ZINCRBY--ZSCORE--------------------------
      127.0.0.1:6379> ZINCRBY myzset 5 m2 # 將成員m2的score +5
      "7"
      127.0.0.1:6379> ZSCORE myzset m1 # 獲取成員m1的score
      "1"
      127.0.0.1:6379> ZSCORE myzset m2
      "7"
      
      --------------ZRANK--ZRANGE-----------------------------------
      127.0.0.1:6379> ZRANK myzset m1 # 獲取成員m1的索引,索引按照score排序,score相同索引值按字典順序順序增加
      (integer) 0
      127.0.0.1:6379> ZRANK myzset m2
      (integer) 2
      127.0.0.1:6379> ZRANGE myzset 0 1 # 獲取索引在 0~1的成員
      1) "m1"
      2) "m3"
      127.0.0.1:6379> ZRANGE myzset 0 -1 # 獲取全部成員
      1) "m1"
      2) "m3"
      3) "m2"
      
      #testset=>{abc,add,amaze,apple,back,java,redis} score均為0
      ------------------ZRANGEBYLEX---------------------------------
      127.0.0.1:6379> ZRANGEBYLEX testset - + # 返回所有成員
      1) "abc"
      2) "add"
      3) "amaze"
      4) "apple"
      5) "back"
      6) "java"
      7) "redis"
      127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 0 3 # 分頁 按索引顯示查詢結(jié)果的 0,1,2條記錄
      1) "abc"
      2) "add"
      3) "amaze"
      127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 3 3 # 顯示 3,4,5條記錄
      1) "apple"
      2) "back"
      3) "java"
      127.0.0.1:6379> ZRANGEBYLEX testset (- [apple # 顯示 (-,apple] 區(qū)間內(nèi)的成員
      1) "abc"
      2) "add"
      3) "amaze"
      4) "apple"
      127.0.0.1:6379> ZRANGEBYLEX testset [apple [java # 顯示 [apple,java]字典區(qū)間的成員
      1) "apple"
      2) "back"
      3) "java"
      
      -----------------------ZRANGEBYSCORE---------------------
      127.0.0.1:6379> ZRANGEBYSCORE myzset 1 10 # 返回score在 [1,10]之間的的成員
      1) "m1"
      2) "m3"
      3) "m2"
      127.0.0.1:6379> ZRANGEBYSCORE myzset 1 5
      1) "m1"
      2) "m3"
      
      --------------------ZLEXCOUNT-----------------------------
      127.0.0.1:6379> ZLEXCOUNT testset - +
      (integer) 7
      127.0.0.1:6379> ZLEXCOUNT testset [apple [java
      (integer) 3
      
      ------------------ZREM--ZREMRANGEBYLEX--ZREMRANGBYRANK--ZREMRANGEBYSCORE--------------------------------
      127.0.0.1:6379> ZREM testset abc # 移除成員abc
      (integer) 1
      127.0.0.1:6379> ZREMRANGEBYLEX testset [apple [java # 移除字典區(qū)間[apple,java]中的所有成員
      (integer) 3
      127.0.0.1:6379> ZREMRANGEBYRANK testset 0 1 # 移除排名0~1的所有成員
      (integer) 2
      127.0.0.1:6379> ZREMRANGEBYSCORE myzset 0 3 # 移除score在 [0,3]的成員
      (integer) 2
      
      
      # testset=> {abc,add,apple,amaze,back,java,redis} score均為0
      # myzset=> {(m1,1),(m2,2),(m3,3),(m4,4),(m7,7),(m9,9)}
      ----------------ZREVRANGE--ZREVRANGEBYSCORE--ZREVRANGEBYLEX-----------
      127.0.0.1:6379> ZREVRANGE myzset 0 3 # 按score遞減排序,然后按索引,返回結(jié)果的 0~3
      1) "m9"
      2) "m7"
      3) "m4"
      4) "m3"
      127.0.0.1:6379> ZREVRANGE myzset 2 4 # 返回排序結(jié)果的 索引的2~4
      1) "m4"
      2) "m3"
      3) "m2"
      127.0.0.1:6379> ZREVRANGEBYSCORE myzset 6 2 # 按score遞減順序 返回集合中分?jǐn)?shù)在[2,6]之間的成員
      1) "m4"
      2) "m3"
      3) "m2"
      127.0.0.1:6379> ZREVRANGEBYLEX testset [java (add # 按字典倒序 返回集合中(add,java]字典區(qū)間的成員
      1) "java"
      2) "back"
      3) "apple"
      4) "amaze"
      
      -------------------------ZREVRANK------------------------------
      127.0.0.1:6379> ZREVRANK myzset m7 # 按score遞減順序,返回成員m7索引
      (integer) 1
      127.0.0.1:6379> ZREVRANK myzset m2
      (integer) 4
      
      
      # mathscore=>{(xm,90),(xh,95),(xg,87)} 小明、小紅、小剛的數(shù)學(xué)成績
      # enscore=>{(xm,70),(xh,93),(xg,90)} 小明、小紅、小剛的英語成績
      -------------------ZINTERSTORE--ZUNIONSTORE-----------------------------------
      127.0.0.1:6379> ZINTERSTORE sumscore 2 mathscore enscore # 將mathscore enscore進行合并 結(jié)果存放到sumscore
      (integer) 3
      127.0.0.1:6379> ZRANGE sumscore 0 -1 withscores # 合并后的score是之前集合中所有score的和
      1) "xm"
      2) "160"
      3) "xg"
      4) "177"
      5) "xh"
      6) "188"
      
      127.0.0.1:6379> ZUNIONSTORE lowestscore 2 mathscore enscore AGGREGATE MIN # 取兩個集合的成員score最小值作為結(jié)果的
      (integer) 3
      127.0.0.1:6379> ZRANGE lowestscore 0 -1 withscores
      1) "xm"
      2) "70"
      3) "xg"
      4) "87"
      5) "xh"
      6) "93"
      

      應(yīng)用案例:

      • set排序 存儲班級成績表 工資表排序!
      • 普通消息,1.重要消息 2.帶權(quán)重進行判斷
      • 排行榜應(yīng)用實現(xiàn),取Top N測試

      三種特殊數(shù)據(jù)類型

      Geospatial(地理位置)

      使用經(jīng)緯度定位地理坐標(biāo)并用一個有序集合zset保存,所以zset命令也可以使用。

      命令 描述
      geoadd key longitud(經(jīng)度) latitude(緯度) member [..] 將具體經(jīng)緯度的坐標(biāo)存入一個有序集合
      geopos key member [member..] 獲取集合中的一個/多個成員坐標(biāo)
      geodist key member1 member2 [unit] 返回兩個給定位置之間的距離。默認(rèn)以米作為單位。
      `georadius key longitude latitude radius m km
      GEORADIUSBYMEMBER key member radius... 功能與GEORADIUS相同,只是中心位置不是具體的經(jīng)緯度,而是使用結(jié)合中已有的成員作為中心點。
      geohash key member1 [member2..] 返回一個或多個位置元素的Geohash表示。使用Geohash位置52點整數(shù)編碼。

      有效經(jīng)緯度

      • 有效的經(jīng)度從-180度到180度。
      • 有效的緯度從-85.05112878度到85.05112878度。

      指定單位的參數(shù) unit 必須是以下單位的其中一個:

      • m 表示單位為米。
      • km 表示單位為千米。
      • mi 表示單位為英里。
      • ft 表示單位為英尺。

      關(guān)于GEORADIUS的參數(shù)

      通過georadius就可以完成 附近的人功能

      withcoord:帶上坐標(biāo)

      withdist:帶上距離,單位與半徑單位相同

      COUNT n : 只顯示前n個(按距離遞增排序)

      ----------------georadius---------------------
      127.0.0.1:6379> GEORADIUS china:city 120 30 500 km withcoord withdist # 查詢經(jīng)緯度(120,30)坐標(biāo)500km半徑內(nèi)的成員
      1) 1) "hangzhou"
         2) "29.4151"
         3) 1) "120.20000249147415"
            2) "30.199999888333501"
      2) 1) "shanghai"
         2) "205.3611"
         3) 1) "121.40000134706497"
            2) "31.400000253193539"
           
      ------------geohash---------------------------
      127.0.0.1:6379> geohash china:city yichang shanghai # 獲取成員經(jīng)緯坐標(biāo)的geohash表示
      1) "wmrjwbr5250"
      2) "wtw6ds0y300"
      

      Hyperloglog(基數(shù)統(tǒng)計)

      Redis HyperLogLog 是用來做基數(shù)(數(shù)據(jù)集中不重復(fù)的元素的個數(shù))統(tǒng)計的數(shù)據(jù)結(jié)構(gòu),HyperLogLog 的優(yōu)點是,在輸入元素的數(shù)量或者體積非常非常大時,計算基數(shù)所需的空間總是固定的、并且是很小的。

      花費 12 KB 內(nèi)存,就可以計算接近 2^64 個不同元素的基數(shù)。

      因為 HyperLogLog 只會根據(jù)輸入元素來計算基數(shù),而不會儲存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個元素。

      應(yīng)用場景:網(wǎng)頁的訪問量(UV),一個用戶多次訪問,也只能算作一個人。

      傳統(tǒng)實現(xiàn),存儲用戶的id,然后每次進行比較。當(dāng)用戶變多之后這種方式及其浪費空間,而我們的目的只是計數(shù),Hyperloglog就能幫助我們利用最小的空間完成。

      命令 描述
      PFADD key element1 [elememt2..] 添加指定元素到 HyperLogLog 中
      PFCOUNT key [key] 返回給定 HyperLogLog 的基數(shù)估算值。
      PFMERGE destkey sourcekey [sourcekey..] 將多個 HyperLogLog 合并為一個 HyperLogLog
      ----------PFADD--PFCOUNT---------------------
      127.0.0.1:6379> PFADD myelemx a b c d e f g h i j k # 添加元素
      (integer) 1
      127.0.0.1:6379> type myelemx # hyperloglog底層使用String
      string
      127.0.0.1:6379> PFCOUNT myelemx # 估算myelemx的基數(shù)
      (integer) 11
      127.0.0.1:6379> PFADD myelemy i j k z m c b v p q s
      (integer) 1
      127.0.0.1:6379> PFCOUNT myelemy
      (integer) 11
      
      ----------------PFMERGE-----------------------
      127.0.0.1:6379> PFMERGE myelemz myelemx myelemy # 合并myelemx和myelemy 成為myelemz
      OK
      127.0.0.1:6379> PFCOUNT myelemz # 估算基數(shù)
      (integer) 17
      

      如果允許容錯,那么一定可以使用Hyperloglog !

      如果不允許容錯,就使用set或者自己的數(shù)據(jù)類型即可 !

      BitMaps(位圖)

      使用位存儲,信息狀態(tài)只有 0 和 1,Bitmap是一串連續(xù)的2進制數(shù)字(0或1),每一位所在的位置為偏移(offset),在bitmap上可執(zhí)行AND,OR,XOR,NOT以及其它位操作。

      應(yīng)用場景:簽到統(tǒng)計、狀態(tài)統(tǒng)計

      命令 描述
      setbit key offset value 為指定key的offset位設(shè)置值
      getbit key offset 獲取offset位的值
      bitcount key [start end] 統(tǒng)計字符串被設(shè)置為1的bit數(shù),也可以指定統(tǒng)計范圍按字節(jié)
      bitop operration destkey key[key..] 對一個或多個保存二進制位的字符串 key 進行位元操作,并將結(jié)果保存到 destkey 上。
      BITPOS key bit [start] [end] 返回字符串里面第一個被設(shè)置為1或者0的bit位。start和end只能按字節(jié),不能按位
      ------------setbit--getbit--------------
      127.0.0.1:6379> setbit sign 0 1 # 設(shè)置sign的第0位為 1 
      (integer) 0
      127.0.0.1:6379> setbit sign 2 1 # 設(shè)置sign的第2位為 1  不設(shè)置默認(rèn) 是0
      (integer) 0
      127.0.0.1:6379> setbit sign 3 1
      (integer) 0
      127.0.0.1:6379> setbit sign 5 1
      (integer) 0
      127.0.0.1:6379> type sign
      string
      
      127.0.0.1:6379> getbit sign 2 # 獲取第2位的數(shù)值
      (integer) 1
      127.0.0.1:6379> getbit sign 3
      (integer) 1
      127.0.0.1:6379> getbit sign 4 # 未設(shè)置默認(rèn)是0
      (integer) 0
      
      -----------bitcount----------------------------
      127.0.0.1:6379> BITCOUNT sign # 統(tǒng)計sign中為1的位數(shù)
      (integer) 4
      

      這樣設(shè)置以后你能get到的值是:\xA2\x80,所以bitmaps是一串從左到右的二進制串

      Redis事務(wù)

      Redis的事務(wù)就是指一組命令的集合,Redis的單條命令是保證原子性的,但是redis事務(wù)不能保證原子性,并且Redis事務(wù)沒有隔離級別的概念。

      事務(wù)中每條命令都會被序列化,執(zhí)行過程中按順序執(zhí)行,不允許其他命令進行干擾。

      • 一次性
      • 順序性
      • 排他性

      操作過程

      • 開啟事務(wù)(multi
      • 命令入隊
      • 執(zhí)行事務(wù)(exec

      所以事務(wù)中的命令在加入時都沒有被執(zhí)行,直到提交時才會開始執(zhí)行(Exec)一次性完成。

      127.0.0.1:6379> multi # 開啟事務(wù)
      OK
      127.0.0.1:6379> set k1 v1 # 命令入隊
      QUEUED
      127.0.0.1:6379> set k2 v2 # ..
      QUEUED
      127.0.0.1:6379> get k1
      QUEUED
      127.0.0.1:6379> set k3 v3
      QUEUED
      127.0.0.1:6379> keys *
      QUEUED
      127.0.0.1:6379> exec # 事務(wù)執(zhí)行
      1) OK
      2) OK
      3) "v1"
      4) OK
      5) 1) "k3"
         2) "k2"
         3) "k1"
      

      取消事務(wù)(discurd)

      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> set k1 v1
      QUEUED
      127.0.0.1:6379> set k2 v2
      QUEUED
      127.0.0.1:6379> DISCARD # 放棄事務(wù)
      OK
      127.0.0.1:6379> EXEC 
      (error) ERR EXEC without MULTI # 當(dāng)前未開啟事務(wù)
      127.0.0.1:6379> get k1 # 被放棄事務(wù)中命令并未執(zhí)行
      (nil)
      

      事務(wù)錯誤

      代碼語法錯誤(編譯時異常)所有的命令都不執(zhí)行:

      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> set k1 v1
      QUEUED
      127.0.0.1:6379> set k2 v2
      QUEUED
      127.0.0.1:6379> error k1 # 這是一條語法錯誤命令
      (error) ERR unknown command `error`, with args beginning with: `k1`, # 會報錯但是不影響后續(xù)命令入隊 
      127.0.0.1:6379> get k2
      QUEUED
      127.0.0.1:6379> EXEC
      (error) EXECABORT Transaction discarded because of previous errors. # 執(zhí)行報錯
      127.0.0.1:6379> get k1 
      (nil) # 其他命令并沒有被執(zhí)行
      

      當(dāng)代碼邏輯錯誤 (運行時異常) ,其他命令可以正常執(zhí)行,因此說Redis所以不保證事務(wù)原子性:

      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> set k1 v1
      QUEUED
      127.0.0.1:6379> set k2 v2
      QUEUED
      127.0.0.1:6379> INCR k1 # 這條命令邏輯錯誤(對字符串進行增量)
      QUEUED
      127.0.0.1:6379> get k2
      QUEUED
      127.0.0.1:6379> exec
      1) OK
      2) OK
      3) (error) ERR value is not an integer or out of range # 運行時報錯
      4) "v2" # 其他命令正常執(zhí)行
      
      # 雖然中間有一條命令報錯了,但是后面的指令依舊正常執(zhí)行成功了。
      # 所以說Redis單條指令保證原子性,但是Redis事務(wù)不能保證原子性。
      

      監(jiān)控

      使用watch key監(jiān)控指定數(shù)據(jù),相當(dāng)于樂觀鎖加鎖。

      正常執(zhí)行:

      127.0.0.1:6379> set money 100 # 設(shè)置余額:100
      OK
      127.0.0.1:6379> set use 0 # 支出使用:0
      OK
      127.0.0.1:6379> watch money # 監(jiān)視money (上鎖)
      OK
      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> DECRBY money 20
      QUEUED
      127.0.0.1:6379> INCRBY use 20
      QUEUED
      127.0.0.1:6379> exec # 監(jiān)視值沒有被中途修改,事務(wù)正常執(zhí)行
      1) (integer) 80
      2) (integer) 20
      

      測試多線程修改值,使用watch可以當(dāng)做redis的樂觀鎖操作(相當(dāng)于getversion):

      我們啟動另外一個客戶端模擬插隊線程。

      線程1:

      127.0.0.1:6379> watch money # money上鎖
      OK
      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> DECRBY money 20
      QUEUED
      127.0.0.1:6379> INCRBY use 20
      QUEUED
      127.0.0.1:6379> 	# 此時事務(wù)并沒有執(zhí)行
      

      模擬線程插隊,線程2:

      127.0.0.1:6379> INCRBY money 500 # 修改了線程一中監(jiān)視的money
      (integer) 600
      12
      

      回到線程1,執(zhí)行事務(wù):

      127.0.0.1:6379> EXEC # 執(zhí)行之前,另一個線程修改了我們的值,這個時候就會導(dǎo)致事務(wù)執(zhí)行失敗
      (nil) # 沒有結(jié)果,說明事務(wù)執(zhí)行失敗
      
      127.0.0.1:6379> get money # 線程2 修改生效
      "600"
      127.0.0.1:6379> get use # 線程1事務(wù)執(zhí)行失敗,數(shù)值沒有被修改
      "0"
      

      解鎖獲取最新值,然后再加鎖進行事務(wù)。unwatch進行解鎖。注意:每次提交執(zhí)行exec后都會自動釋放鎖,不管是否成功

      持久化策略

      Redis支持RDB和AOF兩種持久化機制,持久化功能有效避免因進程退出造成的數(shù)據(jù)丟失問題,當(dāng)下次重啟時利用之前持久化的文件即可實現(xiàn)數(shù)據(jù)恢復(fù)。

      RDB持久化

      什么是RDB

      RDB持久化是把當(dāng)前進程數(shù)據(jù)生成快照保存到硬盤的過程,觸發(fā)RDB持久化過程可以分為手動觸發(fā)和自動觸發(fā)。默認(rèn)情況下, Redis 將數(shù)據(jù)庫快照保存在名字為 dump.rdb的二進制文件中。文件名可以在配置文件中進行自定義。

      工作原理

      RDB的手動觸發(fā)分別對應(yīng)save和bgsave命令:

      • save命令:阻塞當(dāng)前Redis服務(wù)器,直到RDB過程完成為止,對于內(nèi)存比較大的示例會造成長時間則色,線上環(huán)境不建議使用。
      • bgsave命令:Redis進程執(zhí)行fork操作創(chuàng)建子進程,RDB持久化過程由子進程負(fù)責(zé),完成后自動結(jié)束。阻塞只發(fā)生在fork階段,一般時間很短。

      顯然bgsave命令是針對save阻塞問題做的優(yōu)化。因此Redis內(nèi)部所有的涉及RDB的操作都采用bgsave的方式,而save命令已經(jīng)廢棄,因此這里不做過多介紹。

      RDB的自動觸發(fā)只需要在配置文件redis.conf中開啟相關(guān)配置即可:

      save 900 1:表示900 秒內(nèi)如果至少有 1 個 key 的值變化,則保存
      save 300 10:表示300 秒內(nèi)如果至少有 10 個 key 的值變化,則保存
      save 60 10000:表示60 秒內(nèi)如果至少有 10000 個 key 的值變化,則保存
      

      bgsave

      bgsave是異步進行,進行持久化的時候,Redis還可以將繼續(xù)響應(yīng)客戶端請求 ;

      詳細(xì)具體步驟如下:

      • 執(zhí)行bgsave命令,Redis父進程判斷當(dāng)前是否存在正在執(zhí)行的子進程,如RDB/AOF子進程,如果存在bgsave命令直接返回
      • 父進程執(zhí)行fork操作創(chuàng)建子進程,fork操作過程中父進程會阻塞,通過info stats命令查看latest_fork_usec選項,可以獲取最近一個fork操作的耗時,單位為微秒
      • 父進程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父進程,可以繼續(xù)響應(yīng)其他命令
      • 父進程創(chuàng)建RDB文件,根據(jù)父進程內(nèi)存生成臨時快照文件,完成后對原有文件進行原子替換。執(zhí)行lastsave命令可以獲取最后一次生成RDB的時間,對應(yīng)info統(tǒng)計的rdb_last_save_time選項
      • 進程發(fā)送信號給父進程表示完成,父進程更新統(tǒng)計信息

      這種工作方式使得 Redis 可以從寫時復(fù)制(copy-on-write)機制中獲益(因為是使用子進程進行寫操作,而父進程依然可以接收來自客戶端的請求)。

      bgsave和save對比

      命令 save bgsave
      IO類型 同步 異步
      阻塞 是(阻塞發(fā)生在fock(),通常非常快)
      復(fù)雜度 O(n) O(n)
      優(yōu)點 不會消耗額外的內(nèi)存 不阻塞客戶端命令
      缺點 阻塞客戶端命令 需要fork子進程,消耗內(nèi)存

      優(yōu)點和缺點

      優(yōu)點:

      • 適合大規(guī)模的數(shù)據(jù)恢復(fù)
      • 對數(shù)據(jù)的完整性要求不高
      • Redis加載RDB恢復(fù)數(shù)據(jù)遠(yuǎn)遠(yuǎn)快于AOF的方式

      缺點:

      • 需要一定的時間間隔進行操作,如果redis意外宕機了,這個最后一次修改的數(shù)據(jù)就沒有了
      • fork進程的時候,會占用一定的內(nèi)存空間
      • RDB文件使用特定二進制格式保存,Redis版本演進過程中有多個格式的RDB版本,存在老版本Redis服務(wù)無法兼容RDB格式的問題

      也可以簡單的說,RDB不適合實時持久化。

      持久化AOF

      AOF表示Append Only File,這種模式會將所有的命令都記錄下來,恢復(fù)的時候就把這個文件全部再執(zhí)行一遍。

      以日志的形式來記錄每個寫的操作,將Redis執(zhí)行過的所有指令記錄下來(讀操作不記錄),只許追加文件但不可以改寫文件,redis啟動之初會讀取該文件重新構(gòu)建數(shù)據(jù),換言之,redis重啟的話就根據(jù)日志文件的內(nèi)容將寫指令從前到后執(zhí)行一次以完成數(shù)據(jù)的恢復(fù)工作。

      什么是AOF

      快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因為某些原因而造成故障停機, 那么服務(wù)器將丟失最近寫入、以及未保存到快照中的那些數(shù)據(jù)。 從 1.1 版本開始, Redis 增加了一種完全耐久的持久化方式: AOF 持久化。

      AOF默認(rèn)是不開啟的,需要進行配置才可以:

      appendonly yes  # 默認(rèn)是不開啟aof模式的,默認(rèn)是使用rdb方式持久化的,在大部分的情況下,rdb完全夠用
      appendfilename "appendonly.aof"
      
      # appendfsync always # 每次修改都會sync 消耗性能
      appendfsync everysec # 每秒執(zhí)行一次 sync 可能會丟失這一秒的數(shù)據(jù)
      # appendfsync no # 不執(zhí)行 sync ,這時候操作系統(tǒng)自己同步數(shù)據(jù),速度最快
      

      優(yōu)點和缺點

      優(yōu)點

      1. 每一次修改都會同步,文件的完整性會更加好
      2. 沒秒同步一次,可能會丟失一秒的數(shù)據(jù)
      3. 從不同步,效率最高

      缺點

      1. 相對于數(shù)據(jù)文件來說,AOF遠(yuǎn)遠(yuǎn)大于RDB,修復(fù)速度比RDB慢
      2. AOF運行效率也要比RDB慢,所以我們Redis默認(rèn)的配置就是RDB持久化

      RDB和AOP選擇

      RDB 和 AOF 對比

      比較項 RDB AOF
      啟動優(yōu)先級
      體積
      恢復(fù)速度
      數(shù)據(jù)安全性 丟數(shù)據(jù) 根據(jù)策略決定

      如何選擇使用哪種持久化方式?

      一般來說, 如果想達(dá)到足以媲美 PostgreSQL 的數(shù)據(jù)安全性, 你應(yīng)該同時使用兩種持久化功能。如果你非常關(guān)心你的數(shù)據(jù), 但仍然可以承受數(shù)分鐘以內(nèi)的數(shù)據(jù)丟失, 那么你可以只使用 RDB 持久化。有很多用戶都只使用 AOF 持久化, 但并不推薦這種方式: 因為定時生成 RDB 快照(snapshot)非常便于進行數(shù)據(jù)庫備份, 并且 RDB 恢復(fù)數(shù)據(jù)集的速度也要比 AOF 恢復(fù)的速度要快。

      Redis發(fā)布與訂閱

      Redis 發(fā)布訂閱(pub/sub)是一種消息通信模式:發(fā)送者(pub)發(fā)送消息,訂閱者(sub)接收消息。

      下圖展示了頻道 channel1 , 以及訂閱這個頻道的三個客戶端 —— client2 、 client5 和 client1 之間的關(guān)系:

      在這里插入圖片描述

      當(dāng)有新消息通過 PUBLISH 命令發(fā)送給頻道 channel1 時, 這個消息就會被發(fā)送給訂閱它的三個客戶端:

      在這里插入圖片描述

      命令

      命令 描述
      PSUBSCRIBE pattern [pattern..] 訂閱一個或多個符合給定模式的頻道。
      PUNSUBSCRIBE pattern [pattern..] 退訂一個或多個符合給定模式的頻道。
      PUBSUB subcommand [argument[argument]] 查看訂閱與發(fā)布系統(tǒng)狀態(tài)。
      PUBLISH channel message 向指定頻道發(fā)布消息
      SUBSCRIBE channel [channel..] 訂閱給定的一個或多個頻道。
      SUBSCRIBE channel [channel..] 退訂一個或多個頻道

      示例

      ------------訂閱端----------------------
      127.0.0.1:6379> SUBSCRIBE sakura # 訂閱sakura頻道
      Reading messages... (press Ctrl-C to quit) # 等待接收消息
      1) "subscribe" # 訂閱成功的消息
      2) "sakura"
      3) (integer) 1
      1) "message" # 接收到來自sakura頻道的消息 "hello world"
      2) "sakura"
      3) "hello world"
      1) "message" # 接收到來自sakura頻道的消息 "hello i am sakura"
      2) "sakura"
      3) "hello i am sakura"
      
      --------------消息發(fā)布端-------------------
      127.0.0.1:6379> PUBLISH sakura "hello world" # 發(fā)布消息到sakura頻道
      (integer) 1
      127.0.0.1:6379> PUBLISH sakura "hello i am sakura" # 發(fā)布消息
      (integer) 1
      
      -----------------查看活躍的頻道------------
      127.0.0.1:6379> PUBSUB channels
      1) "sakura"
      

      原理

      每個 Redis 服務(wù)器進程都維持著一個表示服務(wù)器狀態(tài)的 redis.h/redisServer 結(jié)構(gòu), 結(jié)構(gòu)的 pubsub_channels 屬性是一個字典, 這個字典就用于保存訂閱頻道的信息,其中,字典的鍵為正在被訂閱的頻道, 而字典的值則是一個鏈表, 鏈表中保存了所有訂閱這個頻道的客戶端。

      在這里插入圖片描述

      客戶端訂閱,就被鏈接到對應(yīng)頻道的鏈表的尾部,退訂則就是將客戶端節(jié)點從鏈表中移除。

      缺點

      1. 如果一個客戶端訂閱了頻道,但自己讀取消息的速度卻不夠快的話,那么不f/斷積壓的消息會使redis輸出緩沖區(qū)的體積變得越來越大,這可能使得redis本身的速度變慢,甚至直接崩潰。
      2. 這和數(shù)據(jù)傳輸可靠性有關(guān),如果在訂閱方斷線,那么他將會丟失所有在短線期間發(fā)布者發(fā)布的消息。

      應(yīng)用.消息訂閱:公眾號訂閱,微博關(guān)注等等(起始更多是使用消息‘f/隊列來進行實現(xiàn))

      1. 多人在線聊天室。

      稍微復(fù)雜的場景,我們就會使用消息中間件MQ處理。

      Redis線程IO模型

      在從前的版本中,Redis是個單線程的程序,除了Redis之外,Node.js與Nginx也是單線程,但是它們都是高性能服務(wù)器的典范,不過從Redis6.0開始,增加了多線程的支持,以滿足更高的性能,具體可以參考:支持多線程的Redis 6.0終于發(fā)布了,那么Redis是如何使用單線程處理那么多的并發(fā)客戶端連接的?答案就是多路復(fù)用。

      非阻塞IO

      當(dāng)我們調(diào)用套節(jié)字的讀寫方法,默認(rèn)它們是阻塞的,比如read方法要傳遞進去一個參數(shù)n,表示讀取這么多字節(jié)后再返回,如果沒有讀夠線程就會卡在那里,直到新的數(shù)據(jù)刀來或者連接關(guān)閉了,read方法才會返回,線程才能繼續(xù)處理。而write方法一般來說不會阻塞,除非內(nèi)核為套接字分配的寫緩沖區(qū)已經(jīng)滿了,write方法就會阻塞,直到緩存區(qū)中有空閑空間挪出來了。

      非阻塞IO在套接字對象上提供了一個線程Non_Blocking,當(dāng)這個選項打開時,讀寫方法不會阻塞,而是能讀多少讀多少,能寫多少寫多少。能讀多少取決于內(nèi)核為套接字分配的讀緩沖區(qū)內(nèi)部的數(shù)據(jù)字節(jié)數(shù),能寫多少取決于內(nèi)核為套接字分配的寫緩沖區(qū)的空閑空間字節(jié)數(shù)。讀方法和寫方法都會通過返回值來告知程序?qū)嶋H讀寫了多少字節(jié)。

      有了非阻塞IO意味著線程在讀寫IO時可以不必再阻塞了,讀寫可以瞬間完成然后程序可以繼續(xù)干別的事了。

      多路復(fù)用

      非阻塞IO有個問題,那就是線程要讀數(shù)據(jù),結(jié)果讀了一部分就返回了,線程如何知道何時才應(yīng)該繼續(xù)。也就是當(dāng)數(shù)據(jù)到來時,線程如何得到通知。寫也是一樣,如果緩沖區(qū)滿了,寫不完,剩下的數(shù)據(jù)何時才應(yīng)該續(xù)寫,線程也應(yīng)該得到通知。

      多路復(fù)用(事件輪詢)API就是用來解決這個問題的,最簡單的事件輪詢API是select函數(shù),它是操作系統(tǒng)提供給用戶程序的API。輸入是讀寫描述符列表read_fds & write_fds,輸出是與之對應(yīng)的可讀可寫事件。同時還提供了timeout參數(shù),如果沒有任何事件到來,那么久最多等待timeout時間,線程處于阻塞狀態(tài)。一旦期間有任何事情刀來,就可以立即返回。時間過了之后還是沒有任何事件到來,也會立即返回。拿到事件后,線程就可以繼續(xù)挨個處理相應(yīng)的事件。處理完了繼續(xù)過來輪詢。于是線程就進入了一個死循環(huán),我們把這個死循環(huán)稱為事件循環(huán),一個循環(huán)為一個周期。

      每個客戶端套接字socket都有對應(yīng)的讀寫文件描述符。

      read_events,write_events = select(read_fds,write_fds,timeout)
      for event in read_events:
      	handle_read(event.fd)
      for event in write_events:
          handle_write(event.fd)
      # 處理其它事情,如定時任務(wù)等
      handle_others()
      

      因為我們通過select系統(tǒng)調(diào)用同時處理多個通道描述符的讀寫事件,因此我們將這類系統(tǒng)調(diào)用稱為多路復(fù)用API。現(xiàn)代操作系統(tǒng)的多路復(fù)用API已經(jīng)不再使用使用select系統(tǒng)調(diào)用,而改用epoll(linux)和kqueue(freebsd & macosx),因為select系統(tǒng)調(diào)用的性能再描述符特別多時性能會非常差。它使用起來可能在形式上略有差異,但是本質(zhì)上都是差不多的,都可以使用上面的偽代碼邏輯進行理解。

      指令隊列

      Redis會將每個客戶端套接字都關(guān)聯(lián)一個指令隊列。客戶端的指令通過隊列來排隊進行順序處理,先到先服務(wù)。

      響應(yīng)隊列

      Redis同樣會為每個客戶端套接字關(guān)聯(lián)一個響應(yīng)隊列。Redis服務(wù)器通過響應(yīng)隊列來將指令的返回結(jié)果回復(fù)給客戶端。如果隊列為空,那么意味著連接暫時處于空閑狀態(tài),不需要去獲取寫事件,也就是可以將當(dāng)前的客戶端描述符write_fds里面移出來。等到隊列有數(shù)據(jù)了,再將描述符放進去。避免select系統(tǒng)調(diào)用立即返回寫事件,結(jié)果發(fā)現(xiàn)沒什么數(shù)據(jù)可以寫。出現(xiàn)這種情況的線程會飆高CPU。

      Redis主從復(fù)制

      概念

      主從復(fù)制,是指將一臺Redis服務(wù)器的數(shù)據(jù),復(fù)制到其他的Redis服務(wù)器。前者稱為主節(jié)點(Master/Leader),后者稱為從節(jié)點(Slave/Follower), 數(shù)據(jù)的復(fù)制是單向的!只能由主節(jié)點復(fù)制到從節(jié)點(主節(jié)點以寫為主、從節(jié)點以讀為主)。

      默認(rèn)情況下,每臺Redis服務(wù)器都是主節(jié)點,一個主節(jié)點可以有0個或者多個從節(jié)點,但每個從節(jié)點只能由一個主節(jié)點。

      作用

      1. 數(shù)據(jù)冗余:主從復(fù)制實現(xiàn)了數(shù)據(jù)的熱備份,是持久化之外的一種數(shù)據(jù)冗余的方式。
      2. 故障恢復(fù):當(dāng)主節(jié)點故障時,從節(jié)點可以暫時替代主節(jié)點提供服務(wù),是一種服務(wù)冗余的方式
      3. 負(fù)載均衡:在主從復(fù)制的基礎(chǔ)上,配合讀寫分離,由主節(jié)點進行寫操作,從節(jié)點進行讀操作,分擔(dān)服務(wù)器的負(fù)載;尤其是在多讀少寫的場景下,通過多個從節(jié)點分擔(dān)負(fù)載,提高并發(fā)量。
      4. 高可用基石:主從復(fù)制還是哨兵和集群能夠?qū)嵤┑幕A(chǔ)。

      為什么使用集群

      1. 單臺服務(wù)器難以負(fù)載大量的請求
      2. 單臺服務(wù)器故障率高,系統(tǒng)崩壞概率大
      3. 單臺服務(wù)器內(nèi)存容量有限。

      環(huán)境配置

      我們在講解配置文件的時候,注意到有一個replication模塊 (見Redis.conf中第8條)

      查看當(dāng)前庫的信息:info replication

      127.0.0.1:6379> info replication
      # Replication
      role:master # 角色
      connected_slaves:0 # 從機數(shù)量
      master_replid:3b54deef5b7b7b7f7dd8acefa23be48879b4fcff
      master_replid2:0000000000000000000000000000000000000000
      master_repl_offset:0
      second_repl_offset:-1
      repl_backlog_active:0
      repl_backlog_size:1048576
      repl_backlog_first_byte_offset:0
      repl_backlog_histlen:0
      

      既然需要啟動多個服務(wù),就需要多個配置文件。每個配置文件對應(yīng)修改以下信息:

      • 端口號
      • pid文件名
      • 日志文件名
      • rdb文件名

      啟動單機多服務(wù)集群:

      在這里插入圖片描述

      一主二從配置

      默認(rèn)情況下,每臺Redis服務(wù)器都是主節(jié)點;我們一般情況下只用配置從機就好了!

      認(rèn)老大!一主(79)二從(80,81)

      使用SLAVEOF host port就可以為從機配置主機了。

      在這里插入圖片描述

      然后主機上也能看到從機的狀態(tài):

      在這里插入圖片描述

      我們這里是使用命令搭建,是暫時的,真實開發(fā)中應(yīng)該在從機的配置文件中進行配置,這樣的話是永久的。

      在這里插入圖片描述

      使用規(guī)則

      1. 從機只能讀,不能寫,主機可讀可寫但是多用于寫。

         127.0.0.1:6381> set name sakura # 從機6381寫入失敗
        (error) READONLY You can't write against a read only replica.
        
        127.0.0.1:6380> set name sakura # 從機6380寫入失敗
        (error) READONLY You can't write against a read only replica.
        
        127.0.0.1:6379> set name sakura
        OK
        127.0.0.1:6379> get name
        "sakura"
        
      2. 當(dāng)主機斷電宕機后,默認(rèn)情況下從機的角色不會發(fā)生變化 ,集群中只是失去了寫操作,當(dāng)主機恢復(fù)以后,又會連接上從機恢復(fù)原狀。

      3. 當(dāng)從機斷電宕機后,若不是使用配置文件配置的從機,再次啟動后作為主機是無法獲取之前主機的數(shù)據(jù)的,若此時重新配置稱為從機,又可以獲取到主機的所有數(shù)據(jù)。這里就要提到一個同步原理。

      4. 第二條中提到,默認(rèn)情況下,主機故障后,不會出現(xiàn)新的主機,有兩種方式可以產(chǎn)生新的主機:

        • 從機手動執(zhí)行命令slaveof no one,這樣執(zhí)行以后從機會獨立出來成為一個主機
        • 使用哨兵模式(自動選舉)

      如果沒有老大了,這個時候能不能選擇出來一個老大呢?手動!

      如果主機斷開了連接,我們可以使用SLAVEOF no one讓自己變成主機!其他的節(jié)點就可以手動連接到最新的主節(jié)點(手動)!如果這個時候老大修復(fù)了,那么久重新連接!

      增量復(fù)制

      主從服務(wù)器在完成第一次同步后,就會基于長連接進行命令傳播。

      可是,網(wǎng)絡(luò)總是不按套路出牌的嘛,說延遲就延遲,說斷開就斷開。

      如果主從服務(wù)器間的網(wǎng)絡(luò)連接斷開了,那么就無法進行命令傳播了,這時從服務(wù)器的數(shù)據(jù)就沒辦法和主服務(wù)器保持一致了,客戶端就可能從「從服務(wù)器」讀到舊的數(shù)據(jù)。

      圖片

      那么問題來了,如果此時斷開的網(wǎng)絡(luò),又恢復(fù)正常了,要怎么繼續(xù)保證主從服務(wù)器的數(shù)據(jù)一致性呢?

      在 Redis 2.8 之前,如果主從服務(wù)器在命令同步時出現(xiàn)了網(wǎng)絡(luò)斷開又恢復(fù)的情況,從服務(wù)器就會和主服務(wù)器重新進行一次全量復(fù)制,很明顯這樣的開銷太大了,必須要改進一波。

      所以,從 Redis 2.8 開始,網(wǎng)絡(luò)斷開又恢復(fù)后,從主從服務(wù)器會采用增量復(fù)制的方式繼續(xù)同步,也就是只會把網(wǎng)絡(luò)斷開期間主服務(wù)器接收到的寫操作命令,同步給從服務(wù)器。

      網(wǎng)絡(luò)恢復(fù)后的增量復(fù)制過程如下圖:

      圖片

      主要有三個步驟:

      • 從服務(wù)器在恢復(fù)網(wǎng)絡(luò)后,會發(fā)送 psync 命令給主服務(wù)器,此時的 psync 命令里的 offset 參數(shù)不是 -1;
      • 主服務(wù)器收到該命令后,然后用 CONTINUE 響應(yīng)命令告訴從服務(wù)器接下來采用增量復(fù)制的方式同步數(shù)據(jù);
      • 然后主服務(wù)將主從服務(wù)器斷線期間,所執(zhí)行的寫命令發(fā)送給從服務(wù)器,然后從服務(wù)器執(zhí)行這些命令。

      那么關(guān)鍵的問題來了,主服務(wù)器怎么知道要將哪些增量數(shù)據(jù)發(fā)送給從服務(wù)器呢?

      答案藏在這兩個東西里:

      • repl_backlog_buffer,是一個「環(huán)形」緩沖區(qū),用于主從服務(wù)器斷連后,從中找到差異的數(shù)據(jù);
      • replication offset,標(biāo)記上面那個緩沖區(qū)的同步進度,主從服務(wù)器都有各自的偏移量,主服務(wù)器使用 master_repl_offset 來記錄自己「」到的位置,從服務(wù)器使用 slave_repl_offset 來記錄自己「」到的位置。

      那repl_backlog_buffer 緩沖區(qū)是什么時候?qū)懭氲哪兀?/p>

      在主服務(wù)器進行命令傳播時,不僅會將寫命令發(fā)送給從服務(wù)器,還會將寫命令寫入到 repl_backlog_buffer 緩沖區(qū)里,因此 這個緩沖區(qū)里會保存著最近傳播的寫命令。

      網(wǎng)絡(luò)斷開后,當(dāng)從服務(wù)器重新連上主服務(wù)器時,從服務(wù)器會通過 psync 命令將自己的復(fù)制偏移量 slave_repl_offset 發(fā)送給主服務(wù)器,主服務(wù)器根據(jù)自己的 master_repl_offset 和 slave_repl_offset 之間的差距,然后來決定對從服務(wù)器執(zhí)行哪種同步操作:

      • 如果判斷出從服務(wù)器要讀取的數(shù)據(jù)還在 repl_backlog_buffer 緩沖區(qū)里,那么主服務(wù)器將采用增量同步的方式;
      • 相反,如果判斷出從服務(wù)器要讀取的數(shù)據(jù)已經(jīng)不存在
        repl_backlog_buffer 緩沖區(qū)里,那么主服務(wù)器將采用全量同步的方式。

      當(dāng)主服務(wù)器在 repl_backlog_buffer 中找到主從服務(wù)器差異(增量)的數(shù)據(jù)后,就會將增量的數(shù)據(jù)寫入到 replication buffer 緩沖區(qū),這個緩沖區(qū)我們前面也提到過,它是緩存將要傳播給從服務(wù)器的命令。

      圖片

      repl_backlog_buffer 緩行緩沖區(qū)的默認(rèn)大小是 1M,并且由于它是一個環(huán)形緩沖區(qū),所以當(dāng)緩沖區(qū)寫滿后,主服務(wù)器繼續(xù)寫入的話,就會覆蓋之前的數(shù)據(jù)。

      因此,當(dāng)主服務(wù)器的寫入速度遠(yuǎn)超于從服務(wù)器的讀取速度,緩沖區(qū)的數(shù)據(jù)一下就會被覆蓋。

      那么在網(wǎng)絡(luò)恢復(fù)時,如果從服務(wù)器想讀的數(shù)據(jù)已經(jīng)被覆蓋了,主服務(wù)器就會采用全量同步,這個方式比增量同步的性能損耗要大很多。

      因此,為了避免在網(wǎng)絡(luò)恢復(fù)時,主服務(wù)器頻繁地使用全量同步的方式,我們應(yīng)該調(diào)整下 repl_backlog_buffer 緩沖區(qū)大小,盡可能的大一些,減少出現(xiàn)從服務(wù)器要讀取的數(shù)據(jù)被覆蓋的概率,從而使得主服務(wù)器采用增量同步的方式。

      那 repl_backlog_buffer 緩沖區(qū)具體要調(diào)整到多大呢?

      repl_backlog_buffer 最小的大小可以根據(jù)這面這個公式估算。

      圖片

      我來解釋下這個公式的意思:

      • second 為從服務(wù)器斷線后重新連接上主服務(wù)器所需的平均 時間(以秒計算)。
      • write_size_per_second 則是主服務(wù)器平均每秒產(chǎn)生的寫命令數(shù)據(jù)量大小。

      舉個例子,如果主服務(wù)器平均每秒產(chǎn)生 1 MB 的寫命令,而從服務(wù)器斷線之后平均要 5 秒才能重新連接主服務(wù)器。

      那么 repl_backlog_buffer 大小就不能低于 5 MB,否則新寫地命令就會覆蓋舊數(shù)據(jù)了。

      當(dāng)然,為了應(yīng)對一些突發(fā)的情況,可以將 repl_backlog_buffer 的大小設(shè)置為此基礎(chǔ)上的 2 倍,也就是 10 MB。

      關(guān)于 repl_backlog_buffer 大小修改的方法,只需要修改配置文件里下面這個參數(shù)項的值就可以。

      repl-backlog-size 1mb
      

      總結(jié)

      主從復(fù)制共有三種模式:全量復(fù)制、基于長連接的命令傳播、增量復(fù)制

      主從服務(wù)器第一次同步的時候,就是采用全量復(fù)制,此時主服務(wù)器會兩個耗時的地方,分別是生成 RDB 文件和傳輸 RDB 文件。為了避免過多的從服務(wù)器和主服務(wù)器進行全量復(fù)制,可以把一部分從服務(wù)器升級為「經(jīng)理角色」,讓它也有自己的從服務(wù)器,通過這樣可以分?jǐn)傊鞣?wù)器的壓力。

      第一次同步完成后,主從服務(wù)器都會維護著一個長連接,主服務(wù)器在接收到寫操作命令后,就會通過這個連接將寫命令傳播給從服務(wù)器,來保證主從服務(wù)器的數(shù)據(jù)一致性。

      如果遇到網(wǎng)絡(luò)斷開,增量復(fù)制就可以上場了,不過這個還跟 repl_backlog_size 這個大小有關(guān)系。

      如果它配置的過小,主從服務(wù)器網(wǎng)絡(luò)恢復(fù)時,可能發(fā)生「從服務(wù)器」想讀的數(shù)據(jù)已經(jīng)被覆蓋了,那么這時就會導(dǎo)致主服務(wù)器采用全量復(fù)制的方式。所以為了避免這種情況的頻繁發(fā)生,要調(diào)大這個參數(shù)的值,以降低主從服務(wù)器斷開后全量同步的概率。

      主節(jié)點不但負(fù)責(zé)數(shù)據(jù)讀寫,還負(fù)責(zé)把寫命令同步給從節(jié)點。寫命令的發(fā)送過程是異步完成的,也就是說主節(jié)點自身處理完寫命令后直接返回給客戶端,并不等待從節(jié)點復(fù)制完成,如下圖所示:

      哨兵模式

      作用

      主從切換技術(shù)的方法是:當(dāng)主服務(wù)器宕機后,需要手動把一臺從服務(wù)器切換為主服務(wù)器,這就需要人工干預(yù),費事費力,還會造成一段時間內(nèi)服務(wù)不可用。這不是一種推薦的方式,更多時候,我們優(yōu)先考慮哨兵模式。

      單機單個哨兵

      哨兵的作用:

      • 通過發(fā)送命令,讓Redis服務(wù)器返回監(jiān)控其運行狀態(tài),包括主服務(wù)器和從服務(wù)器。
      • 當(dāng)哨兵監(jiān)測到master宕機,會自動將slave切換成master,然后通過發(fā)布訂閱模式通知其他的從服務(wù)器,修改配置文件,讓它們切換主機。

      多哨兵模式

      Redis的主從復(fù)制模式可以將主節(jié)點的數(shù)據(jù)改變同步給從節(jié)點,這樣從節(jié)點就可以起到兩個作用:

      • 作為主節(jié)點的備份,一旦主節(jié)點出了故障不可達(dá)的情況,從節(jié)點可以作為后備“頂上來”,并且保證數(shù)據(jù)盡量不丟失(主從復(fù)制時最終一致性)。第二,從節(jié)點可以擴展主節(jié)點的讀能力,一旦主節(jié)點不能支撐住大并發(fā)量的讀操作
      • 第二,從節(jié)點可以擴展主節(jié)點的讀能力,一旦主節(jié)點不能支撐住大并發(fā)量的讀操作,從節(jié)點可以在以頂程度上幫助主節(jié)點分擔(dān)讀壓力

      但是主從復(fù)制也帶來了以下問題:

      • 一旦主節(jié)點出現(xiàn)故障,需要手動將一個從節(jié)點晉升為主節(jié)點,同時需要修改應(yīng)用方的主節(jié)點地址,還需要命令其他從節(jié)點去復(fù)制新的主節(jié)點,整個過程都需要人工干預(yù)
      • 主節(jié)點的寫能力收到單機的限制
      • 主節(jié)點的存儲能力收到單機的限制

      當(dāng)主節(jié)點出現(xiàn)故障時,Redis的哨兵模式能自動完成故障發(fā)現(xiàn)和故障轉(zhuǎn)移,并通知應(yīng)用方,從而實現(xiàn)真正的高可用。Redis Sentinel是一個分布式架構(gòu),其中包含了若干個Sentinel節(jié)點和Redis數(shù)據(jù)節(jié)點,每個Sentinel節(jié)點會對數(shù)據(jù)節(jié)點和其余Sentinel節(jié)點進行監(jiān)控,當(dāng)它發(fā)現(xiàn)節(jié)點不可達(dá)時,會對節(jié)點但做下線標(biāo)識。如果被標(biāo)識的是主節(jié)點,它還會和其他Sentinel節(jié)點進行“協(xié)商”,當(dāng)大多數(shù)Sentinel節(jié)點都認(rèn)為主節(jié)點不可達(dá)時,它們會選舉出一個Sentinel節(jié)點來完成自動故障轉(zhuǎn)移的工作,同時會將這個變化實時通知給Redis應(yīng)用方。整個過程完全時自動的,不需要人工來介入,所以這套方案很有效地解決了Redis高可用的問題。

      哨兵的核心配置

      sentinel monitor mymaster 127.0.0.1 6379 1
      
      • 數(shù)字1表示 :當(dāng)一個哨兵主觀認(rèn)為主機斷開,就可以客觀認(rèn)為主機故障,然后開始選舉新的主機。

      測試

      redis-sentinel xxx/sentinel.conf
      

      成功啟動哨兵模式

      在這里插入圖片描述

      此時哨兵監(jiān)視著我們的主機6379,當(dāng)我們斷開主機后:

      在這里插入圖片描述

      哨兵模式優(yōu)缺點

      優(yōu)點

      1. 哨兵集群,基于主從復(fù)制模式,所有主從復(fù)制的優(yōu)點,它都有
      2. 主從可以切換,故障可以轉(zhuǎn)移,系統(tǒng)的可用性更好
      3. 哨兵模式是主從模式的升級,手動到自動,更加健壯

      缺點

      1. Redis不好在線擴容,集群容量一旦達(dá)到上限,在線擴容就十分麻煩
      2. 實現(xiàn)哨兵模式的配置其實是很麻煩的,里面有很多配置項

      完整的哨兵模式配置文件 sentinel.conf

      # Example sentinel.conf
       
      # 哨兵sentinel實例運行的端口 默認(rèn)26379
      port 26379
       
      # 哨兵sentinel的工作目錄
      dir /tmp
       
      # 哨兵sentinel監(jiān)控的redis主節(jié)點的 ip port 
      # master-name  可以自己命名的主節(jié)點名字 只能由字母A-z、數(shù)字0-9 、這三個字符".-_"組成。
      # quorum 當(dāng)這些quorum個數(shù)sentinel哨兵認(rèn)為master主節(jié)點失聯(lián) 那么這時 客觀上認(rèn)為主節(jié)點失聯(lián)了
      # sentinel monitor <master-name> <ip> <redis-port> <quorum>
      sentinel monitor mymaster 127.0.0.1 6379 1
       
      # 當(dāng)在Redis實例中開啟了requirepass foobared 授權(quán)密碼 這樣所有連接Redis實例的客戶端都要提供密碼
      # 設(shè)置哨兵sentinel 連接主從的密碼 注意必須為主從設(shè)置一樣的驗證密碼
      # sentinel auth-pass <master-name> <password>
      sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
       
       
      # 指定多少毫秒之后 主節(jié)點沒有應(yīng)答哨兵sentinel 此時 哨兵主觀上認(rèn)為主節(jié)點下線 默認(rèn)30秒
      # sentinel down-after-milliseconds <master-name> <milliseconds>
      sentinel down-after-milliseconds mymaster 30000
       
      # 這個配置項指定了在發(fā)生failover主備切換時最多可以有多少個slave同時對新的master進行 同步,
      這個數(shù)字越小,完成failover所需的時間就越長,
      但是如果這個數(shù)字越大,就意味著越 多的slave因為replication而不可用。
      可以通過將這個值設(shè)為 1 來保證每次只有一個slave 處于不能處理命令請求的狀態(tài)。
      # sentinel parallel-syncs <master-name> <numslaves>
      sentinel parallel-syncs mymaster 1
       
       
       
      # 故障轉(zhuǎn)移的超時時間 failover-timeout 可以用在以下這些方面: 
      #1. 同一個sentinel對同一個master兩次failover之間的間隔時間。
      #2. 當(dāng)一個slave從一個錯誤的master那里同步數(shù)據(jù)開始計算時間。直到slave被糾正為向正確的master那里同步數(shù)據(jù)時。
      #3.當(dāng)想要取消一個正在進行的failover所需要的時間。  
      #4.當(dāng)進行failover時,配置所有slaves指向新的master所需的最大時間。不過,即使過了這個超時,slaves依然會被正確配置為指向master,但是就不按parallel-syncs所配置的規(guī)則來了
      # 默認(rèn)三分鐘
      # sentinel failover-timeout <master-name> <milliseconds>
      sentinel failover-timeout mymaster 180000
       
      # SCRIPTS EXECUTION
       
      #配置當(dāng)某一事件發(fā)生時所需要執(zhí)行的腳本,可以通過腳本來通知管理員,例如當(dāng)系統(tǒng)運行不正常時發(fā)郵件通知相關(guān)人員。
      #對于腳本的運行結(jié)果有以下規(guī)則:
      #若腳本執(zhí)行后返回1,那么該腳本稍后將會被再次執(zhí)行,重復(fù)次數(shù)目前默認(rèn)為10
      #若腳本執(zhí)行后返回2,或者比2更高的一個返回值,腳本將不會重復(fù)執(zhí)行。
      #如果腳本在執(zhí)行過程中由于收到系統(tǒng)中斷信號被終止了,則同返回值為1時的行為相同。
      #一個腳本的最大執(zhí)行時間為60s,如果超過這個時間,腳本將會被一個SIGKILL信號終止,之后重新執(zhí)行。
       
      #通知型腳本:當(dāng)sentinel有任何警告級別的事件發(fā)生時(比如說redis實例的主觀失效和客觀失效等等),將會去調(diào)用這個腳本,
      #這時這個腳本應(yīng)該通過郵件,SMS等方式去通知系統(tǒng)管理員關(guān)于系統(tǒng)不正常運行的信息。調(diào)用該腳本時,將傳給腳本兩個參數(shù),
      #一個是事件的類型,
      #一個是事件的描述。
      #如果sentinel.conf配置文件中配置了這個腳本路徑,那么必須保證這個腳本存在于這個路徑,并且是可執(zhí)行的,否則sentinel無法正常啟動成功。
      #通知腳本
      # sentinel notification-script <master-name> <script-path>
        sentinel notification-script mymaster /var/redis/notify.sh
       
      # 客戶端重新配置主節(jié)點參數(shù)腳本
      # 當(dāng)一個master由于failover而發(fā)生改變時,這個腳本將會被調(diào)用,通知相關(guān)的客戶端關(guān)于master地址已經(jīng)發(fā)生改變的信息。
      # 以下參數(shù)將會在調(diào)用腳本時傳給腳本:
      # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
      # 目前<state>總是“failover”,
      # <role>是“l(fā)eader”或者“observer”中的一個。 
      # 參數(shù) from-ip, from-port, to-ip, to-port是用來和舊的master和新的master(即舊的slave)通信的
      # 這個腳本應(yīng)該是通用的,能被多次調(diào)用,不是針對性的。
      # sentinel client-reconfig-script <master-name> <script-path>
      sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
      

      Redis當(dāng)中使用了Raft算法實現(xiàn)領(lǐng)導(dǎo)者選舉。

      緩存穿透與雪崩

      緩存穿透

      概念

      在默認(rèn)情況下,用戶請求數(shù)據(jù)時,會先在緩存(Redis)中查找,若沒找到即緩存未命中,再在數(shù)據(jù)庫中進行查找,數(shù)量少可能問題不大,可是一旦大量的請求數(shù)據(jù)(例如秒殺場景)緩存都沒有命中的話,就會全部轉(zhuǎn)移到數(shù)據(jù)庫上,造成數(shù)據(jù)庫極大的壓力,就有可能導(dǎo)致數(shù)據(jù)庫崩潰。網(wǎng)絡(luò)安全中也有人惡意使用這種手段進行攻擊被稱為洪水攻擊。

      解決方案

      布隆過濾器:對所有可能查詢的參數(shù)以Hash的形式存儲,以便快速確定是否存在這個值,在控制層先進行攔截校驗,校驗不通過直接打回,減輕了存儲系統(tǒng)的壓力。

      緩存空對象:一次請求若在緩存和數(shù)據(jù)庫中都沒找到,就在緩存中方一個空對象用于處理后續(xù)這個請求。

      在這里插入圖片描述

      這樣做有一個缺陷:存儲空對象也需要空間,大量的空對象會耗費一定的空間,存儲效率并不高。解決這個缺陷的方式就是設(shè)置較短過期時間

      即使對空值設(shè)置了過期時間,還是會存在緩存層和存儲層的數(shù)據(jù)會有一段時間窗口的不一致,這對于需要保持一致性的業(yè)務(wù)會有影響。

      緩存擊穿

      概念

      相較于緩存穿透,緩存擊穿的目的性更強,一個存在的key,在緩存過期的一刻,同時有大量的請求,這些請求都會擊穿到DB,造成瞬時DB請求量大、壓力驟增。這就是緩存被擊穿,只是針對其中某個key的緩存不可用而導(dǎo)致?lián)舸瞧渌膋ey依然可以使用緩存響應(yīng)。

      比如熱搜排行上,一個熱點新聞被同時大量訪問就可能導(dǎo)致緩存擊穿。

      解決方案

      1. 設(shè)置熱點數(shù)據(jù)永不過期

        這樣就不會出現(xiàn)熱點數(shù)據(jù)過期的情況,但是當(dāng)Redis內(nèi)存空間滿的時候也會清理部分?jǐn)?shù)據(jù),而且此種方案會占用空間,一旦熱點數(shù)據(jù)多了起來,就會占用部分空間。

      2. 加互斥鎖(分布式鎖)

        在訪問key之前,采用SETNX(set if not exists)來設(shè)置另一個短期key來鎖住當(dāng)前key的訪問,訪問結(jié)束再刪除該短期key。保證同時刻只有一個線程訪問。這樣對鎖的要求就十分高。

      緩存雪崩

      概念

      大量的key設(shè)置了相同的過期時間,導(dǎo)致在緩存在同一時刻全部失效,造成瞬時DB請求量大、壓力驟增,引起雪崩。

      在這里插入圖片描述

      解決方案

      • redis高可用

        這個思想的含義是,既然redis有可能掛掉,那我多增設(shè)幾臺redis,這樣一臺掛掉之后其他的還可以繼續(xù)工作,其實就是搭建的集群

      • 限流降級

        這個解決方案的思想是,在緩存失效后,通過加鎖或者隊列來控制讀數(shù)據(jù)庫寫緩存的線程數(shù)量。比如對某個key只允許一個線程查詢數(shù)據(jù)和寫緩存,其他線程等待。

      • 數(shù)據(jù)預(yù)熱

        數(shù)據(jù)加熱的含義就是在正式部署之前,我先把可能的數(shù)據(jù)先預(yù)先訪問一遍,這樣部分可能大量訪問的數(shù)據(jù)就會加載到緩存中。在即將發(fā)生大并發(fā)訪問前手動觸發(fā)加載緩存不同的key,設(shè)置不同的過期時間,讓緩存失效的時間點盡量均勻。

        為什么需要分布式鎖

        我們前面文章synchronized同步鎖的使用與原理中已經(jīng)講過了synchronized的作用及原理,知道了synchronized是一把對象鎖,當(dāng)多個線程并發(fā)操作某個對象時,可以通過synchronized來保證同一時刻只能有一個線程獲取到對象鎖進而處理synchronized關(guān)鍵字修飾的代碼塊或方法。既然已經(jīng)有了synchronized鎖,為什么這里又要引入分布式鎖呢?

        因為現(xiàn)在的系統(tǒng)基本都是分布式部署的,一個應(yīng)用會被部署到多臺服務(wù)器上,synchronized只能控制當(dāng)前服務(wù)器自身的線程安全,并不能跨服務(wù)器控制并發(fā)安全。比如下圖,同一時刻有4個線程新增同一件商品,其中兩個線程由服務(wù)器A處理,另外兩個線程由服務(wù)器B處理,那么最后的結(jié)果就是兩臺服務(wù)器各執(zhí)行了一次新增動作。這顯然不符合預(yù)期。

        img

        而本篇文章要介紹的分布式鎖就是為了解決這種問題的。

        什么是分布式鎖

        分布式鎖,就是控制分布式系統(tǒng)中不同進程共同訪問同一共享資源的一種鎖的實現(xiàn)。

        所謂當(dāng)局者迷,旁觀者清,先舉個生活中的例子,就拿高鐵舉例,每輛高鐵都有自己的運行路線,但這些路線可能會與其他高鐵的路線重疊,如果只讓高鐵內(nèi)部的司機操控路線,那就可能出現(xiàn)撞車事故,因為司機不知道其他高鐵的運行路線是什么。所以,中控室就發(fā)揮作用了,中控室會監(jiān)控每輛高鐵,高鐵在什么時間走什么樣的路線全部由中控室指揮。

        分布式鎖就是基于這種思想實現(xiàn)的,它需要在我們分布式應(yīng)用的外面使用一個第三方組件(可以是數(shù)據(jù)庫、Redis、Zookeeper等)進行全局鎖的監(jiān)控,由這個組件決定什么時候加鎖,什么時候釋放鎖。

        img

        Redis如何實現(xiàn)分布式鎖

        在聊Redis如何實現(xiàn)分布式鎖之前,我們要先聊一下redis的一個命令:setnx key value。我們知道,Redis設(shè)置一個key最常用的命令是:set key value,該命令不管key是否存在,都會將key的值設(shè)置成value,并返回成功:

        img

        setnx key value 也是設(shè)置key的值為value,不過,它會先判斷key是否已經(jīng)存在,如果key不存在,那么就設(shè)置key的值為value,并返回1;如果key已經(jīng)存在,則不更新key的值,直接返回0:

        img

        最簡單的版本:setnx key value

        基于setnx命令的特性,我們就可以實現(xiàn)一個最簡單的分布式鎖了。我們通過向Redis發(fā)送 setnx 命令,然后判斷Redis返回的結(jié)果是否為1,結(jié)果是1就表示setnx成功了,那本次就獲得鎖了,可以繼續(xù)執(zhí)行業(yè)務(wù)邏輯;如果結(jié)果是0,則表示setnx失敗了,那本次就沒有獲取到鎖,可以通過循環(huán)的方式一直嘗試獲取鎖,直至其他客戶端釋放了鎖(delete掉key)后,就可以正常執(zhí)行setnx命令獲取到鎖。流程如下:

        img

        這種方式雖然實現(xiàn)了分布式鎖的功能,但有一個很明顯的問題:沒有給key設(shè)置過期時間,萬一程序在發(fā)送delete命令釋放鎖之前宕機了,那么這個key就會永久的存儲在Redis中了,其他客戶端也永遠(yuǎn)獲取不到這把鎖了。

        ● 升級版本:設(shè)置key的過期時間

        針對上面的問題,我們可以基于setnx key value的基礎(chǔ)上,同時給key設(shè)置一個過期時間。Redis已經(jīng)提供了這樣的命令:set key value ex seconds nx。其中,ex seconds 表示給key設(shè)置過期時間,單位為秒,nx 表示該set命令具備setnx的特性。效果如下:

        img

        我們設(shè)置name的過期時間為60秒,60秒內(nèi)執(zhí)行該set命令時,會直接返回nil。60秒后,我們再執(zhí)行set命令,可以執(zhí)行成功,效果如下:

        img

        基于這個特性,升級后的分布式鎖流程如下:

        img

        這種方式雖然解決了一些問題,但卻引來了另外一個問題:存在鎖誤刪的情況,也就是把別人加的鎖釋放了。例如,client1獲得鎖之后開始執(zhí)行業(yè)務(wù)處理,但業(yè)務(wù)處理耗時較長,超過了鎖的過期時間,導(dǎo)致業(yè)務(wù)處理還沒結(jié)束時,鎖卻過期自動刪除了(相當(dāng)于屬于client1的鎖被釋放了),此時,client2就會獲取到這把鎖,然后執(zhí)行自己的業(yè)務(wù)處理,也就在此時,client1的業(yè)務(wù)處理結(jié)束了,然后向Redis發(fā)送了delete key的命令來釋放鎖,Redis接收到命令后,就直接將key刪掉了,但此時這個key是屬于client2的,所以,相當(dāng)于client1把client2的鎖給釋放掉了:

        img

        ● 二次升級版本:value使用唯一值,刪除鎖時判斷value是否當(dāng)前線程的

        要解決上面的問題,最省事的做法就是把鎖的過期時間設(shè)置長一點,要遠(yuǎn)大于業(yè)務(wù)處理時間,但這樣就會嚴(yán)重影響系統(tǒng)的性能,假如一臺服務(wù)器在釋放鎖之前宕機了,而鎖的超時時間設(shè)置了一個小時,那么在這一個小時內(nèi),其他線程訪問這個服務(wù)時就一直阻塞在那里。所以,一般不推薦使用這種方式。

        另一種解決方法就是在set key value ex seconds nx時,把value設(shè)置成一個唯一值,每個線程的value都不一樣,在刪除key之前,先通過get key命令得到value,然后判斷value是否是自己線程生成的,如果是,則刪除掉key釋放鎖,如果不是,則不刪除key。正常流程如下:

        img

        當(dāng)業(yè)務(wù)處理還沒結(jié)束的時候,key自動過期了,也可以正常釋放自己的鎖,不影響其他線程:

        img

        二次升級后的方案看起來似乎已經(jīng)沒什么問題了,但其實不然。仔細(xì)分析流程后我們發(fā)現(xiàn),判斷鎖是否屬于當(dāng)前線程和釋放鎖兩個步驟并不是原子操作。正常來說,如果線程1通過get操作從Redis中得到的value是123,那么就會執(zhí)行刪除鎖的操作,但假如在執(zhí)行刪除鎖的動作之前,系統(tǒng)卡頓了幾秒鐘,恰好在這幾秒鐘內(nèi),key自動過期了,線程2就順利獲取到鎖開始執(zhí)行自己的邏輯了,此時,線程1卡頓恢復(fù)了,開始繼續(xù)執(zhí)行刪除鎖的動作,那么此時刪除的還是線程2的鎖。

        img

        ● 終極版本:Lua腳本

        針對上述Redis原始命令無法滿足部分業(yè)務(wù)原子性操作的問題,Redis提供了Lua腳本的支持。Lua腳本是一種輕量小巧的腳本語言,它支持原子性操作,Redis會將整個Lua腳本作為一個整體執(zhí)行,中間不會被其他請求插入,因此Redis執(zhí)行Lua腳本是一個原子操作。

        在上面的流程中,我們把get key value、判斷value是否屬于當(dāng)前線程、刪除鎖這三步寫到Lua腳本中,使它們變成一個整體交個Redis執(zhí)行,改造后流程如下:

        img

        這樣改造之后,就解決了釋放鎖時取值、判斷值、刪除鎖等多個步驟無法保證原子操作的問題了。關(guān)于Lua腳本的語法可以自行學(xué)習(xí),并不復(fù)雜,很簡單,這里就不做過多講述。

        既然Lua腳本可以在釋放鎖時使用,那肯定也能在加鎖時使用,而且一般情況下,推薦使用Lua腳本,因為在使用上面set key value ex seconds nx命令加鎖時,并不能做到重入鎖的效果,也就是當(dāng)一個線程獲取到鎖后,在沒有釋放這把鎖之前,當(dāng)前線程自己也無法再獲得這把鎖,這顯然會影響系統(tǒng)的性能。使用Lua腳本就可以解決這個問題,我們可以在Lua腳本中先判斷鎖(key)是否存在,如果存在則再判斷持有這把鎖的線程是否是當(dāng)前線程,如果不是則加鎖失敗,否則當(dāng)前線程再次持有這把鎖,并把鎖的重入次數(shù)+1。在釋放鎖時,也是先判斷持有鎖的線程是否是當(dāng)前線程,如果是則將鎖的重入次數(shù)-1,直至重入次數(shù)減至0,即可刪除該鎖(key)。

        img

        實際項目開發(fā)中,其實基本不用自己寫上面這些分布式鎖的實現(xiàn)邏輯,而是使用一些很成熟的第三方工具,當(dāng)下比較流行的就是Redisson,它既提供了Redis的基本命令的封裝,也提供了Redis分布式鎖的封裝,使用非常簡單,只需直接調(diào)用相應(yīng)方法即可。但工具雖然好用,底層原理還是要理解的,這就是本篇文章的目的。


      參考文獻

      [1] Redis開發(fā)與運維

      [2] Raft維基百科

      posted @ 2022-07-11 21:10  hyz00  閱讀(77)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 韩国V欧美V亚洲V日本V| 亚洲欧美综合一区二区三区| 中文字幕在线视频不卡一区二区| 资源在线观看视频一区二区| 亚洲久悠悠色悠在线播放| 国产精品伊人久久综合网| 垣曲县| 日本一区二区三区激情视频| 99中文字幕国产精品| 国产第一区二区三区精品| 亚洲精品香蕉一区二区| 2021国产精品视频网站| 四虎精品视频永久免费| 亚洲大老师中文字幕久热| 自拍偷自拍亚洲精品播放| 99在线 | 亚洲| 免费视频爱爱太爽了| 在线播放国产精品三级网| 久久精品蜜芽亚洲国产AV| gogogo高清在线播放免费| caoporn成人免费公开| 色综合天天综合天天综| 东京热高清无码精品| 内射一区二区三区四区| 亚洲嫩模喷白浆在线观看| 国产剧情福利一区二区麻豆| 中文字幕一卡二卡三卡| 少妇高潮潮喷到猛进猛出小说 | 一亚洲一区二区中文字幕| 国产高清视频一区二区三区| 亚洲欧洲中文日韩久久av乱码| 国产专区一va亚洲v天堂| 国产综合精品一区二区在线| 蜜桃av亚洲精品一区二区| 伊人激情一区二区三区av| 最新亚洲人成网站在线影院| 小嫩批日出水无码视频免费| 亚洲线精品一区二区三区| 中文字幕乱码无码人妻系列蜜桃| 乱人伦人妻中文字幕无码久久网 | 欧美日韩一线|