1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

[refractor]缓存常见问题更新完善

This commit is contained in:
guide 2020-08-03 20:09:51 +08:00
parent b4552a6436
commit ce53c55ecb
3 changed files with 206 additions and 151 deletions

View File

@ -77,7 +77,7 @@
- [MyBatis](#mybatis)
- [认证授权(JWT、SSO)](#认证授权)
- [分布式](#分布式)
- [Elasticsearch(分布式搜索引擎)](#elasticsearch分布式搜索引擎)
- [分布式搜索引擎](#分布式搜索引擎)
- [RPC](#rpc)
- [消息队列](#消息队列)
- [API 网关](#api-网关)
@ -220,6 +220,7 @@
### Redis
* [Redis前置菜-关于缓存的一些概念](docs/database/Redis/some-concepts-of-caching.md)
* [Redis 常见问题总结](docs/database/Redis/redis-all.md)
* **Redis 系列文章合集:**
1. 数据结构和算法 [5种基本数据结构](docs/database/Redis/redis-collection/Redis(1)——5种基本数据结构.md)、[跳跃表](docs/database/Redis/redis-collection/Redis(2)——跳跃表.md)、[神奇的HyperLoglog解决统计问题](docs/database/Redis/redis-collection/Reids(4)——神奇的HyperLoglog解决统计问题.md)、[亿级数据过滤和布隆过滤器](docs/database/Redis/redis-collection/Redis(5)——亿级数据过滤和布隆过滤器.md)、[GeoHash查找附近的人](docs/database/Redis/redis-collection/Redis(6)——GeoHash查找附近的人.md)
@ -270,11 +271,13 @@ SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中
[分布式相关概念入门](docs/system-design/website-architecture/分布式.md)
#### Elasticsearch(分布式搜索引擎)
#### 分布式搜索引擎
提高搜索效率。常见于电商购物网站的商品搜索于分类。
代办......
比较常用的是 Elasticsearch 和 Solr。
代办。
#### RPC

View File

@ -1,107 +1,45 @@
点击关注[公众号](#公众号)及时获取笔主最新更新文章并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [1. 简单说说有哪些本地缓存解决方案?](#1-简单说说有哪些本地缓存解决方案)
- [2. 为什么要有分布式缓存?/为什么不直接用本地缓存?](#2-为什么要有分布式缓存为什么不直接用本地缓存)
- [3. 分布式缓存有哪些常见的技术选型方案呢?](#3-分布式缓存有哪些常见的技术选型方案呢)
- [4. 简单介绍一下 Redis 呗!](#4-简单介绍一下-redis-呗)
- [5. 说一下 Redis 和 Memcached 的区别和共同点](#5-说一下-redis-和-memcached-的区别和共同点)
- [6. 缓存数据的处理流程是怎样的?](#6-缓存数据的处理流程是怎样的)
- [7. 为什么要用 Redis/为什么要用缓存?](#7-为什么要用-redis为什么要用缓存)
- [8. Redis 常见数据结构以及使用场景分析](#8-redis-常见数据结构以及使用场景分析)
- [8.1. string](#81-string)
- [8.2. list](#82-list)
- [8.3. hash](#83-hash)
- [8.4. set](#84-set)
- [8.5. sorted set](#85-sorted-set)
- [9. Redis 单线程模型详解](#9-redis-单线程模型详解)
- [10. Redis 没有使用多线程?为什么不使用多线程?](#10-redis-没有使用多线程为什么不使用多线程)
- [11. Redis6.0 之后为何引入了多线程?](#11-redis60-之后为何引入了多线程)
- [12. Redis 给缓存数据设置过期时间有啥用?](#12-redis-给缓存数据设置过期时间有啥用)
- [13. Redis是如何判断数据是否过期的呢](#13-redis是如何判断数据是否过期的呢)
- [14. 过期的数据的删除策略了解么?](#14-过期的数据的删除策略了解么)
- [15. Redis 内存淘汰机制了解么?](#15-redis-内存淘汰机制了解么)
- [16. Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)](#16-redis-持久化机制怎么保证-redis-挂掉之后再重启数据可以进行恢复)
- [17. Redis 事务](#17-redis-事务)
- [18. 缓存穿透](#18-缓存穿透)
- [18.1. 什么是缓存穿透?](#181-什么是缓存穿透)
- [18.2. 缓存穿透情况的处理流程是怎样的?](#182-缓存穿透情况的处理流程是怎样的)
- [18.3. 有哪些解决办法?](#183-有哪些解决办法)
- [19. 缓存雪崩](#19-缓存雪崩)
- [19.1. 什么是缓存雪崩?](#191-什么是缓存雪崩)
- [19.2. 有哪些解决办法?](#192-有哪些解决办法)
- [20. 如何解决 Redis 的并发竞争 Key 问题](#20-如何解决-redis-的并发竞争-key-问题)
- [21. 如何保证缓存与数据库双写时的数据一致性?](#21-如何保证缓存与数据库双写时的数据一致性)
- [22. 参考](#22-参考)
- [23. 公众号](#23-公众号)
- [1. 简单介绍一下 Redis 呗!](#1-简单介绍一下-redis-呗)
- [2. 分布式缓存常见的技术选型方案有哪些?](#2-分布式缓存常见的技术选型方案有哪些)
- [3. 说一下 Redis 和 Memcached 的区别和共同点](#3-说一下-redis-和-memcached-的区别和共同点)
- [4. 缓存数据的处理流程是怎样的?](#4-缓存数据的处理流程是怎样的)
- [5. 为什么要用 Redis/为什么要用缓存?](#5-为什么要用-redis为什么要用缓存)
- [6. Redis 常见数据结构以及使用场景分析](#6-redis-常见数据结构以及使用场景分析)
- [6.1. string](#61-string)
- [6.2. list](#62-list)
- [6.3. hash](#63-hash)
- [6.4. set](#64-set)
- [6.5. sorted set](#65-sorted-set)
- [7. Redis 单线程模型详解](#7-redis-单线程模型详解)
- [8. Redis 没有使用多线程?为什么不使用多线程?](#8-redis-没有使用多线程为什么不使用多线程)
- [9. Redis6.0 之后为何引入了多线程?](#9-redis60-之后为何引入了多线程)
- [10. Redis 给缓存数据设置过期时间有啥用?](#10-redis-给缓存数据设置过期时间有啥用)
- [11. Redis是如何判断数据是否过期的呢](#11-redis是如何判断数据是否过期的呢)
- [12. 过期的数据的删除策略了解么?](#12-过期的数据的删除策略了解么)
- [13. Redis 内存淘汰机制了解么?](#13-redis-内存淘汰机制了解么)
- [14. Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)](#14-redis-持久化机制怎么保证-redis-挂掉之后再重启数据可以进行恢复)
- [15. Redis 事务](#15-redis-事务)
- [16. 缓存穿透](#16-缓存穿透)
- [16.1. 什么是缓存穿透?](#161-什么是缓存穿透)
- [16.2. 缓存穿透情况的处理流程是怎样的?](#162-缓存穿透情况的处理流程是怎样的)
- [16.3. 有哪些解决办法?](#163-有哪些解决办法)
- [17. 缓存雪崩](#17-缓存雪崩)
- [17.1. 什么是缓存雪崩?](#171-什么是缓存雪崩)
- [17.2. 有哪些解决办法?](#172-有哪些解决办法)
- [18. 如何保证缓存与数据库双写时的数据一致性?](#18-如何保证缓存与数据库双写时的数据一致性)
- [19. 参考](#19-参考)
- [20. 公众号](#20-公众号)
<!-- /code_chunk_output -->
*正式开始 Redis 之前,我们先来回顾一下在 Java 后端领域,有哪些可以用来当做缓存。*
### 1. 简单说说有哪些本地缓存解决方案?
_先来聊聊本地缓存这个实际在很多项目中用的蛮多特别是单体架构的时候。数据量不大并且没有分布式要求的话使用本地缓存还是可以的。_
常见的单体架构图如下,我们使用 **Nginx** 来做**负载均衡**,部署两个相同的服务到服务器,两个服务使用同一个数据库,并且使用的是本地缓存。
![单体架构](./images/redis-all/单体架构.png)
_那本地缓存的方案有哪些呢且听 Guide 给你来说一说。_
**一JDK 自带的 `HashMap``ConcurrentHashMap` 了。**
`ConcurrentHashMap` 可以看作是线程安全版本的 `HashMap` ,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。
**二: `Ehcache``Guava Cache``Spring Cache` 这三者是使用的比较多的本地缓存框架。**
`Ehcache` 的话相比于其他两者更加重量。不过,相比于 `Guava Cache``Spring Cache` 来说, `Ehcache` 支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。
`Guava Cache``Spring Cache` 两者的话比较像。
`Guava` 相比于 `Spring Cache` 的话使用的更多一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方都和 `ConcurrentHashMap` 的思想有异曲同工之妙。
使用 `Spring Cache` 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。
**三: 后起之秀Caffeine。**
相比于 `Guava` 来说 `Caffeine` 在各个方面比如性能要更加优秀,一般建议使用其来替代 `Guava` 。并且, `Guava``Caffeine` 的使用方式很像!
本地缓存固然好,但是缺陷也很明显,比如多个相同服务之间的本地缓存的数据无法共享。
_下面我们从为什么要有分布式缓存为接入点来正式进入 Redis 的相关问题总结。_
### 2. 为什么要有分布式缓存?/为什么不直接用本地缓存?
_我们可以把分布式缓存Distributed Cache 看作是一种内存数据库的服务它的最终作用就是提供缓存数据的服务。_
如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用 Nginx 来做负载均衡,部署两个相同的服务到服务器,两个服务使用同一个数据库和缓存。
![集中式缓存架构](./images/redis-all/集中式缓存架构.png)
本地的缓存的优势是低依赖,比较轻量并且通常相比于使用分布式缓存要更加简单。
再来分析一下本地缓存的局限性:
1. **本地缓存对分布式架构支持不友好**,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
2. **本地缓存容量受服务部署所在的机器限制明显。** 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。
使用分布式缓存之后,缓存部署在一台单独的服务器上,即使同一个相同的服务部署在再多机器上,也是使用的同一份缓存。 并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。
使用分布式缓存的缺点呢,也很显而易见,那就是你需要为分布式缓存引入额外的服务比如 Redis 或 Memcached你需要单独保证 Redis 或 Memcached 服务的高可用。
### 3. 分布式缓存有哪些常见的技术选型方案呢?
分布式缓存的话,使用的比较多的主要是 **Memcached****Redis**。不过,现在基本没有看过还有项目使用 **Memcached** 来做缓存,都是直接用 **Redis**
Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。因为,本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共同的。
### 4. 简单介绍一下 Redis 呗!
### 1. 简单介绍一下 Redis 呗!
简单来说 **Redis 就是一个使用 C 语言开发的数据库**,不过与传统数据库不同的是 **Redis 的数据是存在内存中的** ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。
@ -109,7 +47,15 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来
**Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。**
### 5. 说一下 Redis 和 Memcached 的区别和共同点
### 2. 分布式缓存常见的技术选型方案有哪些?
分布式缓存的话,使用的比较多的主要是 **Memcached****Redis**。不过,现在基本没有看过还有项目使用 **Memcached** 来做缓存,都是直接用 **Redis**
Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。因为,本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共同的。
### 3. 说一下 Redis 和 Memcached 的区别和共同点
现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据!
@ -132,7 +78,7 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来
相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。
### 6. 缓存数据的处理流程是怎样的?
### 4. 缓存数据的处理流程是怎样的?
作为暖男一号,我给大家画了一个草图。
@ -145,7 +91,7 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来
3. 数据库中存在的话就更新缓存中的数据。
4. 数据库中不存在的话就返回空数据。
### 7. 为什么要用 Redis/为什么要用缓存?
### 5. 为什么要用 Redis/为什么要用缓存?
_简单来说使用缓存主要是为了提升用户体验以及应对更多的用户。_
@ -171,13 +117,13 @@ _简单来说使用缓存主要是为了提升用户体验以及应对更多
所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。
### 8. Redis 常见数据结构以及使用场景分析
### 6. Redis 常见数据结构以及使用场景分析
你可以自己本机安装 redis 或者通过 redis 官网提供的[在线 redis 环境](https://try.redis.io/)。
![try-redis](./images/redis-all/try-redis.png)
#### 8.1. string
#### 6.1. string
1. **介绍** string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 **简单动态字符串**simple dynamic string**SDS**)。相比于 C 的原生字符串Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)C 字符串为 O(N),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
2. **常用命令:** `set,get,strlen,exists,dect,incr,setex` 等等。
@ -239,7 +185,7 @@ OK
(integer) 56
```
#### 8.2. list
#### 6.2. list
1. **介绍** **list** 即是 **链表**。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 **LinkedList**,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 **双向链表**,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
2. **常用命令:** `rpush,lpop,lpush,rpop,lrange、llen` 等。
@ -300,7 +246,7 @@ OK
(integer) 3
```
#### 8.3. hash
#### 6.3. hash
1. **介绍** hash 类似于 JDK1.8 前的 HashMap内部实现也差不多(数组 + 链表)。不过Redis 的 hash 做了更多优化。另外hash 是一个 string 类型的 field 和 value 的映射表,**特别适合用于存储对象**,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
2. **常用命令:** `hset,hmset,hexists,hget,hgetall,hkeys,hvals` 等。
@ -337,7 +283,7 @@ OK
"GuideGeGe"
```
#### 8.4. set
#### 6.4. set
1. **介绍 ** set 类似于 Java 中的 `HashSet` 。Redis 中的 set 类型是一种无序集合集合中的元素没有先后顺序。当你需要存储一个列表数据又不希望出现重复数据时set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如你可以将一个用户所有的关注人存在一个集合中将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
2. **常用命令:** `sadd,spop,smembers,sismember,scard,sinterstore,sunion` 等。
@ -365,7 +311,7 @@ OK
1) "value2"
```
#### 8.5. sorted set
#### 6.5. sorted set
1. **介绍:** 和 set 相比sorted set 增加了一个权重参数 score使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。
2. **常用命令:** `zadd,zcard,zscore,zrange,zrevrange,zrem` 等。
@ -392,7 +338,7 @@ OK
2) "value2"
```
### 9. Redis 单线程模型详解
### 7. Redis 单线程模型详解
**Redis 基于 Reactor 模式来设计开发了自己的一套高效的事件处理模型** Netty 的线程模型也基于 Reactor 模式Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器file event handler。由于文件事件处理器file event handler是单线程方式运行的所以我们一般都说 Redis 是单线程模型。
@ -425,7 +371,7 @@ Redis 通过**IO 多路复用程序** 来监听来自客户端的大量连接(
<p style="text-align:right; font-size:14px; color:gray">《Redis设计与实现12章》</p>
### 10. Redis 没有使用多线程?为什么不使用多线程?
### 8. Redis 没有使用多线程?为什么不使用多线程?
虽然说 Redis 是单线程模型,但是, 实际上,**Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。**
@ -443,7 +389,7 @@ Redis 通过**IO 多路复用程序** 来监听来自客户端的大量连接(
2. Redis 的性能瓶颈不再 CPU ,主要在内存和网络;
3. 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
### 11. Redis6.0 之后为何引入了多线程?
### 9. Redis6.0 之后为何引入了多线程?
**Redis6.0 引入多线程主要是为了提高网络 IO 读写性能**,因为这个算是 Redis 中的一个性能瓶颈Redis 的瓶颈主要受限于内存和网络)。
@ -466,7 +412,7 @@ io-threads 4 #官网建议4核的机器建议设置为2或3个线程8核的
1. [Redis 6.0 新特性-多线程连环 13 问!](https://mp.weixin.qq.com/s/FZu3acwK6zrCBZQ_3HoUgw)
2. [为什么 Redis 选择单线程模型](https://draveness.me/whys-the-design-redis-single-thread/)
### 12. Redis 给缓存数据设置过期时间有啥用?
### 10. Redis 给缓存数据设置过期时间有啥用?
一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?
@ -491,7 +437,7 @@ OK
如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。
### 13. Redis是如何判断数据是否过期的呢
### 11. Redis是如何判断数据是否过期的呢
Redis 通过一个叫做过期字典可以看作是hash表来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键)过期字典的值是一个long long类型的整数这个整数保存了key所指向的数据库键的过期时间毫秒精度的UNIX时间戳
@ -509,7 +455,7 @@ typedef struct redisDb {
} redisDb;
```
### 14. 过期的数据的删除策略了解么?
### 12. 过期的数据的删除策略了解么?
如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后Redis 是怎么对这批 key 进行删除的呢?
@ -524,9 +470,9 @@ typedef struct redisDb {
怎么解决这个问题呢?答案就是: **Redis 内存淘汰机制。**
### 15. Redis 内存淘汰机制了解么?
### 13. Redis 内存淘汰机制了解么?
相关问题MySQL 里有 2000w 数据Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
> 相关问题MySQL 里有 2000w 数据Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 提供 6 种数据淘汰策略:
@ -542,7 +488,7 @@ Redis 提供 6 种数据淘汰策略:
7. **volatile-lfu**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
8. **allkeys-lfu**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
### 16. Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)
### 14. Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
@ -598,11 +544,7 @@ AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的
在执行 BGREWRITEAOF 命令时Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作
**更多内容可以查看我的这篇文章:**
* [Redis 持久化](Redis持久化.md)
### 17. Redis 事务
### 15. Redis 事务
Redis 可以通过 **MULTIEXECDISCARD 和 WATCH** 等命令来实现事务(transaction)功能。
@ -641,19 +583,19 @@ Redis官网也解释了自己为啥不支持回滚。简单来说就是Redis开
**相关issue** :[issue452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452) ,推荐阅读:[https://zhuanlan.zhihu.com/p/43897838](https://zhuanlan.zhihu.com/p/43897838) 。
### 18. 缓存穿透
### 16. 缓存穿透
#### 18.1. 什么是缓存穿透?
#### 16.1. 什么是缓存穿透?
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
#### 18.2. 缓存穿透情况的处理流程是怎样的?
#### 16.2. 缓存穿透情况的处理流程是怎样的?
如下图所示,用户的请求最终都要跑到数据库中查询一遍。
![缓存穿透情况](./images/redis-all/缓存穿透情况.png)
#### 18.3. 有哪些解决办法?
#### 16.3. 有哪些解决办法?
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
@ -714,9 +656,9 @@ _为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理
更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。
### 19. 缓存雪崩
### 17. 缓存雪崩
#### 19.1. 什么是缓存雪崩?
#### 17.1. 什么是缓存雪崩?
我发现缓存雪崩这名字起的有点意思,哈哈。
@ -728,7 +670,7 @@ _为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理
举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。
#### 19.2. 有哪些解决办法?
#### 17.2. 有哪些解决办法?
**针对 Redis 服务不可用的情况:**
@ -740,35 +682,20 @@ _为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理
1. 设置不同的失效时间比如随机设置缓存的失效时间。
2. 缓存永不失效。
### 20. 如何解决 Redis 的并发竞争 Key 问题
### 18. 如何保证缓存和数据库数据的一致性?
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
细说的话可以扯很多但是我觉得其实没太大必要小声BB很多解决方案我也没太弄明白。我个人觉得引入缓存之后如果为了短时间的不一致性问题选择让系统设计变得更加复杂的话完全没必要。
推荐一种方案分布式锁zookeeper 和 Redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
下面单独对 **Cache Aside Pattern旁路缓存模式** 来聊聊。
基于 zookeeper 临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在 zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁
Cache Aside Pattern 中遇到写请求是这样的:更新 DB然后直接删除 cache
在实践中,当然是从以可靠性为主。所以首推 Zookeeper。
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:
参考:
1. **缓存失效时间变短(不推荐,治标不治本)** :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
2. **增加cache更新重试机制常用** 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。
* https://www.jianshu.com/p/8bddd381de06
### 21. 如何保证缓存与数据库双写时的数据一致性?
> 一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况。
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
更多内容可以查看https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/Redis-consistence.md
**参考:** Java 工程师面试突击第 1 季(可能是史上最好的 Java 面试突击课程)-中华石杉老师公众号后台回复关键字“1”即可获取该视频内容。
### 22. 参考
### 19. 参考
* 《Redis 开发与运维》
* 《Redis 设计与实现》
@ -776,7 +703,7 @@ _为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理
* 通俗易懂的 Redis 数据结构基础教程:[https://juejin.im/post/5b53ee7e5188251aaa2d2e16](https://juejin.im/post/5b53ee7e5188251aaa2d2e16)
* WHY Redis choose single thread (vs multi threads): [https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153](https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153)
### 23. 公众号
### 20. 公众号
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。

View File

@ -0,0 +1,125 @@
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [1. 缓存的基本思想](#1-缓存的基本思想)
- [2. 使用缓存为系统带来了什么问题](#2-使用缓存为系统带来了什么问题)
- [3. 本地缓存解决方案](#3-本地缓存解决方案)
- [4. 为什么要有分布式缓存?/为什么不直接用本地缓存?](#4-为什么要有分布式缓存为什么不直接用本地缓存)
- [5. 缓存读写模式/更新策略](#5-缓存读写模式更新策略)
- [5.1. Cache Aside Pattern旁路缓存模式](#51-cache-aside-pattern旁路缓存模式)
- [5.2. Read/Write Through Pattern读写穿透](#52-readwrite-through-pattern读写穿透)
- [5.3. Write Behind Pattern异步缓存写入](#53-write-behind-pattern异步缓存写入)
<!-- /code_chunk_output -->
### 1. 缓存的基本思想
很多朋友,只知道缓存可以提高系统性能以及减少请求相应时间,但是,不太清楚缓存的本质思想是什么。
缓存的基本思想其实很简单,就是我们非常熟悉的空间换时间。不要把缓存想的太高大上,虽然,它的确对系统的性能提升的性价比非常高。
其实,我们在学习使用缓存的时候,你会发现缓存的思想实际在操作系统或者其他地方都被大量用到。 比如 **CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。** **再比如操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器Cache。**
回归到业务系统来说:**我们为了避免用户在请求数据的时候获取速度过于缓慢,所以我们在数据库之上增加了缓存这一层来弥补。**
当别人再问你,缓存的基本思想的时候,就把上面 👆 这段话告诉他,我觉得会让别人对你刮目相看。
### 2. 使用缓存为系统带来了什么问题
**软件系统设计中没有银弹,往往任何技术的引入都像是把双刃剑。** 但是,你使用好了之后,这把剑就是好剑。
简单来说,为系统引入缓存之后往往会带来下面这些问题:
_ps:其实我觉得引入本地缓存来做一些简单业务场景的话,实际带来的代价几乎可以忽略,下面 👇 主要是针对分布式缓存来说的。_
1. **系统复杂性增加** :引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存等等。
2. **系统开发成本往往会增加** :引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。但是,如果你只是简单的使用一下本地缓存存储一下简单的数据,并且数据量不大的话,那么就不需要单独去弄一个缓存服务。
### 3. 本地缓存解决方案
_先来聊聊本地缓存这个实际在很多项目中用的蛮多特别是单体架构的时候。数据量不大并且没有分布式要求的话使用本地缓存还是可以的。_
常见的单体架构图如下,我们使用 **Nginx** 来做**负载均衡**,部署两个相同的服务到服务器,两个服务使用同一个数据库,并且使用的是本地缓存。
![单体架构](./images/redis-all/单体架构.png)
_那本地缓存的方案有哪些呢且听 Guide 给你来说一说。_
**一JDK 自带的 `HashMap``ConcurrentHashMap` 了。**
`ConcurrentHashMap` 可以看作是线程安全版本的 `HashMap` ,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。
**二: `Ehcache``Guava Cache``Spring Cache` 这三者是使用的比较多的本地缓存框架。**
`Ehcache` 的话相比于其他两者更加重量。不过,相比于 `Guava Cache``Spring Cache` 来说, `Ehcache` 支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。
`Guava Cache``Spring Cache` 两者的话比较像。
`Guava` 相比于 `Spring Cache` 的话使用的更多一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方都和 `ConcurrentHashMap` 的思想有异曲同工之妙。
使用 `Spring Cache` 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。
**三: 后起之秀 Caffeine。**
相比于 `Guava` 来说 `Caffeine` 在各个方面比如性能要更加优秀,一般建议使用其来替代 `Guava` 。并且, `Guava``Caffeine` 的使用方式很像!
本地缓存固然好,但是缺陷也很明显,比如多个相同服务之间的本地缓存的数据无法共享。
_下面我们从为什么要有分布式缓存为接入点来正式进入 Redis 的相关问题总结。_
### 4. 为什么要有分布式缓存?/为什么不直接用本地缓存?
_我们可以把分布式缓存Distributed Cache 看作是一种内存数据库的服务它的最终作用就是提供缓存数据的服务。_
如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用 Nginx 来做负载均衡,部署两个相同的服务到服务器,两个服务使用同一个数据库和缓存。
![集中式缓存架构](./images/redis-all/集中式缓存架构.png)
本地的缓存的优势是低依赖,比较轻量并且通常相比于使用分布式缓存要更加简单。
再来分析一下本地缓存的局限性:
1. **本地缓存对分布式架构支持不友好**,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
2. **本地缓存容量受服务部署所在的机器限制明显。** 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。
使用分布式缓存之后,缓存部署在一台单独的服务器上,即使同一个相同的服务部署在再多机器上,也是使用的同一份缓存。 并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。
使用分布式缓存的缺点呢,也很显而易见,那就是你需要为分布式缓存引入额外的服务比如 Redis 或 Memcached你需要单独保证 Redis 或 Memcached 服务的高可用。
### 5. 缓存读写模式/更新策略
**下面介绍到的三种模式各有优劣,不存在最佳模式,根据具体的业务场景选择适合自己的缓存读写模式。**
#### 5.1. Cache Aside Pattern旁路缓存模式
1. 写:更新 DB然后直接删除 cache 。
2. 读:从 cache 中读取数据,读取到就直接返回,读取不到的话,就从 DB 中取数据返回,然后再把数据放到 cache 中。
Cache Aside Pattern 中服务端需要同时维系 DB 和 cache并且是以 DB 的结果为准。另外Cache Aside Pattern 有首次请求数据一定不在 cache 的问题,对于热点数据可以提前放入缓存中。
**Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。**
#### 5.2. Read/Write Through Pattern读写穿透
Read/Write Through 套路是:服务端把 cache 视为主要数据存储从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB从而减轻了应用程序的职责。
1. 写Write Through先查 cachecache 中不存在,直接更新 DB。 cache 中存在,则先更新 cache然后 cache 服务自己更新 DB**同步更新 cache 和 DB**)。
2. 读(Read Through) 从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 DB 加载,写入到 cache 后返回响应。
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
#### 5.3. Write Behind Pattern异步缓存写入
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。
但是,两个又有很大的不同:**Read/Write Through 是同步更新 cache 和 DB而 Write Behind Caching 则是只更新缓存,不直接更新 DB而是改为异步批量的方式来更新 DB。**
**Write Behind Pattern 下 DB 的写性能非常高,尤其适合一些数据经常变化的业务场景比如说一篇文章的点赞数量、阅读数量。** 往常一篇文章被点赞 500 次的话,需要重复修改 500 次 DB但是在 Write Behind Pattern 下可能只需要修改一次 DB 就可以了。
但是,这种模式同样也给 DB 和 Cache 一致性带来了新的考验,很多时候如果数据还没异步更新到 DB 的话Cache 服务宕机就 gg 了。