Redis
Redis
标签(空格分隔): SQL
1. 什么是Redis
Redis是基于内存的高性能
key-value
数据库.
2. Redis的数据类型
String, Hash, List, Set, Sorted Set.
3. 内部结构
Redis内部使用一个RedisObject对象来表示所有的
key
和value
.
- type: 表示一个
value
对象具体是哪一种数据类型. - encoding: 不同数据类型在redis内部的存储方式. 如:
type = String
代表value
存储的是一个普通字符串, 那么对应的encoding
可以使raw
或者是int
, 如果是int
则代表实际redis
内部是按照数值型类存储和表示这个字符串的, 当然前提是这个字符串本身可以用数值表示如"123"
,"345"
这样的字符串.
4. 内存淘汰机制
Redis内存淘汰机制指的是用户存储的一些键可以被Redis主动的从实例中删除, 从而产生读
miss
的情况, 那么redis
为什么要有这种功能呢? 这就是我们需要探究的设计初衷.redis
最常见的两种应用场景为缓存
和数据持久存储
. 首先需要明确的一个问题是内存淘汰策略更适合于那种场景? 是持久存储还是缓存?
假设我们有一个Redis服务器, 服务器的物理内存大小为1G
, 我们需要存在redis
中的数据量很小, 这看起来似乎足够用很长时间了, 但是随着业务量的不断增长, 我们在其中放的数据也会越来越多, 数据量大小似乎超过了1G
, 但是应用还可以正常运行, 这是因为操作系统的课件内存并不受物理内存的限制, 而是虚拟内存, 当物理内存不够用的时候, 操作系统会在硬盘上面划分出来一块虚拟内存, 那么这个时候我们的可用内存就是2^32
大约为3G
, 但是这个时候会在物理内存和虚拟内存之间发生频繁的内存交换(在访问虚拟内存中的数据的时候). 这种交换会严重的降低磁盘性能 和 Redis读写数据的性能. 所以我们需要一定的miss
来换取内存的使用效率.
4.1 如何用.
作为Redis用户我们如何使用Redis提供的这个特性呢?
maxmemory <bytes>
我们可以通过配置redis.conf
中的maxmemory
这个值来开启内存淘汰功能, 至于这个值有什么意义, 我们可以通过了解内存淘汰过程来了解.
- 客户端发起需要申请内存的命令.
- Redis检查内存的使用情况, 如果消耗的总内存大于
maxmemory
则根据用户配置的淘汰策略来淘汰key
, 从而换取一定的内存. - 如果上面都没问题, 则这个命令执行成功.
maxmemory
为0的时候表示我们的Redis的内存使用没有限制.
4.2 内存淘汰策略
内存淘汰是redis
提供的的一个功能, 为了更好的使用这个功能必须为不同的应用场景下提供不同的策略, 内存淘汰策略讲的是为实现内存淘汰策略我们应该具体怎么做, 要解决的问题包括键空间怎么选择, 在键空间中淘汰键如何选择?
- Redis提供了下面几种淘汰策略以供用户选择, 其中默认的策略为
noevication
策略:
- noeviction: 当内存使用达到阈值的时候, 所有引起申请内存的命令都会报错.
- allkeys-lru: 在主键空间中, 优先移除最近未使用的
key
. - volatile-lru: 在设置了过期时间的键空间中, 优先移除最近未使用的key.
- allkeys-random: 在主键空间中, 随机移除某个key.
- volatile-random: 在设置了过期时间的键空间中, 随机移除某个key.
- volatile-ttl: 在设置了过期时间的键空间中, 具有更早过期时间的key优先移除.
- 在这里补充一下主键空间和设置了过期时间的键空间, 举个栗子, 假设我们有一批键存储在Redis中, 则有那么一个哈希表用于存储这批键和值, 如果这批键中有一部分设置了过期时间, 那么这批键还会被存储到另一个哈希表中, 这个哈希表中的值对应的是键被设置的过期时间, 设置了过期时间的键空间为主键空间的子集.
下面看看几种策略的具体适用场景:
allkeys-lru
: 如果我们应对缓存的访问符合幂律分布(也就是存在相对热点), 或者我们不太清除我们应用缓存访问分布状况, 我们可以采取allkeys-lru
. 相对于volatile-lru
占用的内存空间更低.volatile-lru
: 策略适合我们将一个Redis实例即应用于缓存又应用于数据持久化的时候, 然而我们也可以通过使用两个Redis实例, 来达到相同的效果, 值得一提的是将key
设置的有过期时间的时候实际上会消耗更多的内存(主键空间/设置了过期时间的主键空间).allkeys-random
: 如果我们的应用对于缓存key
的访问概率相等的话, 则可以使用这个策略.volatile-random
: 策略适合我们将一个Redis实例即应用于缓存又应用于数据持久化的时候, 然而我们也可以通过使用两个Redis实例, 来达到相同的效果, 值得一提的是将key
设置的有过期时间的时候实际上会消耗更多的内存(主键空间/设置了过期时间的主键空间).volatile-ttl
: 使得我们可以向Redis提示那些key
更适合被eviction
5. 非精准的LRU
上面提到的LRU(Least Recently Used)策略, 实际上Redis实现的LRU并不是可靠的LRU, 也就是名义上我们使用LRU算法淘汰键, 但实际上被淘汰的键并不一定是真正的最久没用的, 这里涉及到一个权衡的问题, 如果需要在全部键空间内搜索最优解, 则必然会增加CPU的负载, 但是因为Redis是单线程的, 也就是同一个实例在每一个时刻只能服务于一个客户端, 所以耗时的操作一定要谨慎. 为了在一定成本内实现相对的LRU, 早起的Redis版本是基于采样的LRU, 也就是放弃全部键空间内搜索解改为采样空间内搜索最优解. 自从Redis3.0之后,其作者对LRU算法进行了优化, 目的就是在一定的成本内得到的结果更加接近真实的LRU.
- 在这种情况下就有可能发生热点数据被删除然后 发生缓存击穿的情况.
6. 缓存穿透, 雪崩, 击穿.
6.1 缓存穿透.
如果有人恶意的向服务器获取并不存在的数据, 这个时候Redis服务器就不产生作用了, 如果发生大量这样的情况的时候, 会给数据库带来巨大的压力.
实现流程如下:
- 参数传入对象主键ID
- 根据key从缓存中获取对象.
- 如果对象不为空, 直接返回.
- 如果对象为空, 则从数据库读取.
- 如果对象不为空, 则存储到缓存并返回.
- 如果传入的主键Id为-1, 并且开始大量的查询的时候就产生了上述状况.
实际上在工作过程中, 如果第五步发现对象为空的时候, 会给该对象的值设为空, 存储到缓存中, 并且设置过期时间(60S).
6.2 缓存雪崩.
在某一个时间段, 缓存集中过期失效. 例如电商项目, 在某个时间段 大量的商品被放入缓存, 然后它们又集体过期, 这个时候如果有查询的话会给数据库带来比较大的压力. 造成缓存雪崩.
在存入缓存的时候, 在设置的过期时间上可以加上一个随机数, 防止集体过期这种情况出现. 随机数在热门资源上可以长一点, 冷门资源上可以短一点.
6.3 缓存击穿
某一个非常热门的资源在缓存数据库中, 如果某个时间点该缓存过期会造成大量线程去数据库访问并缓存该资源, 一般情况下是不会产生这样的情况的, 如果出现这种情况可以直接设置永不过期(更新数据的时候另操作), 或者使用mutex key
互斥锁.
7. Redis High Available
高可用集群, 是保证业务连续性的有效解决方案, 一般有两个或者两个以上的节点, 且分为活动节点和备用节点. 通常把正在执行业务的成为活动节点, 而作为活动节点的一个或多个备份成为备份节点. 当活动 节点出现问题的时候导致正在运行的业务中断, 备用节点此时就会侦测到, 并理解使用备用节点执行也许, 从而实现业务的连续性.
Redis一般以主/从方式部署(这里讨论的从实例
主要用于备份, 主实例提供读写) 实现HA主要有如下几种方案:
- keepalived: 通过keepalived的虚拟IP, 提供主从的统一访问, 在主节点出现问题的时候, 通过keepalived运行脚本将从节点提升为主节点, 等到主节点回复之后 先将数据同步, 然后将自动变回主节点. 这样做的优点是应用程序不需要知道Redis那边出现的状况(虚拟的IP不变), 坏处是引入keepalived增加部署复杂度, 而且在某些情况下会导致丢失数据.
- Zookeeper: 通过Zookeeper主从实例, 维护最新的有效的IP, 应用通过Zookeeper去取得IP对Redis进行访问, 该方案需要编写大量的监控代码.
- sentinel: 通过Sentinel监控主从实例, 自动进行故障恢复, 该方案有个缺陷: 因为主从实例地址(IP&PORT)是不同的, 当故障发生主从切换之后, 应用程序无法知道新地址, 故在Jedis2.2.2中新增了对Sentinel的支持, 应用通过
redis.clients.jedis.JedisSentinelPool.getResource()
取得的Jedis实例会及时更新到新的主实例地址.