redis是一个非关系型的数据库,它的数据存储在内存中,所以读写速度很快。
一、redis的基础数据结构
redis有5种基础数据结构
1.string(字符串)
内部表示是一个字符数组,redis字符串是动态字符串,是可以修改的字符串。
应用广泛,可以保存用户信息,将用户信息使用JSON序列化为字符串保存。
2.list(列表)
它是链表(双向)不是数组,这意味着list的插入和删除操作非常快,时间复杂度为O(1),但是索引定位很慢,时间复杂度为O(n)。可以在两端进行插入和删除。常用来做异步队列。
它的底层是一个快速链表(quicklist)结构。首先在元素较少时,会使用一块连续的内存存储,这个结构是ziplist,即压缩链表。当数据量较多时会改成quicklist。将多个ziplist使用双向指针连接起来。
3.hash(字典)
内部存储了很多键值对。实现结构上为数组+链表(类似于Java的HashMap)。
在rehash时,redis为了追求高性能,采用了渐进式rehash策略。渐进式rehash会在rehash时,保留新旧两个hash结构,查询时会同时查询两个hash结构,在后续操作中循序渐进地把旧hash内容一点点地迁移到新的hash结构中。迁移完成后使用新hash结构取而代之。
hash可以用于存储用户信息,将用户结构的每个字段单独存储。
4.set(集合)
set是无序的,唯一的(不保存重复值)。
5.sortedset(有序集合)
一方面它是一个set,保证value的唯一性。另一方面它给每个value赋予一个score,代表这个value的排序权重。
它的内部实现是“跳跃链表”数据结构。跳表是层级制的,最下面一层所有的元素会串起来。然后从几个元素中挑选出一个代表,再将这几个代表使用另外一级指针串起来。
二、redis的线程模型
redis采用单线程的线程模型,是由于它的文件事件处理器为单线程的。
文件事件处理器使用 I/O 多路复用程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
问:redis为什么那么快?
(1)纯内存访问
(2)单线程模型避免了线程切换和竞争的消耗
(3)非阻塞IO:
redis采用非阻塞IO,读写方法不会阻塞,能读多少读多少,能写多少写多少。
非阻塞IO有个问题,当数据到来时,线程如何得到通知。事件轮询API就是用于解决这个问题的。
三、redis的部署
redis常见的部署方式有
1.单机模式
仅有一台redis服务,
2.哨兵(sentinel)模式
是Redis官方推荐的高可用性(HA)解决方案。由一个或多个Sentinel 实例组成的Sentinel 系统监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。
哨兵负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换成主节点。客户端连接时,首先连接到哨兵,通过哨兵来查询主节点地址,再连接主节点进行数据交互。
3.集群(cluster)模式
cluster方案主要解决分片问题,即把整个数据按照规则分成多个子集存储在多个不同几点上,每个节点负责自己整个数据的一部分。
redis cluster是去中心化的,每个节点负责整个集群的一部分数据,每个节点负责的数据或多或少,相互连接组成一个对等的集群。redis cluster将所有数据划分为16384个槽位,每个节点负责其中一部分槽位。槽位的信息存储在每个节点中。
槽位定位算法:redis cluster会默认对key使用crc16算法进行hash,得到一个整数值,然后用这个整数值对16384进行取模得到具体槽位。redis cluster还允许用户强制把某个key挂在特定槽位上。通过在key字符串里嵌入tag标记。
迁移:redis迁移的单位是槽。
四、redis分布式锁
在一个分布式系统中,我们从数据库中读取一个数据并修改,这种情况很容易遇到并发问题。因为读取和更新保存不是一个原子操作,在并发时就会导致数据的不正确。如果是单机应用,直接使用本地锁就可以避免。如果是分布式应用,本地锁派不上用场,这时就需要引入分布式锁来解决。由此可见,设置分布式锁的目的是:多台服务器在执行某一段代码时,保证某一时刻只有一台服务器执行。
Redis实现分布式锁主要利用Redis的setnx命令。setnx是SET if not exists(如果不存在,则 SET)的简写。
加锁:使用setnx key value命令,如果key不存在,设置value(加锁成功)。如果已经存在lock(也就是有客户端持有锁了),则设置失败(加锁失败)。
解锁:使用del命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过setnx命令进行加锁。
五、redis持久化
redis持久化是指将内存中redis的数据写入到硬盘中,有两种方式:
- RDB(快照):存储内存中的数据在某一时间的快照。快照是内存数据的序列化二进制形式。
- AOF日志:将更改redis中数据的命令存储在AOF文件中。
六、redis设置过期时间和内存淘汰机制
redis可以给存储的key设置一个过期时间。
有8种内存淘汰机制:
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集中挑选即将过期的数据淘汰
- volatile-random:从已设置过期时间的数据集中随机挑选数据淘汰
- allkeys-lru:从数据集中选择最近最少使用的数据淘汰
- allkeys-random:从数据集中随机选择数据淘汰
- no-eviction:禁止驱逐数据。
- volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
- allkeys-lfu:从数据集中挑选最不经常使用的数据淘汰
七、缓存雪崩和缓存穿透问题
1.缓存雪崩
在某一时刻,缓存集大量失效,后续所有流量直接打到数据库上,对数据库造成很大的压力。
场景:例如电商抢购的现象。比如在12点开始,在1点的时候就会大量同时失效。
解决方案:
优化缓存过期时间:避免大量的key在同一时间同时失效。
保证缓存层的高可用性:使用Redis 哨兵模式或者Redis 集群部署方式,即便个别Redis 节点下线,整个缓存层依然可以使用。
2.缓存穿透
大量请求的key不存在于缓存中,导致请求直接到了数据库上。
场景:一般来说,发生在故意攻击的场景下。
解决方法:使用布隆过滤器。通过布隆过滤器我们可以快速判断一个给定的数据是否存在于海量数据中。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。
八、redis的高级数据结构
1.位图
2.HyperLogLog
HyperLogLog提供不精确的去重计数方案。但误差率不是很高。pfadd指令用于增加计数,pfcount指令用于获取计数。这个数据结构可以用于统计网站的用户数量。
3.布隆过滤器
布隆过滤器可以理解为不怎么准确的set结构,可以判断某个对象是否存在,它可能会误判。当布隆过滤器说某个值存在时,这个值可能不存在;当它说某个值不存在时,那就肯定不存在。即布隆过滤器对于已经见过的元素肯定不会误判,只会误判没有见过的元素。
布隆过滤器可以作为一个插件加载到redis server中,给redis提供了强大的布隆去重功能。两个基本指令:bf.add用于添加元素,bf.exists用于查询元素是否存在。
布隆过滤器原理
每个布隆过滤器对应到redis的数据结构里就是一个大型的位数组和几个不一样的无偏hash函数。无偏是指能把元素的hash值算得比较均匀,让元素被hash映射到维数组的位置比较随机。
当往布隆过滤器中添加值的时候,会使用这些hash函数对key进行hash,得到一个整数索引值,再对位数组的长度进行取模运算后得到一个位置。每个hash函数都会得到一个位置,再把位数组的这几个位置都设置为1,完成add操作。
向布隆过滤器询问key是否存在时,也会把hash的几个位置都算出来,看看位数组中的这几个位置是否都为1,只要有1个位为0,就说明这个值不存在。
如果位数组比较稀疏,那么判断正确的概率会很大,如果位数组比较拥挤,那么判断正确的概率会降低。
布隆过滤器的应用
去重:爬虫系统中,需要对URL去重,爬过的网页就不用爬了。
应用在NoSQL数据库中,可以显著地降低数据库的IO请求数量。当用户查询时,可以先通过布隆过滤器过滤掉大量不存在的请求,再去磁盘中查询。
九、Geo模块和GeoHash算法
redis在3.2版本以后增加了地理位置Geo模块,因此可以使用redis实现附近的...功能。
1.用数据库来计算附近的人
给定一个元素坐标,计算这个坐标附近的其他元素,按照距离排序。
2.GeoHash算法
业界比较通用的地理位置距离排序算法是GeoHash算法。redis也使用该算法。GeoHash算法将二维的经纬度数据映射到一维的整数,这样所有元素都将挂载到一条线上。计算附近的人时,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行。
映射算法具体如何?例如,二刀法。把地球看成一个平面,然后两刀划分为4个小正方形,分别标记为00,01,10,11,用2bit的二进制整数表示。在继续切割,正方形会越来越小,二进制整数越来越长,精确度也越来越高。
每个地图元素的坐标经过编码后会变成一个整数,GeoHash算法会继续对这个整数做一次base32编码,变成一个字符串。
在redis里,经纬度用52位整数编码,放进了zset种,zset的value是元素的key,score是GeoHash的52位整数值。
redis提供了6个Geo指令。
十、scan
如何从海量的key种找出满足特定前缀的key列表?
redis提供一个指令keys,用来列出所有满足特定正则表达式字符串规则的key。缺点
- 一次性吐出所有满足条件的key
- keys是遍历算法,如果有千万级别的key,会导致服务卡顿。
redis在2.8版本后加入了scan指令解决这个问题。scan具有以下特点:
- 复杂度虽然也是o(n),但是它通过游标分步进行,不会阻塞线程。
- 提供limit参数,可以控制每次返回结果的最大条数。
- 提供模式匹配功能
- ...