redis-all.md 32.0 KB
Newer Older
S
shuang.kou 已提交
1
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
S
SnailClimb 已提交
2

S
shuang.kou 已提交
3 4 5 6 7 8
- [简单说说有哪些本地缓存解决方案?](#简单说说有哪些本地缓存解决方案)
- [为什么要有分布式缓存?/为什么不直接用本地缓存?](#为什么要有分布式缓存为什么不直接用本地缓存)
- [分布式缓存有哪些常见的技术选型方案呢?](#分布式缓存有哪些常见的技术选型方案呢)
- [简单介绍一下 Redis 呗!](#简单介绍一下-redis-呗)
- [说一下 Redis 和 Memcached 的区别和共同点](#说一下-redis-和-memcached-的区别和共同点)
- [为什么要用 Redis/为什么要用缓存?](#为什么要用-redis为什么要用缓存)
S
shuang.kou 已提交
9 10 11 12 13 14 15 16 17 18 19
- [Redis 的线程模型](#redis-的线程模型)
- [Redis 常见数据结构以及使用场景分析](#redis-常见数据结构以及使用场景分析)
    - [1.String](#1string)
    - [2.Hash](#2hash)
    - [3.List](#3list)
    - [4.Set](#4set)
    - [5.Sorted Set](#5sorted-set)
- [Redis 设置过期时间](#redis-设置过期时间)
- [Redis 内存淘汰机制(MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?)](#redis-内存淘汰机制mysql-里有-2000w-数据redis-中只存-20w-的数据如何保证-redis-中的数据都是热点数据)
- [Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)](#redis-持久化机制怎么保证-redis-挂掉之后再重启数据可以进行恢复)
- [Redis 事务](#redis-事务)
20
- [缓存雪崩和缓存穿透问题解决方案](#缓存雪崩和缓存穿透问题解决方案)
S
shuang.kou 已提交
21 22
    - [**缓存雪崩**](#缓存雪崩)
    - [**缓存穿透**](#缓存穿透)
23
- [如何解决 Redis 的并发竞争 Key 问题](#如何解决-redis-的并发竞争-key-问题)
S
SnailClimb 已提交
24
- [如何保证缓存与数据库双写时的数据一致性?](#如何保证缓存与数据库双写时的数据一致性)
S
shuang.kou 已提交
25
- [参考](#参考)
S
Snailclimb 已提交
26

S
shuang.kou 已提交
27
*正式开始 Redis 之前,我们先来回顾一下在 Java 后端领域,有哪些可以用来当做缓存。*
S
Snailclimb 已提交
28

S
shuang.kou 已提交
29
### 简单说说有哪些本地缓存解决方案?
S
Snailclimb 已提交
30

S
shuang.kou 已提交
31
*先来聊聊本地缓存,这个实际在很多项目中用的蛮多,特别是单体架构的时候。数据量不大,并且没有分布式要求的话,使用本地缓存还是可以的。*
S
Snailclimb 已提交
32

S
shuang.kou 已提交
33
常见的单体架构图如下,我们使用 Nginx 来做负载均衡,部署两个相同的服务到服务器,两个服务使用同一个数据库,并且使用的是本地缓存。
S
Snailclimb 已提交
34

S
shuang.kou 已提交
35
![单体架构](./images/redis/单体架构.png)
S
Snailclimb 已提交
36

S
shuang.kou 已提交
37
*那本地缓存的方案有哪些呢?且听 Guide 给你来说一说。*
S
Snailclimb 已提交
38

S
shuang.kou 已提交
39
**一:JDK 自带的 `HashMap` 和 `ConcurrentHashMap`了。**
S
Snailclimb 已提交
40

S
shuang.kou 已提交
41
 `ConcurrentHashMap` 可以看作是线程安全版本的 `HashMap` ,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。
S
Snailclimb 已提交
42

S
shuang.kou 已提交
43
**二:`Ehcache` 、 `Guava Cache` 、`Spring Cache`这三者是使用的比较多的本地缓存框架。**
S
Snailclimb 已提交
44

S
shuang.kou 已提交
45
`Ehcache` 的话相比于其他两者更加重量。不过,相比于 `Guava Cache``Spring Cache`来说, `Ehcache` 支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。
S
Snailclimb 已提交
46

S
shuang.kou 已提交
47
`Guava Cache``Spring Cache`两者的话比较像。
S
Snailclimb 已提交
48

S
shuang.kou 已提交
49
`Guava` 相比于 `Spring Cache` 的话使用的更多一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方都和`ConcurrentHashMap`的思想有异曲同工之妙。
S
Snailclimb 已提交
50

S
shuang.kou 已提交
51
使用 `Spring Cache` 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。
S
Snailclimb 已提交
52

S
shuang.kou 已提交
53
**三 :`Caffeine`**
S
Snailclimb 已提交
54

S
shuang.kou 已提交
55
相比于 `Guava`来说  `Caffeine` 在各个方面比如性能要更加优秀,一般建议使用其来替代  `Guava`。并且, `Guava``Caffeine` 的使用方式很像!
A
Anthony 已提交
56

S
shuang.kou 已提交
57
本地缓存固然好,但是缺陷也很明显,比如多个相同服务之间的本地缓存的数据无法共享。
A
Anthony 已提交
58

S
shuang.kou 已提交
59
*下面我们从为什么要有分布式缓存为接入点来正式进入 Redis 的相关问题总结。*
A
Anthony 已提交
60

S
shuang.kou 已提交
61
### 为什么要有分布式缓存?/为什么不直接用本地缓存?
A
Anthony 已提交
62

S
shuang.kou 已提交
63
*我们可以把分布式缓存(Distributed Cache) 看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。*
A
Anthony 已提交
64

S
shuang.kou 已提交
65 66 67 68 69 70 71
如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用 Nginx 来做负载均衡,部署两个相同的服务到服务器,两个服务使用同一个数据库和缓存。

![集中式缓存架构](./images/redis/集中式缓存架构.png)

本地的缓存的优势是低依赖,比较轻量并且通常相比于使用分布式缓存要更加简单。

再来分析一下本地缓存的局限性:
A
Anthony 已提交
72

S
shuang.kou 已提交
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
1. 本地缓存对分布式架构支持不友好,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
2. 容量跟随服务器限制明显。

使用分布式缓存之后,缓存部署在一台单独的服务器上,即使同一个相同的服务部署在再多机器上,也是使用的同一份缓存。 并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。

使用分布式缓存的缺点呢,也很显而易见,那就是你需要为分布式缓存引入额外的服务比如 Redis 或 Memcached,你需要单独保证 Redis 或 Memcached 服务的高可用。

### 分布式缓存有哪些常见的技术选型方案呢?

分布式缓存的话,使用的比较多的主要是 Memcached 和 Redis。不过,现在基本没有看过还有项目使用 Memcached 来做缓存,都是直接用 Redis。

Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。

分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。因为,本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共同的。

### 简单介绍一下 Redis 呗!

简单来说 Redis 就是一个 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。

另外,除了做缓存之外,Redis 也经常用来做分布式锁,甚至是消息队列。

Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。

### 说一下 Redis 和 Memcached 的区别和共同点
S
Snailclimb 已提交
97

S
shuang.kou 已提交
98
现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据!
S
Snailclimb 已提交
99

S
shuang.kou 已提交
100
**共同点**
S
shuang.kou 已提交
101 102 103 104 105

1. 都是基于内存的缓存。
2. 都有过期策略。
3. 两者的性能都非常高。

S
shuang.kou 已提交
106
**区别**
S
shuang.kou 已提交
107

S
shuang.kou 已提交
108
1. **Redis 支持更丰富的数据类型(支持更复杂的应用场景)**。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
S
shuang.kou 已提交
109
2. **Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。**
S
shuang.kou 已提交
110 111 112 113
3. **Redis 有灾难恢复机制。** 因为可以把缓存中的数据持久化到磁盘上。
4. **Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。**
5. **Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的.**
6. **Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。** (Redis 6.0 引入了多线程 IO )
S
shuang.kou 已提交
114 115 116
7. **Redis 支持发布订阅模型、Lua脚本、事务等功能,而Memcached不支持。并且,Redis支持更多的编程语言。**

相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached来作为自己项目的分布式缓存了。
S
Snailclimb 已提交
117

S
shuang.kou 已提交
118 119 120 121 122 123 124 125 126 127 128 129 130
### 缓存数据的处理流程是怎样的?

作为暖男一号,我给大家画了一个草图。

![正常缓存处理流程](https://static01.imgkr.com/temp/bc0cfbc9911148eeb0542d84a049d9f2.png)

简单来说就是:

1. 如果用户请求的数据在缓存中就直接返回。
2. 缓存中不存在的话就看数据库中是否存在。
3. 数据库中存在的话就更新缓存中的数据。
4. 数据库中不存在的话就返回空数据。

S
shuang.kou 已提交
131
### 为什么要用 Redis/为什么要用缓存?
132

S
shuang.kou 已提交
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
*简单,来说使用缓存主要是为了提升用户体验以及应对更多的用户。*

下面我们主要从“高性能”和“高并发”这两点来看待这个问题。

![](./images/redis/使用缓存之后.png)

**高性能**

对照上面👆我画的图。我们设想这样的场景:

假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。

**这样有什么好处呢?** 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。

不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

**高并发:**

S
shuang.kou 已提交
151
一般像 MySQL这类的数据库的 QPS 大概都在 1w 左右(4核8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到30w+(就单机redis的情况,redis 集群的话会更高)。
S
shuang.kou 已提交
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168

>  QPS(Query Per Second):服务器每秒可以执行的查询次数;

所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。

### Redis 的线程模型

Redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
169

S
shuang.kou 已提交
170
### Redis 常见数据结构以及使用场景分析
S
Snailclimb 已提交
171

S
SnailClimb 已提交
172
#### 1.String
S
Snailclimb 已提交
173

S
shuang.kou 已提交
174
> **常用命令:** set,get,decr,incr,mget 等。
S
Snailclimb 已提交
175

S
shuang.kou 已提交
176 177
String 数据结构是简单的 key-value 类型,value 其实不仅可以是 String,也可以是数字。
常规 key-value 缓存应用;
S
Snailclimb 已提交
178
常规计数:微博数,粉丝数等。
S
Snailclimb 已提交
179

S
Snailclimb 已提交
180
#### 2.Hash
S
shuang.kou 已提交
181

S
Snailclimb 已提交
182
> **常用命令:** hget,hset,hgetall 等。
S
Snailclimb 已提交
183

D
dongzl 已提交
184
hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:
185 186 187 188 189 190 191 192 193

```
key=JavaUser293847
value={
  “id”: 1,
  “name”: “SnailClimb”,
  “age”: 22,
  “location”: “Wuhan, Hubei”
}
S
Snailclimb 已提交
194

195
```
S
Snailclimb 已提交
196

S
Snailclimb 已提交
197
#### 3.List
S
Snailclimb 已提交
198

S
shuang.kou 已提交
199 200 201
> **常用命令:** lpush,rpush,lpop,rpop,lrange 等

list 就是链表,Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用 Redis 的 list 结构来实现。
S
Snailclimb 已提交
202

203
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
S
Snailclimb 已提交
204

S
shuang.kou 已提交
205
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
S
Snailclimb 已提交
206

S
Snailclimb 已提交
207
#### 4.Set
208

S
Snailclimb 已提交
209
> **常用命令:**
S
shuang.kou 已提交
210
> sadd,spop,smembers,sunion 等
S
Snailclimb 已提交
211

S
shuang.kou 已提交
212
Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。
S
Snailclimb 已提交
213

S
shuang.kou 已提交
214
当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
S
Snailclimb 已提交
215

S
shuang.kou 已提交
216
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:
S
Snailclimb 已提交
217

218 219 220
```
sinterstore key1 key2 key3     将交集存在key1内
```
S
Snailclimb 已提交
221 222 223

#### 5.Sorted Set

S
shuang.kou 已提交
224
> **常用命令:** zadd,zrange,zrem,zcard 等
S
Snailclimb 已提交
225

S
shuang.kou 已提交
226
和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。
S
Snailclimb 已提交
227

D
dongzl 已提交
228
**举例:** 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
229

S
shuang.kou 已提交
230
### Redis 设置过期时间
231

S
shuang.kou 已提交
232 233
*一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。*

S
shuang.kou 已提交
234
Redis 中有个设置时间过期的功能,即对存储在 Redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
235 236 237

我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。

S
shuang.kou 已提交
238
如果假设你设置了一批 key 只能存活 1 个小时,那么接下来 1 小时后,Redis 是怎么对这批 key 进行删除的?
239 240 241 242 243

**定期删除+惰性删除。**

通过名字大概就能猜出这两个删除方式的意思了。

S
shuang.kou 已提交
244 245
- **定期删除**:Redis 默认是每隔 100ms 就**随机抽取**一些设置了过期时间的 key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 Redis 存了几十万个 key ,每隔 100ms 就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
- **惰性删除** :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被 Redis 给删除掉。这就是所谓的惰性删除,也是够懒的哈!
S
Snailclimb 已提交
246

S
shuang.kou 已提交
247
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 Redis 内存块耗尽了。怎么解决这个问题呢? **Redis 内存淘汰机制。**
S
Snailclimb 已提交
248

S
shuang.kou 已提交
249
### Redis 内存淘汰机制(MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?)
250

S
shuang.kou 已提交
251
Redis 配置文件 Redis.conf 中有相关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看: [http://download.Redis.io/Redis-stable/Redis.conf](http://download.Redis.io/Redis-stable/Redis.conf)
252

S
shuang.kou 已提交
253
**Redis 提供 6 种数据淘汰策略:**
S
Snailclimb 已提交
254 255 256 257

1. **volatile-lru**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
2. **volatile-ttl**:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
3. **volatile-random**:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
S
shuang.kou 已提交
258
4. **allkeys-lru**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
S
Snailclimb 已提交
259
5. **allkeys-random**:从数据集(server.db[i].dict)中任意选择数据淘汰
李文文丶's avatar
李文文丶 已提交
260
6. **no-eviction**:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
S
Snailclimb 已提交
261

S
shuang.kou 已提交
262
4.0 版本后增加以下两种:
Fantasywk0224's avatar
Fantasywk0224 已提交
263 264

7. **volatile-lfu**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
S
shuang.kou 已提交
265
8. **allkeys-lfu**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
Fantasywk0224's avatar
Fantasywk0224 已提交
266

S
shuang.kou 已提交
267
**备注: 关于 Redis 设置过期时间以及内存淘汰机制,我这里只是简单的总结一下,后面会专门写一篇文章来总结!**
S
Snailclimb 已提交
268

S
shuang.kou 已提交
269
### Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)
S
Snailclimb 已提交
270

S
SnailClimb 已提交
271
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
S
Snailclimb 已提交
272

S
shuang.kou 已提交
273
Redis 不同于 Memcached 的很重一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。**Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)**。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
S
Snailclimb 已提交
274

275
**快照(snapshotting)持久化(RDB)**
S
Snailclimb 已提交
276

S
shuang.kou 已提交
277
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
S
Snailclimb 已提交
278

S
shuang.kou 已提交
279
快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:
S
Snailclimb 已提交
280

281
```conf
S
Snailclimb 已提交
282

D
dongzl 已提交
283
save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
S
Snailclimb 已提交
284

D
dongzl 已提交
285
save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
S
Snailclimb 已提交
286

287 288
save 60 10000        #60秒(1分钟)之后,如果至少有10000key发生变化,Redis就会自动触发BGSAVE命令创建快照。
```
S
Snailclimb 已提交
289

290
**AOF(append-only file)持久化**
S
Snailclimb 已提交
291

S
shuang.kou 已提交
292
与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:
S
Snailclimb 已提交
293

294 295 296
```conf
appendonly yes
```
S
Snailclimb 已提交
297

S
shuang.kou 已提交
298
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。
S
Snailclimb 已提交
299

S
shuang.kou 已提交
300
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
S
Snailclimb 已提交
301

302
```conf
D
dongzl 已提交
303
appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
304
appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
D
dongzl 已提交
305
appendfsync no        #让操作系统决定何时进行同步
306
```
S
Snailclimb 已提交
307

S
shuang.kou 已提交
308
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
S
Snailclimb 已提交
309

S
shuang.kou 已提交
310
[相关 Issue783:Redis 的 AOF 方式](https://github.com/Snailclimb/JavaGuide/issues/783)
S
shuang.kou 已提交
311

312
**Redis 4.0 对于持久化机制的优化**
S
Snailclimb 已提交
313

314
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。
S
Snailclimb 已提交
315

316 317 318 319
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

**补充内容:AOF 重写**

S
shuang.kou 已提交
320
AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
321

S
shuang.kou 已提交
322
AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。
323

S
shuang.kou 已提交
324
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作
325 326

**更多内容可以查看我的这篇文章:**
S
Snailclimb 已提交
327

S
shuang.kou 已提交
328
- [Redis 持久化](Redis持久化.md)
S
Snailclimb 已提交
329

S
shuang.kou 已提交
330
### Redis 事务
S
Snailclimb 已提交
331

332
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
S
Snailclimb 已提交
333

D
dongzl 已提交
334
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
S
Snailclimb 已提交
335

K
Kou Shuang 已提交
336 337
补充内容:

S
shuang.kou 已提交
338
> 1. Redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。(来自[issue:关于 Redis 事务不是原子性问题](https://github.com/Snailclimb/JavaGuide/issues/452) )
K
Kou Shuang 已提交
339

S
shuang.kou 已提交
340
### 缓存穿透
S
Snailclimb 已提交
341

S
shuang.kou 已提交
342
#### 什么是缓存穿透?
343

S
shuang.kou 已提交
344
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
345

S
shuang.kou 已提交
346
#### 缓存穿透情况的处理流程是怎样的?
347

S
shuang.kou 已提交
348
如下图所示,用户的请求最终都要跑到数据库中查询一遍。
349

S
shuang.kou 已提交
350
![缓存穿透情况](./images/redis/缓存穿透情况.png)
351

S
shuang.kou 已提交
352
#### 有哪些解决办法?
S
Snailclimb 已提交
353 354 355

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

S
shuang.kou 已提交
356 357 358
**1)缓存无效 key** 

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
S
Snailclimb 已提交
359 360 361

另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`

S
shuang.kou 已提交
362
如果用 Java 代码展示的话,差不多是下面这样的:
S
Snailclimb 已提交
363 364 365 366 367 368

```java
public Object getObjectInclNullById(Integer id) {
    // 从缓存中获取数据
    Object cacheValue = cache.get(id);
    // 缓存为空
J
Jiabin 已提交
369
    if (cacheValue == null) {
S
Snailclimb 已提交
370 371 372 373 374 375 376 377 378 379 380 381 382 383
        // 从数据库中获取
        Object storageValue = storage.get(key);
        // 缓存空对象
        cache.set(key, storageValue);
        // 如果存储数据为空,需要设置一个过期时间(300秒)
        if (storageValue == null) {
            // 必须设置过期时间,否则有被攻击的风险
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    }
    return cacheValue;
}
```
384

S
shuang.kou 已提交
385 386 387 388 389 390 391
**2)布隆过滤器**

布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

加入布隆过滤器之后的缓存处理流程图如下。
392

S
shuang.kou 已提交
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
![image](https://static01.imgkr.com/temp/e384cec584314b019de6e3a39ee56425.png)

但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: **布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。**  

*为什么会出现误判的情况呢?我们还要从布隆过滤器的原理来说!*

我们先来看一下,**当一个元素加入布隆过滤器中的时候,会进行哪些操作:**

1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

我们再来看一下,**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:**

1. 对给定元素再次进行相同的哈希计算;
2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

然后,一定会出现这样一种情况:**不同的字符串可能哈希出来的位置相同。** (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)
S
Snailclimb 已提交
410 411

更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。
412

S
shuang.kou 已提交
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
### 缓存雪崩

#### 什么是缓存雪崩?

我发现缓存雪崩这名字起的有点意思,哈哈。

实际上,缓存雪崩描述的就是这样一个简单的场景:**缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。** 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。

举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。

还有一种缓存雪崩的场景是:**有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。** 这样的情况,有下面几种解决办法:

举个例子 :秒杀开始12个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是12个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。

#### 有哪些解决办法?

Redis服务不可用:

1. 采用Redis集群,避免单机出现问题整个缓存服务都没办法使用。
2. 限流,避免同时处理大量的请求。

热点缓存失效:

1. 设置不同的失效时间比如随机设置缓存的失效时间。
2. 缓存永不失效。

439 440 441 442
### 如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

S
shuang.kou 已提交
443
推荐一种方案:分布式锁(zookeeper 和 Redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
444

S
shuang.kou 已提交
445
基于 zookeeper 临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在 zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
446

S
shuang.kou 已提交
447
在实践中,当然是从以可靠性为主。所以首推 Zookeeper。
448 449 450 451 452

参考:

- https://www.jianshu.com/p/8bddd381de06

S
SnailClimb 已提交
453
### 如何保证缓存与数据库双写时的数据一致性?
454

K
Kou Shuang 已提交
455 456
> 一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况。

457 458 459 460 461 462
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

S
shuang.kou 已提交
463
更多内容可以查看:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/Redis-consistence.md
K
Kou Shuang 已提交
464

S
shuang.kou 已提交
465
**参考:** Java 工程师面试突击第 1 季(可能是史上最好的 Java 面试突击课程)-中华石杉老师!公众号后台回复关键字“1”即可获取该视频内容。
S
Snailclimb 已提交
466

S
Snailclimb 已提交
467 468
### 参考

S
shuang.kou 已提交
469
- 《Redis 开发与运维》
S
shuang.kou 已提交
470
- Redis 命令总结:http://Redisdoc.com/string/set.html
S
Snailclimb 已提交
471

S
SnailClimb 已提交
472
## 公众号
S
Snailclimb 已提交
473

S
SnailClimb 已提交
474 475
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。

S
shuang.kou 已提交
476
**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取!
S
SnailClimb 已提交
477

S
shuang.kou 已提交
478
**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
S
SnailClimb 已提交
479

C
chenai 已提交
480
![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)