关于缓存的介绍
缓存是一个数据模型对象,那么必然有它的一些特征:
命中率
命中率=返回正确结果数/请求缓存次数,命中率问题是缓存中的一个非常重要的问题,它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。
最大元素(或最大空间)
缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间),那么将会触发缓存启动清空策略根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的时候缓存。
清空策略
如上描述,缓存的存储空间有限制,当缓存空间被用满时,如何保证在稳定服务的同时有效提升命中率?这就由缓存清空策略来处理,设计适合自身数据特征的清空策略能有效提升命中率。常见的一般策略有:
- FIFO(first in first out)
先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
- LFU(less frequently used)
最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。
- LRU(least recently used)
最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
除此之外,还有一些简单策略比如:
- 根据过期时间判断,清理过期时间最长的元素;
- 根据过期时间判断,清理最近要过期的元素;
- 随机清理;
- 根据关键字(或元素内容)长短清理等。
在目前的应用服务框架中,缓存分为local cache(本地缓存)和remote cache(分布式缓存):
- 本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是因为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
- 分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
简单说说有哪些本地缓存解决方案?
先来聊聊本地缓存,这个实际在很多项目中用的蛮多,特别是单体架构的时候。数据量不大,并且没有分布式要求的话,使用本地缓存还是可以的。
1、 HashMap
和 ConcurrentHashMap
ConcurrentHashMap
可以看作是线程安全版本的 HashMap
,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。
2、Ehcache
、 Guava Cache
、Spring Cache
这三者是使用的比较多的本地缓存框架。
Ehcache
是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,是一个非常轻量级的缓存实现 ,Ehcache
支持可以嵌入到 hibernate 和 mybatis 作为多级缓存。更多介绍推荐阅读:玩转EhCache之最简单的缓存框架
Guava Cache
和Spring Cache
两者的话比较像。
Guava
相比于 Spring Cache
的话使用的更多一点, Guava Cache继承了ConcurrentHashMap的思路,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。
使用 Spring Cache
的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。
3、Caffeine
Caffeine 是使用 Java8 对 Guava 缓存的重写版本 ,在 Spring Boot 2.0中将取代 Guava。如果出现 Caffeine,
CaffeineCacheManager 将会自动配置。推荐阅读:本地缓存解决方案-Caffeine Cache
为什么要有分布式缓存?/为什么不直接用本地缓存?
我们可以把分布式缓存(Distributed Cache) 看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。
本地的缓存的优势是低依赖,比较轻量并且通常相比于使用分布式缓存要更加简单。
再来分析一下本地缓存的局限性:
- 本地缓存对分布式架构支持不友好,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
- 容量跟随服务器限制明显。
分布式缓存优点:
缓存部署在一台单独的服务器上,与应用分离,多个应用共享缓存。 分布式缓存服务的性能、容量和提供的功能都要更加强大。
缺点:
分布式缓存需要引入额外的服务比如 Redis 或 Memcached,你需要单独保证 Redis 或 Memcached 服务的高可用。
什么是Redis
Redis是一个开源的,基于内存的数据结构存储,可用作于数据库、缓存、消息中间件。
- Redis基于内存,支持多种数据结构。
- Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。
缺点:
- 容量受到物理内存的限制;
- 不具备自动容错和恢复功能;
- 降低了系统的可用性;
- Redis 较难支持在线扩容。
Redis为什么比MySQL快?
1、Redis 是基于内存存储的,MySQL 是基于磁盘的;
2、Redis 数据结构为 k-v 结构,查找时间复杂度为 O(1),MySQL 数据结构为 B+树,查找速度为 O(logn);
3、Redis 是单线程的多路复用,避免了线程切换带来的开销,而多路复用避免了 IO 等待的开销。
为什么要用Redis缓存?
使用缓存主要是为了提升用户体验以及应对更多的用户,从程序设计而言是为了高性能和高并发。
高性能 :
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4核8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到30w+(就单机redis的情况,redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。
为什么Redis可以实现分布式锁?
首先声明 Redis 可以实现分布式锁与它是单线程的无关。
然后我们来看看实现分布式锁的两个要求:
- 分布式锁的加锁和释放锁的过程,涉及多个操作。所以,在实现分布式锁时,我们需要保证这些锁操作的原子性;
- 共享存储系统保存了锁变量,如果共享存储系统发生故障或宕机,那么客户端也就无法进行锁操作了。在实现分布式锁时,我们需要考虑保证共享存储系统的可靠性,进而保证锁的可靠性。
Redis 在多种数据库中的独特性
- Redis 是基于内存的采用单线程模型的 KV 数据库,支持高并发。
- 支持多种数据结构,操作简单。
- 单线程模式,避免了不必要的上下文切换和竞争条件,不需要考虑锁的问题。
- 使用多路 I/O 复用模型,非阻塞IO。
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
为什么 Redis 选择单线程?
官方解释:因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是 机器内存的大小 或者 网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
说一下 Redis 和 Memcached 的区别和共同点
Redis 因为功能更为强大,所以使用更为广泛,在选择分布式缓存方案时优先推荐 Redis。
共同点 :
- 都是基于内存的缓存。
- 都有过期策略。
- 两者的性能都非常高。
区别 :
- Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
- Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
- Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
- Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的.
- Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
- Redis 支持发布订阅模型、Lua脚本、事务等功能,而Memcached不支持。并且,Redis支持更多的编程语言。
Redis 常见数据结构以及使用场景分析
Redis 支持丰富的数据结构,常用的有 string、list、hash、set、sortset 这几种。 Redis 的存储是以key-value
的形式的。Redis 中的 key 一定是字符串,value 可以是 string、list、hash、set、sortset 这几种常用的。
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=user
value={
“id”: 1,
“name”: “hresh”,
“age”: 24,
“location”: “中国”
}
3.List
常用命令: lpush,rpush,lpop,rpop,lrange 等
list 就是链表,Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用 Redis 的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
4.Set
常用命令: sadd,spop,smembers,sunion 等
Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。
当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:
sinterstore key1 key2 key3 将交集存在key1内
5.Sorted Set
常用命令: zadd,zrange,zrem,zcard 等
和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
扩展:
跳跃表
跳跃表(shiplist)是实现 sortset(有序集合)的底层数据结构之一!
它类似于 Java 中的 SortedSet 和 HashMap 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 排序 的目的。
为什么使用跳跃表?
首先,因为 sortset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢?
- 性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 (下面详细说);
- 实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;
关于跳跃表的创建与插入过程,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,这就降低了插入操作的复杂度。
推荐阅读: https://github.com/wmyskxz/MoreThanJava#part3-redis
Redis对象一些细节
- 服务器在执行某些命令的时候,会先检查给定的键的类型能否执行指定的命令。
- 比如我们的数据结构是 sortset,但你使用了 list 的命令。这是不对的,服务器会检查一下我们的数据结构是什么才会进一步执行命令。
- Redis 的对象系统带有引用计数实现的内存回收机制。
- 对象不再被使用的时候,对象所占用的内存会释放掉,
- Redis 会共享值为0到 9999 的字符串对象
- 对象会记录自己的最后一次被访问时间,这个时间可以用于计算对象的空转时间
推荐阅读:《Redis设计与实现》
Redis中哈希表的特殊性
首先我们查看其结构如下:
我们可以发现,Redis 中有两个哈希表:
- ht[0]:用于存放真实的
key-vlaue
数据 - ht[1]:用于扩容(rehash)
Redis 中哈希算法和哈希冲突跟 Java 实现的差不多,它俩差异就是:
- Redis 哈希冲突时:是将新节点添加在链表的表头。
- JDK1.8后,Java 在哈希冲突时:是将新的节点添加到链表的表尾。
关于 Redis 是怎么 rehash 的,首先需要知道它不是一次性完成的,因为 Redis 是单线程的,所以它被称为渐进式 rehash。推荐阅读:redis 哈希表的 rehash 分析 和 渐进式 rehash
Redis 设置过期时间
Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除。
作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。
如果有大量的key需要设置同一时间过期,一般需要注意什么?
如果大量的 key 过期时间设置的过于集中,到过期的那个时间点,Redis 可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。
Redis 是怎么对过期的key 进行删除的?
1、定时删除
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。 即从设置 key 的 Expire 开始,就启动一个定时器,到时间就删除该 key;这样会对内存比较友好,但浪费 CPU 资源。
2、惰性删除
放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。 即平时不处理,在使用的时候,先检查该 key 是否已过期,已过期则删除,否则不做处理;这样对 CPU 友好,但是浪费内存资源。
3、定期删除
每隔一段时间,扫描 Redis 中过期 key 字典,并随机清除部分过期的 key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。
Redis 服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
推荐阅读:理解Redis的内存回收机制
Redis 内存淘汰机制
Redis 提供 6 种数据淘汰策略:
- volatile-lru(least frequently used):从已设置过期时间的数据集(
server.db[i].expires
)中挑选最近最少使用的数据淘汰 -
volatile-ttl:从已设置过期时间的数据集(
server.db[i].expires
)中挑选将要过期的数据淘汰 -
volatile-random:从已设置过期时间的数据集(
server.db[i].expires
)中随机选择数据淘汰 -
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
-
allkeys-random:从数据集(
server.db[i].dict
)中任意选择数据淘汰 -
no-eviction: 当内存不足以容纳新写入数据时,新写入操作会报错。
Redis 会在每一次处理命令的时候(processCommand函数调用freeMemoryIfNeeded)判断当前 redis 是否达到了内存的最大限制,如果达到限制,则使用对应的算法去处理需要删除的 key。
本文作者为hresh,转载请注明。