转载自JavaGuide/redis-all.md at master · Snailclimb/JavaGuide (github.com)html
简单来讲 Redis 就是一个使用 C 语言开发的数据库,不过与传统数据库不一样的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,因此读写速度很是快,所以 Redis 被普遍应用于缓存方向。java
另外,Redis 除了作缓存以外,也常常用来作分布式锁,甚至是消息队列。git
Redis 提供了多种数据类型来支持不一样的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。github
分布式缓存的话,使用的比较多的主要是 Memcached 和 Redis。不过,如今基本没有看过还有项目使用 Memcached 来作缓存,都是直接用 Redis。web
Memcached 是分布式缓存最开始兴起的那会,比较经常使用的。后来,随着 Redis 的发展,你们慢慢都转而使用更增强大的 Redis 了。面试
分布式缓存主要解决的是单机缓存的容量受服务器限制而且没法保存通用信息的问题。由于,本地缓存只在当前服务里有效,好比若是你部署了两个相同的服务,他们二者之间的缓存数据是没法共同的。redis
如今公司通常都是用 Redis 来实现缓存,并且 Redis 自身也愈来愈强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于咱们在作相应的技术选型的时候,可以作到有理有据!shell
共同点 :数据库
区别 :编程
相信看了上面的对比以后,咱们已经没有什么理由能够选择使用 Memcached 来做为本身项目的分布式缓存了。
做为暖男一号,我给你们画了一个草图。
简单来讲就是:
简单,来讲使用缓存主要是为了提高用户体验以及应对更多的用户。
下面咱们主要从“高性能”和“高并发”这两点来看待这个问题。
高性能 :
对照上面 👆 我画的图。咱们设想这样的场景:
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。可是,若是说,用户访问的数据属于高频数据而且不会常常改变的话,那么咱们就能够很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就能够直接从缓存中获取了。操做缓存就是直接操做内存,因此速度至关快。
不过,要保持数据库和缓存中的数据的一致性。 若是数据库中的对应数据改变的以后,同步改变缓存中相应的数据便可!
高并发:
通常像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,可是使用 Redis 缓存以后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的状况,redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒能够执行的查询次数;
因而可知,直接操做缓存可以承受的数据库请求数量是远远大于直接访问数据库的,因此咱们能够考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用通过数据库。进而,咱们也就提升了系统总体的并发。
你能够本身本机安装 redis 或者经过 redis 官网提供的在线 redis 环境。
set,get,strlen,exists,decr,incr,setex
等等。下面咱们简单看看它的使用!
普通字符串的基本操做:
127.0.0.1:6379> set key value #设置 key-value 类型的值 OK 127.0.0.1:6379> get key # 根据 key 得到对应的 value "value" 127.0.0.1:6379> exists key # 判断某个 key 是否存在 (integer) 1 127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。 (integer) 5 127.0.0.1:6379> del key # 删除某个 key 对应的值 (integer) 1 127.0.0.1:6379> get key (nil)
批量设置 :
127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值 OK 127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value 1) "value1" 2) "value2"
计数器(字符串的内容为整数的时候可使用):
127.0.0.1:6379> set number 1 OK 127.0.0.1:6379> incr number # 将 key 中储存的数字值增一 (integer) 2 127.0.0.1:6379> get number "2" 127.0.0.1:6379> decr number # 将 key 中储存的数字值减一 (integer) 1 127.0.0.1:6379> get number "1"
过时(默认为永不过时):
127.0.0.1:6379> expire key 60 # 数据在 60s 后过时 (integer) 1 127.0.0.1:6379> setex key 60 value # 数据在 60s 后过时 (setex:[set] + [ex]pire) OK 127.0.0.1:6379> ttl key # 查看数据还有多久过时 (integer) 56
rpush,lpop,lpush,rpop,lrange,llen
等。下面咱们简单看看它的使用!
经过 rpush/lpop
实现队列:
127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素 (integer) 1 127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素 (integer) 3 127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出 "value1" 127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end 1) "value2" 2) "value3" 127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的全部元素,-1表示倒数第一 1) "value2" 2) "value3"
经过 rpush/rpop
实现栈:
127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出 "value3"
我专门花了一个图方便小伙伴们来理解:
经过 lrange
查看对应下标范围的列表元素:
127.0.0.1:6379> rpush myList value1 value2 value3
(integer) 3
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end 1) "value1" 2) "value2" 127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的全部元素,-1表示倒数第一 1) "value1" 2) "value2" 3) "value3"
经过 lrange
命令,你能够基于 list 实现分页查询,性能很是高!
经过 llen
查看链表长度:
127.0.0.1:6379> llen myList
(integer) 3
hset,hmset,hexists,hget,hgetall,hkeys,hvals
等。下面咱们简单看看它的使用!
127.0.0.1:6379> hmset userInfoKey name "guide" description "dev" age "24" OK 127.0.0.1:6379> hexists userInfoKey name # 查看 key 对应的 value中指定的字段是否存在。 (integer) 1 127.0.0.1:6379> hget userInfoKey name # 获取存储在哈希表中指定字段的值。 "guide" 127.0.0.1:6379> hget userInfoKey age "24" 127.0.0.1:6379> hgetall userInfoKey # 获取在哈希表中指定 key 的全部字段和值 1) "name" 2) "guide" 3) "description" 4) "dev" 5) "age" 6) "24" 127.0.0.1:6379> hkeys userInfoKey # 获取 key 列表 1) "name" 2) "description" 3) "age" 127.0.0.1:6379> hvals userInfoKey # 获取 value 列表 1) "guide" 2) "dev" 3) "24" 127.0.0.1:6379> hset userInfoKey name "GuideGeGe" # 修改某个字段对应的值 127.0.0.1:6379> hget userInfoKey name "GuideGeGe"
HashSet
。Redis 中的 set 类型是一种无序集合,集合中的元素没有前后顺序。当你须要存储一个列表数据,又不但愿出现重复数据时,set 是一个很好的选择,而且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。能够基于 set 轻易实现交集、并集、差集的操做。好比:你能够将一个用户全部的关注人存在一个集合中,将其全部粉丝存在一个集合。Redis 能够很是方便的实现如共同关注、共同粉丝、共同喜爱等功能。这个过程也就是求交集的过程。sadd,spop,smembers,sismember,scard,sinterstore,sunion
等。下面咱们简单看看它的使用!
127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去 (integer) 2 127.0.0.1:6379> sadd mySet value1 # 不容许有重复元素 (integer) 0 127.0.0.1:6379> smembers mySet # 查看 set 中全部的元素 1) "value1" 2) "value2" 127.0.0.1:6379> scard mySet # 查看 set 的长度 (integer) 2 127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素 (integer) 1 127.0.0.1:6379> sadd mySet2 value2 value3 (integer) 2 127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放在 mySet3 中 (integer) 1 127.0.0.1:6379> smembers mySet3 1) "value2"
zadd,zcard,zscore,zrange,zrevrange,zrem
等。127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重 (integer) 1 127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # 一次添加多个元素 (integer) 2 127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量 (integer) 3 127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重 "3" 127.0.0.1:6379> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出全部元素 1) "value3" 2) "value2" 3) "value1" 127.0.0.1:6379> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为 stop 1) "value3" 2) "value2" 127.0.0.1:6379> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1 为 stop 1) "value1" 2) "value2"
setbit
、getbit
、bitcount
、bitop
# SETBIT 会返回以前位的值(默认是 0)这里会生成 7 个位 127.0.0.1:6379> setbit mykey 7 1 (integer) 0 127.0.0.1:6379> setbit mykey 7 0 (integer) 1 127.0.0.1:6379> getbit mykey 7 (integer) 0 127.0.0.1:6379> setbit mykey 6 1 (integer) 0 127.0.0.1:6379> setbit mykey 8 1 (integer) 0 # 经过 bitcount 统计被被设置为 1 的位的数量。 127.0.0.1:6379> bitcount mykey (integer) 2
针对上面提到的一些场景,这里进行进一步说明。
使用场景一:用户行为分析 不少网站为了分析你的喜爱,须要研究你点赞过的内容。
# 记录你喜欢过 001 号小姐姐 127.0.0.1:6379> setbit beauty_girl_001 uid 1
使用场景二:统计活跃用户
使用时间做为 key,而后用户 ID 为 offset,若是当日活跃过就设置为 1
那么我该如何计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只要有一天在线就称为活跃),有请下一个 redis 的命令
# 对一个或多个保存二进制位的字符串 key 进行位元操做,并将结果保存到 destkey 上。 # BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操做中的任意一种参数 BITOP operation destkey key [key ...]
初始化数据:
127.0.0.1:6379> setbit 20210308 1 1
(integer) 0
127.0.0.1:6379> setbit 20210308 2 1 (integer) 0 127.0.0.1:6379> setbit 20210309 1 1 (integer) 0
统计 20210308~20210309 总活跃用户数: 1
127.0.0.1:6379> bitop and desk1 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk1 (integer) 1
统计 20210308~20210309 在线活跃用户数: 2
127.0.0.1:6379> bitop or desk2 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk2 (integer) 2
使用场景三:用户在线状态
对于获取或者统计用户在线状态,使用 bitmap 是一个节约空间且效率又高的一种方法。
只须要一个 key,而后用户 ID 为 offset,若是在线就设置为 1,不在线就设置为 0。
Redis 基于 Reactor 模式来设计开发了本身的一套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。因为文件事件处理器(file event handler)是单线程方式运行的,因此咱们通常都说 Redis 是单线程模型。
既然是单线程,那怎么监听大量的客户端链接呢?
Redis 经过IO 多路复用程序 来监听来自客户端的大量链接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每一个事件是否发生。
这样的好处很是明显: I/O 多路复用技术的使用让 Redis 不须要额外建立多余的线程来监听客户端的大量链接,下降了资源的消耗(和 NIO 中的 Selector
组件很像)。
另外, Redis 服务器是一个事件驱动程序,服务器须要处理两类事件:1. 文件事件; 2. 时间事件。
时间事件不须要多花时间了解,咱们接触最多的仍是 文件事件(客户端进行读取写入等操做,涉及一系列网络通讯)。
《Redis 设计与实现》有一段话是如是介绍文件事件的,我以为写得挺不错。
Redis 基于 Reactor 模式开发了本身的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不一样的事件处理器。
当被监听的套接字准备好执行链接应答(accept)、读取(read)、写入(write)、关 闭(close)等操做时,与操做相对应的文件事件就会产生,这时文件事件处理器就会调用套接字以前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但经过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通讯模型,又能够很好地与 Redis 服务器中其余一样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
能够看出,文件事件处理器(file event handler)主要是包含 4 个部分:
《Redis设计与实现:12章》
虽说 Redis 是单线程模型,可是,实际上,Redis 在 4.0 以后的版本中就已经加入了对多线程的支持。
不过,Redis 4.0 增长的多线程主要是针对一些大键值对的删除操做的命令,使用这些命令就会使用主处理以外的其余线程来“异步处理”。
大致上来讲,Redis 6.0 以前主要仍是单线程处理。
那,Redis6.0 以前 为何不使用多线程?
我以为主要缘由有下面 3 个:
Redis6.0 引入多线程主要是为了提升网络 IO 读写性能,由于这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。
虽然,Redis6.0 引入了多线程,可是 Redis 的多线程只是在网络数据的读写这类耗时操做上使用了,执行命令仍然是单线程顺序执行。所以,你也不须要担忧线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启须要修改 redis 配置文件 redis.conf
:
io-threads-do-reads yes
开启多线程后,还须要设置线程数,不然是不生效的。一样须要修改 redis 配置文件 redis.conf
:
io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
推荐阅读:
通常状况下,咱们设置保存的缓存数据的时候都会设置一个过时时间。为何呢?
由于内存是有限的,若是缓存中的全部数据都是一直保存的话,分分钟直接 Out of memory。
Redis 自带了给缓存数据设置过时时间的功能,好比:
127.0.0.1:6379> exp key 60 # 数据在 60s 后过时 (integer) 1 127.0.0.1:6379> setex key 60 value # 数据在 60s 后过时 (setex:[set] + [ex]pire) OK 127.0.0.1:6379> ttl key # 查看数据还有多久过时 (integer) 56
注意:**Redis 中除了字符串类型有本身独有设置过时时间的命令 setex
外,其余方法都须要依靠 expire
命令来设置过时时间 。另外, persist
命令能够移除一个键的过时时间。 **
过时时间除了有助于缓解内存的消耗,还有什么其余用么?
不少时候,咱们的业务场景就是须要某个数据只在某一时间段内存在,好比咱们的短信验证码可能只在 1 分钟内有效,用户登陆的 token 可能只在 1 天内有效。
若是使用传统的数据库来处理的话,通常都是本身判断过时,这样更麻烦而且性能要差不少。
Redis 经过一个叫作过时字典(能够看做是 hash 表)来保存数据过时的时间。过时字典的键指向 Redis 数据库中的某个 key(键),过时字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过时时间(毫秒精度的 UNIX 时间戳)。
过时字典是存储在 redisDb 这个结构里的:
typedef struct redisDb { ... dict *dict; //数据库键空间,保存着数据库中全部键值对 dict *expires // 过时字典,保存着键的过时时间 ... } redisDb;
若是假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?
经常使用的过时数据的删除策略就两个(重要!本身造缓存轮子的时候须要格外考虑的东西):
按期删除对内存更加友好,惰性删除对 CPU 更加友好。二者各有千秋,因此 Redis 采用的是 按期删除+惰性/懒汉式删除 。
可是,仅仅经过给 key 设置过时时间仍是有问题的。由于仍是可能存在按期删除和惰性删除漏掉了不少过时 key 的状况。这样就致使大量过时 key 堆积在内存里,而后就 Out of memory 了。
怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 提供 6 种数据淘汰策略:
4.0 版本后增长如下两种:
不少时候咱们须要持久化数据也就是将内存中的数据写入到硬盘里面,大部分缘由是为了以后重用数据(好比重启机器、机器故障以后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis 不一样于 Memcached 的很重要一点就是,Redis 支持持久化,并且支持两种不一样的持久化操做。Redis 的一种持久化方式叫快照(snapshotting,RDB),另外一种方式是只追加文件(append-only file, AOF)。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合本身的持久化方法。
快照(snapshotting)持久化(RDB)
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(append-only file)持久化
与快照持久化相比,AOF 持久化的实时性更好,所以已成为主流的持久化方案。默认状况下 Redis 没有开启 AOF(append only file)方式的持久化,能够经过 appendonly 参数开启:
appendonly yes
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf
中,而后再根据 appendfsync
配置来决定什么时候将其同步到硬盘中的 AOF 文件。
AOF 文件的保存位置和 RDB 文件的位置相同,都是经过 dir 参数设置的,默认的文件名是 appendonly.aof
。
在 Redis 的配置文件中存在三种不一样的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重下降Redis的速度 appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 appendfsync no #让操做系统决定什么时候进行同步
为了兼顾数据和写入性能,用户能够考虑 appendfsync everysec
选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。并且这样即便出现系统崩溃,用户最多只会丢失一秒以内产生的数据。当硬盘忙于执行写入操做的时候,Redis 还会优雅的放慢本身的速度以便适应硬盘的最大写入速度。
相关 issue :783:Redis 的 AOF 方式
拓展:Redis 4.0 对于持久化机制的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,能够经过配置项 aof-use-rdb-preamble
开启)。
若是把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样作的好处是能够结合 RDB 和 AOF 的优势, 快速加载同时避免丢失过多的数据。固然缺点也是有的, AOF 里面的 RDB 部分是压缩格式再也不是 AOF 格式,可读性较差。
官方文档地址:https://redis.io/topics/persistence
补充内容:AOF 重写
AOF 重写能够产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态同样,但体积更小。
AOF 重写是一个有歧义的名字,该功能是经过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操做。
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程建立新 AOF 文件期间,记录服务器执行的全部写命令。当子进程完成建立新 AOF 文件的工做以后,服务器会将重写缓冲区中的全部内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操做。
Redis 能够经过 MULTI
,EXEC
,DISCARD
和 WATCH
等命令来实现事务(transaction)功能。
> MULTI
OK
> SET USER "Guide哥" QUEUED > GET USER QUEUED > EXEC 1) OK 2) "Guide哥"
使用 MULTI
命令后能够输入多个命令。Redis 不会当即执行这些命令,而是将它们放到队列,当调用了 EXEC
命令将执行全部命令。
这个过程是这样的:
MULTI
)。EXEC
)。你也能够经过 DISCARD
命令取消一个事务,它会清空事务队列中保存的全部命令。
> MULTI
OK
> SET USER "Guide哥" QUEUED > GET USER QUEUED > DISCARD OK
WATCH
命令用于监听指定的键,当调用 EXEC
命令执行事务时,若是一个被 WATCH
命令监视的键被修改的话,整个事务都不会执行,直接返回失败。
> WATCH USER
OK
> MULTI > SET USER "Guide哥" OK > GET USER Guide哥 > EXEC ERR EXEC without MULTI
Redis 官网相关介绍 https://redis.io/topics/transactions 以下:
可是,Redis 的事务和咱们平时理解的关系型数据库的事务不一样。咱们知道事务具备四大特性: 1. 原子性,2. 隔离性,3. 持久性,4. 一致性。
Redis 是不支持 roll back 的,于是不知足原子性的(并且不知足持久性)。
Redis 官网也解释了本身为啥不支持回滚。简单来讲就是 Redis 开发者们以为不必支持回滚,这样更简单便捷而且性能更好。Redis 开发者以为即便命令执行错误也应该在开发过程当中就被发现而不是生产过程当中。
你能够将 Redis 中的事务就理解为 :Redis 事务提供了一种将多个命令请求打包的功能。而后,再按顺序执行打包的全部命令,而且不会被中途打断。
相关 issue :
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,致使请求直接到了数据库上,根本没有通过缓存这一层。举个例子:某个黑客故意制造咱们缓存中不存在的 key 发起大量请求,致使大量请求落到数据库。
以下图所示,用户的请求最终都要跑到数据库中查询一遍。
最基本的就是首先作好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。好比查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
1)缓存无效 key
若是缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过时时间,具体命令以下: SET key value EX 10086
。这种方式能够解决请求的 key 变化不频繁的状况,若是黑客恶意攻击,每次构建不一样的请求 key,会致使 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。若是非要用这种方式来解决穿透问题的话,尽可能将无效的 key 的过时时间设置短一点好比 1 分钟。
另外,这里多说一嘴,通常状况下咱们是这样设计 key 的: 表名:列名:主键名:主键值
。
若是用 Java 代码展现的话,差很少是下面这样的:
public Object getObjectInclNullById(Integer id) { // 从缓存中获取数据 Object cacheValue = cache.get(id); // 缓存为空 if (cacheValue == null) { // 从数据库中获取 Object storageValue = storage.get(key); // 缓存空对象 cache.set(key, storageValue); // 若是存储数据为空,须要设置一个过时时间(300秒) if (storageValue == null) { // 必须设置过时时间,不然有被攻击的风险 cache.expire(key, 60 * 5); } return storageValue; } return cacheValue; }
2)布隆过滤器
布隆过滤器是一个很是神奇的数据结构,经过它咱们能够很是方便地判断一个给定数据是否存在于海量数据中。咱们须要的就是判断 key 是否合法,有没有感受布隆过滤器就是咱们想要找的那个“人”。
具体是这样作的:把全部可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
加入布隆过滤器以后的缓存处理流程图以下。
可是,须要注意的是布隆过滤器可能会存在误判的状况。总结来讲就是: 布隆过滤器说某个元素存在,小几率会误判。布隆过滤器说某个元素不在,那么这个元素必定不在。
为何会出现误判的状况呢? 咱们还要从布隆过滤器的原理来讲!
咱们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操做:
咱们再来看一下,当咱们须要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操做:
而后,必定会出现这样一种状况:不一样的字符串可能哈希出来的位置相同。 (能够适当增长位数组大小或者调整咱们的哈希函数来下降几率)
更多关于布隆过滤器的内容能够看个人这篇原创:《不了解布隆过滤器?一文给你整的明明白白!》 ,强烈推荐,我的感受网上应该找不到总结的这么明明白白的文章了。
我发现缓存雪崩这名字起的有点意思,哈哈。
实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,形成数据库短期内承受大量请求。 这就比如雪崩同样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
举个例子:系统的缓存模块出了问题好比宕机致使不可用。形成系统的全部访问,都要走数据库。
还有一种缓存雪崩的场景是:有一些被大量访问数据(热点缓存)在某一时刻大面积失效,致使对应的请求直接落到了数据库上。 这样的状况,有下面几种解决办法:
举个例子 :秒杀开始 12 个小时以前,咱们统一存放了一批商品到 Redis 中,设置的缓存过时时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。致使的状况就是,相应的请求直接就落到了数据库上,就像雪崩同样可怕。
针对 Redis 服务不可用的状况:
针对热点缓存失效的状况:
细说的话能够扯不少,可是我以为其实没太大必要(小声 BB:不少解决方案我也没太弄明白)。我我的以为引入缓存以后,若是为了短期的不一致性问题,选择让系统设计变得更加复杂的话,彻底不必。
下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。
Cache Aside Pattern 中遇到写请求是这样的:更新 DB,而后直接删除 cache 。
若是更新数据库成功,而删除缓存这一步失败的状况的话,简单说两个解决方案:
若是你们想要实时关注我更新的文章以及分享的干货的话,能够关注个人公众号。
《Java 面试突击》: 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本公众号后台回复 "Java 面试突击" 便可免费领取!
Java 工程师必备学习资源: 一些 Java 工程师经常使用学习资源公众号后台回复关键字 “1” 便可免费无套路获取。