Redis
(1)介绍
REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。
Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
基本数据结构
- String
- Hash
- List
- Set
- Sorted Set(有序集合)
(2)特点
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
(3)优势
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
(4)Redis与其他key-value存储的区别
1.Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。
2.Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
3.Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
(5)Hello,Redis
1.在安装目录下输入命令redis-server.exe redis.windows.conf
2.另在redis目录下启动一个cmd窗口,原来的不要关闭,否则无法访问服务端
输入命令:redis-cli.exe -h 127.0.0.1 -p 6379
然后设置键值对:set mykey abc
取出键值对:get mykey
(6)Redis配置
Redis的配置文件位于安装目录下,文件名为redis.windows.conf,可以通过config命令查看或者设置配置项。
eg:config get * :查看所有配置项
(7)Redis数据类型
Redis支持五种数据类型:String、Hash(哈希)、List(列表)、Set(集合)、Sorted Set(有序集合)
String
String类型是二进制安全的,意思是Redis中String类型可以包含任何数据,比如jpg图像或者序列化的对象;
String类型是Redis最基本的数据类型,String类型最大能存储512MB;
set命令:设置一个key对应的String类型数据
语法:set key “value”
eg:
Hash
Redis hash是一个键值对(key-value)集合;
Redis hash是一个String类型的filed和value的映射表,hash特别适合用于存储对对象
eg:
del stringkey:删除前面测试的key
测试中:
HMSET 设置了两个filed=>value对;
HGET获得filed对应的value;
每个 hash 可以存储 232 -1 键值对(40多亿);
List
Redis List列表是简单的字符串列表,按照插入的顺序排序,可以添加一个元素到列表头部左边或者尾部右边;
LPUSH:将数据加进key对应的列表
LRANGE:从start到stop显示列表的数据(下标从0开始)
列表最多可存储 232 - 1 元素 (4294967295, 每个列表可存储40多亿)。
Set(集合)
Redis的Set是String类型的无序集合;
集合是通过哈希表实现的,所以添加、删除、查找的复杂度都是O(1);
sadd命令:添加一个String元素到key对应的set集合中,成功返回1,如果元素已经在集合中返回0;
semembers key:查看key对应集合中的所有元素;
根据集合内元素的唯一性,第二次插入相同元素会被忽略;
集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。
ZSET(sorted set:有序集合)
Redis zset和set一样也是String类型元素的集合,不允许相同的元素;
不同的是zset中每个元素都会关联一个double类型的分数,redis是通过分数来为集合中的成员进行从小到大的排序;
zset中的成员是唯一的,但是score可以相同,可以多个成员关联一个score;
zadd命令:添加元素到集合中,成功返回1,元素在集合中存在则更新对应score(double类型分数),返回0;
语法:zadd key score member
eg:
(8)应用场景
- 缓存(数据查询、短连接、新闻内容、商品信息等)
- 聊天室的在线好友列表
- 任务队列(秒杀、抢购、12306等)
- 应用排行榜
- 网站访问统计
- 数据过期处理
- 分布式集群架构中的session分离
(9)Redis持久化方案
RDB方式
RDB:Redis DataBase;
Redis默认通过快照(RDB)来将数据持久化到磁盘中,在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时是将快照文件直接读到内存;
步骤
1.设置持久化快照的条件;
2.指定持久化文件存储的目录;
快照保存过程
1.Redis单独创建一个子进程来进行持久化;
2.先将数据写入到一个临时文件中;
3持久化过程结束后,用临时文件替换上次持久化好的RDB文件,子进程退出;
在整个过程中,主进程是不进行任何IO操作的,这保证了极高的性能,如果需要进行大规模的数据的恢复,且对于数据恢复的完整性不敏感,那么RDB方式比AOF方式更高效;
触发机制
- 满足save规则,会自动触发RDB;
- 执行flushall命令,会触发RDB;
- 退出redis也会产生RDB文件;
优点
大规模的数据恢复;对数据完整性要求不高;
缺点
最后一次持久化之后的数据可能丢失;子进程占用一定的内存空间;需要一定时间隔离进程操作;
AOF方式
AOF:Append Only File;
一旦Redis非法关闭,那么会丢失最后一次持久化之后的数据;如果数据不能允许丢失,那么要使用aof方式,Redis默认不使用AOF持久化;
实现过程
AOF以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件构建数据,即根据日志文件内容将写指令从前往后执行一次来完成数据的恢复工作;
优点
每一次修复都同步,文件完整性好;
缺点
数据文件大、修复速度慢;运行效率比RDB慢;
(10)Redis的主从复制
主从复制
什么是主从复制
持久化保证了即使redis服务重启也不会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,如果通过从redis的主从复制机制就可以避免这种单点故障;
说明
- 主redis中的数据有两个副本即从redis_1和从redis_2,即使一台redis服务器宕机其他两台redis服务也可以继续提供服务;
- 主redis中的数据和从redis中的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上;
- 只有一个主redis,可以有多个从redis;
- 主从复制不会阻塞master,在同步数据时,master可以继续处理client请求;
- 一个redis可以既是主又是从;
主从复制作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式;
- 故障恢复:当节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis时应用连接从节点),分担服务器负载;尤其是在写多读少的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量;
哨兵
哨兵模式
后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库;
哨兵模式是一种特殊的模式,Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程它会独立运行;
哨兵原理
哨兵通过发送命令,等待Redis服务器的响应,从而监控运行的多个Redis实例;
哨兵作用
通过发送命令,让Redis服务器返回,监控其运行状态,包括主服务器和从服务器;
当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,切换主机;
多哨兵模式
只有一个哨兵监控Redis服务器时可能会出问题,因此可以使用多个哨兵监控,各个哨兵之间也各自监控,形成多哨兵模式;
优点
基于主从复制模式,监测Redis服务器;
主从可以切换、故障实现转换、系统可用性更高;
手动部署,自动实现;
缺点
配置麻烦;
集群容量达到上限后,在线扩容困难;
(11)Redis集群
架构细节
- 所有的redis节点彼此互联(Ping-Pong机制),内部使用二进制协议优化传输速度和带宽;
- 节点的fail是通过集群中超过半数的节点检测失效时才生效;
- 客户端与redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可;
- Redis集群中内置了16384个哈希槽,当需要在Redis集群中放置一个key-value时,redis先对key使用src16算法算出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,redis会根据节点数量大致均等地将哈希槽映射到不同节点;
Redis-cluster投票:容错
- 集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信超过cluster-node-timeout,认为该master节点挂掉;
- 什么时候整个集群不可用:集群任意master挂掉且当前master没有slave、集群超过半数以上master挂掉无论是否有slave集群进入fail状态;
(12)Redis缓存穿透、雪崩和击穿
缓存穿透
缓存穿透:是指查询一个一定不存在的数据,由于缓存没有命中,于是向持久层查询,但是依旧无法获得数据——这就导致对这个不存在的数据每一次请求都要去持久层中查询,失去了缓存的意义,也给持久层造成了很大的压力;
解决方案
有很多方法可以有效解决缓存穿透的问题,最常见的是采用布隆过滤器;
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
有一个简单粗暴的方法:如果一个查询返回的数据为空(不管是数据不存在还是系统故障),我们仍然会把这个空结果进行缓存,但它的过期时间会很短,这样保护了后端数据源;
但是这种方法存在问题:
1.如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中有很多空值键;
2.即使对空值设置了短的过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致的一致性业务有影响;
缓存雪崩
缓存雪崩:是指我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,使DB瞬间压力过大雪崩;(或者缓存服务器某个节点宕机)
解决方案
- Redis高可用——搭建Redis集群,保证某节点宕机的情况下继续提供服务;
- 限流降级——在缓存失效以后,通过加锁或者队列来控制读数据库、写缓存的线程数量,从而避免缓存失效时大量的并发请求落到底层存储系统上;
- 数据预热——在正式部署之前把所有可能的数据先预先访问一遍,使大部分可能被大量访问的数据加载到缓存中,并在大并发访问前手动触发加载缓存不同的key,设置不同的过期时间;
- 分散时间——可以在原有设计的缓存失效时间基础上增加一个随机值,比如1—5分钟随机,这样使得每一个缓存的过期时间重复率降低,就不容易引发集体失效的事件;
缓存击穿
缓存击穿:是指缓存在某一个时间点过期的时候,恰好在这个时间点对这个key有大量的并发请求过来,这类数据一般是热点数据,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发请求可能会瞬间把后端DB压垮;
解决方案
- 设置热点数据缓存永不过期
- 分布式锁——使用分布式锁,保证对于每个key同时只有一个线程去查询后端DB,其他线程没有获得分布式锁的权限,只需要等待缓存中的数据被这一个线程重新写入;(具体实现:互斥锁)
- 互斥锁(mutex key)——就是在缓存失效的时候(判断拿出来的值为空),不是立即去DB中加载,而是先使用缓存工具的某些带成功操作返回值的操作去set一个mutex key,当操作返回成功时,再进行DB的加载并回设缓存;否则,就重试整个get缓存的方法;实现方式:Redis的SETNX(set if not exists):当进行setnx操作时会获得锁;
public String get(key) {//模拟请求数据
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
- 提前使用互斥锁——在value内部设置1个超时值timeout1,timeout1比实际memcache timeout小,当从cache读取到timeout1过期时,马上延长timeout1并重新设置到cache,然后再从数据库中加载数据到cache中;
(13)Redis分布式锁
分布式锁:控制分布式系统不同进程共同访问共享资源的一种锁的实现;
SETNX互斥锁
存在问题
1.锁过期释放了,但是业务还没有执行完——假设线程A获得了锁,执行线程的任务,但是过了超时设置的时间,还没执行完,这时候线程B请求锁成功导致线程A中的任务并没有执行完成;
2.锁被别的线程误删——假设线程A执行完以后去删除key来释放锁,但是有可能持有锁的时间已经超时,锁已经被线程B持有,这时候线程A将线程B持有的锁释放了,导致线程B的任务没有执行完成;
Redission
Java的Redis客户端之一,RedissionLock这个类实现加锁/释放锁的操作;
Redission底层原理图
如图所示,只要线程一加锁成功,就会启动一个看门狗,它是一个后台线程,会每隔秒检查一下线程一是否还持有锁,如果还持有,就会不断地延长锁key地生存时间。因此,Redission解决了“锁过期释放,业务没有执行完”地问题;
Redlock
红锁是Redis官方提出的一种分布式锁的算法,算法认为只要(N/2+1)个节点加锁成功,那么就认为获取了锁,解锁时将所有实例解锁;
思想
RedLock 的思想是使用多台 Redis Master ,节点完全独立,节点间不需要进行数据同步,因为 Master-Slave 架构一旦 Master 发生故障时数据没有复制到 Slave,被选为 Master 的 Slave 就丢掉了锁,另一个客户端就可以再次拿到锁。锁通过 setNX(原子操作) 命令设置,在有效时间内当获得锁的数量大于 (n/2+1) 代表成功,失败后需要向所有节点发送释放锁的消息。
流程
1.顺序向五个节点请求加锁;
2.根据一定的超时时间来推断是不是跳过该节点;
3.(N/2+1)个节点加锁成功并且花费小于锁的有效期;
4.认定加锁成功;
(14)Redis使用要点
Redis的使用规范
1)key
-
以业务名为key前缀,需要用冒号隔开,防止key冲突覆盖;
-
key长度尽量小于30字符;
-
key尽量设置过期时间,保证不使用的key能被及时清理;
-
给key设置过期时间时,注意不同业务的key尽量过期时间分散,放置缓存雪崩;
2)value
- 控制value的大小,过多的大value值会导致查询慢;
- 选择合适的value数据类型;
- 可使用批量操作提高操作效率;
命令的使用规范
1)Flush
- 禁止使用Flushall(清空整个Redis服务器)、Flushdb(清空当前数据库);
- 这两个命令是原子性的;
2)Del
如果删除一个String类型的key,del时间复杂度是O(1),可以直接使用del;
如果删除一个List/Hash/Set/ZSet类型时,时间复杂度是O(n),元素越多越慢,会阻塞主线程;
删除方案:如果是List类型,执行lpop或者rpo,直到所有元素删除完成;如果是Hash/Set/ZSet类型,可以先执行hscan/sscan/scan查询,再执行hdel/srem/zrem依次删除每个元素;
实战时操作规范
1)分布式锁
2)缓存一致性
- 读请求时,先读缓存,后读数据库;
- 写请求时,先更新数据库,再写缓存;
- 每次更新数据后,需要清除缓存;
- 缓存一般都需要设置过期时间;
- 一致性要求高的话,可以使用biglog+MQ保证;
3)连接方式
使用短连接时每次都需要建立TCP三次握手、四次挥手,会增加耗时;而使用长连接可以建立一次连接后,一直使用redis命令,减少redis建立的时间;
合理设置连接池参数,长时间不操作redis时,也需要及时释放连接资源;
4)lazy-free机制
开启Redis中lazy-free机制后,如果删除一个big key时,释放内存的耗时操作会放到后台线程去执行,减少对主线程的阻塞影响;
保证Redis的可靠性
- 按不同的业务线部署Redis实例,当一个实例故障时,不影响其他业务;
- 部署主从集群,合理配置主从复制参数,避免数据丢失风险,降低服务不可用时间;
- 部署哨兵集群,实现故障时主机自动切换;