Memcache:代码层次类似Hash
- 支持简单数据类型
- 不支持数据持久化存储
- 不自持主从
- 不支持分片
Redis
- 数据类型丰富
- 支持数据磁盘持久化存储
- 支持主从
- 支持分片
为什么Redis能特别快
官方数据,可以达到10万+QPS(QPS即query per second,每秒内查询次数)
- 完全基于内存,绝大部分请求是内存操作,执行效率高
- 数据结构简单,对数据操作也简单
- 使用I/O多路复用模型,非阻塞IO
- 采用单线程(单线程也能处理高并发请求),避免多线程上下文切换。想要多核也可以启动多个实例。处理网络请求的时候只有一个线程,但是实际上一个Server肯定不止一个线程,例如Redis持久化的时候以之进程或者子线程的形式执行。
I/O多路复用模型
FD:File Descriptor 文件描述符
一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的元数据到文件本身的映射。
Redis采用的I/O多路复用函数:epoll/kqueue/select
- 根据不通的平台选择
- 优先选择时间复杂度为O(1)的多路复用函数做为底层实现
- 以时间复杂度为O(n)的select作为保底,(windows平台没有epoll)
- 基于react设计模式监听I/O事件
Redis支持的数据类型
- String:最基本的数据类型,二进制
- Hash:string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。每个 hash 可以存储 2^32 - 1 键值对(40多亿)
- List:字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边),一个列表最多可以包含 2^32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
- Set:String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
- Sorted Set:和Set相同,元素不可以重复,不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 23^2 - 1 (4294967295, 每个集合可存储40多亿个成员)
Redis的过期key删除机制
-
定期删除:redis每100ms会定期去抽一批设置了过期时间的key去检查是否过期
-
惰性删除:当你通过redis获取该key的时候,redis会去check一下
这样就会导致一个问题,假设一个key过期了,程序也不去访问。他就会一直存在内存当中,所以可能会导致内存满掉,因此Redis还引入了内存淘汰机制。
Redis内存淘汰策略
主要采用了6种方式进行内存对象的释放操作
volatile-lru
:从设置了过期时间的数据集中,选择最近最久未使用的数据释放allkeys-lru
:从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放volatile-random
:从设置了过期时间的数据集中,随机选择一个数据进行释放allkeys-random
:从数据集中(包括了设置过期时间以及未设置过期时间)随机选择一个数据进行入释放volatile-ttl
:从设置了过期时间的数据集中,选择马上就要过期的数据进行释放操作noeviction
:不删除任意数据(但redis还会根据引用计数器进行释放呦~),这时如果内存不够时,会直接返回错误
缓存问题
缓存穿透
大量数据库中不存在的数据,查询不到对应的数据,导致缓存中也没有数据,每次都查询数据库。
解决方案:
- 缓存空对象
- 布隆过滤器,要求缓存的key相对固定,或者能够动态的更新布隆过滤器
- 设置校验规则,防止攻击
- 访问限流
缓存击穿
大量的请求访问的都是同一个key,但是key在某一个时刻过期,导致大量的请求全部落在数据库中,给数据库造成压力,甚至打垮数据库。
解决方案:
- 互斥锁(redis锁):优点是思路简单,保证一致性;缺点是代码复杂度增加,存在死锁的风险。
- 缓存永不过期,定时更新缓存或者缓存逻辑过期:优点是杜绝key重建问题;缺点是逻辑过期不保证一致性,逻辑过期增加维护成本。
缓存雪崩
缓存服务器宕机,大量的请求落到数据库层,造成数据库压力
解决方案:
- 缓存高可用
- 客户端降级(内存缓存)
- 提前演练缓存雪崩。
大量的Key同时过期注意事项
集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象
解决方案:在设置key的过期时间的时候,给每个key加上随机值,使得过期时间分散,避免卡顿。
从海量key里面查询出某一固定前缀的key?
keys [pattern]
: 查找所有符合给定模式pattern的key
缺点:keys指令一次性返回所有匹配的key;key的数量过大会使得服务卡顿;
通过scan命令从海量key里面查询出某一固定前缀的key
scan cursor [pattern] [count]
通过游标无阻塞的提取指定模式的key列表
- 基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
- 以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历
- 每次返回的游标可能比上一次小,因此可能获取到重复的key,需要自己代码做去重
- 不保证每次执行都返回给定数量的元素,支持模糊查询
- 一次返回的数量不可控,只能是大概率符合count参数
如何通过Redis实现分布式锁
互斥性:任意时刻只能由一个客户端获取锁
安全性:只能由获取该锁的客户端删除,不能由其它客户端删除
避免死锁:获取锁的客户端可能宕机没有释放锁,导致其它客户端无法获取锁
容错:redis部分节点宕机时,客户端仍然能够获取锁、释放锁
原子命令: set key value [EX seconds] [PX milliseconds] [NX|XX]
从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
- EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
- PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行PSETEX key milliseconds value 。
- NX : 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。
- XX : 只在键已经存在时, 才对键进行设置操作。
jedis-3.3.0.jar
@Test
public void contextLoads() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
SetParams setParams = new SetParams();
//setParams.nx().ex(10);//key不存在时,设置key,并设置2s过期
setParams.nx().px(10000);//key不存在时,设置key,并设置2000ms过期
String result = jedis.set("lock", "11", setParams);
if ("OK".equals(result)) {
System.out.println("获取锁成功:" + result);
} else {
System.out.println("获取锁失败:" + result);
}
}
spring-data-redis-2.4.3.jar
stringRedisTemplate.opsForValue().setIfAbsent("aa", "11",10000, TimeUnit.MILLISECONDS);
使用redisson客户端自带redis锁实现方法
如何使用Redis做异步队列
使用List作为队列
RPUSH生产消息,LPOP 消费消息,但是不会等待;BLPOP:移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
缺点:同一条数据只能一个消费者消费。
pub/sub 主题订阅者模式
发送者(pub)发送消息,订阅者(sub)接收消息;订阅者可以订阅任意数量的频道;
缺点: 消息的发布是无状态的,无法保证可达
Redis如何做持久化
-
RDB(快照)持久化:保存某个时间点的全量数据快照。
save 900 1 #900s内有1条写入指令,进行一次备份 save 300 10 #300s内10条写入,进行一次备份 save 60 10000 #60s内10000条写入,进行一次备份 #save "" #配置后会禁止RDB持久化
stop-writes-on-bgsave-error yes 建议开启 #当备份进程出错时,主进程停止接收新的写入操作,这样可以保护持久化数据一致性问题
rdbcompression no 开启表示把rdb文件压缩后再保存,建议关闭 #redis属于cpu密集型服务,开启压缩会带来更多的cpu消耗
手动创建RDB文件
save
命令:阻塞Redis的服务器进程,直到RDB文件被创建完成。很少使用,因为在redis主线程中保存,客户端会出现短暂的不可用。
BGSAVE
:Fork出一个之进程来创建RDB文件,不阻塞服务器进程。
通过lastsave
:查看上次创建RDB的时间。自动化创建RDB文件
- 根据redis.conf配置里的save m n定时触发(用的是BGSAVE)
- 主从复制时,主节点自动触发
- 执行Debug Reload
- 执行Shutdown且没有开启AOF持久化会触发
RDB持久化优点:
全量数据快照,文件小,恢复快RDB持久化缺点:
- 内存数据的全量同步,数据量大会由于I/O而影响性能
- 可能会因为Redis挂掉而丢失当前至最近一次快照期间的数据
-
AOF(Append-Only-File)持久化:保存写状态
记录除查询以外的所有变更数据库状态的指令,以append的形式增量追加保存到AOF文件中。默认关闭appendonly no appendfilename "appendonly.aof" # appendfsync always 主要有变更则写入aof文件 appendfsync everysec 每隔1s,向aof文件写入操作记录 # appendfsync no 由操作系统决定,一般操作系统是等待缓冲区填满,写入磁盘
AOF优点:可读性高,适合保存增量数据,数据不易丢失
AOF缺点:aof文件会不断的增大,文件体积大,恢复时间长
解决方案:通过bgrewriteaof
命令日志重写解决AOF文件不断增大问题。
原理如下:- 调用fork(),创建一个子进程
- 子进程把新的AOF写到一个临时文件里,不依赖原来的AOF文件
- 主进程持续将新的变动同时写到内存和原来的AOF里面
- 主进程获取子进程重写AOF的完成信号,将内存中的记录往新的AOF同步增量变动
- 使用新的AOF文件替换旧的AOF文件
-
Redis4.0 RDB-AOF混合持久化
BGSAVE做镜像全量持久化,AOF做增量持久化
Redis数据的恢复
重启redis,如果存在AOF文件则直接加载AOF文件,如果不存在则尝试加载RDB文件。
使用Pipeline的好处
- 和linux的管道类似
- Redis基于请求/响应模型,单个请求处理需要一一应答
- Pipeline批量执行指令,节省多次I/O往返的时间,前提是命令之间没有依赖
Redis的同步机制
主从同步原理:
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
增量同步
Redis增量复制是指Slave初始化后开始正常工作时,主服务器发生的写操作,同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
Redis Sentinel
解决主从同步Master宕机后的主从切换问题。
监控:检查主从服务器是否运行正常
提醒:通过API向管理员或其它应用程序发送故障通知
自动故障迁移:主从切换
Redis的集群原理
一致性Hash原理与实现 https://www.jianshu.com/p/528ce5cd7e8f
一致性哈希算法:对2^32次方取模,将哈希值空间组织成虚拟的圆环。