1-5W 字的 Redis 學習手冊

本文用 1.5 W 字介紹 Redis 緩存數據的基礎知識,包括基礎知識、基礎操作、數據類型、通用指令、Jedis、緩存預熱、緩存雪崩、緩存擊穿、緩存穿透、事務、哨兵、主從複製等。

下載與安裝

作爲緩存數據庫,後期肯定是要部署在 Linux 系統上的,但鑑於是初學,我們先在 Windows 上操作一下,下載地址:

1https://github.com/microsoftarchive/redis/releases/download/win-3.2.100/Redis-x64-3.2.100.zip
2

下載完成後解壓即可。執行解壓後文件夾中的 redis-server.exe 即可啓動 redis:

出現下圖則啓動成功:

端口號默認爲 6379。

剛纔我們啓動的是 redis 的服務端,那麼該如何操作 redis 呢?我們還需要啓動它的客戶端,執行文件夾中的 redis-cli.exe:

基本操作

既然是數據庫,那就少不了與數據打交道,我們先來看看如何通過 redis 進行數據的保存和讀取。

首先是保存數據:

1set key value
2
3

redis 中的數據都是以鍵值對進行存儲的,所以在保存數據的時候只需要指定數據的鍵名和鍵值即可,讀取的時候通過鍵名獲取:

1get key
2
3

若是指定的 key 不存在,則會返回空 (nil)。

redis 也提供了幫助命令,用於查詢一些指令的用法:

1help 指令名
2
3

比如想知道 set 指令的作用:

1help set
2
3

得到結果:

1  SET key value [EX seconds] [PX milliseconds] [NX|XX]
2  summary: Set the string value of a key
3  since: 1.0.0
4  group: string
5
6

數據類型

redis 有五種常用的數據類型:

下面分別介紹這五種類型。

string

string 是 redis 中最簡單的數據類型,也是最常用的數據類型,比如:

1set name zhangsan
2
3

我們還能一次性保存多個數據:

1mset name lisi age 30 gender 1
2
3

這樣我們就同時設置了三個鍵值,當然也可以一次性取出多個數據:

1mget name age gender
2
3

還可以獲取字符串長度,比如:

1strlen name
2
3

需要注意的是當 set 保存的數據,其鍵名已經存在的情況下,新的值會覆蓋舊的值,redis 提供了一種追加方式,以適應更靈活的場景:

1append name abc
2
3

此時會在 key 爲 name 字符串後拼接上 abc ,若 name 不存在,則創建新的數據。

若是數據的鍵值爲數字,則 redis 會提供一些數字特有的功能,但它本質上還是字符串:

1set num 1
2
3

比如自增操作:

1incr num
2
3

結果如下:

 1127.0.0.1:6379> set num 1
 2OK
 3127.0.0.1:6379> incr num
 4(integer) 2
 5127.0.0.1:6379> incr num
 6(integer) 3
 7127.0.0.1:6379> incr num
 8(integer) 4
 9
10

自減操作:

1decr key
2
3

結果如下:

1127.0.0.1:6379> decr num
2(integer) 3
3127.0.0.1:6379> decr num
4(integer) 2
5127.0.0.1:6379> decr num
6(integer) 1
7
8

默認自增自減操作爲加 1 或減 1,若是想設置步長,則:

1incrby num 2
2
3

結果如下:

1127.0.0.1:6379> incrby num 2
2(integer) 3
3127.0.0.1:6379> incrby num 2
4(integer) 5
5127.0.0.1:6379> incrby num 2
6(integer) 7
7
8

自減也是如此:

1decrby num 2
2
3

步長還支持設置爲小數,但 incrby 是不支持的,我們需要使用 incrbyfloat:

1incrbyfloat num 0.5
2
3

作爲緩存數據庫,redis 的數據都是存儲在內存中的,內存十分寶貴,所以我們不應該讓一些垃圾數據還殘留在 redis 中浪費資源,爲此,我們需要爲數據設置它的時效,即:時間一到,就刪除對應的數據信息。

redis 有兩種方式設置數據的時效:

1setex key seconds value
2psetex key milliseconds value
3
4

這兩種方式的區別在於時間單位不同,第一種設置的是秒,第二種設置的是毫秒,比如:

1setex number 10 5
2
3

則 5 秒後,number 數據將會被刪除。

最後是對數據的刪除操作:

1del name
2
3

指定需要刪除的鍵名,則執行該操作後,對應的數據會從 redis 中刪除。

hash

對於一個商城的秒殺業務,其訪問量無疑是巨大的,爲此,我們應該將一些固定不變的信息提前抽取到 redis 中,而不是在用戶進行秒殺活動的時候再去數據庫查詢。比如商品的名稱、商品的描述、商品的秒殺價格等等,這些都是在活動即將開始之前就確定好並且不會改變的,我們將其存放到 redis 中:

1 set product:seckill:id:001:name 榨汁機
2 set product:seckill:id:001:name 美的榨汁機
3 set product:seckill:id:001:price 120 
4
5

這是一種較爲規範的存儲方式,以表名加業務場景加屬性名作爲數據的鍵,我們也可以將所有數據封裝成一段 json 數據進行存儲:

1set product:seckill:id:001 {id:001,name:榨汁機,desc:美的榨汁機,price:120}
2
3

然而對於 json 字符串方式的存儲,其弊端是非常明顯的,因爲若是需要修改商品中的數據,則修改操作就會變得非常麻煩,爲此,我們可以使用 hash 類型來存儲。

hash 類型不同於 string,string 的特點是一個鍵對應一個字符串,而 hash 一個鍵會對應一組數據,而這組數組也是以鍵值對的形式進行存儲的,對於 hash 的保存數據和讀取數據,只需要在 set 指令的基礎上加一個 h 即可:

1hset user name zhangsan
2hset user age 20
3
4

其中 user 爲整個 hash 類型數據的鍵名,而 name zhangsan 和 age 20 均爲 hash 中的數據,這兩個數據也是鍵值對的形式。需要注意的是,hset 在保存數據時需要一個屬性一個屬性地進行保存,而不能這樣做:

1hset user name zhangsan age 20
2
3

若是想同時設置多個屬性,需要使用 hmset 指令:

1hmset user name zhangsan age 20
2
3

通過 hgetall 指令可以將屬性一次性讀取出來:

1hgetall user
2
3

結果如下:

1127.0.0.1:6379> hgetall user
21) "name"
32) "zhangsan"
43) "age"
54) "20"
6
7

當然也可以通過 hmget 指令獲取所有的屬性:

1hmget user name age
2
3

這種方式相比 hgetall 更加地靈活,也可以讀取指定的一個屬性:

1hget user name
2hget user age
3
4

刪除屬性也是如此:

1hdel user name
2hdel user age
3
4

通過 hlen 可以獲取 hash 中屬性的數量:

1hlen user
2
3

通過 hexists 指令可以判斷 hash 中是否存在指定的屬性:

1hexists user name
2
3

存在返回 1,不存在返回 0。

基於 hash 的特殊結構,redis 也提供了 hash 的特有功能,比如獲取 hash 中的所有屬性名和屬性值:

1hkeys user
2hvals user
3
4

設置 hash 中的指定屬性進行自增:

1hincrby user age 1
2hincrbyfloat user age 0.5
3
4

hash 類型是不支持 hdecrby 和 hdecrbyfloat 的,所以若是想實現自減,可以這麼做:

1hincrby user age -1
2
3

list

list 類型適用於存儲多個數據,而且能夠保持這些數據間的某些順序。

list 類型保持數據方式如下:

1lpush key value1 [value2] ......
2rpush key value1 [value2] ......
3
4

其中 lpush 表示從左側添加, rpush 表示從右側添加,比如:

1lpush nums 1 2 3 4 5
2
3

使用 lrange 指令可以讀取數據:

1lrange nums 0 4
2
3

其中 0 4 表示需要讀取的索引範圍,結果如下:

1127.0.0.1:6379> lrange nums 0 4
21) "5"
32) "4"
43) "3"
54) "2"
65) "1"
7
8

這說明 lpush 都是從左側加入,也就是從序列的前面加入數據的,那麼相應的:

1rpush nums 1 2 3 4 5
2
3

rpush 就是從右側加入,即:從序列的後面加入數據的,其讀取順序應爲 1 2 3 4 5 ,結果如下:

1127.0.0.1:6379> lrange nums 0 4
21) "1"
32) "2"
43) "3"
54) "4"
65) "5"
7
8

list 類型讀取數據的方式如下:

1lpop key
2rpop key
3
4

分別對應從左邊獲取和從右邊獲取,若是保存了這樣一個數據:

1rpush nums 1 2 3 4 5
2
3

lpop 的效果如下:

1127.0.0.1:6379> lpop nums
2"1"
3
4

左邊的第一個數據 1 就成功被獲取了,注意了,通過 lpop 和 rpop 指令獲取數據之後,該數據會從 list 中移除,我們再來試試 rpop 從右邊獲取,按道理獲取到的應該就是 5 :

1127.0.0.1:6379> rpop nums
2"5"
3
4

最後看看 list 中的數據發生了什麼變化:

1127.0.0.1:6379> lrange nums 0 4
21) "2"
32) "3"
43) "4"
5
6

有時候我們需要移除中間的某個數據,那麼 lpop 和 rpop 肯定是無能爲力了,爲此,我們需要使用 lrem 指令:

1lrem nums 1 2
2
3

它表示從 nums 中移除一個數據 2 ,因爲 list 是允許數據重複出現的,所以需要指定移除的數據數量。

我們還能通過 lindex 指令獲取指定索引上的數據:

1lindex nums 0
2
3

通過llen 指令獲取 list 的長度,即:數據的個數:

1llen nums
2
3

list 類型也有其特有的功能,它可以在規定時間內獲取並移除數據,實現如下:

1blpop key1 [key2] timeout
2brpop key1 [key2] timeout
3
4

比如:

1blpop nums nums2 num3 30
2
3

它表示從 nums、nums2、nums3 的左邊獲取數據,在 30 秒之內,若是 30 秒過後獲取不到數據,則輸出 nil;若是在某個時間內獲取到了數據,則直接結束,輸出內容。

set

set 類型同樣提供大量數據的存儲,但 set 的優勢在於查詢速度更快,list 類型底層實質上是一個雙向鏈表,我們都知道,鏈表的查詢效率是比較低的,所以若是出於性能的考慮,set 絕對是更勝一籌。

set 類型保持數據的方式如下:

1sadd key member1 [member2]
2
3

比如:

1sadd nums 1 2 3
2
3

使用 smembers 指令可以獲取到 set 中的所有數據:

1smember nums
2
3

刪除數據:

1srem nums 1 2
2
3

表示刪除 nums 中的數據 1 和 2 。

獲取 set 的長度:

1scard nums
2
3

判斷 set 中是否包含指定的數據:

1sismember nums 1
2
3

set 類型比較特別的地方在於它能夠進行隨機操作,比如隨機獲取 set 中指定數量的數據:

1srandmember nums 5
2
3

它表示從 nums 中隨機獲取 5 個數據,這 5 個數據獲取後並不會消失,仍然存在 nums 中,相較於接下來的這種方式:

1spop nums 5
2
3

它表示從 nums 中隨機獲取 5 個數據,但這些數據都會從 nums 中移除掉。

sorted_set

sorted_set 類型支持存儲大量的數據,同時還提供這些數據按某種方式進行排序的功能。

其保存數據的方式如下:

1zadd key score1 member1 [score2 member2]
2
3

比如:

1zadd scores 95 chinese 98 math 85 english
2
3

通過 zrange 指令獲取全部數據:

1zrange scores 0 -1
2
3

攜帶 withscores 還能夠將數據的分數輸出:

1zrange scores 0 -1 withscores
2
3

結果如下:

1127.0.0.1:6379> zrange scores 0 -1 withscores
21) "english"
32) "85"
43) "chinese"
54) "95"
65) "math"
76) "98"
8
9

通過 zrevrange 指令可以以逆序的方式獲取數據:

1zrevrange scores 0 -1 withscores
2
3

刪除數據:

1zrem scores chinese math
2
3

這裏需要注意一點,在保存數據的時候每個數據前都跟着一個數字,比如:95 chinese ,這個 95 其實是一個分數,它並不具有特殊的含義,通過該分數,使得 sorted_set 類型具有一些特殊的功能。

首先準備一些數據:

1zadd scores 100 zhangsan 90 lisi 95 wangwu
2
3

比如查找分數在 95 以下的數據:

1zrangebyscore scores 0 95
2
3

其中 scores 是 key, 0 95 是查找範圍,結果如下:

1127.0.0.1:6379> zrangebyscore scores 0 95
21) "lisi"
32) "wangwu"
4
5

它還能夠通過 limit 來限定查詢的結果,比如查詢分數在 100 以下的前兩個數據:

1zrangebyscore scores 0 100 limit 0 2
2
3

按條件刪除數據:

1zremrangebyscore scores 0 95
2
3

表示刪除 95 分以下的數據,它也支持按照索引刪除數據,比如:

1zremrangebyrank scores 0 2
2
3

它表示刪除索引 0 到索引 2 的數據。

通用指令

對於各個數據類型,redis 提供了各自的指令來操作,而對於所有的數據類型,它們都有着一些通用的指令用來控制,一起來看看吧。

首先是刪除指定的 key:

1del key
2
3

判斷指定的 key 是否存在:

1exists key
2
3

獲取 key 的類型:

1type key
2
3

key 的時效性操作在前面已經簡單接觸了一下,現在來仔細瞭解瞭解,首先是爲 key 設置有效時間:

1expire key seconds          # 設置有效時間,單位:秒
2pexpire key milliseconds       # 設置有效時間,單位:毫秒
3expireat key timestamp        # 設置時間戳,單位:秒
4pexpireat key milliseconds-timestamp # 設置時間戳,單位:毫秒
5
6

獲取 key 的剩餘有效時間:

1ttl key   # 返回有效時間,單位:秒
2pttl key  # 返回有效時間,單位:毫秒
3
4

需要注意的是,這兩個指令都能夠返回 key 的剩餘有效時間,所以若是 key 不存在,則返回 - 2;若是 key 存在但未設置有效時間,則返回 - 1;否則返回 key 的剩餘有效時間。

將 key 從時效性切換爲永久性:

1persist key
2
3

redis 提供了一些常用的查詢指令幫助我們瞭解 key 的信息,比如查詢指定條件的 key:

1keys pattern
2
3

其中 pattern 是匹配模式,若是指定爲 * 則查詢所有 key:

1keys *
2
3

它提供了三種匹配模式:

爲 key 修改名字:

1rename key newkey
2renamenx key newkey
3
4

需要注意 rename 指令將當前 key 修改爲已經存在的 key 時,該 key 的值會被覆蓋,而 renamenx 會報錯,所以 renamenx 能夠避免覆蓋的情況發生。

對所有 key 排序:

1sort
2
3

隨着數據量的逐漸增大,key 極易出現重複、出錯的情況,大量的數據混雜在一起也很難分別處理,爲此,redis 提供了數據庫的概念。redis 爲每個服務提供了 16 個數據庫,編號爲 0~15,每個數據庫之間的數據是相互獨立的。

切換數據庫:

1select index
2
3

默認使用的是 0 號數據庫,若是想切換至 3 號,則:

1select 3
2
3

數據移動:

1move key db
2
3

將當前數據庫指定的 key 移動到指定的數據庫,比如將 name 移動到 3 號數據庫 (移動之後,原數據庫的 key 就不存在了):

1move name 3
2
3

數據清除:

1dbsize  # 返回當前數據庫的key數量
2flushdb  # 清空當前數據庫的key
3flushall # 清空所有數據庫的key
4
5

Jedis

在項目開發中,我們需要使用 Java 來幫助我們操作 redis,所以來了解一下 Java 操作 redis 的工具——Jedis。

引入依賴:

1<dependency>
2  <groupId>redis.clients</groupId>
3  <artifactId>jedis</artifactId>
4  <version>3.3.0</version>
5</dependency>
6
7

首先來測試一下能夠連接成功:

1@Test
2public void testJedis(){
3    // 連接redis
4    Jedis jedis = new Jedis("127.0.0.1", 6379);
5    System.out.println(jedis.ping());
6}
7
8

輸出結果:

1PONG
2
3

輸出 PONG 則說明連接成功了,那麼 jedis 該如何操作 redis 呢?

操作方法與在 redis 中的操作一模一樣,所以我們可以直接調用同名的方法即可,比如保存一個 string 類型的數據:

1@Test
2public void testJedis(){
3    Jedis jedis = new Jedis("127.0.0.1", 6379);
4    jedis.set("name","zhangsan");
5    String name = jedis.get("name");
6    System.out.println(name);
7}
8
9

若是保存 list 數據,則調用 lpush 或 rpush 方法:

 1@Test
 2public void testJedis(){
 3    Jedis jedis = new Jedis("127.0.0.1", 6379);
 4    jedis.lpush("nums","1","2","3","4","5");
 5    // 獲取所有數據
 6    List<String> nums = jedis.lrange("nums",0,-1);
 7    for (String num : nums) {
 8        System.out.println(num);
 9    }
10}
11
12

若是保存 hash 數據,則調用 hset 方法:

 1@Test
 2public void testJedis() {
 3    Jedis jedis = new Jedis("127.0.0.1", 6379);
 4    jedis.hset("user", "name", "zhangsan");
 5    jedis.hset("user", "age", "20");
 6    String name = jedis.hget("user", "name");
 7    String age = jedis.hget("user", "age");
 8    System.out.println(name + ":" + age);
 9}
10
11

因爲需要頻繁操作 jedis,所以我們可以爲其編寫一個簡單的工具類:

 1package com.wwj.util;
 2
 3import redis.clients.jedis.Jedis;
 4import redis.clients.jedis.JedisPool;
 5import redis.clients.jedis.JedisPoolConfig;
 6import java.util.ResourceBundle;
 7
 8public class JedisUtil {
 9
10    private static JedisPool jedisPool = null;
11    private static final String host;
12    private static final Integer port;
13    private static final Integer maxTotal;
14    private static final Integer maxIdle;
15
16    /**
17     * 加載連接池,只執行一次
18     */
19    static {
20        // 加載配置文件
21        ResourceBundle bundle = ResourceBundle.getBundle("redis");
22        host = bundle.getString("redis.host");
23        port = Integer.parseInt(bundle.getString("redis.port"));
24        maxTotal = Integer.parseInt(bundle.getString("redis.maxTotal"));
25        maxIdle = Integer.parseInt(bundle.getString("redis.maxIdle"));
26        // 配置連接池信息
27        JedisPoolConfig config = new JedisPoolConfig();
28        // 設置最大連接數
29        config.setMaxTotal(maxTotal);
30        // 設置活動連接數
31        config.setMaxIdle(maxIdle);
32        jedisPool = new JedisPool(config, host, port);
33    }
34
35    /**
36     * 獲取Jedis連接
37     *
38     * @return
39     */
40    public static Jedis getJedis() {
41        return jedisPool.getResource();
42    }
43}
44
45

配置文件:

1redis.host=127.0.0.1
2redis.port=6379
3redis.maxTotal=30
4redis.maxIdle=10
5
6

Linux 下的 Redis

現在我們已經對 redis 有了一個大致的認識,下面我們就在 linux 環境下來看看 redis 的一些更加高級的操作,首先下載 redis 的壓縮包:

1wget http://download.redis.io/releases/redis-4.0.0.tar.gz
2
3

解壓一下:

1tar -zxvf redis-4.0.0.tar.gz
2
3

進入解壓後的目錄,然後進行編譯安裝:

1cd redis-4.0.0
2make install
3
4

首先將 redis 中的配置文件複製一份:

1cp redis.conf redis-6379.conf
2
3

並修改配置文件:

1daemonize yes        # 以守護進程的方式啓動
2logfile "6379.log"     # 指定日誌文件名
3dir /opt/redis-4.0.0/logs  # 指定日誌的存放位置
4
5

此時以該配置文件啓動 redis:

1redis-server redis-6379.conf
2
3

我們可以查看日誌來判斷 redis 是否成功啓動了:

1cd /opt/redis-4.0.0/logs
2cat 6379.log
3
4

日誌內容:

18454:C 15 Mar 02:57:25.275 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
28454:C 15 Mar 02:57:25.276 # Redis version=4.0.0, bits=64, commit=00000000, modified=0, pid=8454, just started
38454:C 15 Mar 02:57:25.276 # Configuration loaded
48455:M 15 Mar 02:57:25.279 * Increased maximum number of open files to 10032 (it was originally set to 1024).
58455:M 15 Mar 02:57:25.281 # Creating Server TCP listening socket 127.0.0.1:6379: bind: Address already in use
6
7

通過日誌我們發現,6379 端口好像被佔用了,我們只需查看哪個應用佔用了 6379 端口,並將其 kill 掉然後重新啓動 redis 即可。

通過配置文件啓動的方式可以非常方便地實現多個 redis 服務的啓動,現在複製一個剛纔的配置文件:

1cp redis-6379.conf redis-6380.conf
2
3

然後修改 redis-6380.conf:

1port 6380
2
3

把端口修改爲 6380,然後啓動該 redis 服務:

1redis-server redis-6380.conf
2logfile "6380.log"
3
4

同樣檢查日誌即可判斷 redis 服務是否成功啓動。

服務啓動完成後,若是想連接 redis 進行操作,則:

1redis-cli -p 6379
2
3

若是不指定端口號,則默認使用 6379 端口進行連接。

持久化

redis 的數據都是存放在內存中的,所以當出現斷電、系統崩潰等異常時,這些數據就丟失了,爲此,我們需要利用永久性的存儲介質將這些數據保存起來,這樣就可以在出現問題的時候再將數據從存儲介質上恢復回來。redis 提供了兩種數據持久化的方式,RDB 和 AOF,其中 RDB 是以快照的方式進行保存,而 AOF 則以日誌的方式進行保存,下面分別介紹。

RDB

在前面我們已經設置了 dir 的配置值爲 /opt/redis-4.0.0/logs ,所以 redis 生成的 rdb 文件也會在該目錄出現,我們使用客戶端連接上一個 redis 服務:

1redis-cli -p 6380
2
3

然後設置幾個數據:

1set name zhangsan
2set age 20
3
4

執行 save 指令即可進行持久化,此時在 logs 目錄下即可看到持久化文件:

1[root@centos-7 logs]# ls
26379.log  6380.log  dump.rdb
3
4

該文件的相關信息可以在配置文件中進行設置,比如:

1dbfilename dump.rdb  # 持久化文件名,默認爲dump.rdb
2dir ./        # 文件存放位置,默認爲當前目錄
3rdbcompression yes  # 設置存儲至本地時是否壓縮數據,默認爲yes,採用LZF壓縮
4rdbchecksum yes    # 設置是否進行RDB文件格式校驗,默認爲yes
5
6

修改一下 6380 端口的配置文件:

1dbfilename dump-6380.rdb
2
3

通過該文件即可恢復數據。

需要注意,save 指令的執行會阻塞當前 redis 服務,直到 RDB 過程完成爲止,有可能會造成長時間的阻塞,線上環境不建議使用 save 指令。

redis 提供了第二種 RDB 指令, bgsave ,該指令的執行將會在後臺進行,當我們執行該指令後,redis 會調用 fork 函數生成一個子進程,然後在子進程中創建 RDB 文件。

bgsave 指令也有一個可配置項:

1stop-writes-on-bgsave-error yes 
2
3

默認爲 yes,表示後臺存儲過程中如果發生了錯誤,是否停止保存操作。

我們已經知道通過 bgsave 指令能夠備份數據,然而手動執行備份指令並不好,爲此,可以讓 redis 自動爲我們進行備份,還可以設置備份的頻率和觸發條件。需要在配置文件中進行配置:

1save 10 2
2
3

它表示在 10 秒的時間內若是有 2 個 key 及以上的數據發生了變化,無論是添加、修改還是刪除,只要發生了變化,那麼就會觸發自動備份。

AOF

RDB 的方式有着一定的缺陷,因爲每次是基於所有數據的備份,所以文件存儲大、導致 IO 性能下降,而且是基於子進程實現的,會額外消耗內存資源。而 AOF 以獨立日誌的方式記錄每次的寫指令,重啓時再重新執行 AOF 文件中記錄的指令,以達到恢復數據的目的,與 RDB 的區別在於:RDB 記錄的是數據;而 AOF 記錄的是操作數據的指令。AOF 方式共有三種寫策略,分別是:

redis 默認是關閉了 AOF 功能的,所以我們需要在配置文件中開啓它:

1appendonly yes   # 開啓AOF
2appendfsync always # 指定策略爲每次都同步
3
4

AOF 的寫策略默認爲每秒同步一次,我們可以將其修改爲每次都同步,配置完畢後啓動 redis 服務,查看 logs 目錄:

1[root@centos-7 logs]# ll
2總用量 28
3-rw-r--r--. 1 root root  4650 3月  15 03:35 6379.log
4-rw-r--r--. 1 root root 14704 3月  15 19:41 6380.log
5-rw-r--r--. 1 root root     0 3月  15 19:41 appendonly.aof
6-rw-r--r--. 1 root root   184 3月  15 19:31 dump-6380.rdb
7
8

會發現出現了 appendonly.aof 文件,這就是 AOF 的持久化文件,接下來我們連接客戶端,並寫入一個數據後,再觀察該文件:

1[root@centos-7 logs]# ll
2總用量 32
3-rw-r--r--. 1 root root  4650 3月  15 03:35 6379.log
4-rw-r--r--. 1 root root 14704 3月  15 19:41 6380.log
5-rw-r--r--. 1 root root    60 3月  15 19:42 appendonly.aof
6-rw-r--r--. 1 root root   184 3月  15 19:31 dump-6380.rdb
7
8

會發現文件大小由原來的 0 變爲了 60,這說明我們的配置生效了,該文件只會記錄寫指令,即添加、修改、刪除數據的指令,諸如 get 指令是不會被它記錄的。

AOF 的持久化文件名也可以通過配置來修改:

1appendfilename appendonly-6380.aof
2
3

隨着指令不斷寫入 AOF,文件也會越來越大,但有時候會出現這樣一種情況:

1set name zs
2set name ls
3set name ww
4
5

這裏雖然執行了三條 set 指令,但事實上,它的效果等價於一條指令:set name ww ,因爲後面的數據覆蓋了前面的數據,那麼如果 AOF 記錄了這三條指令,很顯然就造成了資源的浪費。爲此,redis 提供了 AOF 重寫機制,它能夠將對同一個數據的若干條指令轉換爲最終結果數據對應的指令記錄,這樣便極大地壓縮了 AOF 文件的體積。

只需執行指令即可實現 AOF 重寫:

1bgrewriteaof
2
3

事務

事務也是一個老生常談的話題了,這裏我們就不說事務的概念了,直接來看看 redis 中的事務。

比如這樣的一個現象:

1127.0.0.1:6380> set name zhangsan
2OK
3127.0.0.1:6380> get name
4"lisi"
5
6

該客戶端設置了一個數據,key 爲 name,值爲 zhangsan,而在獲取的時候卻得到了 lisi,這是爲什麼呢?原來,在該客戶端獲取數據之前,又有別的客戶端搶在了它前面修改了數據,由此導致了這樣的問題發生,爲此,我們需要使用事務控制來避免這一問題:

 1127.0.0.1:6380> multi
 2OK
 3127.0.0.1:6380> set name zhangsan
 4QUEUED
 5127.0.0.1:6380> get name
 6QUEUED
 7127.0.0.1:6380> exec
 81) OK
 92) "zhangsan"
10
11

首先通過 multi 指令開啓事務,然後添加數據,在獲取數據之前同樣另一個客戶端修改了數據,最後執行 exec 指令提交事務,redis 便會輸出在這次事務中所有指令的結果,可以看到數據並沒有被別的客戶端修改。

當在事務過程中執行了錯誤的指令時,我們可以使用 discard 指令來取消此時事務,取消之後事務中的所有指令操作都將失效。而且在事務過程中,執行了語法錯誤的指令,比如 set 打成了 sat ,redis 會自動幫助我們取消事務,又比如對 string 類型的數據執行 incr操作,redis 會自動執行事務中正確的操作指令,並取消執行那些錯誤的指令。

假設有這樣一個需求,天貓雙 11 熱賣過程中,需要對已經售罄的商品進行補貨,有 4 個業務員都擁有補貨的權限,補貨這一過程也涉及到多個操作,那麼如何保證這些操作不會重複進行呢?

使用 watch指令可以監控某個數據,當數據發生變化時取消所有操作,比如:

1set name zs
2watch name
3multi
4set age 20
5exec
6
7

在這組指令中,首先添加了一個數據,然後使用 watch 監視了 name,隨之開啓事務,並在事務中添加了另一個數據,但是在提交事務之前,別的客戶端修改了 name 數據,此時再提交事務便會輸出 nil:

 1127.0.0.1:6380> set name zs
 2OK
 3127.0.0.1:6380> watch name
 4OK
 5127.0.0.1:6380> multi
 6OK
 7127.0.0.1:6380> get name
 8QUEUED
 9127.0.0.1:6380> set age 20
10QUEUED
11127.0.0.1:6380> exec
12(nil)
13127.0.0.1:6380> keys *
141) "name"
15
16

而且添加的數據 age 也不存在了,需要注意的是 watch 操作必須在事務開啓之前執行,若在事務中執行則會報錯。通過 unwatch 指令可以取消所有數據的監控。

分佈式鎖

繼續看一個場景,雙 11 的網站流量是非常巨大的,一些商家也會在雙 11 推出一些秒殺活動,當然了,秒殺的商品是有數量限制的,比如說,某個手機廠商設置了 100 個蘋果手機用於秒殺活動,而事實上,參與此次秒殺的人是非常多的,它將遠遠大於手機數量,那麼怎麼保證商品不會出現超賣的現象,即:最後一件商品不會被多個人同時購買,此時用剛纔的 watch 指令已經無法解決這個問題了,我們需要使用——分佈式鎖。

使用 setnx指令設置一個分佈式鎖:

1setnx lock-key value
2
3

比如這樣的一組指令:

1127.0.0.1:6380> setnx lock-num true
2(integer) 1
3127.0.0.1:6380> incrby num -1
4(integer) 9
5127.0.0.1:6380> del lock-num
6(integer) 1
7
8

這是將商品庫存減 1 的正常流程,首先設置一個 lock-num 鎖,值是無所謂的,然後將庫存減 1,完成操作後刪除鎖,假如該客戶端在減庫存操作完成之前別的客戶端也進行減庫存操作,則會出現:

1127.0.0.1:6380> setnx lock-num true
2(integer) 0
3
4

這是 setnx 指令的特性所導致的,setnx 指令會判斷當前數據庫中是否存在 lock-num 鍵,若存在則返回 0,此時說明別的客戶端正在修改庫存,那麼當前客戶端就應該進入等待狀態或者做別的操作,只有別的客戶端執行完減庫存操作並刪除了鎖之後,setnx 就返回了 1,這時候就可以正常進行減庫存操作了。

死鎖

在分佈式鎖的基礎上可能會出現死鎖的問題,當客戶端添加了分佈式鎖進行操作後,在刪除鎖之前出現了突發情況,比如斷電、宕機等問題,此時就無法釋放該鎖,導致別的客戶端一直在等待鎖的釋放。

我們可以使用 expire 指令爲分佈式鎖設置一個有效時間,當有效時間過後 redis 便會自動刪除該鎖,這樣就解決了死鎖問題:

1expire lock-num 60
2
3

當 60 秒時間過後,鎖會被自動刪除。

服務器配置

到這裏關於 redis 的一些基本操作就學習完了,接下來我們就來看看 redis 中更加高級的部分,首先是配置文件中的配置信息。

| 配置項 | 說明 | | --- | --- | | daemonize yes | no | | bind 127.0.0.1 | 綁定主機地址 | | port 6379 | 設置服務器端口號 | | databases 16 | 設置數據庫數量 | | loglevel debug | verbose | | logfile 端口號. log | 設置日誌文件名 | | maxclients 0 | 設置同一時間最大客戶端連接數,默認無限制,當客戶端連接達到上限時,redis 會關閉新的連接 | | timeout 300 | 客戶端閒置等待最大時長,達到最大值後關閉連接,如需關閉該功能, 設置爲 0 | | include /path/server - 端口號. conf | 導入並加載指定配置文件信息,用於快速創建 redis 公共配置較多的 redis 實例配置文件,便於維護 |

主從複製

現在我們只是在使用一個 redis,它就會出現一些問題,比如服務器宕機後,該服務器上的 redis 將無法提供服務,而此時應用又只有一個 redis 服務支撐,那麼我們的業務將無法提供正常的服務,爲了保證高可用,我們需要爲 redis 搭建集羣。

redis 中以 master 爲主機,slave 爲從機,一個 master 可以對應多個 slave,而一個 slave 只能對應一個 master。那麼首先我們需要建立 slave 到 master 的連接,使 master 能夠識別 slave,並保存 slave 的端口號,啓動四個窗口模擬這一過程:

在 6380 服務和 6381 服務窗口分別開啓 6380 端口、6381 端口的 redis 服務,然後來到 slave 窗口:

1redis-cli -p 6381
2slaveof 127.0.0.1 6380
3
4

這裏表示使用 6381 端口連接 6380 端口,作爲它的從機,此時我們再來到 master 窗口,連接客戶端:

1redis-cli -p 6380
2set name zs
3
4

會發現,slave 窗口中 6381 端口的 redis 也能夠獲取到該數據,此時證明主從搭建好了。

我們也可以在啓動 redis 服務的時候就進行連接:

1redis-server redis-6381.conf --slaveof 127.0.0.1 6380
2
3

redis 推薦使用配置文件的方式搭建主從結構,修改 redis-6381.conf:

1slaveof 127.0.0.1 6380
2
3

此時 6381 就成了 6380 的從機了。

哨兵

在主從的環境下也可能會產生問題,比如作爲主機的 master 服務宕機了,此時作爲它的從機都無法正常工作了,這個時候我們需要在 slave 中選出一個作爲新的 master,以支撐主從繼續提供服務。

哨兵則是爲了解決上述問題的,它是一個分佈式的系統,用於對主從結構中的每臺服務器進行監控,當出現故障時通過投票機制選擇新的 master 並將所有的 slave 連接到新的 master。

哨兵的啓動方式如下:

1redis-sentinel sentinel.conf
2
3

哨兵的客戶端鏈接方式:

1redis-cli -p 26379
2
3

需要注意的是哨兵客戶端不支持數據操作,它只作監控用途。

企業級解決方案

下面介紹一些企業中常用的 redis 解決方案,這也是一些崗位面試的重點。

緩存預熱

緩存預熱指的是在系統啓動前,提前將相關的緩存數據直接加載到緩存系統,避免在用戶請求的時候,先查詢數據庫,然後再將數據緩存的問題,這樣用戶就可以直接查詢到事先被預熱的緩存數據。

比如日常例行統計數據訪問記錄,統計出訪問頻度較高的熱點數據,並將統計結果中的數據分類,根據級別,讓 redis 優先加載級別較高的熱點數據,一般使用腳本程序來固定觸發數據預熱過程。

緩存雪崩

緩存在同一時間大面積的失效,導致請求直接落到了數據庫上,造成數據庫短時間內承受大量請求,很有可能導致數據庫直接崩潰。亦或者一些熱點數據在某一時刻大面積失效,導致對應的請求直接落到了數據庫上也導致數據庫直接崩潰。解決辦法:

對於熱點數據在某一時刻大面積失效的情況,我們可以爲這些數據設置不同的失效時間,比如給它們隨機設置失效時間,那麼就不會出現數據大面積失效的情況了。

緩存擊穿

緩存擊穿與緩存雪崩非常類似,也是因爲大量的請求落到了數據庫上導致數據庫崩潰,但不同的是,緩存雪崩是大量緩存數據同時失效導致大量的請求落到了數據庫;而緩存擊穿通常指併發訪問同一條數據,解決方案也與緩存雪崩大體相同。

緩存穿透

緩存穿透指的是用戶查詢的數據在緩存中沒有,在數據庫中也沒有,如發起 id 等於 - 1 或者 id 等於非常大的值的請求,這些數據肯定是不存在的,這個時候的用戶很有可能是網站的攻擊者,這樣的攻擊請求會導致數據庫壓力過大進而崩潰。

解決方案:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/A36j9OSug0H0ti30ebN6cA