Redis
1、为什么要用Redis?
为了提升用户体验 以及 应对更多得用户
高性能
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
2.Redis 常见数据结构以及使用场景分析
2.1 string
- 简单动态字符串
- 常用命令:
set,get,strlen,exists,decr,incr,setex
等 - 应用场景:需要技术的场景,比如用户的访问次数、热点文章的点赞转发数量等。
基本操作:
set key value #设置 key ,value 值
get key #根据key 获取对应的value
exists key #判断某个key 是否存在
strlen key #返回 key 所存储的字符串值的长度
del key #删除某个 key 对应的值
### 批量设置:
mset key1 value1 key2 value2 # 批量设置 key-value
mget key1 key2 #批量获取多个 key 对应的 value
### 计数器(字符串的内容为整数的时候可以使用):
set number 1
incr number # 将 key 中存储的数字值加1
decr number # 将 key 中存储的数字值减1
### 过期(默认为永不过期):
expire key 60 # 数据在 60s 后过期
setex key 60 value # 数据在 60s 后过期
ttl key # 查看数据还有多久过期
2.2 list
- 双向链表
- 常用命令:
rpush,rpop,lpush,lpop,lrange,llen
等 - 应用场景:发布与订阅 或者消息队列、慢查询
通过 rpush/lpop 实现队列:
lpush mylist 1 2 3 # 向 list 的头部(左边)添加元素
rpop mylist # 将 list 的尾部(最右边)元素取出
lrange mylist 0 1 # 查看对应下标的list 列表,0 为start,1 为 end
lrange mylist 0 -1 # 查看列表中的所有元素,-1表示倒数第一个
通过rpush/rpop 实现栈
rpush mystack 1 2 3
(integer)3
rpop mystack
1
通过 lrange 查看对应下表范围的列表元素
lpush mylist 1 2 3
lrange mylist 0 -1 # c
通过llen 查看链表长度
llen myList
2.3 hash
- 适合存储对象,是一个string 类型的 field 和 value映射表
- 常用命令:hset,hmset,hexists,hget,hgetall,hkeys,hvals
- 应用场景:系统中对象数据的存储
hmset user name yijian age 23 address changsha
hexists user name #查看key 对应的 value中指定的字段是否存在
hget user name #获取存储在表中指定字段的值
hgetall user #获取在表中指定key 的所有字段和值
2.4 set
- 无序集合
- 常用命令:sadd,spop,smembers,sismerber,scard,sinterstore,sunion
- 应用场景:需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。比如:你可以将一个用户所有的关注人存放在一个结合中,将其所有粉丝存在一个集合中。方便实现共同关注、共同粉丝、共同喜好
sadd hobity basketball swimming singing #添加元素
smembers hobity #查看set中所有元素
scard hobity #查看set的长度
sismerbers hobity #检查某个元素是否存在set中,只能接受单个元素
sinter key1 key2 ... #获取key1,key2中相同的部分。交集
sunion key1 key2 ... #获取key1,key2中所有的部分,并集
sinterstore mySet3 mySet1 mySet2 #获取mySet1、mySet2 的交集并存放在 mySet3 中
2.5 sorted set
-
和set 相比,增加了一个权重参数 score,使得集合中的元素能够按照score 进行排序,还可以通过score 的范围来获取元素的列表。
-
常用命令:zadd,zcard,zscore,zrange,zrevrange,zrem
-
应用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
zadd score 90.0 english 80.0 math 70.0 chinese #添加元素到 sorted set zcard socre #查看 sorted set 中的元素数量 zscore score english #查看english的权重 zrang score 0 -1 zrevrange socre 0 1
2.6 bitmap
-
bitmap 存储的是连续的二进制数字(0和1),通过bitmap,只需要一个bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。
-
常用命令: setbit,getbit、bitcount、bittop
-
应用场景:适合需要保存状态信息(比如是否签到、是否登录)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况,用户行为统计(比如是否点赞过某个视频)。
-
空间占用、以及第一次分配空间需要的时间
在一台2010MacBook Pro上,offset为232-1(分配512MB)需要~300ms,offset为230-1(分配128MB)需要~80ms,offset为228-1(分配32MB)需要~30ms,offset为226-1(分配8MB)需要8ms。<来自官方文档>
计算公式:($offset/8/1024/1024)
MB -
使用场景一:用户签到
很多网站都提供了签到功能(这里不考虑数据落地事宜),并且需要展示最近一个月的签到情况
根据日期 offset =hash % 365 ; key = 年份#用户id
6、使用场景二:统计活跃用户
使用时间作为cacheKey,然后用户ID为offset,如果当日活跃过就设置为1
那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个redis的命令
命令 BITOP operation destkey key [key ...]
说明:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
说明:BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数
20190216 活跃用户 【1,2】
20190217 活跃用户 【1】
统计20190216~20190217 总活跃用户数: 1
统计20190216~20190217 在线活跃用户数: 2
**7、使用场景三:用户在线状态
**前段时间开发一个项目,对方给我提供了一个查询当前用户是否在线的接口。不了解对方是怎么做的,自己考虑了一下,
使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户ID为offset,如果在线就设置为1,
不在线就设置为0,和上面的场景一样,5000W用户只需要6MB的空间。
3.Redis 给缓存数据设置过期时间有啥用?
一般情况下,会对保存的缓存数据都设置一个过期时间。
因为内存是有限的。如果缓存中的所有数据一直保存的话,会导致Out of Memory。
Redis 给数据设置过期时间:
expire key 60 # 数据在 60s 后过期
setex key 60 value # 数据在 60s 后过期
ttl key # 查看数据还有多久过期
注意:redis 中除了字符串类型有 设置过期时间的命令
setex
外,其他方法都需要依靠expire
命令来设置过期时间。persist
命令移除一个键的过期时间。
过期时间的其他作用:
业务场景:某个数据只在某一时间段内存在,比如我们的短信验证码1分钟内有效,用户登录的 token 1天内有效。
4.Redis 如何判断数据是否过期了?
Redis 通过过期字典(可以看作是 hash 表) 来保存数据过期时间。过期字典的键指向 Redis 中的某个 key(键),过期字典的值 是一个long 类型的整数,这个整数保存了 key 所指向的数据库的过期时间(毫秒精度的 UNIX 时间戳)
5.过期的数据的删除策略?
- 惰性删除:只会在取出key 的时候才对数据进行过期检查。
- 优点:对CPU友好
- 缺点:会造成太多过期 key 没有被删除
- **定期删除:**每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis底层会通过限制删除操作执行的时长和频率来减少删除操作。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。而Redis 采用的是定期删除+惰性/懒汉式删除
但是,仅给key 设置过期时间还是有问题。因为可能存在定期删除和惰性删除漏掉了很多过期 key 的情况,这样就导致大量过期key 堆积在内存中,然后Out of memory。
解决方案:Redis 内存淘汰机制
6.Redis 内存淘汰机制?
相关问题:MySQL 里面有200w 数据,Redis 中只存 20W 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 提供了 8 种数据淘汰策略:
- volatile-lru (least recently used):从已设置过期时间的数据集(server.db[i].expires) 中挑选最近最少使用的数据淘汰。
- volatile-ttl :从已设置过期时间的数据集(server.db[i].expires) 中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires) 中任意选择数据淘汰
- allkeys-lru:当内存不足以容纳新写入的数据时,在键空间中,移除最近最少使用的 key (这个最常用)
- allkeys-random:从数据集(server.db[i].dict) 中任意选择数据淘汰
- no-eviction:禁止添加数据,当内存不足以容纳新写入的数据时,新写入操作会报错。
- volatile-lfu (least frequently used):从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。
- allkeys-lfu (least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key
7.Redis 持久化机制
相关问题:怎么保证Redis 挂掉之后再重启数据可以进行恢复?
- 快照的方式(RDB)
- 命令追加的方式(append-only file,AOF)
快照持久化(RDB)
原理:
创建快照来获取某个时间点上内存数据的副本。
创建快照之后,可以对快照进行备份,可以讲快照复制到其他服务器从而船舰具有相同数据的服务器副本(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 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
8.缓存穿透
8.1 什么是缓存穿透?
缓存穿透:
大量的请求key 根本不存在于缓存中,导致请求直接到了数据库上。
8.2 缓存穿透情况的处理流程?
如下图所示,用户的请求最终都要跑到数据库中查询一遍。
8.3 解决办法?
最基本的首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库id 不能小于0 ,传入的邮箱格式不对的时候直接返回错误消息给客户端。
-
缓存无效key
如果缓存和数据库都差不到某个key 的数据就写一个到redis 中并设置过期时间。
举例:
set key value ex 10086
这种方式可以解决请求的key 变化不频繁的情况,如果黑客恶心攻击,每次构建不同的请求key,会导致 redis 中缓存大量无效的key。很明显,这种方案并不能从根本上解决次问题,如果非要用这种方式来解决穿透的话,尽量将无效的 key 的过期时间设置短一点,比如 1 分钟。
设计key原则:
表名:列名:主键名:主键值
Java 代码展示,是下面这样:
public Object getObject(Integer id){ // 从缓存中获取数据 Object cacheValue = cache.get(id); // 缓存为空 if(cacheValue == null){ // 就从数据库中获取 Object storageValue = storage.get(key); // 更新缓存 cache.set(key,storageValue); // 如果存储数据为空,需要设置一个过期时间(300 m) // 否则有可能导致缓存穿透 if(storageValue == null){ cache.set(key,60*5) } return storageValue; } return cacheValue; }
-
布隆过滤器
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程
加入布隆过滤器之后的缓存处理流程图如下。
但是,布隆过滤器也可能会存在误判的情况。
布隆过滤器说某个元素存在,小概率会误判。
布隆过滤器说某个元素不在,那么这个元素一定不存在。
为什么会出现误判的情况?从布隆过滤器的原理解释:
当一个元素加入布隆过滤器中的操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
- 根据得到的哈希值,在位数组中把对应的值置为 1。
当判断一个元素是否存在于布隆过滤器的操作:
- 对给定元素再次进行相同的哈希计算。
- 得到值之后判断位数组中的每隔元素是否都为1,如果值都为1,那么说明这个值在布隆过滤器中,如果存在一个值不为1,说明该元素不在布隆过滤器中
不同的字符串可能哈希出来的位置相同
可以适当增加位数组大小或者调整我们的哈希函数来降低概率
9. 缓存雪崩
9.1. 什么是缓存雪崩?
我发现缓存雪崩这名字起的有点意思,哈哈。
实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。
还有一种缓存雪崩的场景是:有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。 这样的情况,有下面几种解决办法:
举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。
9.2. 有哪些解决办法?
针对 Redis 服务不可用的情况:
- 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
- 限流,避免同时处理大量的请求。
针对热点缓存失效的情况:
- 设置不同的失效时间比如随机设置缓存的失效时间。
- 缓存永不失效。
10.如何保证缓存和数据库数据的一致性?
先更新数据库,然后再删除缓存。
下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。
Cache Aside Pattern 中遇到写请求是这样的:先更新 DB,然后直接删除 cache 。
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:
- 缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
- 增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。