redis应用场景与最佳实践

    Redis作为当下最流行的内存Nosql数据库,有着诸多的应用场景。在不同的应用场景,对Redis的部署、配置以及使用方式都存在的不同地方。根据我的工作经验,把队列、缓存、归并、去重等应用场景的“最佳实践”整理如下。

    本文中的所有代码,均可在github上找到:https://github.com/huyanping/RedisStudy

队列

     Redis的list数据结构经常会被用作队列来使用,常用的方法有:lpop/rpop、lpush/rpush、llen、lindex等。由于Redis提供的list是一个双向链表,我们也可以把list当做栈来使用。使用Redis的list作为队列时,需要注意以下几个问题:

  1. 队列中的数据一般具有比较高的可靠性要求,Redis的持久化机制最好使用AOF方式,保证数据不丢失
  2. 同样由于对数据的可靠性要求较高,内存监控尤为重要,如果出现队列堆积内存用光造成无法提供服务的情况
  3. 如果只有一个消费者在消费队列,推荐使用lindex先读取消息,消费完之后在lpop扔掉,这样可以保证事务性,避免消息处理失败后消息丢失
  4. 如果是多个消费者在消费队列,消息处理失败的情况下可以将消息重新写入队列,前期是消息没有有序性要求

    通过批量(multi)和并行的方式可以提高生产者和消费者的处理能力。批量处理可以减少网络通信量,同时减少Redis在不同任务间切换的开销。并行的好处就是当一个客户端在准备或处理数据时并且Redis空闲时,另一个客户端可以从Redis读取数据;这样可以尽量保证Redis始终保持在繁忙状态。

    如果通过以上优化,仍然有队列堆积的情况,建议启动多个Redis实例。由于Redis是单线程模型,无法利用多核CPU,开启多个实例能够明显提升吐吞量。Redis的集群方案有很多种,也可以简单的在客户端使用hash算法实现。具体实现方案已经超出本文叙述范围,不再累赘。

    基于Redis list的消息队列使用示例代码如下:

1

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’即可。

猜你喜欢

转载自my.oschina.net/u/232595/blog/1788494