本文内容大量转自Java技术栈、 菜鸟架构 ,再加自己的理解有少许改动。
请大家关注公众号【Java技术栈 】【 菜鸟架构 】,尊重作者的辛苦付出。
本文只是为了方便学习。
如给原作者带来不便,请联系我删除。
考虑到绝大部分写业务的程序员,在实际开发中使用 Redis 的时候,只会 Set Value 和 Get Value 两个操作,对 Redis 整体缺乏一个认知。所以我斗胆以 Redis 为题材,对 Redis 常见问题做一个总结,希望能够弥补大家的知识盲点。
主要分为:
Remote Dictionary Server。
Redis本质上是一个Key-Value类型的内存型数据库,如果开启了持久化会定期异步把数据flush到硬盘。因为是纯内存操作,Redis的性能非常出色,读写可达10万/S,是已知性能最快的Key-Value DB。
相对于memcached
来说,Redis的出色之处不仅是性能,还支持多种数据结构;此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis应用广泛。
比如用List来做FIFO双向链表,实现一个轻量级的高性能MQ;用Set可以做高性能的tag系统;Redis还可以对存入的Key-Value设置expire时间,因此也可以被当作一个功能加强版的memcached来用。
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
我觉得在项目中使用 Redis,主要是从两个角度去考虑:性能和并发。
当然,Redis 还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件,如 Zookeeper 等代替,并不是非要使用 Redis。
如下图所示,我们在碰到需要执行耗时特别久且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存,使得以后的请求直接从缓存中读取,能够快速响应。
也就是说,在适合的情况下使用Redis做缓存,我们可以大大提升请求的响应速度。
理论上Redis可以处理多达2^32的keys,并且在实际中进行了测试,每个实例至少存放了2亿5千万的keys。我们正在测试一些较大的值。
换句话说,Redis的存储极限是系统中的可用内存值。
可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的,
所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。
如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。
这个时候,就需要使用 Redis 做为缓存首先来面对高并发的请求压力,而不是直接把压力丢给数据库。
Redis支持丰富的数据类型,主要有:字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets) , bitmaps, hyperloglogs 和 地理空间(geospatial)
String
是最简单Redis类型,就是一个key对应一个value的pair。如果你只用这种类型,Redis就像一个可以持久化的memcached服务器(注:memcache的数据仅保存在内存中,服务器重启后,数据将丢失)。
可以用String类型做原子类型计数器:
Redis Hash类型类似Java中的HashMap,他是一个拥有指定key名字的,由field和value组成的键值对映射表,注意field和value都是String。Hash 可以存储2^32 - 1 个键值对(40多亿)。值得注意的是,小的 hash 被用特殊方式编码,非常节约内存。
Redis Hash一般用于存储对象。
按插入顺序排序的字符串元素的集合,数据结构是链表(linked list)。这意味着在一个list中有数百万个元素,在头部或尾部添加一个元素的操作,其时间复杂度也是常数级别的。但是,如果要用索引进行随机访问,那么效率会比较低。Redis list 使用链表的主要考虑就是能快速插入数据。
List 可以做简单的消息队列MQ(利用LPUSH和RPOP或是阻塞的BRPOPLPUSH和BRPOP,详情点击这里 )。也可以利用 lrange 顺序范围访问命令做基于 Redis 的高性能分页功能。
不重复且无序的字符串元素的集合,类似java中的hashset,可以求交集、并集、差集等。
类似Set,但Sorted Set中的每个字符串元素关联到一个叫score浮动数值,集合中的元素按 Score 进行排序。所以它是可以进行有序搜索的元素集合(例如取出前面10个或者后面10个元素)。
Sorted Set可以做排行榜应用如求 TOP N 、范围查找等。
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品.
异步复制
ping
一致性是分布式重要问题,可分为强一致性和最终一致性。
数据库和缓存双写,就必然会存在不一致的问题。需明白一个前提:即如果对数据有强一致性要求,就不能放缓存。我们为缓存所做的一切,只能保证最终一致性。
另外,我们所做的方案从根本上来说,只能说降低不一致发生的概率,无法完全避免。
因此,有强一致性要求的数据,不能放缓存。
回答:首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
如果有大并发、流量达到几百万时,这两个问题一定要深入研究。
即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而导致数据库连接异常。
假如客户端每秒发送5000个请求,其中4000个为黑客的恶意攻击,即在数据库中也查不到。举个例子,用户id为正数,黑客构造的用户id为负数,
如果黑客每秒一直发送这4000个请求,缓存就不起作用,数据库也很快被打死。
缓存雪崩,即缓存同一时间大面积的失效。这个时候又来了一波请求,结果请求都打到数据库上,从而导致数据库连接异常甚至崩溃。
假设有如下一个系统:高峰期请求为5000次/秒,4000次走了缓存,只有1000次落到了数据库上,数据库每秒1000的并发是一个正常的指标,完全可以正常工作。但如果缓存宕机了,每秒5000次的请求会全部落到数据库上,数据库立马就死掉了,因为数据库一秒最多抗2000个请求,如果DBA重启数据库,立马又会被新的请求打死了,这就是缓存雪崩。
事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死
事后:redis持久化,快速恢复缓存数据
随机过期时间
给缓存的失效时间再加上一个随机值,避免缓存同一时间集体失效
使用互斥锁
缓存失效的时候,先去获得锁SETNX
(SET if Not eXists
),得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试获取。但是该方案吞吐量明显下降了。
双缓存
有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作:
请求先读缓存 A ,有则返回,无则从B读 。并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。
此方案问题是缓存B导致Redis占用内存越来越大。
异步更新
缓存击穿是指,某些key被大量用户并发访问,在过期时会导致这些大并发访问全部转到数据库,可能瞬间把数据库打挂。
缓存击穿和缓存雪崩的区别在于缓存击穿是针对某一特定key缓存的大并发访问。
SETNX
(SET if Not eXists
),得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试获取。但是该方案吞吐量明显下降了。既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。
一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。
这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。
Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。
EXPIRE和PERSIST命令。
MULTI、EXEC、DISCARD、WATCH
Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。
Redisson、Jedis、lettuce等等,官方推荐使用Redisson。
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件。
全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。
再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。
此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。
如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。
排行榜/计数器
Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:
ZRANGE user_scores 0 10 WITHSCORES
Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。
发布/订阅
最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。
还可以用bitmap搞签到系统
同时有多个子系统去 Set 一个 Key。
不推荐使用 Redis 的事务机制。因为我们的生产环境,基本都是 Redis 集群环境,做了数据分片操作。但Redis事务不支持数据分片。
那么我们必须考虑别的解决方案。
如果对这个 Key 操作的不要求顺序,可以准备一个分布式互斥锁,抢到锁就做 set 操作即可,比较简单。
如果对这个 Key 操作,要求顺序
假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为 valueC。
期望按照 key1 的 value 值按照 valueA -> valueB -> valueC 的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。
假设时间戳如下:
系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}
那么,假设这会系统 B 先抢到锁,将 key1 设置为{valueB 3:05}。接下来系统 A 抢到锁,发现自己的 valueA 的时间戳早于缓存中的时间戳,那就不做 set 操作了,以此类推。
其他方法,比如利用队列,将 set 方法变成串行访问也可以。
针对运行实例,有许多配置选项可以通过 CONFIG SET 命令进行修改,而无需执行任何形式的重启。 从 Redis 2.2 开始,可以从 AOF 切换到 RDB 的快照持久性或其他方式而不需要重启 Redis。检索 ‘CONFIG GET *’ 命令获取更多信息。
有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。
RDB
RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储.
AOF
AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大.
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
你也可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
一般来说, 如果想达到足以媲美PostgreSQL的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
有很多用户都只使用AOF持久化,但并不推荐这种方式:因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外, 使用RDB还可以避免之前提到的AOF程序的bug。
如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。
给你举个例子: 100万个键值对(键是0到999999值是字符串“hello world”)在我的32位的Mac笔记本上 用了100MB。同样的数据放到一个key里只需要16MB, 这是因为键值有一个很大的开销。 在Memcached上执行也是类似的结果,但是相对Redis的开销要小一点点,因为Redis会记录类型信息引用计数等等。
当然,大键值对时两者的比例要好很多。
64位的系统比32位的需要更多的内存开销,尤其是键值对都较小时,这是因为64位的系统里指针占用了8个字节。 但是,当然,64位系统支持更大的内存,所以为了运行大型的Redis服务器或多或少的需要使用64位的系统。
如果你使用的是32位的Redis实例,可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。
如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以将Redis当缓存来使用配置LRU淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。
默认为LRU算法
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面.
如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
noeviction
默认值。当内存不足以容纳新写入数据时,新写入操作会报错。
不推荐使用。
allkeys-lru
当内存不足以容纳新写入数据时,在key中移除最近最少使用的 Key(LRU)。
推荐使用。
allkeys-random
当内存不足以容纳新写入数据时,在key中随机移除某个 Key。
不推荐使用。
volatile-lru
当内存不足以容纳新写入数据时,在设置了过期时间的key中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。
不推荐。
volatile-random
当内存不足以容纳新写入数据时,在设置了过期时间的key中,随机移除某个 Key。
不推荐。
volatile-ttl
当内存不足以容纳新写入数据时,在设置了过期时间的key中,有更早过期时间的 Key 优先移除。
不推荐。
如果没有设置 expire 的 Key,那么 volatile-lru,volatile-random 和 volatile-ttl 策略的行为,和 noeviction(不删除) 基本上一致。
分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。
客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。
代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy
查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。
Redis有自动过期功能,可以对key设置超时时间,当时间到达后该key数据会被自动删除。精度可为毫秒或秒。下面是一个示例:
> set key some-value
OK
> expire key 5
(integer) 1
> get key (immediately)
"some-value"
> get key (after some time)
(nil)
上面的例子使用了EXPIRE来设置超时时间(也可以再次调用这个命令来改变超时时间,还可以使用PERSIST命令去除超时时间 )。
我们也可以直接在创建值的时候设置超时时间:
> set key 100 ex 10
OK
> ttl key
(integer) 9
上文中TTL命令用来查看key对应的值剩余存活时间。
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
但是Redis事务有一点不同,当EXEC命令执行后,就算有命令不成功,后面的命令还是会执行。而且Redis事务不能回滚。
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多POP3协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
这是一个总结
Redis中文网站
RedisCommands
扫盲,为什么分布式一定要有Redis
Redis面试题汇总
Redis的n种妙用,不仅仅是缓存