转自:https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/Redis/Redis.md
Redis简介:
简单来说,Redis就是一个数据库,不过与传统数据库不同的是Redis的数据是存在内存中的,所以读写速度非常快,因此Redis被广泛应用于缓存方向。另外,Redis也经常用来做分布式锁。Redis提供了多种数据类型来支持不同的业务场景。除此之外,Redis支持事物、持久化、LUA脚本、LRU驱动事件、多种集群方案。
为什么要用Redis/为什么要用缓存?
主要从“高性能”,“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据,这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下次再访问这些数据就可以直接从缓存中读取了。操作缓存就是操作内存,速度相当快。如果数据库中的对应数据发生改变,同步改变缓存中的数据即可。
高并发:
直接操作缓存能够承受的请求是远远大于直接操作数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存而不用经过数据库。
为什么要用Redis而不用map/guava做缓存?
缓存分为本地缓存和分布式缓存。以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量及快速,生命周期随着JVM的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用Redis或者memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持Redis或memcached服务的高可用,整个程序架构上比较复杂。
Redis和memcached的区别:
1)Redis支持更丰富的数据类型(支持更负责的应用场景):Redis不仅支持简单的key/value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcached支持简单的数据类型:String。
2)Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载并使用,而memcached把数据全部存在内存之中。
3)集群模式:memcached没有原生的集群模式,需要依靠客户端来实现集群中分片写入数据,但Redis目前是原生支持cluster模式的。
4)memcached是多线程,非阻塞IO复用的网络模型,Redis使用单线程的多路IO复用模型。
Redis常见数据结构及使用场景分析:
1、String
常用命令:set、get、decr、incr、mget等
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数据。常规key-value缓存应用:常规计数:微博数,粉丝数等。
2、Hash
常用命令:hget、hset、hgetall等
hash是一个String类型的field和value的映射表,hash特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。比如我们可以hash数据结构来存储用户信息,商品信息等。
key=JavaUser28976
value={
“id”:1,
“name”:Rain,
"age":22
}
3、List
常用命令:lpush、rpush、lpop、rpop、lrange等
list就是链表,Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的list结构来实现。
Redis list的实现为一个双向,可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这是个很棒的功能,基于Redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的功能,性能高。
4、Set
常用命令:sadd、spop、smembers、sunion等
set对外提供的功能与list类似是一个列表功能,特殊之处在于set是可以自动排重的。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了某个成员是否在一个set集合内的重要接口,这个也是list不能提供的。可以基于set轻易实现交集、并集、差集的操作。
比如,在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现共同关注,共同粉丝、共同喜好等功能。这个过程就是取交集的过程。
5、Sorted Set
常用命令:zadd、zrange、zrem、zcard等
和Set相比,sorted set增加了一个权重参数score,使得集合中的元素能按score进行有序排序。
举例:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕信息(可以理解为按消息维度的信息排行榜)等信息,适合使用Redis的Sorted Set结构进行存储。
Redis设置过期时间:
Redis中有个设置时间过期的功能,即在存储在Redis数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如项目中的token或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目的性能。
我们 set key的时候,可以给一个expire time,就是过期时间,通过过期时间我们可以指定这个key可以存活的时间。
假如你设置了一批key只能存活一个小时,那么接下来一个小时后,Redis是怎么对这批key进行删除的?
定期删除+惰性删除
定期删除:Redis默认是每隔100ms就随机抽取一些设置了过期时间段的key,检查其是否过期,如果过期就删除。注意,这里是随机抽取的,为什么要随机呢?你想一想假如Redis存了几十万个key,如果没隔100ms就遍历所有设置过期时间的key,会给CPU带来很大负担的。
惰性删除:定期删除可能会导致很多过期key到了时间并没有被删除掉,所以有了惰性删除。假如你的过期key靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一个那个key,才会被Redis删除,这就是所谓的惰性删除。
但是仅仅通过设置过期时间还是有问题的。我们想一下,如果定期删除漏掉了很多过期key,然后你也没有及时去查,也就没走惰性删除,此时会怎么样?如果大量过期的key堆积在内存中,导致Redis内存块耗尽了,怎么解决这个问题呢?Redis内存淘汰机制。
Redis内存淘汰机制(MySQL里有2000W数据,Redis中只有20W数据,如何保证Redis中的数据都是热点数据?)
Redis提供六种数据淘汰策略:
1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰;
2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰;
3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰;
4)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的);
5)allkeys-random:从数据集中任意选择数据淘汰;
6)no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入的数据时,新写入操作会报错!(这个应该没人会使用吧 ̄□ ̄||。。)
4.0版本后增加以下两种:
7)volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰;
8)allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。
Redis持久化机制(怎么保证Redis挂掉之后再重启数据可以进行恢复?)
很多时候我们需要持久化数据,也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器,机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis不同于memcached的一点就是,Redis支持持久化,而且支持两种不同的持久化操作。一种是快照(snapshotting,RDB),一种是只追加文件(append-only file,AOF)。
快照持久化:
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间节点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在Redis.conf文件中默认有以下配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
AOF持久化:
与快照持久化相比,AOF持久化的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF方式的持久化,需要自己通过appendonly参数开启:
appendonly yes
开启AOF持久化后每执行一条会更改Redis中数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。
在Redis的配置文件中存在三种不同的AOF持久化方式,分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑appendfsync everysec选项,让Redis每秒同步一次AOF文件,Redis性能几乎不收到任何影响。而且即使出现系统崩溃,用户最多只丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
Redis4.0对于持久化机制的优化:
Redis4.0开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
如果把混合持久化打开,AOF重写的时候就直接把RDB的内容写到AOF文件开头。这样做的好处是可以结合RDB和AOF的优点,快速加载的同时避免丢失过多的数据。当然缺点也是有的,AOF里面的RDB部分是压缩格式不再是AOF格式,可读性较差。
补充内容:AOF重写
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无需对现有AOF文件进行任何读入、分析或者写入操作。
在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。
Redis事物:
Redis通过MULTI、EXEC、WATCH等命令来实现事物功能。事物提供一种将多个命令请求打包,然后一次性、按顺序执行多个命令的机制,并且在事物执行期间,服务器不会中断事物而改去执行其他客户端的命令请求,它会将事物中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
在传统的关系型数据库中,常常用ACID性质来检验事物功能的可靠性和安全性。在Redis中,事物总是具有原子性,一致性和隔离性,并且当Redis运行在某种特定的持久化模式下时,事物也具有持久性。
缓存雪崩和缓存穿透问题解决方案?
缓存雪崩:
缓存同一时间大面积失效,所以,后面的请求都会落在数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
事前:尽量保证整个Redis集群的高可用性,发现机器宕机尽快补上,选择合适的内存淘汰策略。
事中:本地echcache缓存+hystrix 限流&降级,避免MySQL崩掉。
事后:利用Redis持久化机制保存的数据尽快恢复缓存。
缓存穿透:
一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法:有很多种方法可以有效解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更简单粗暴的方式,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们依然将这个空结果缓存,但它过期的时候很短,最长不超过五分钟。
参考:
如何解决Redis的并发竞争key问题?
所谓Redis的并发竞争Key的主要问题也就是多个系统同时对一个key进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果不同。
推荐一种方案:分布式锁(zookeeper和Redis都可以实现分布式锁)(如果不存在Redis并发竞争key的问题,不要使用分布式锁,这样会影响性能)。
基于zookeeper临时有序节点可以实现分布式锁。大致的思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放而产生死锁的问题。完成业务流程后,删除对应子节点释放锁。
参考:
如何保证缓存与数据库双写时的数据一致性?
你只要用缓存,就可能涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性的问题?
一般来说,如果你的系统不是严格要求缓存、数据库必须一致的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统吞吐量大幅度的下降,用比正常情况下多几倍的机器去支撑线上的一个请求。
一些问题:
Redis常见性能问题和解决方案?
1)Master最好不要做任何持久化的工作,如RDB内存快照和AOF日志文件;
2)如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次;
3)为了主从复制的速度和连接的稳定性,Master和Slave最好在同一局域网内;
4)尽量避免在压力很大的主库上增加从库;
5)主从复制不要用图状结构,用单向链表结构更为稳定,即:Master<--Slave1<--Slave2<--Slave3...
这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂掉了,可以立刻启用Slave1做Master,其他不变。
使用Redis有哪些好处?
1)速度快,因为数据存储在内存中,类似于HashMap,HashMap的优势就是查找和操作的世界复杂度都是O(1);
2)支持丰富数据类型,支持String、List、Set、ZSet,Hash;
3)支持事物,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行;
4)丰富的特性,可用于缓存,消息,按key设置过期时间,到期后会自动删除。
Redis相比memcached有哪些优势?
1)memcached所有的值均是简单的字符串,Redis作为其替代者,支持更为丰富的数据类型;
2)Redis速度比memcached快很多;
3)Redis支持持久化数据。
MySQL里有2000W数据,Redis中只有20W数据,如何保证Redis中的数据都是热点数据(Redis的淘汰机制)?
Redis内存数据集大小上升到一定大小的时候,就是实行数据淘汰策略。Redis提供6种数据淘汰策略:
1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰;
2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰;
3)volatile-random:从已设置过期时间的数据集中随机选择数据淘汰;
4)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰;
5)allkeys-random:从数据集中任意选择数据淘汰;
6)no-enviction(驱逐):禁止驱逐数据。
Memcached与Redis的区别都有哪些?
1)存储方式:
Memcached把数据全部存储在内存中,断电后会挂掉,数据不能超过内存大小;
Redis有部分存在硬盘上,这样能保证数据的持久性。
2)数据支持类型:
Memcached对数据类型支持相对简单,只支持String类型;
Redis有复杂的数据类型,包括:String,List,Set,ZSet,Hash五种;
3)使用底层模型不同:
它们之间底层实现方式以及与客户端之间通信的应用协议不一样。
Redis直接构建了自己的VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4)value大小:
Redis最大可以达到1G,而memcached只有1MB。
Redis同步机制?
从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
是否使用过Redis集群,集群的原理是什么?
Redis Sentinal着眼于高可用,在Master宕机的时会自动将Slave提升为Master,继续提供服务;
Redis Cluster着眼于扩展性,在单个Redis内存不足时,使用Cluster进行分片存储。
Redis的并发竞争问题如何解决?
Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时,数据转换错误,阻塞,客户端关闭等问题,这些问题均是由于客户端连接混乱造成。对此有2种解决方案:
1)客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized;
2)服务器角度,利用setnx实现锁。
注:对应第一种,需要应用程序自己处理资源的同步,可以使用的方法比较通俗,可以使用synchronized也可以使用lock;第二种需要用到Redis的setnx命令,但是需要注意一些问题。
Redis中两种持久化过程?
1)Snapshotting(快照):
- Redis调用fork,现在有了子进程和父进程;
- 父进程继续处理客户端的请求,子进程负责将内存内容写入到临时文件。由于os的临时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时,os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程地址空间内的数据是fork时刻整个数据库的快照。
- 当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。客户端也可以使用save或者bgsave命令通知Redis做一次快照持久化。save操作是在主线程中保存快照的,由于Redis是用一个主线程来处理所有客户端的请求,这种方式会阻塞所有客户端的请求。所以不推荐使用。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步变更数据。如果数据量大且写的操作比较多,必然会引起大量的磁盘IO操作,可能会严重影响性能。
2)AOF(append-only file)
- 由于快照方式是在一定时间间隔执行一次的,所以如果Redis意外down掉的话,就是失去最后一次快照后的所有修改。如果应用要求不能丢失任何修改的话,可以采用AOF方式。AOF比快照方式有更好的持久性,在使用AOF持久化方式时,Redis会将每一个收到的写命令都通过write函数追加到文件中。当Redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内存中缓存write做的操作,所以可能不是立刻写到磁盘上的。这样AOF方式的持久化也可能丢失部分数据。不过我们可以通过配置文件告诉Redis我们想要通过fsync函数强制os写入磁盘的时机。有以下三种方式:
appendonly yes //启用 aof 持久化方式
# appendfsync always //收到写命令就立即写入磁盘,最慢,但是保证完全的持久化
#appendfsync everysec //每秒钟写入磁盘一次,在性能和持久化方面做了很好的折中
# appendfsync no //完全依赖 os,性能最好,持久化没保证
AOF的方式也同时带来了另一个问题,持久化文件会越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其中99条是多余的。为了压缩AOF的持久化文件,Redis提供了bgrewriteaof命令,收到此命令Redis将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件。