从零学习Redis

316 阅读23分钟

1、简介

Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。

redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

Redis 是一个高性能的key-value数据库。 redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。

Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

2、其它常见缓存对比

2.1、redis 和 memcached 的区别是什么?

  • Redis和Memcached都是将数据存放在内存中,都是内存数据库,不过memcache还用于缓存其他东西,例如:图片、视频等等
  • Redis不仅仅支持简单的k/v类型的数据,同时还提供list、set、hash等数据结构的存储
  • 虚拟内存--Redis当物理内存用完时,可以将一些很久没用到的value交换到磁盘
  • 过期策略--memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通过例如expire设定,例如expire name 10;
  • 分布式--设定memcached集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从
  • 存储数据安全--memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化)
  • 灾难恢复--memcached挂掉后,数据不可恢复;redis数据丢失后可以通过aof恢复
  • Redis支持数据的备份,即master-slave模式的数据备份
  • memcached的key的最大长度是250个字符,key 不能有空格和控制字符,value 不能超过 1M,过期时间最大30天。
  • redis的key和string类型value限制均为512MB

3、redis详解

3.1、redis 常见数据结构以及使用场景

redis的键key只能为字符串

String:string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。 string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。 string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。

应用场景:

  • 缓存功能:可以利用JSON强大的兼容性、可读性和易用性,将对象转换为JSON字符串,再以string进行存储,比如用户的session。
  • 计数器:string类型的incr和decr命令的作用是将key中储存的数字值加一/减一,这两个操作具有原子性,总能安全地进行加减操作,因此可以用string类型进行计数,如微博的评论数、点赞数、分享数,抖音作品的收藏数,京东商品的销售量、评价数等。
  • 分布式锁:string类型的setnx的作用是“当key不存在时,设值并返回1,当key已经存在时,不设值并返回0”,“判断key是否存在”和“设值”两个操作是原子性地执行的,因此可以用string类型作为分布式锁,返回1表示获得锁,返回0表示没有获得锁

Hash: hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象(前提是这个对象没嵌套其他的对象),总得来说有局限性,自己基本没用到。

List: 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

应用场景:

  • 消息队列:list类型的lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能,故而可以用Redis的list类型实现简单的点对点的消息队列。不过不推荐在实战中这么使用,因为现在已经有Kafka、RabbitMQ等成熟的消息队列供大家使用。

Set: Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。 Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 应用场景

  • 可以基于 Set 做交集、并集、差集的操作,比如共同好友之类的功能。

sorted set: 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。 不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。 有序集合的成员是唯一的,但分数(score)却可以重复。 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

应用场景

  • 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、热度、按照播放量、按照获得的赞数等。

3.2、redis 高级用法以及使用场景

bitmap位图: 通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上底层也是通过对String(字符串)的操作来实现。Redis 从 2.2 版本之后新增了setbit, getbit, bitcount 等几个 bitmap 相关命令。虽然是新命令,但是本身都是对字符串的操作

应用场景

  • 用户在线状态、在线人数:这里只需要一个 key,然后把用户 ID (整型)作为 offset,如果在线就设置为 1,不在线就设置为 0。

    实例代码:

    //设置在线状态
    setbit online 1 1
    
    //设置离线状态
    setbit online 1 0
    
    //获取状态
     getbit online 1
    
    //获取在线人数
     bitcount online
    

HyperLogLog: Redis 的基数统计,这个结构可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV)、在线用户数等。但是它也有局限性,就是只能统计数量,有一定的错误率。

GEORADIUS(GEO): 即地址信息定位,可以用来存储经纬度,计算两地距离,范围计算等,功能基于zset实现。

应用场景

  • 附近的人

实例代码:

# 添加北京的经纬度
geoadd cities 116.20 39.56 beijing
# 添加天津的经纬度
geoadd cities 117.10 39.10 tianjin
# 获取地理位置信息
geopos cities beijing
# 获取两个地理位置的距离,unit:m(米),km(千米),mi(英里),ft(尺)
geodist cities beijing tianjin km

3.3、持久化

Redis 提供了 RDB 和 AOF 两种持久化方式,RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作。

3.3.1、RDB

RDB持久化方式是通过快照(snapshotting)完成的,当符合一定条件时,redis会自动将内存中所有数据以二进制方式生成一份副本并存储在硬盘上。当redis重启时,并且AOF持久化未开启时,redis会读取RDB持久化生成的二进制文件(默认名称dump.rdb,可通过设置dbfilename修改)进行数据恢复,对于持久化信息可以用过命令“info Persistence”查看。

快照文件位置:

直接通过配置文件查找:

命令行方式:

快照触发条件:

  • 客户端执行命令save会生成快照:客户端执行save命令,该命令强制redis执行快照,这时候redis处于阻塞状态,不会响应任何其他客户端发来的请求,直到RDB快照文件执行完毕,所以请慎用。;
  • 客户端执行命令bgsave会生成快照:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
  • 根据配置文件save m n规则进行自动快照:在指定的m秒内,redis中有n个键发生改变,则自动触发bgsave。该规则默认也在redis.conf中进行了配置,并且可组合使用,满足其中一个规则,则触发bgsave;

  • 主从复制时,从库全量复制同步主库数据,此时主库会执行bgsave命令进行快照:在redis主从复制中,从节点执行全量复制操作,主节点会执行bgsave命令,并将rdb文件发送给从节点;
  • 客户端执行数据库清空命令FLUSHALL时候,触发快照:flushall命令用于清空数据库,请慎用,当我们使用了则表明我们需要对数据进行清空,那redis当然需要对快照文件也进行清空,所以会触发bgsave。;
  • 客户端执行shutdown关闭redis时,触发快照:redis在关闭前处于安全角度将所有数据全部保存下来,以便下次启动会恢复。;

数据恢复:

将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可,redis就会自动加载文件数据至内存了。Redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。

3.3.2、AOF

AOF可以将Redis执行的每一条写命令追加到磁盘文件(appendonly.aof)中,在redis启动时候优先选择从AOF文件恢复数据。由于每一次的写操作,redis都会记录到文件中,所以开启AOF持久化会对性能有一定的影响,但是大部分情况下这个影响是可以接受的,我们可以使用读写速率高的硬盘提高AOF性能。与RDB持久化相比,AOF持久化数据丢失更少,其消耗内存更少(RDB方式执行bgsve会有内存拷贝)。

开启AOF:

AOF 保存文件的位置和 RDB 保存文件的位置一样,都是通过 redis.conf 配置文件的 dir 配置

修改配置文件方式:将配置文件appendonly改为yes即可,默认情况下,redis是关闭了AOF持久化的

命令行方式:

config rewrite 报错找不到配置文件的,使用配置文件启动即可。

appendfsync:aof持久化策略的配置

  • no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全;

  • always表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低;

  • everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。通常选择 everysec ,兼顾安全性和效率。

AOF持久化过程 :

  • 追加写入:redis将每一条写命令以redis通讯协议添加至缓冲区aof_buf,这样的好处在于在大量写请求情况下,采用缓冲区暂存一部分命令随后根据策略一次性写入磁盘,这样可以减少磁盘的I/O次数,提高性能。

  • 同步命令到硬盘:当写命令写入aof_buf缓冲区后,redis会将缓冲区的命令写入到文件,redis提供了三种同步策略,由配置参数appendfsync决定

  • 文件重写(bgrewriteaof):当开启的AOF时,随着时间推移,AOF文件会越来越大,当然redis也对AOF文件进行了优化,即触发AOF文件重写条件(后续会说明)时候,redis将使用bgrewriteaof对AOF文件进行重写。这样的好处在于减少AOF文件大小,同时有利于数据的恢复。

重写触发条件

  • 手动触发:客户端执行bgrewriteaof命令

  • 自动触发:自动触发通过以下两个配置协作生效:auto-aof-rewrite-min-size: AOF文件最小重写大小,只有当AOF文件大小大于该值时候才可能重写,4.0默认配置64mb。 auto-aof-rewrite-percentage:当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比,如100代表当前AOF文件是上次重写的两倍时候才重写。 

数据恢复:

与RDB一样,将备份文件 (appendonly.aof) 移动到 redis 安装目录并启动服务即可,redis数据恢复优先选用AOF进行数据恢复。

3.3.3、RDB-AOF混合持久化

redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混合持久化方式。混合持久化就是同时结合RDB持久化以及AOF持久化混合写入AOF文件。这样做的好处是可以结合 rdb 和 aof 的优点, 快速加载同时避免丢失过多的数据,缺点是 aof 里面的 rdb 部分就是压缩格式不再是 aof 格式,可读性差。

开启混合持久化

4.0版本的混合持久化默认关闭的,通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,默认是禁用的。

修改配置文件方式:

命令行方式:

混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图:

数据恢复

当我们开启了混合持久化时,启动redis依然优先加载aof文件,aof文件加载可能有两种情况如下:

  • aof文件开头是rdb的格式, 先加载 rdb内容再加载剩余的 aof。
  • aof文件开头不是rdb的格式,直接以aof格式加载整个文件。

3.4、优缺点

RDB

优点:

  • RDB 是一个非常紧凑(compact)的文件,体积小,因此在传输速度上比较快,因此适合灾难恢复。

  • RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。

  • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

缺点:

  • RDB是一个快照过程,无法完整的保存所以数据,尤其在数据量比较大时候,一旦出现故障丢失的数据将更多。 当redis中数据集比较大时候,RDB由于RDB方式需要对数据进行完成拷贝并生成快照文件,fork的子进程会耗CPU,并且数据越大,RDB快照生成会越耗时。
  • RDB文件是特定的格式,阅读性差,由于格式固定,可能存在不兼容情况。

AOF

优点:

  • 数据更完整,秒级数据丢失(取决于设置fsync策略)。
  • 兼容性较高,由于是基于redis通讯协议而形成的命令追加方式,无论何种版本的redis都兼容,再者aof文件是明文的,可阅读性较好。

缺点:

  • 数据文件体积较大,即使有重写机制,但是在相同的数据集情况下,AOF文件通常比RDB文件大。
  • 相对RDB方式,AOF速度慢于RDB,并且在数据量大时候,恢复速度AOF速度也是慢于RDB。
  • 由于频繁地将命令同步到文件中,AOF持久化对性能的影响相对RDB较大,但是对于我们来说是可以接受的。

混合持久化

优点:

  • 混合持久化结合了RDB持久化 和 AOF 持久化的优点, * 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。 缺点:

  • 兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差

3.5、高可用

为了达到高可用,所以需要在单机的基础上建立集群,首先了解下它的集群模式,大概有以下几种:

  • 1、主从复制
  • 2、哨兵模式
  • 3、Redis官方提供的Cluster集群模式(服务端)
  • 4、Jedis sharding集群(客户端sharding)
  • 5、利用中间件代理,比如豌豆荚的codis等

这一部分之前有详细介绍过《Redis(5.0) 集群搭建》,这里就不展开了。

3.6、常见问题分析

3.6.1、Redis更新缓存策略

主动更新

  • DB更新之后查找最新数据集合,将最新的数据到缓存。
  • 问题:如果服务在更新时出现异常,则会导致数据不可用。

被动更新

  • 超时剔除:通过给缓存数据设置过期时间,让其在过期时间后自动删除,例如Redis提供的expire命令。如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,那么可以为其设置过期时间。在数据过期后,再DB源获取数据,重新放到缓存并设置过期时间。

3.6.2、Redis和DB数据不一致解决方法

大多情况下,我们使用缓存都是这样的策略:先读缓存,读取不到就读数据库然后同步到缓存中。

问题出现场景

问题就是在并发访问中,不论是先写库,再删除缓存;还是先删缓存,再写库,都有可能出现数据不一致的情况

  • 1、在并发中是无法保证读写的先后顺序的,如果删掉了缓存还没来得及写库,另一个线程就过来读取发现缓存为空就去数据库读取并写入缓存,此* 时缓存中为脏数据。
  • 2、如果先写了库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
  • 3、如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。

优化解决方案

网上看到了很多方式,先记录个容易理解的,

双删 + 超时

1、在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。

具体的步骤就是:

  • 1)先删除缓存
  • 2)再写数据库
  • 3)休眠500毫秒
  • 4)再次删除缓存

那么,这个500毫秒怎么确定的,具体该休眠多久呢?

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

3.设置缓存过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

4.该方案的弊端

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

3.6.3、缓存穿透

缓存穿透是指查询一个一定不存在的数据,因为缓存中也无该数据的信息,则会直接去数据库层进行查询,从系统层面来看像是穿透了缓存层直接达到db,从而称为缓存穿透,没有了缓存层的保护,这种查询一定不存在的数据对系统来说可能是一种危险,如果有人恶意用这种一定不存在的数据来频繁请求系统(准确的说是攻击系统),请求都会到达数据库层导致db瘫痪从而引起系统故障。

解决方案:

  • bloom filter(布隆过滤器):类似于哈希表的一种算法,用所有可能的查询条件生成一个bitmap,在进行数据库查询之前会使用这个bitmap进行过滤,如果不在其中则直接过滤,从而减轻数据库层面的压力。
  • 空值缓存:一种比较简单的解决办法,在第一次查询完不存在的数据后,将该key与对应的空值也放入缓存中,只不过设定为较短的失效时间,例如几分钟,这样则可以应对短时间的大量的该key攻击,设置为较短的失效时间是因为该值可能业务无关,存在意义不大,且该次的查询也未必是攻击者发起,无过久存储的必要,故可以早点失效。

3.6.4、缓存雪崩

在普通的缓存系统中一般例如redis、memcache等中,我们会给缓存设置一个失效时间,但是如果所有的缓存的失效时间相同,那么在同一时间失效时,所有系统的请求都会发送到数据库层,db可能无法承受如此大的压力导致系统崩溃。

解决方案:

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  • 设置热点数据永远不过期。
  • 限流降级组件

3.6.5、缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。击穿与雪崩的区别即在于击穿是对于某一特定的热点数据来说,而雪崩是全部数据。

解决方案:

  • 可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
  • 设置热点数据永远不过期。

3.6.6、使用过Redis分布式锁么,它是怎么实现的?

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令【set key value [ex seconds] [px milliseconds] [nx|xx]】来用的!

set key value [ex seconds] [px milliseconds] [nx|xx]

  • ex seconds: 为键设置秒级过期时间。
  • px milliseconds: 为键设置毫秒级过期时间。
  • nx: 键必须不存在, 才可以设置成功, 用于添加。
  • xx: 与nx相反, 键必须存在, 才可以设置成功, 用于更新。

3.6.7、怎么从redis中查询一个以什么名字开头的key呢?

keys指令可以扫出指定模式的key列表。

但是Redis是单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。 通过keys命令的话如果数据量太大的话会占用线程卡住,不适用于生产环境中。

这个时候可以使用SCAN指令,SCAN命令是一个基于游标的迭代器, 这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程, 当SCAN命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束

SCAN cursor [MATCH pattern] [COUNT count]

  • 基于游标的迭代器,基于上一次的游标延续之前的迭代过程。
  • 以0作为游标开始一次新的迭代,知道命令返回游标0完成一次遍历。
  • 不保证每次执行都返回某个给定数量的元素,支持模糊查询。
  • 一次返回的数量不可控,只能大概率符合count参数。

3.6.8、如果机器突然掉电会怎样?

取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

3.6.9、是否使用过Redis集群,集群的原理是什么?

  • Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

  • Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

持续更新中。。。