Redis 简介

崔志华 2017-10-25 19:24:56

简介

  • 名字来源:REmote DIctionary Server
  • 非关系数据库
  • 可以存储key和5种不同类型的值Value
  • 可以将存储在内存中的数据持久化到硬盘
  • 可以使用复制特性来扩展读性能
  • 可以使用客户端分片来扩展写性能(线性提升)
    • 客户端分片:基于hash或者包含键的Id将数据存储到多台机器,也可以从多台机器里面获取数据,这种办法在处理某些问题时可以获得线性级别的性能提升。
  • redis 不支持嵌套结构特性(如果需要可以通过合理组织命名空间一定程度上行模拟。)

一、和memcache对比

  • 都可用于存储键值对
  • 性能相差无几
  • 部署成本接近(要求越高memcache越便宜)
  • redis集群功能受限制比较严重,大规模应用没有明显的优势
  • redis单机功能完爆memcache
  • redis单机很难升级(各种多机架构对redis协议的高级特性一般都支持的不怎么好)
  • Redis可以以两种方式进行持久化
  • Redis除了存储字符串之外还可以存储其他4种结构

结论:redis可以作为主数据库使用,又可以作为其他存储系统的辅助数据库

阿里云 redis 标准版 的使用场景

  • 对 Redis 协议兼容性要求较高的业务
    • 标准版完全兼容 Redis 协议,业务可以平滑迁移。
  • Redis 作为持久化数据存储使用的业务
    • 标准版提供持久化机制及备份恢复机制,极大的保证数据可靠性。
  • 单个 Redis 性能压力可控
    • 由于 Redis 原生采用单线程机制,性能在10wQPS以下的业务建议使用。如果需要更高的性能要求,请选用集群版本。
  • Redis 命令相对简单,排序、计算类命令较少
    • 由于 Redis 的单线程机制,CPU会成为主要瓶颈。如排序、计算类较多的业务建议选用集群版配置。

二、持久化选项(两种方式)

  • 时间点转储(快照方式)
    • 指定时间段内有指定数量的写操作(bgsave)
    • 使用(SAVE/BGSAVE)两者之一
    • 关闭(SHUTDOWN)时会执行阻塞save
    • 从服务器开始sync命令时会执行bgsave
  • 只追加写入(AOF)
    • 将所有修改了数据库的命令都写入一只追加的文件里面,用户可以根据数据的重要程度,将只追加的文件里面,用户可以根据数据的重要程度将只追加写入设置为:从不同步、每秒同步一次、或者每写入一次命令就同步一次。

两种方法可以都用也可以都不用很随意
bgsave的创建子进程速度非常快(几GB毫秒级,取决于剩余内存,就是有没有使用虚存),慢的是存到硬盘上
AOF的问题是AOF文件会比较大,所以需要执行BGREWRITEAOF来压缩AOF文件的体积,这个命令通过移除冗余命令来实现。

主从复制

  • 执行复制的从服务器会链接上主服务器,接受主服务器发送的整个数据库的初始副本;然后主服务器执行的写命令都会发送给从服务器去执行,从而实时的更新从服务器的数据集。

从服务器在执行同步时,会清空自己的所有数据
Redis不支持主主复制(Redis cluster)

字符串处理

  • 长度
    • 整数:和系统的长整型相同(32/64位有符号整数)
    • 浮点数:双精度浮点数(doubule)
  • INCR/DECR
    • 如果一个值可以被解释为整数或者浮点数那么redis允许用户执行这些命令,反之则会报错
    • 对一个不存在的键执行INCR那么redis会把这个键的值当成0来处理

三、不常用但很有用的命令

  1. 字符串

SETNX、APPEND、GETRANGE、SETRANGE、GETBIT、SETBIT、BUTCOUNT、BITOP

  1. 链表

LINDEX、LRANGE、LTRIM

  1. 集合

SRANDOMMEMBER、SPOP、SMOVE

  1. 散列

HKEYS、HVALS

  1. 有序集合

有序集合并运算比较特殊可以用来解决一些权重问题:ZINTERSTORE、ZUNIONSTORE

  1. SORT

根据给定选项,对输入列表、集合或者有序集合进行排序,然后返回存储排序的结果

  1. KEY相关命令
命令 功能
PERSIST 移除键的过期时间
[P]TTL 查看还有多久过期
[P]EXPIRE 设置键的过期时间
[P]EXPIREAT 设置键的过期时间为指定时间戳

四、事务和流水线

  • Redis 事务以MULT开始以exec结束,在EXEC执行之前不会执行任何实际的操作,所以用户没有办法根据读取到的数据来做决定。
  • 使用WATCH/DISCARD(UNWATCH)的乐观锁来避免并发环境下数据出错
  • Redis可以满足完整的ACID特性只是和Mysql是实现方式不同。

为什么redis没有实现典型的悲观锁功能?

悲观锁持有锁的客户端运行越慢,等待解锁的客户端被阻塞的时间就越长。
reids的setnx命令配合lua脚本可以很方便的实现高性能的分布式悲观锁。

  • 非事务流水线:multi/exec会保证事务原子性,而这也是有开销的,redis会为每个客户端创建一个任务队列处理事务,也会消耗资源。常用的客户端都封装了一种pipeline方式,来处理一个命令的执行结果不会影响另一个命令的输入的场景,pipeline可以减少在网络传输上的小号。

redis里就没有pipeline这个命令,只是各种redis客户端实现的一种方式(打个比方Yii2默认的reids客户端就没实现这个功能。)

从redis命令协议理解pipeline:

Redis协议

常见redis客户端对于pipeline的支持:

  • Yii2:不支持
  • Laravel(predis):
 protected function executePipeline(ConnectionInterface $connection, \SplQueue $commands)
    {
        foreach ($commands as $command) {
            $connection->writeRequest($command);
        }

        $responses = array();
        $exceptions = $this->throwServerExceptions();

        while (!$commands->isEmpty()) {
            $command = $commands->dequeue();
            $response = $connection->readResponse($command);//不是很懂,这样真的能降低网络消耗吗?

            if (!$response instanceof ResponseInterface) {
                $responses[] = $command->parseResponse($response);
            } elseif ($response instanceof ErrorResponseInterface && $exceptions) {
                $this->exception($connection, $response);
            } else {
                $responses[] = $response;
            }
        }

        return $responses;
    }
  • phpRedis
/* exec */
PHP_METHOD(Redis, exec)
{
    RedisSock *redis_sock;
    char *cmd;
    int cmd_len, ret;
    zval *object;

    ....

    IF_PIPELINE() {
        if (redis_sock->pipeline_cmd == NULL) {
            /* Empty array when no command was run. */
            array_init(return_value);
        } else {
            if (redis_sock_write(redis_sock, redis_sock->pipeline_cmd,
                    redis_sock->pipeline_len TSRMLS_CC) < 0) {
                ZVAL_FALSE(return_value);
            } else {
                array_init(return_value);
                redis_sock_read_multibulk_multi_reply_loop(
                    INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, return_value, 0);
            }
            efree(redis_sock->pipeline_cmd);
            redis_sock->pipeline_cmd = NULL;
            redis_sock->pipeline_len = 0;
        }
        free_reply_callbacks(redis_sock);
        REDIS_DISABLE_MODE(redis_sock, PIPELINE);
    }
}

五、典型实践

  1. 分布式锁:悲观锁,消除竞争条件
  2. 公平计数信号量:限制资源(API)的并发数量(通过计数器替换时间戳作为信号量分值)

    计数器信号量和锁的区别在于客户端获取锁失败时一般会等待,而计数器信号量会选择直接返回错误

  3. 任务队列:

    • 优先级队列:blpop命令支持多个列表作为参数并按照顺序弹出数据,实现很简单(ResQueue)
    • 延迟任务队列(3种简单实现方式):
      1. 在任务信息中包含任务的额执行时间,如果时间没到呢,短暂等待把任务重新推入队列里面
      2. 使用等待列表记录需要在未来执行的任务,并在每次进行循环的时候,检查并执行已经到期的任务
      3. 把所有需要在未来执行的任务放有序集合里,并将执行时间设置为分值,扫描并移除合适的任务,并添加到合适的任务队列。
  4. 秒杀: 高并发、防超卖

六、LUA的应用

Redis的Lua脚本编程的实现和应用

七、Redis优化

降低内存占用

原理

降低redis内存的占用有助于减少创建快照和加载快照所需的时间、缩短从服务器进行同步所需要的时间(同步初始化那步的时间,主从同步靠的是发送命令也就是不能降低delay)

  1. 使用短结构来更高效的表示数据
  2. 使用分片技术将体积较大的结构分割为多个体积较小的结构(单机分片)
  3. 将固定长度的数据打包存储到字符串键里面,从而进一步降低内存占用

扩展Redis

前提:
确认合理使用了Redis提供的数据结构(比如:不把列表当集合使用;也不要获取整个散列然后在客户端里进行排序,而是直接使用有序集合);合理使用流水线和连接池;将大体积的对象缓存到Redis里面之前应该合理进行压缩(lz4、gzip、bzip2) 做完上述准备之后可以考虑架构上扩展读写性能了

  • 扩展读性能
    • 扩展读性能的最简单方法就是添加只读从服务器
    • 使用从服务器树来解决重同步的问题
    • 使用Redis Sentinel 对下线的主服务器提供故障转移功能(RS还提供了转移通知的功能,可以在故障转移的时候调用用户脚本。)
    • 使用带压缩的隧道来减低从服务器的传送的数据量(如果使这个功能需要ssh自带的选项让ssh自动重连)

重同步:每当有从服务器尝试与主服务器建立连接的时候,主服务器就会为从服务器传建一个快照,如果在快照创建完毕之前,有多个服务器都尝试与主服务器进行连接,那么这些从服务器将会接收到同一个快照。从效率角度将这是合理的,但是这可能会将主服务器的带宽打满,导致主服务器的延迟变高甚至导致主服务器已经建立的链接被断开。

  • 扩展写性能

前提:
1.进行功能拆分
2.在对redis写入之前,先尝试在本地内存中对将要写入的数据进行聚合计算
3.使用锁或者Lua脚本去替换redis事务段
在以上方法都试过后说明遇到了只使用一台机器带来的瓶颈,是时候将数据分片到多台机器上面了
数据分片的架构有很多种
4. 可以使用分片来扩展复杂查询(并行处理复杂命令)

Redis 相关资源