缓存的概念
缓存是介入应用程序和物理数据之间,其作用是为了降低应用程序对物理数据源访问的频次,从而提高应用的运行性能。
缓存内的数据对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻会同步缓存数据和物理数据源的数据。
比如我们通常是直接查询MySQL数据库,那么在高并发情况下,大量查询MySQL数据库会导致数据库性能变慢。
因此我们在应用层与MySQL之间搭建一个Cache层,让请求先访问Cache,这样能大大的降低数据库的压力,提高性能
。
分布式缓存
简单来说,缓存系统能跨进程我们就称为分布式缓存
在分布式系统开发中,系统与系统之间都是属于进程级别,缓存系统也能跨进程叫分布式缓存,市面上分布式技术有Memcached
和Redis
这两种,二者区别大致如下:
性能
- Redis是单核的
- Memcahed是多核的
内存空间和数据大小
- Memcached : 可以修改最大内存大小,进行LRU算法进行淘汰
- Redis : 不仅可以修改最大内存,因为redis的缓存是存储内存上的,一些数据可以通过VM的特性突破物理空间的限制。(可以理解:外挂一个磁盘当做数据存储源使用)
操作比较
- Memcacehd : 操作比较单一,只有一种数据类型String,用来缓存。
- Redis : 支持比较丰富的数据类型,(String、哈希、集合、有序集合等),比如做一些签到、附近的人都可以(用bitmap、geo实现),减少服务端的操作,减少IO读写。
可靠性
- Memcacehd :不支持持久化 ,断电重启数据就丢失,只能做缓存使用(需要重新从关系型数据库获取)
- Redis : 支持持久化 (RDB、AOF的2种机制) ,从持久化的数据磁盘加载并保存到缓存上,能处理单点故障、设置主从、集群、哨兵等机制。
应用场景
- Memcacehd : 减轻数据库的压力,做缓存 (适合写少读多,或者纯读做缓存用的应用场景)
- Redis:也可以减少数据库的压力,做缓存,(对读多写多场景高 都适合),也可以实现比较复杂的业务实现(积分、签到、附近的人)
谨慎考虑的部分
- Memcacehd :
- 单个key-value的存储,value最大的上限是1M。
- 只是内存缓存,对可靠性没太高要求。
- Reids :
- 单String类型的key-value的存储,最大的上限是512M (42亿的字节)。
- Redis的可靠性强,可以理解内存性数据库。
- 还有多个数据类型,一些高阶的数据类型,可以应付一些特殊的业务场景(签单存储bitMap , 附近的人Geo 、好友列表用Set)
存储方式
使用Redis做缓存的话,数据的存储结构有两种,一种采用String
存储,另外使用Hashes
存储。
那使用哪种更好呢?得具体情况具体分析:
- String 存储比较简单,固定的数据,比如存储一个简单的用户信息(用户名、昵称、头像、年龄等)。 存储时需要将数据进行序列化,获取时需要反序列化 , 在数据比较小的情况下还是可以忽略这种开销的。
- 但如果存储的数据可能某些属性会有些变化,比如餐厅数据中,他有likeVotes(喜欢) 、 dislikeVotes(不喜欢)的数量,这类型的数据,那么我们采用
Hashes
会更好,而且存储的时候没有序列化开销。 - 官方推荐使用
Hashes
缓存异常解决
如果缓存(异常)清空了,或者时间失效设计的不合理导致缓存删除,一下子突然千万用户访问进来导致数据库崩溃(应用宕机)
缓存常见的三大经典问题:缓存击穿、缓存穿透、缓存雪崩
缓存击穿
原因
存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
解决方法:
- 永不过期
- 逻辑过期
- 服务降级
缓存穿透
原因
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
就是明知道这个Key不在缓存,也不在数据库里,请求大量的这种导致数据库压力剧增。
解决
- 布隆过滤器
- 空值存储 (上面有说)
- 互斥锁 (不推荐 , 拿到锁请求数据库,没拿到锁就等待阻塞,只要用到锁 用不好锁必然会降低性能损耗)
- 异步更新
- 服务降级
缓存雪崩
原因:
当大量缓存数据在同一时间过期(失效) 或者 Redis 故障宕机 时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增
,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
解决
- 随机失效时间
- 双缓存 (用的不多,备份缓存,主数据失效了,那么去加载副的)
- 互斥锁 (不推荐)
- 服务降级
其实最致命的是Redis节点故障,如果那个节点出问题访问不了缓存,那么数据的请求全怼在数据库里,造成数据库崩溃。
解决方法:
- 配置主从,集群、开启哨兵机制(防止某个节点宕机,如果某个节点宕机,就自动切换其他节点)
缓存淘汰
最大内存参数
我们的redis数据库最大缓存、主键失效、淘汰机制等参数都是可以通过配置文件来配置的。
这个文件是我们的redis.conf
文件。
maxmemory <bytes> : 设置最大内存
#比如 maxmemory 500mb
超过最大的内存,就会采用内存淘汰策略,执行淘汰,如下。
内存淘汰策略
截止目前redis一共为我们提供了八个不同的内存置换策略,很早之前提供了6种。
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
#
# Note: with any of the above policies, Redis will return an error on write
# operations, when there are no suitable keys for eviction.
#
# At the date of writing these commands are: set setnx setex append
# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
# getset mset msetnx exec sort
#
# The default is:
#
# maxmemory-policy noeviction
默认不淘汰 , 开发中经常使用的是volatile-lru
- volatile-lru:从已经设置过期时间的数据集中,挑选最近最少使用的数据淘汰。
- volatile-ttl:从已经设置过期时间的数据集中,挑选即将要过期的数据淘汰。
- volatile-random:从已经设置过期时间的数据集中,随机挑选数据淘汰。
- volatile-lfu:从已经设置过期时间的数据集中,会使用LFU算法选择设置了过期时间的键值对。
- allkeys-lru:从所有的数据集中,挑选最近最少使用的数据淘汰。
- allkeys-random:从所有的数据集中,随机挑选数据淘汰。
- no-enviction:禁止淘汰数据,如果redis写满了将不提供写请求,直接返回错误。
注意这里的6种机制,volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略
使用策略规则:
如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru
如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用 allkeys-random
淘汰机制的实现
既然是淘汰,那就需要把这些数据给删除,然后保存新的
Redis删除策略主要有以下几种:
- 惰性删除
- 内存没满,则使用了这个,不额外开启一个线程去删除,就是保证高性能,在重新获取key的时候就知道这个key是否过期存在。
- 定时删除
- 每隔一段时间获取过期的key进行删除
- 主动删除
- (内存满了,就主动删除多余的Key)