redis應用場景與最佳實踐

    Redis做爲當下最流行的內存Nosql數據庫,有着諸多的應用場景。在不一樣的應用場景,對Redis的部署、配置以及使用方式都存在的不一樣地方。根據個人工做經驗,把隊列、緩存、歸併、去重等應用場景的「最佳實踐」整理以下。php

    本文中的全部代碼,都可在github上找到:https://github.com/huyanping/RedisStudyhtml

隊列

     Redis的list數據結構常常會被用做隊列來使用,經常使用的方法有:lpop/rpop、lpush/rpush、llen、lindex等。因爲Redis提供的list是一個雙向鏈表,咱們也能夠把list當作棧來使用。使用Redis的list做爲隊列時,須要注意如下幾個問題:git

  1. 隊列中的數據通常具備比較高的可靠性要求,Redis的持久化機制最好使用AOF方式,保證數據不丟失
  2. 一樣因爲對數據的可靠性要求較高,內存監控尤其重要,若是出現隊列堆積內存用光形成沒法提供服務的狀況
  3. 若是隻有一個消費者在消費隊列,推薦使用lindex先讀取消息,消費完以後在lpop扔掉,這樣能夠保證事務性,避免消息處理失敗後消息丟失
  4. 若是是多個消費者在消費隊列,消息處理失敗的狀況下能夠將消息從新寫入隊列,前期是消息沒有有序性要求

    經過批量(multi)和並行的方式能夠提升生產者和消費者的處理能力。批量處理能夠減小網絡通訊量,同時減小Redis在不一樣任務間切換的開銷。並行的好處就是當一個客戶端在準備或處理數據時而且Redis空閒時,另外一個客戶端能夠從Redis讀取數據;這樣能夠儘可能保證Redis始終保持在繁忙狀態。github

    若是經過以上優化,仍然有隊列堆積的狀況,建議啓動多個Redis實例。因爲Redis是單線程模型,沒法利用多核CPU,開啓多個實例可以明顯提高吐吞量。Redis的集羣方案有不少種,也能夠簡單的在客戶端使用hash算法實現。具體實現方案已經超出本文敘述範圍,再也不累贅。redis

    基於Redis list的消息隊列使用示例代碼以下:算法

1sql

2數據庫

3緩存

4服務器

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

<?php

// 生產者

namespace jenner\redis\study\queue;

use Jenner\SimpleFork\Process;

use Jenner\SimpleFork\Queue\RedisQueue;

 

class Producer extends Process

{

    /**

     * start producer process

     */

    public function run()

    {

        $queue new RedisQueue('127.0.0.1', 6379, 1);

        for ($i = 0; $i < 100000; $i++) {

            $queue->put(getmypid() . '-' . mt_rand(0, 1000));

        }

        $queue->close();

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

<?php

// 消費者

namespace jenner\redis\study\queue;

use Jenner\SimpleFork\Process;

use Jenner\SimpleFork\Queue\RedisQueue;

 

class Consumer extends Process

{

    /**

     * start consumer process

     */

    public function run()

    {

        $queue new RedisQueue('127.0.0.1', 6379, 1);

        while (true) {

            $res $queue->get();

            if ($res !== false) {

                echo $res . PHP_EOL;

            else {

                break;

            }

        }

    }

}

緩存

這裏咱們說的緩存,是能夠丟失或過時的數據,不能丟失或過時的緩存(或者應該叫作數據庫了)不在本文的敘述範圍。Redis的字典結構經常使用來作緩存使用,經常使用的方法有:set/get、hget/hgetall/hset等。使用redis做爲緩存時,須要注意如下幾個問題:

  1. 因爲Redis可用的內存是有限的,不能容忍redis內存的無限增長,最好設置最大內存maxmemory
  2. 在開啓maxmemory的狀況下,能夠啓用lru機制,設置key的expire,當到達Redis最大內存時,Redis會根據最近最少用算法對key進行自動淘汰;lru的策略有6種,可參考:http://www.aikaiyuan.com/7089.html
  3. Redis的持久化策略和Redis故障恢復時間是一個博弈的過程,若是你但願在發生故障時可以儘快恢復,應該啓用dump備份機制,但dump機制要求你必須保留至少1/3(經驗值)的可用內存(寫時複製),因此你可能沒辦法分配儘量多的內存給Redis;若是可以容忍Redis漫長的故障恢復時間,可使用AOF持久化機制,同時關閉dump機制,這樣能夠突破保留1/3內存的限制。

關於緩存的使用方法,不屬於本文敘述範圍,可參考:http://tech.meituan.com/avalanche-study.html

示例代碼太多,這裏就只貼個地址:https://github.com/huyanping/RedisStudy/tree/master/src/spider

計算

Redis提供的原子遞增遞減方法以及有序集合等能夠承擔一些計算任務,例如訪問量統計等。經常使用的方法有:incr/decr、hincrby、zadd/zcard等。

在使用redis做爲計算服務時,須要注意一下幾個問題:

  1. 計算場景的數據通常對可靠性要求比較高,建議啓用AOF持久化機制,根據恢復時間和內容利用率的考慮肯定是否開啓dump機制。
  2. redis的單線程模型決定了redis沒法利用多核CPU,這裏建議引入redis集羣解決方案,固然仍然能夠在客戶端經過hash方案解決。
  3. 批量發送、批量導出

去重

    Redis的hset和HyperLogLog數據結構能夠在使用少許內存的狀況下對數據進行去重。在有大量數據須要去重的場景比較試用。Redis的HyperLogLog只須要使用12K的內存空間便可對2的64次方個記錄進行去重。具體選用哈希字典仍是HyperLogLog須要根據你須要去重的數據量綜合決定,若是你須要去重的數據整體佔用空間遠小於12K,使用哈希字典便可,若是超過12K,推薦使用HyperLogLog。經常使用的命令有:pfadd/pfcount、hset/hlen。這裏須要注意,pfadd返回的是布爾型,表示該值是否已經存在;不能夠經過累加pfadd的結果斷定惟一記錄數,必須調用pfcount獲取,這個應該是算法的緣由,記住就好。有興趣的童鞋能夠深究一下HyperLogLog的算法。

兩種方式的示例代碼分別以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

<?php

// HyperLogLog

namespace jenner\redis\study\unique;

use jenner\redis\study\tool\Logger;

 

class HyperLogLog

{

    /**

     * @var \Redis

     */

    protected $redis;

    /**

     * @var array

     */

    protected $ips;

    /**

     * default hyperloglog key

     */

    const KEY = "ip-unique-hyperloglog";

    /**

     * HyperLogLog constructor.

     * @param array $ips

     */

    public function __construct(array $ips)

    {

        $this->redis = new \Redis();

        $this->redis->connect("127.0.0.1", 6379);

        $this->redis->select(3);

        $this->ips = $ips;

    }

    /**

     * start to count ips using hyperloglog

     */

    public function start()

    {

        Logger::info("unique process start");

        $this->redis->pfadd(self::KEY, $this->ips);

        Logger::info("unique done. ip count:" $this->redis->pfcount(self::KEY));

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

<?php

// set

namespace jenner\redis\study\unique;

use jenner\redis\study\tool\Logger;

 

class Set

{

    /**

     * @var \Redis

     */

    protected $redis;

    /**

     * @var array

     */

    protected $ips;

    /**

     *

     */

    const KEY = "ip-unique-normal";

    /**

     * Set constructor.

     * @param array $ips

     */

    public function __construct(array $ips)

    {

        $this->redis = new \Redis();

        $this->redis->connect("127.0.0.1", 6379);

        $this->redis->select(3);

        $this->ips = $ips;

    }

    /**

     * start to count ips using set

     */

    public function start()

    {

        Logger::info("unique process start");

        foreach ($this->ips as $ip) {

            $this->redis->sAdd(self::KEY, $ip);

        }

        Logger::info("unique done. ip count:" $this->redis->sCard(self::KEY));

    }

}

發佈訂閱

Redis的發佈訂閱機制,在客戶端與服務端因爲某些問題連接失效時,中間訂閱的數據會丟失;因此在實際生產環境中,不多應用這種機制。

示例代碼以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

<?php

// publisher

namespace jenner\redis\study\pubsub;

 

class Publisher

{

    /**

     * @var \Redis

     */

    protected $redis;

    /**

     * default pubsub key

     */

    const KEY = "pubsub-demo";

    /**

     * Publisher constructor.

     */

    public function __construct()

    {

        $this->redis = new \Redis();

        $this->redis->connect("127.0.0.1", 6379);

        $this->redis->select(4);

    }

    public function publish()

    {

        $count = 10;

        for ($i = 0; $i $count$i++) {

            $this->redis->publish(self::KEY, mt_rand(0, 10000));

        }

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

<?php

// subscriber

namespace jenner\redis\study\pubsub;

use jenner\redis\study\tool\Logger;

 

class Subscriber

{

    /**

     * default pubsub key

     */

    const KEY = "pubsub-demo";

    /**

     * @var \Redis

     */

    protected $redis;

    /**

     * Subscriber constructor.

     */

    public function __construct()

    {

        $this->redis = new \Redis();

        $this->redis->connect("127.0.0.1", 6379);

        $this->redis->select(4);

    }

    public function subscribe()

    {

        $this->redis->subscribe(array(self::KEY), function ($redis$channel$message) {

            Logger::info("get message[" $message "] from channel[" $channel "]");

        });

    }

}

Redis lua應用

Lua 腳本功能是 Reids 2.6 版本的最大亮點, 經過內嵌對 Lua 環境的支持, Redis 解決了長久以來不能高效地處理 CAS (check-and-set)命令的缺點, 而且能夠經過組合使用多個命令, 輕鬆實現之前很難實現或者不能高效實現的模式。

優勢:原子性(因爲Redis是單線程模型,同一時刻只能處理一個lua腳本),更小的請求包

應用場景:事務實現,批量處理

如下示例代碼實現了getAndSet命令:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

<?php

// redis lua

namespace jenner\redis\study\lua;

use jenner\redis\study\tool\Logger;

 

class Lua

{

    /**

     * @var \Redis

     */

    protected $redis;

    /**

     * Lua constructor.

     */

    public function __construct()

    {

        $this->redis = new \Redis();

        $this->redis->connect("127.0.0.1", 6379);

        $this->redis->select(2);

    }

    /**

     * @param $key

     * @param $value

     * @return bool

     */

    public function set($key$value)

    {

        return $this->redis->set($key$value);

    }

    /**

     * @param $key

     * @param $value

     */

    public function getAndSet($key$value)

    {

        $lua = <<<GLOB_MARK

local value = redis.call('get', KEYS[1])

redis.call('set', KEYS[1], ARGV[1])

return value

GLOB_MARK;

        $result $this->redis->eval($luaarray($key$value), 1);

        Logger::info("eval script result:" . var_export($result, true));

    }

    /**

     * @return string

     */

    public function error()

    {

        return $this->redis->getLastError();

    }

}

Dump故障

當Redis使用內存大於操做系統剩餘內存的2倍時,使用dump持久化機制可能會形成服務器宕機、假死等狀況。緣由是dump時,Redis會fork一個子進程,根據寫實複製原則,若是Redis中的數據會發生修改時,操做系統會把服務進程的內存copy一份給子進程,具體copy多少根據數據修改的覆蓋度;這時若是內存不夠用,操做系統會使用swap擴展內存,性能急劇降低,若是swap也不夠了,則可能發生宕機、假死等狀況。

解決方案:設置maxmemory,監控Redis內存使用(Redis info命令),場景容許的狀況下開啓lru機制。

maxmemory故障

故障描述:設置了maxmemory,內存用完,客戶端沒法寫入

解決方案:對Redis內存使用進行監控,根據業務場景控制內存使用;若是內存確實不夠用了,考慮引入分佈式Redis集羣方案

redis訪問漏洞

這個漏洞的原理很是簡單,只需執行以幾條命令便可:

1

2

3

4

redis> config set dbfilename authorized_keys

redis> config set dir '/root/.ssh'

redis> set xxoo "\n\n\nyour public ssh key"

redis> save

經過以上幾條命令,能夠將你的ssh公鑰寫入對方的Redis服務器,從而獲取root權限。這個漏洞的利用條件也比較苛刻,須要知足如下幾個條件:

  1. Redis需是root用戶運行,或已知Redis運行用戶
  2. 6379端口無防火牆攔截
  3. Redis無訪問密碼
  4. config set命令沒有被禁用

根據以上利用條件,對應防護手段以下:

  1. 優先監聽127.0.0.1網卡(如過redis是給本機訪問),優先監聽內網網卡
  2. 防火牆對6379端口訪問進行限制
  3. 使用非root用戶運行redis
  4. redis開啓密碼訪問(養成好習慣)
  5. 禁用config set命令

通常狀況下作到第二點,基本就不會被黑了,但咱們應該儘可能作到第四點,盡善盡美。

redis自動重連

RedisRetry是一個支持自動重連的redis客戶端封裝,項目地址:https://github.com/huyanping/RedisRetry

原理:使用__call方法對redis的原生方法封裝,當發生RedisException時,自動關閉並從新創建鏈接,執行n次,每次相隔m毫秒。

常量:

REDIS_RETRY_TIMES 重試次數

REDIS_RETRY_DELAY 間隔時間,單位毫秒

使用方式:把使用Redis類的地方,添加’use \Jenner\RedisRetry\Redis’便可。

相關文章
相關標籤/搜索