redis应用场景与最佳实践

NoSQL Jenner 10129℃ 0评论
    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的消息队列使用示例代码如下:
<?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();
    }
}
<?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的算法。
两种方式的示例代码分别如下:
<?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));
    }
}
<?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的发布订阅机制,在客户端与服务端由于某些问题链接失效时,中间订阅的数据会丢失;所以在实际生产环境中,很少应用这种机制。
示例代码如下:
<?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));
        }
    }
}
<?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命令:
<?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($lua, array($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访问漏洞

这个漏洞的原理非常简单,只需执行以几条命令即可:
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’即可。
以上是我在Redis使用过程中整理的应用场景与“最佳实践”。

原创文章,转载请注明: 转载自始终不够

本文链接地址: redis应用场景与最佳实践

转载请注明:始终不够 » redis应用场景与最佳实践

喜欢 (11)