diff --git a/.gitattributes b/.gitattributes index 05d15409683..2ab2d3c5962 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,4 @@ -# Auto detect text files and perform LF normalization * text=auto *.js linguist-language=java *.css linguist-language=java -*.html linguist-language=java \ No newline at end of file +*.html linguist-language=java diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..853f53da406 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +.gradle +/build/ +/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +/out/ +/**/out/ +.shelf/ +.ideaDataSources/ +dataSources/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/node_modules/ + +### OS ### +.DS_Store diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000000..e69de29bb2d diff --git a/HomePage.md b/HomePage.md new file mode 100644 index 00000000000..43a338951fc --- /dev/null +++ b/HomePage.md @@ -0,0 +1,236 @@ +点击订阅[Java面试进阶指南](https://xiaozhuanlan.com/javainterview?rel=javaguide)(专为Java面试方向准备)。[为什么要弄这个专栏?](https://shimo.im/./9BJjNsNg7S4dCnz3/) + +
+
+
+
+
+## 目录
+
+- [Java](#java)
+ - [基础](#基础)
+ - [容器](#容器)
+ - [并发](#并发)
+ - [JVM](#jvm)
+ - [I/O](#io)
+ - [Java 8](#java-8)
+ - [编程规范](#编程规范)
+- [网络](#网络)
+- [操作系统](#操作系统)
+ - [Linux相关](#linux相关)
+- [数据结构与算法](#数据结构与算法)
+ - [数据结构](#数据结构)
+ - [算法](#算法)
+- [数据库](#数据库)
+ - [MySQL](#mysql)
+ - [Redis](#redis)
+- [系统设计](#系统设计)
+ - [设计模式(工厂模式、单例模式 ... )](#设计模式)
+ - [常用框架(Spring、Zookeeper ... )](#常用框架)
+ - [数据通信(消息队列、Dubbo ... )](#数据通信)
+ - [网站架构](#网站架构)
+- [面试指南](#面试指南)
+ - [备战面试](#备战面试)
+ - [常见面试题总结](#常见面试题总结)
+ - [面经](#面经)
+- [工具](#工具)
+ - [Git](#git)
+ - [Docker](#Docker)
+- [资料](#资料)
+ - [书单](#书单)
+ - [Github榜单](#Github榜单)
+- [待办](#待办)
+- [说明](#说明)
+
+## Java
+
+### 基础
+
+* [Java 基础知识回顾](java/Java基础知识.md)
+* [Java 基础知识疑难点总结](java/Java疑难点.md)
+* [J2EE 基础知识回顾](java/J2EE基础知识.md)
+
+### 容器
+
+* [Java容器常见面试题/知识点总结](java/collection/Java集合框架常见面试题.md)
+* [ArrayList 源码学习](java/collection/ArrayList.md)
+* [LinkedList 源码学习](java/collection/LinkedList.md)
+* [HashMap(JDK1.8)源码学习](java/collection/HashMap.md)
+
+### 并发
+
+* [Java 并发基础常见面试题总结](java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md)
+* [Java 并发进阶常见面试题总结](java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md)
+* [并发容器总结](java/Multithread/并发容器总结.md)
+* [乐观锁与悲观锁](essential-content-for-interview/面试必备之乐观锁与悲观锁.md)
+* [JUC 中的 Atomic 原子类总结](java/Multithread/Atomic.md)
+* [AQS 原理以及 AQS 同步组件总结](java/Multithread/AQS.md)
+
+### JVM
+* [一 Java内存区域](java/jvm/Java内存区域.md)
+* [二 JVM垃圾回收](java/jvm/JVM垃圾回收.md)
+* [三 JDK 监控和故障处理工具](java/jvm/JDK监控和故障处理工具总结.md)
+* [四 类文件结构](java/jvm/类文件结构.md)
+* [五 类加载过程](java/jvm/类加载过程.md)
+* [六 类加载器](java/jvm/类加载器.md)
+
+### I/O
+
+* [BIO,NIO,AIO 总结 ](java/BIO-NIO-AIO.md)
+* [Java IO 与 NIO系列文章](java/Java%20IO与NIO.md)
+
+### Java 8
+
+* [Java 8 新特性总结](java/What's%20New%20in%20JDK8/Java8Tutorial.md)
+* [Java 8 学习资源推荐](java/What's%20New%20in%20JDK8/Java8教程推荐.md)
+
+### 编程规范
+
+- [Java 编程规范](java/Java编程规范.md)
+
+## 网络
+
+* [计算机网络常见面试题](network/计算机网络.md)
+* [计算机网络基础知识总结](network/干货:计算机网络知识总结.md)
+* [HTTPS中的TLS](network/HTTPS中的TLS.md)
+
+## 操作系统
+
+### Linux相关
+
+* [后端程序员必备的 Linux 基础知识](operating-system/后端程序员必备的Linux基础知识.md)
+* [Shell 编程入门](operating-system/Shell.md)
+
+## 数据结构与算法
+
+### 数据结构
+
+- [数据结构知识学习与面试](dataStructures-algorithms/数据结构.md)
+
+### 算法
+
+- [算法学习资源推荐](dataStructures-algorithms/算法学习资源推荐.md)
+- [几道常见的字符串算法题总结 ](dataStructures-algorithms/几道常见的子符串算法题.md)
+- [几道常见的链表算法题总结 ](dataStructures-algorithms/几道常见的链表算法题.md)
+- [剑指offer部分编程题](dataStructures-algorithms/剑指offer部分编程题.md)
+- [公司真题](dataStructures-algorithms/公司真题.md)
+- [回溯算法经典案例之N皇后问题](dataStructures-algorithms/Backtracking-NQueens.md)
+
+## 数据库
+
+### MySQL
+
+* [MySQL 学习与面试](database/MySQL.md)
+* [一千行MySQL学习笔记](database/一千行MySQL命令.md)
+* [MySQL高性能优化规范建议](database/MySQL高性能优化规范建议.md)
+* [数据库索引总结](database/MySQL%20Index.md)
+* [事务隔离级别(图文详解)](database/事务隔离级别(图文详解).md)
+* [一条SQL语句在MySQL中如何执行的](database/一条sql语句在mysql中如何执行的.md)
+
+### Redis
+
+* [Redis 总结](database/Redis/Redis.md)
+* [Redlock分布式锁](database/Redis/Redlock分布式锁.md)
+* [如何做可靠的分布式锁,Redlock真的可行么](database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md)
+
+## 系统设计
+
+### 设计模式
+
+- [设计模式系列文章](system-design/设计模式.md)
+
+### 常用框架
+
+#### Spring
+
+- [Spring 学习与面试](system-design/framework/spring/Spring.md)
+- [Spring 常见问题总结](system-design/framework/spring/SpringInterviewQuestions.md)
+- [Spring中bean的作用域与生命周期](system-design/framework/spring/SpringBean.md)
+- [SpringMVC 工作原理详解](system-design/framework/spring/SpringMVC-Principle.md)
+- [Spring中都用到了那些设计模式?](system-design/framework/spring/Spring-Design-Patterns.md)
+
+#### ZooKeeper
+
+- [ZooKeeper 相关概念总结](system-design/framework/ZooKeeper.md)
+- [ZooKeeper 数据模型和常见命令](system-design/framework/ZooKeeper数据模型和常见命令.md)
+
+### 数据通信
+
+- [数据通信(RESTful、RPC、消息队列)相关知识点总结](system-design/data-communication/summary.md)
+- [Dubbo 总结:关于 Dubbo 的重要知识点](system-design/data-communication/dubbo.md)
+- [消息队列总结](system-design/data-communication/message-queue.md)
+- [RabbitMQ 入门](system-design/data-communication/rabbitmq.md)
+- [RocketMQ的几个简单问题与答案](system-design/data-communication/RocketMQ-Questions.md)
+
+### 网站架构
+
+- [一文读懂分布式应该学什么](system-design/website-architecture/分布式.md)
+- [8 张图读懂大型网站技术架构](system-design/website-architecture/8%20张图读懂大型网站技术架构.md)
+- [【面试精选】关于大型网站系统架构你不得不懂的10个问题](system-design/website-architecture/【面试精选】关于大型网站系统架构你不得不懂的10个问题.md)
+
+## 面试指南
+
+### 备战面试
+
+* [【备战面试1】程序员的简历就该这样写](essential-content-for-interview/PreparingForInterview/程序员的简历之道.md)
+* [【备战面试2】初出茅庐的程序员该如何准备面试?](essential-content-for-interview/PreparingForInterview/interviewPrepare.md)
+* [【备战面试3】7个大部分程序员在面试前很关心的问题](essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md)
+* [【备战面试4】Github上开源的Java面试/学习相关的仓库推荐](essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md)
+* [【备战面试5】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](essential-content-for-interview/PreparingForInterview/如果面试官问你“你有什么问题问我吗?”时,你该如何回答.md)
+* [【备战面试6】美团面试常见问题总结(附详解答案)](essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md)
+
+### 常见面试题总结
+
+* [第一周(2018-8-7)](essential-content-for-interview/MostCommonJavaInterviewQuestions/第一周(2018-8-7).md) (为什么 Java 中只有值传递、==与equals、 hashCode与equals)
+* [第二周(2018-8-13)](essential-content-for-interview/MostCommonJavaInterviewQuestions/第二周(2018-8-13).md)(String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?、什么是反射机制?反射机制的应用场景有哪些?......)
+* [第三周(2018-08-22)](java/collection/Java集合框架常见面试题.md) (Arraylist 与 LinkedList 异同、ArrayList 与 Vector 区别、HashMap的底层实现、HashMap 和 Hashtable 的区别、HashMap 的长度为什么是2的幂次方、HashSet 和 HashMap 区别、ConcurrentHashMap 和 Hashtable 的区别、ConcurrentHashMap线程安全的具体实现方式/底层具体实现、集合框架底层数据结构总结)
+* [第四周(2018-8-30).md](essential-content-for-interview/MostCommonJavaInterviewQuestions/第四周(2018-8-30).md) (主要内容是几道面试常问的多线程基础题。)
+
+### 面经
+
+- [5面阿里,终获offer(2018年秋招)](essential-content-for-interview/BATJrealInterviewExperience/5面阿里,终获offer.md)
+- [蚂蚁金服2019实习生面经总结(已拿口头offer)](essential-content-for-interview/BATJrealInterviewExperience/蚂蚁金服实习生面经总结(已拿口头offer).md)
+- [2019年蚂蚁金服、头条、拼多多的面试总结](essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md)
+
+## 工具
+
+### Git
+
+* [Git入门](tools/Git.md)
+
+### Docker
+
+* [Docker 入门](tools/Docker.md)
+* [一文搞懂 Docker 镜像的常用操作!](tools/Docker-Image.md)
+
+## 资料
+
+### 书单
+
+- [Java程序员必备书单](data/java-recommended-books.md)
+
+### Github榜单
+
+- [Java 项目月榜单](github-trending/JavaGithubTrending.md)
+
+***
+
+## 待办
+
+- [x] [Java 8 新特性总结](./java/What's%20New%20in%20JDK8/Java8Tutorial.md)
+- [x] [Java 8 新特性详解](./java/What's%20New%20in%20JDK8/Java8教程推荐.md)
+- [ ] Java 多线程类别知识重构(---正在进行中---)
+- [x] [BIO,NIO,AIO 总结 ](./java/BIO-NIO-AIO.md)
+- [ ] Netty 总结(---正在进行中---)
+- [ ] 数据结构总结重构(---正在进行中---)
+
+## 公众号
+
+- 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
+- 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本公众号后台回复 **"Java面试突击"** 即可免费领取!
+- 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
+
+
+
+
+
+
+
+
+
+ ![]() |
+
+
+ ![]() |
+
+
+
《Redis设计与实现:12章》
+ +### 10. Redis 没有使用多线程?为什么不使用多线程? + +虽然说 Redis 是单线程模型,但是, 实际上,**Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。** + + + +不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主处理之外的其他线程来“异步处理”。 + +大体上来说,**Redis 6.0 之前主要还是单线程处理。** + +**那,Redis6.0 之前 为什么不使用多线程?** + +我觉得主要原因有下面 3 个: + +1. 单线程编程容易并且更容易维护; +2. Redis 的性能瓶颈不再 CPU ,主要在内存和网络; +3. 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。 + +### 11. Redis6.0 之后为何引入了多线程? + +**Redis6.0 引入多线程主要是为了提高网络 IO 读写性能**,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。 + +虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。 + +Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : + +``` bash +io-threads-do-reads yes +``` + +开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 `redis.conf` : + +``` bash +io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 +``` + +推荐阅读: + +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 给缓存数据设置过期时间有啥用? + +一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢? + +因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接Out of memory。 + +Redis 自带了给缓存数据设置过期时间的功能,比如: + +``` bash +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 天内有效。 + +如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。 + +### 13. Redis是如何判断数据是否过期的呢? + +Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。 + + + +过期字典是存储在redisDb这个结构里的: + +``` c +typedef struct redisDb { + ... + + dict *dict; //数据库键空间,保存着数据库中所有键值对 + dict *expires // 过期字典,保存着键的过期时间 + ... +} redisDb; +``` + +### 14. 过期的数据的删除策略了解么? + +如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢? + +常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西): + +1. **惰性删除** :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。 +2. **定期删除** : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。 + +定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采用的是 **定期删除+惰性/懒汉式删除** 。 + +但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就Out of memory了。 + +怎么解决这个问题呢?答案就是: **Redis 内存淘汰机制。** + +### 15. Redis 内存淘汰机制了解么? + +相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据? + +Redis 提供 6 种数据淘汰策略: + +1. **volatile-lru(least frequently used)**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 +2. **volatile-ttl**:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 +3. **volatile-random**:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 +4. **allkeys-lru(least recently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的) +5. **allkeys-random**:从数据集(server.db[i].dict)中任意选择数据淘汰 +6. **no-eviction**:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧! + +4.0 版本后增加以下两种: + +7. **volatile-lfu**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰 +8. **allkeys-lfu**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key + +### 16. Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复) + +很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。 + +Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。**Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)**。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。 + +**快照(snapshotting)持久化(RDB)** + +Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。 + +快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置: + +``` 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 参数开启: + +``` conf +appendonly yes +``` + +开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。 + +在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是: + +``` conf +appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 +appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 +appendfsync no #让操作系统决定何时进行同步 +``` + +为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。 + +**相关 issue** :[783:Redis 的 AOF 方式](https://github.com/Snailclimb/JavaGuide/issues/783) + +**拓展:Redis 4.0 对于持久化机制的优化** + +Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 + +如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。 + +**补充内容:AOF 重写** + +AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。 + +AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。 + +在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作 + +**更多内容可以查看我的这篇文章:** + +* [Redis 持久化](Redis持久化.md) + +### 17. Redis 事务 + +Redis 可以通过 **MULTI,EXEC,DISCARD 和 WATCH** 等命令来实现事务(transaction)功能。 + +``` bash +> MULTI +OK +> INCR foo +QUEUED +> INCR bar +QUEUED +> EXEC +1) (integer) 1 +2) (integer) 1 +``` + +使用 [MULTI](https://redis.io/commands/multi)命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了[EXEC](https://redis.io/commands/exec)命令将执行所有命令。 + +Redis官网相关介绍 [https://redis.io/topics/transactions](https://redis.io/topics/transactions) 如下: + + + +但是,Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: **1. 原子性**,**2. 隔离性**,**3. 持久性**,**4. 一致性**。 + +1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; +2. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; +3. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 +4. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; + +**Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。** + +Redis官网也解释了自己为啥不支持回滚。简单来说就是Redis开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。 + + + +你可以将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. 缓存穿透 + +#### 18.1. 什么是缓存穿透? + +缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。 + +#### 18.2. 缓存穿透情况的处理流程是怎样的? + +如下图所示,用户的请求最终都要跑到数据库中查询一遍。 + + + +#### 18.3. 有哪些解决办法? + +最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 + +**1)缓存无效 key** + +如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: `SET key value EX 10086` 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 + +另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值` 。 + +如果用 Java 代码展示的话,差不多是下面这样的: + +``` 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 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。 + +具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。 + +加入布隆过滤器之后的缓存处理流程图如下。 + + + +但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: **布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** + +_为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!_ + +我们先来看一下,**当一个元素加入布隆过滤器中的时候,会进行哪些操作:** + +1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 +2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 + +我们再来看一下,**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:** + +1. 对给定元素再次进行相同的哈希计算; +2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 + +然后,一定会出现这样一种情况:**不同的字符串可能哈希出来的位置相同。** (可以适当增加位数组大小或者调整我们的哈希函数来降低概率) + +更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 + +### 19. 缓存雪崩 + +#### 19.1. 什么是缓存雪崩? + +我发现缓存雪崩这名字起的有点意思,哈哈。 + +实际上,缓存雪崩描述的就是这样一个简单的场景:**缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。** 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。 + +举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。 + +还有一种缓存雪崩的场景是:**有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。** 这样的情况,有下面几种解决办法: + +举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。 + +#### 19.2. 有哪些解决办法? + +**针对 Redis 服务不可用的情况:** + +1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。 +2. 限流,避免同时处理大量的请求。 + +**针对热点缓存失效的情况:** + +1. 设置不同的失效时间比如随机设置缓存的失效时间。 +2. 缓存永不失效。 + +### 20. 如何解决 Redis 的并发竞争 Key 问题 + +所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同! + +推荐一种方案:分布式锁(zookeeper 和 Redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能) + +基于 zookeeper 临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在 zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。 + +在实践中,当然是从以可靠性为主。所以首推 Zookeeper。 + +参考: + +* 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. 参考 + +* 《Redis 开发与运维》 +* 《Redis 设计与实现》 +* Redis 命令总结:http://Redisdoc.com/string/set.html +* 通俗易懂的 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. 公众号 + +如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 + +**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取! + +**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 + + diff --git "a/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" new file mode 100644 index 00000000000..a42332f6c2d --- /dev/null +++ "b/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,515 @@ +> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis + + + +# 一、Redis 简介 + +> **"Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker."** —— Redis是一个开放源代码(BSD许可)的内存中数据结构存储,用作数据库,缓存和消息代理。 *(摘自官网)* + +**Redis** 是一个开源,高级的键值存储和一个适用的解决方案,用于构建高性能,可扩展的 Web 应用程序。**Redis** 也被作者戏称为 *数据结构服务器* ,这意味着使用者可以通过一些命令,基于带有 TCP 套接字的简单 *服务器-客户端* 协议来访问一组 **可变数据结构** 。*(在 Redis 中都采用键值对的方式,只不过对应的数据结构不一样罢了)* + +## Redis 的优点 + +以下是 Redis 的一些优点: + +- **异常快** - Redis 非常快,每秒可执行大约 110000 次的设置(SET)操作,每秒大约可执行 81000 次的读取/获取(GET)操作。 +- **支持丰富的数据类型** - Redis 支持开发人员常用的大多数数据类型,例如列表,集合,排序集和散列等等。这使得 Redis 很容易被用来解决各种问题,因为我们知道哪些问题可以更好使用地哪些数据类型来处理解决。 +- **操作具有原子性** - 所有 Redis 操作都是原子操作,这确保如果两个客户端并发访问,Redis 服务器能接收更新的值。 +- **多实用工具** - Redis 是一个多实用工具,可用于多种用例,如:缓存,消息队列(Redis 本地支持发布/订阅),应用程序中的任何短期数据,例如,web应用程序中的会话,网页命中计数等。 + +## Redis 的安装 + +这一步比较简单,你可以在网上搜到许多满意的教程,这里就不再赘述。 + +给一个菜鸟教程的安装教程用作参考:[https://www.runoob.com/redis/redis-install.html](https://www.runoob.com/redis/redis-install.html) + +## 测试本地 Redis 性能 + +当你安装完成之后,你可以先执行 `redis-server` 让 Redis 启动起来,然后运行命令 `redis-benchmark -n 100000 -q` 来检测本地同时执行 10 万个请求时的性能: + + + +当然不同电脑之间由于各方面的原因会存在性能差距,这个测试您可以权当是一种 **「乐趣」** 就好。 + +# 二、Redis 五种基本数据结构 + +**Redis** 有 5 种基础数据结构,它们分别是:**string(字符串)**、**list(列表)**、**hash(字典)**、**set(集合)** 和 **zset(有序集合)**。这 5 种是 Redis 相关知识中最基础、最重要的部分,下面我们结合源码以及一些实践来给大家分别讲解一下。 + +注意: + +> 每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。 +> +> 可以看到每种数据结构都有两种以上的内部编码实现,例如string数据结构就包含了raw、int和embstr三种内部编码。 +> +> 同时,有些内部编码可以作为多种外部数据结构的内部实现,例如ziplist就是hash、list和zset共有的内部编码。 + +## 1)字符串 string + +Redis 中的字符串是一种 **动态字符串**,这意味着使用者可以修改,它的底层实现有点类似于 Java 中的 **ArrayList**,有一个字符数组,从源码的 **sds.h/sdshdr 文件** 中可以看到 Redis 底层对于字符串的定义 **SDS**,即 *Simple Dynamic String* 结构: + +```c +/* Note: sdshdr5 is never used, we just access the flags byte directly. + * However is here to document the layout of type 5 SDS strings. */ +struct __attribute__ ((__packed__)) sdshdr5 { + unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ + char buf[]; +}; +struct __attribute__ ((__packed__)) sdshdr8 { + uint8_t len; /* used */ + uint8_t alloc; /* excluding the header and null terminator */ + unsigned char flags; /* 3 lsb of type, 5 unused bits */ + char buf[]; +}; +struct __attribute__ ((__packed__)) sdshdr16 { + uint16_t len; /* used */ + uint16_t alloc; /* excluding the header and null terminator */ + unsigned char flags; /* 3 lsb of type, 5 unused bits */ + char buf[]; +}; +struct __attribute__ ((__packed__)) sdshdr32 { + uint32_t len; /* used */ + uint32_t alloc; /* excluding the header and null terminator */ + unsigned char flags; /* 3 lsb of type, 5 unused bits */ + char buf[]; +}; +struct __attribute__ ((__packed__)) sdshdr64 { + uint64_t len; /* used */ + uint64_t alloc; /* excluding the header and null terminator */ + unsigned char flags; /* 3 lsb of type, 5 unused bits */ + char buf[]; +}; +``` + +你会发现同样一组结构 Redis 使用泛型定义了好多次,**为什么不直接使用 int 类型呢?** + +因为当字符串比较短的时候,len 和 alloc 可以使用 byte 和 short 来表示,**Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。** + +### SDS 与 C 字符串的区别 + +为什么不考虑直接使用 C 语言的字符串呢?因为 C 语言这种简单的字符串表示方式 **不符合 Redis 对字符串在安全性、效率以及功能方面的要求**。我们知道,C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组最后一个元素总是 `'\0'`。*(下图就展示了 C 语言中值为 "Redis" 的一个字符数组)* + + + +这样简单的数据结构可能会造成以下一些问题: + +- **获取字符串长度为 O(N) 级别的操作** → 因为 C 不保存数组的长度,每次都需要遍历一遍整个数组; +- 不能很好的杜绝 **缓冲区溢出/内存泄漏** 的问题 → 跟上述问题原因一样,如果执行拼接 or 缩短字符串的操作,如果操作不当就很容易造成上述问题; +- C 字符串 **只能保存文本数据** → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的 `'\0'` 可能会被判定为提前结束的字符串而识别不了; + +我们以追加字符串的操作举例,Redis 源码如下: + +```c +/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the + * end of the specified sds string 's'. + * + * After the call, the passed sds string is no longer valid and all the + * references must be substituted with the new pointer returned by the call. */ +sds sdscatlen(sds s, const void *t, size_t len) { + // 获取原字符串的长度 + size_t curlen = sdslen(s); + + // 按需调整空间,如果容量不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中 + s = sdsMakeRoomFor(s,len); + if (s == NULL) return NULL; // 内存不足 + memcpy(s+curlen, t, len); // 追加目标字符串到字节数组中 + sdssetlen(s, curlen+len); // 设置追加后的长度 + s[curlen+len] = '\0'; // 让字符串以 \0 结尾,便于调试打印 + return s; +} +``` + +- **注:Redis 规定了字符串的长度不得超过 512 MB。** + +### 对字符串的基本操作 + +安装好 Redis,我们可以使用 `redis-cli` 来对 Redis 进行命令行的操作,当然 Redis 官方也提供了在线的调试器,你也可以在里面敲入命令进行操作:[http://try.redis.io/#run](http://try.redis.io/#run) + +#### 设置和获取键值对 + +```console +> SET key value +OK +> GET key +"value" +``` + +正如你看到的,我们通常使用 `SET` 和 `GET` 来设置和获取字符串值。 + +值可以是任何种类的字符串(包括二进制数据),例如你可以在一个键下保存一张 `.jpeg` 图片,只需要注意不要超过 512 MB 的最大限度就好了。 + +当 key 存在时,`SET` 命令会覆盖掉你上一次设置的值: + +```console +> SET key newValue +OK +> GET key +"newValue" +``` + +另外你还可以使用 `EXISTS` 和 `DEL` 关键字来查询是否存在和删除键值对: + +```console +> EXISTS key +(integer) 1 +> DEL key +(integer) 1 +> GET key +(nil) +``` + +#### 批量设置键值对 + +```console +> SET key1 value1 +OK +> SET key2 value2 +OK +> MGET key1 key2 key3 # 返回一个列表 +1) "value1" +2) "value2" +3) (nil) +> MSET key1 value1 key2 value2 +> MGET key1 key2 +1) "value1" +2) "value2" +``` + +#### 过期和 SET 命令扩展 + +可以对 key 设置过期时间,到时间会被自动删除,这个功能常用来控制缓存的失效时间。*(过期可以是任意数据结构)* + +```console +> SET key value1 +> GET key +"value1" +> EXPIRE name 5 # 5s 后过期 +... # 等待 5s +> GET key +(nil) +``` + +等价于 `SET` + `EXPIRE` 的 `SETEX` 命令: + +```console +> SETEX key 5 value1 +... # 等待 5s 后获取 +> GET key +(nil) + +> SETNX key value1 # 如果 key 不存在则 SET 成功 +(integer) 1 +> SETNX key value1 # 如果 key 存在则 SET 失败 +(integer) 0 +> GET key +"value" # 没有改变 +``` + +#### 计数 + +如果 value 是一个整数,还可以对它使用 `INCR` 命令进行 **原子性** 的自增操作,这意味着及时多个客户端对同一个 key 进行操作,也决不会导致竞争的情况: + +```console +> SET counter 100 +> INCR counter +(integer) 101 +> INCRBY counter 50 +(integer) 151 +``` + +#### 返回原值的 GETSET 命令 + +对字符串,还有一个 `GETSET` 比较让人觉得有意思,它的功能跟它名字一样:为 key 设置一个值并返回原值: + +```console +> SET key value +> GETSET key value1 +"value" +``` + +这可以对于某一些需要隔一段时间就统计的 key 很方便的设置和查看,例如:系统每当由用户进入的时候你就是用 `INCR` 命令操作一个 key,当需要统计时候你就把这个 key 使用 `GETSET` 命令重新赋值为 0,这样就达到了统计的目的。 + +## 2)列表 list + +Redis 的列表相当于 Java 语言中的 **LinkedList**,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。 + +我们可以从源码的 `adlist.h/listNode` 来看到对其的定义: + +```c +/* Node, List, and Iterator are the only data structures used currently. */ + +typedef struct listNode { + struct listNode *prev; + struct listNode *next; + void *value; +} listNode; + +typedef struct listIter { + listNode *next; + int direction; +} listIter; + +typedef struct list { + listNode *head; + listNode *tail; + void *(*dup)(void *ptr); + void (*free)(void *ptr); + int (*match)(void *ptr, void *key); + unsigned long len; +} list; +``` + +可以看到,多个 listNode 可以通过 `prev` 和 `next` 指针组成双向链表: + + + +虽然仅仅使用多个 listNode 结构就可以组成链表,但是使用 `adlist.h/list` 结构来持有链表的话,操作起来会更加方便: + + + +### 链表的基本操作 + +- `LPUSH` 和 `RPUSH` 分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素; +- `LRANGE` 命令可以从 list 中取出一定范围的元素; +- `LINDEX` 命令可以从 list 中取出指定下表的元素,相当于 Java 链表操作中的 `get(int index)` 操作; + +示范: + +```console +> rpush mylist A +(integer) 1 +> rpush mylist B +(integer) 2 +> lpush mylist first +(integer) 3 +> lrange mylist 0 -1 # -1 表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即所有 +1) "first" +2) "A" +3) "B" +``` + +#### list 实现队列 + +队列是先进先出的数据结构,常用于消息排队和异步逻辑处理,它会确保元素的访问顺序: + +```console +> RPUSH books python java golang +(integer) 3 +> LPOP books +"python" +> LPOP books +"java" +> LPOP books +"golang" +> LPOP books +(nil) +``` + +#### list 实现栈 + +栈是先进后出的数据结构,跟队列正好相反: + +```console +> RPUSH books python java golang +> RPOP books +"golang" +> RPOP books +"java" +> RPOP books +"python" +> RPOP books +(nil) +``` + +## 3)字典 hash + +Redis 中的字典相当于 Java 中的 **HashMap**,内部实现也差不多类似,都是通过 **"数组 + 链表"** 的链地址法来解决部分 **哈希冲突**,同时这样的结构也吸收了两种不同数据结构的优点。源码定义如 `dict.h/dictht` 定义: + +```c +typedef struct dictht { + // 哈希表数组 + dictEntry **table; + // 哈希表大小 + unsigned long size; + // 哈希表大小掩码,用于计算索引值,总是等于 size - 1 + unsigned long sizemask; + // 该哈希表已有节点的数量 + unsigned long used; +} dictht; + +typedef struct dict { + dictType *type; + void *privdata; + // 内部有两个 dictht 结构 + dictht ht[2]; + long rehashidx; /* rehashing not in progress if rehashidx == -1 */ + unsigned long iterators; /* number of iterators currently running */ +} dict; +``` + +`table` 属性是一个数组,数组中的每个元素都是一个指向 `dict.h/dictEntry` 结构的指针,而每个 `dictEntry` 结构保存着一个键值对: + +```c +typedef struct dictEntry { + // 键 + void *key; + // 值 + union { + void *val; + uint64_t u64; + int64_t s64; + double d; + } v; + // 指向下个哈希表节点,形成链表 + struct dictEntry *next; +} dictEntry; +``` + +可以从上面的源码中看到,**实际上字典结构的内部包含两个 hashtable**,通常情况下只有一个 hashtable 是有值的,但是在字典扩容缩容时,需要分配新的 hashtable,然后进行 **渐进式搬迁** *(下面说原因)*。 + +### 渐进式 rehash + +大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个 O(n) 级别的操作,作为单线程的 Redis 很难承受这样耗时的过程,所以 Redis 使用 **渐进式 rehash** 小步搬迁: + + + +渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,如上图所示,查询时会同时查询两个 hash 结构,然后在后续的定时任务以及 hash 操作指令中,循序渐进的把旧字典的内容迁移到新字典中。当搬迁完成了,就会使用新的 hash 结构取而代之。 + +### 扩缩容的条件 + +正常情况下,当 hash 表中 **元素的个数等于第一维数组的长度时**,就会开始扩容,扩容的新数组是 **原数组大小的 2 倍**。不过如果 Redis 正在做 `bgsave(持久化命令)`,为了减少内存也得过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,**达到了第一维数组长度的 5 倍了**,这个时候就会 **强制扩容**。 + +当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 **元素个数低于数组长度的 10%**,缩容不会考虑 Redis 是否在做 `bgsave`。 + +### 字典的基本操作 + +hash 也有缺点,hash 结构的存储消耗要高于单个字符串,所以到底该使用 hash 还是字符串,需要根据实际情况再三权衡: + +```shell +> HSET books java "think in java" # 命令行的字符串如果包含空格则需要使用引号包裹 +(integer) 1 +> HSET books python "python cookbook" +(integer) 1 +> HGETALL books # key 和 value 间隔出现 +1) "java" +2) "think in java" +3) "python" +4) "python cookbook" +> HGET books java +"think in java" +> HSET books java "head first java" +(integer) 0 # 因为是更新操作,所以返回 0 +> HMSET books java "effetive java" python "learning python" # 批量操作 +OK +``` + +## 4)集合 set + +Redis 的集合相当于 Java 语言中的 **HashSet**,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。 + +### 集合 set 的基本使用 + +由于该结构比较简单,我们直接来看看是如何使用的: + +```shell +> SADD books java +(integer) 1 +> SADD books java # 重复 +(integer) 0 +> SADD books python golang +(integer) 2 +> SMEMBERS books # 注意顺序,set 是无序的 +1) "java" +2) "python" +3) "golang" +> SISMEMBER books java # 查询某个 value 是否存在,相当于 contains +(integer) 1 +> SCARD books # 获取长度 +(integer) 3 +> SPOP books # 弹出一个 +"java" +``` + +## 5)有序列表 zset + +这可能使 Redis 最具特色的一个数据结构了,它类似于 Java 中 **SortedSet** 和 **HashMap** 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重。 + +它的内部实现用的是一种叫做 **「跳跃表」** 的数据结构,由于比较复杂,所以在这里简单提一下原理就好了: + + + + +想象你是一家创业公司的老板,刚开始只有几个人,大家都平起平坐。后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人**又是员工又有组长的身份**。 + +再后来,公司规模进一步扩大,公司需要再进入一个层级:部门。于是每个部门又会从组长中推举一位选出部长。 + +跳跃表就类似于这样的机制,最下面一层所有的元素都会串起来,都是员工,然后每隔几个元素就会挑选出一个代表,再把这几个代表使用另外一级指针串起来。然后再在这些代表里面挑出二级代表,再串起来。**最终形成了一个金字塔的结构。** + +想一下你目前所在的地理位置:亚洲 > 中国 > 某省 > 某市 > ....,**就是这样一个结构!** + +### 有序列表 zset 基础操作 + +```console +> ZADD books 9.0 "think in java" +> ZADD books 8.9 "java concurrency" +> ZADD books 8.6 "java cookbook" + +> ZRANGE books 0 -1 # 按 score 排序列出,参数区间为排名范围 +1) "java cookbook" +2) "java concurrency" +3) "think in java" + +> ZREVRANGE books 0 -1 # 按 score 逆序列出,参数区间为排名范围 +1) "think in java" +2) "java concurrency" +3) "java cookbook" + +> ZCARD books # 相当于 count() +(integer) 3 + +> ZSCORE books "java concurrency" # 获取指定 value 的 score +"8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题 + +> ZRANK books "java concurrency" # 排名 +(integer) 1 + +> ZRANGEBYSCORE books 0 8.91 # 根据分值区间遍历 zset +1) "java cookbook" +2) "java concurrency" + +> ZRANGEBYSCORE books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。 +1) "java cookbook" +2) "8.5999999999999996" +3) "java concurrency" +4) "8.9000000000000004" + +> ZREM books "java concurrency" # 删除 value +(integer) 1 +> ZRANGE books 0 -1 +1) "java cookbook" +2) "think in java" +``` + +# 扩展/相关阅读 + +### 优秀文章 + +1. 阿里云 Redis 开发规范 - [https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px](https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px) +2. 为什么要防止 bigkey? - [https://mp.weixin.qq.com/s?__biz=Mzg2NTEyNzE0OA==&mid=2247483677&idx=1&sn=5c320b46f0e06ce9369a29909d62b401&chksm=ce5f9e9ef928178834021b6f9b939550ac400abae5c31e1933bafca2f16b23d028cc51813aec&scene=21#wechat_redirect](https://mp.weixin.qq.com/s?__biz=Mzg2NTEyNzE0OA==&mid=2247483677&idx=1&sn=5c320b46f0e06ce9369a29909d62b401&chksm=ce5f9e9ef928178834021b6f9b939550ac400abae5c31e1933bafca2f16b23d028cc51813aec&scene=21#wechat_redirect) +3. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) + +#### Redis数据结构源码分析 + +1. Redis 数据结构-字符串源码分析:[https://my.oschina.net/mengyuankan/blog/1926320](https://my.oschina.net/mengyuankan/blog/1926320) +2. Redis 数据结构-字典源码分析: [https://my.oschina.net/mengyuankan/blog/1929593](https://my.oschina.net/mengyuankan/blog/1929593) + + + +# 参考资料 + +1. 《Redis 设计与实现》 - [http://redisbook.com/](http://redisbook.com/) +2. 【官方文档】Redis 数据类型介绍 - [http://www.redis.cn/topics/data-types-intro.html](http://www.redis.cn/topics/data-types-intro.html) +3. 《Redis 深度历险》 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) +4. 阿里云 Redis 开发规范 - [https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px](https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px) +5. Redis 快速入门 - 易百教程 - [https://www.yiibai.com/redis/redis_quick_guide.html](https://www.yiibai.com/redis/redis_quick_guide.html) +6. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) + diff --git "a/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" "b/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" new file mode 100644 index 00000000000..2252d9c50b9 --- /dev/null +++ "b/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" @@ -0,0 +1,599 @@ +## Redis构建的类型系统 + +Redis构建了自己的类型系统,主要包括 + ++ redisObject对象 ++ 基于redisObject对象的类型检查 ++ 基于redisObject对象的显示多态函数 ++ 对redisObject进行分配、共享和销毁的机制 + +__C语言不是面向对象语言,这里将redisObject称呼为对象是为了讲述方便,让里面的内容更容易被理解,redisObject其实是一个结构体。__ + +### redisObject对象 + +Redis内部使用一个redisObject对象来表示所有的key和value,每次在Redis数据块中创建一个键值对时,一个是键对象,一个是值对象,而Redis中的每个对象都是由redisObject结构来表示。 + +__在Redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象,所以我们通常说键为字符串键,表示这个键对应的值为字符串对象,我们说一个键为集合键时,表示这个键对应的值为集合对象__ + +redisobject最主要的信息: + +``` +redisobject源码 +typedef struct redisObject{ + //类型 + unsigned type:4; + //编码 + unsigned encoding:4; + //指向底层数据结构的指针 + void *ptr; + //引用计数 + int refcount; + //记录最后一次被程序访问的时间 + unsigned lru:22; +}robj +``` + ++ type代表一个value对象具体是何种数据类型 + + + type key :判断对象的数据类型 ++ encoding属性和*prt指针 + + prt指针指向对象底层的数据结构,而数据结构由encoding属性来决定 + +  + + + 每种类型的对象至少使用了两种不同的编码,而这些编码对用户是完全透明的。 + +  + + + object encoding key命令可以查看值对象的编码 + +### 命令的类型检查和多态 + +#### Redis命令分类 + ++ 一种是只能用于对应数据类型的命令,例如LPUSH和LLEN只能用于列表键, SADD 和 SRANDMEMBER只能用于集合键。 ++ 另一种是可以用于任何类型键的命令。比如TTL。 + +当执行一个处理数据类型的命令时,Redis执行以下步骤: + ++ 根据给定 `key` ,在数据库字典中查找和它相对应的 `redisObject` ,如果没找到,就返回 `NULL` 。 ++ 检查 `redisObject` 的 `type` 属性和执行命令所需的类型是否相符,如果不相符,返回类型错误。 ++ 根据 `redisObject` 的 `encoding` 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。 ++ 返回数据结构的操作结果作为命令的返回值。 + +## 5种数据类型对应的编码和数据结构 + +### string + +__string 是最常用的一种数据类型,普通的key/value存储都可以归结为string类型,value不仅是string,也可以是数字。其他几种数据类型的构成元素也都是字符串,注意Redis规定字符串的长度不能超过512M__ + ++ 编码 + __字符串对象的编码可以是int raw embstr__ + + int编码 + + 保存的是可以用long类型表示的整数值 + + raw编码 + + 保存长度大于44字节的字符串 + + embstr编码 + + 保存长度小于44字节的字符串 + + int用来保存整数值,raw用来保存长字符串,embstr用来保存短字符串。embstr编码是用来专门保存短字符串的一种优化编码。 + + Redis中对于浮点型也是作为字符串保存的,在需要时再将其转换成浮点数类型 + ++ 编码的转换 + + 当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw + + 对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。 + ++ 常用命令 + + + set/get + + + set:设置key对应的值为string类型的value (多次set name会覆盖) + + get:获取key对应的值 + + + mset /mget + + + mset 批量设置多个key的值,如果成功表示所有值都被设置,否则返回0表示没有任何值被设置 + + mget批量获取多个key的值,如果不存在则返回null + + ```shell + 127.0.0.1:6379> mset user1:name redis user1:age 22 + OK + 127.0.0.1:6379> mget user1:name user1:age + 1) "redis" + 2) "22" + ``` + + + 应用场景 + + 类似于哈希操作,存储对象 + + + incr && incrby<原子操作> + + + incr对key对应的值进行加加操作,并返回新的值,incrby加指定的值 + + + decr && decrby<原子操作> + + + decr对key对应的值进行减减操做,并返回新的值,decrby减指定的值 + + + setnx <小小体验一把分布式锁,真香> + + + 设置Key对应的值为string类型的值,如果已经存在则返回0 + + + setex + + + 设置key对应的值为string类型的value,并设定有效期 + + + setrange/getrange + + + setrange从指定位置替换字符串 + + getrange获取key对应value子字符串 + ++ 其他命令 + + + msetnx 同mset,不存在就设置,不会覆盖已有的key + + getset 设置key的值,并返回key旧的值 + + append 给指定的key的value追加字符串,并返回新字符串的长度 + + strlen 返回key对应的value字符串的长度 + ++ 应用场景 + + + 因为string类型是二进制安全的,可以用来存放图片,视频等内容。 + + 由于redis的高性能的读写功能,而string类型的value也可以是数字,可以用做计数器(使用INCR,DECR指令)。比如分布式环境中统计系统的在线人数,秒杀等。 + + 除了上面提到的,还有用于SpringSession实现分布式session + + 分布式系统全局序列号 + +### list + +__list列表,它是简单的字符串列表,你可以添加一个元素到列表的头部,或者尾部__。 + ++ 编码 + + + 列表对象的编码可以是ziplist(压缩列表)和linkedlist(双端链表)。 + + 编码转换 + + 同时满足下面两个条件时使用压缩列表: + + 列表保存元素个数小于512个 + + 每个元素长度小于64字节 + + 不能满足上面两个条件使用linkedlist(双端列表)编码 + ++ 常用命令 + + + lpush: 从头部加入元素 + + ```shell + 127.0.0.1:6379> lpush list1 hello + (integer) 1 + 127.0.0.1:637 9> lpush list1 world + (integer) 2 + 127.0.0.1:6379> lrange list1 0 -1 + 1) "world" + 2) "hello" + ``` + + + rpush:从尾部加入元素 + + ```shell + 127.0.0.1:6379> rpush list2 world + (integer) 1 + 127.0.0.1:6379> rpush list2 hello + (integer) 2 + 127.0.0.1:6379> lrange list2 0 -1 + 1) "world" + 2) "hello" + ``` + + + lpop: 从list的头部删除元素,并返回删除的元素 + + ```shell + 127.0.0.1:6379> lrange list1 0 -1 + 1) "world" + 2) "hello" + 127.0.0.1:6379> lpop list1 + "world" + 127.0.0.1:6379> lrange list1 0 -1 + 1) "hello" + ``` + + + rpop:从list的尾部删除元素,并返回删除的元素 + + ```shell + 127.0.0.1:6379> lrange list2 0 -1 + 1) "hello" + 2) "world" + 127.0.0.1:6379> rpop list2 + "world" + 127.0.0.1:6379> lrange list2 0 -1 + 1) "hello" + ``` + + + rpoplpush: 第一步从尾部删除元素,第二步从首部插入元素 结合着使用 + + linsert :插入方法 linsert listname before [集合的元素] [插入的元素] + + ```shell + 127.0.0.1:6379> lpush list3 hello + (integer) 1 + 127.0.0.1:6379> lpush list3 world + (integer) 2 + 127.0.0.1:6379> linsert list3 before hello start + (integer) 3 + 127.0.0.1:6379> lrange list3 0 -1 + 1) "world" + 2) "start" + 3) "hello" + ``` + + lset :替换指定下标的元素 + ```shell + 127.0.0.1:6379> lrange list1 0 -1 + 1) "a" + 2) "b" + 127.0.0.1:6379> lset list1 0 v + OK + 127.0.0.1:6379> lrange list1 0 -1 + 1) "v" + 2) "b" + ``` + + lrm : 删除元素,返回删除的个数 + ```shell + 127.0.0.1:6379> lrange list1 0 -1 + 1) "b" + 2) "b" + 3) "a" + 4) "b" + 127.0.0.1:6379> lrange list1 0 -1 + 1) "a" + 2) "b" + ``` + + + lindex: 返回list中指定位置的元素 + + llen: 返回list中的元素的个数 ++ 实现数据结构 + + + Stack(栈) + + LPUSH+LPOP + + Queue(队列) + + LPUSH + RPOP + + Blocking MQ(阻塞队列) + + LPUSH+BRPOP + ++ 应用场景 + + + 实现简单的消息队列 + + 利用LRANGE命令,实现基于Redis的分页功能 + +### set + +__集合对象set是string类型(整数也会转成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。__ + ++ 编码 + + + 集合对象的编码可以是intset或者hashtable + + intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。 + + hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值全部设置为null。__当使用HT编码时,Redis中的集合SET相当于Java中的HashSet,内部的键值对是无序的,唯一的。内部实现相当于一个特殊的字典,字典中所有value都是NULL。__ + + + 编码转换 + + 当集合满足下列两个条件时,使用intset编码: + + 集合对象中的所有元素都是整数 + + 集合对象所有元素数量不超过512 + ++ 常用命令 + + + sadd: 向集合中添加元素 (set不允许元素重复) + + smembers: 查看集合中的元素 + + ```shell + 127.0.0.1:6379> sadd set1 aaa + (integer) 1 + 127.0.0.1:6379> sadd set1 bbb + (integer) 1 + 127.0.0.1:6379> sadd set1 ccc + (integer) 1 + 127.0.0.1:6379> smembers set1 + 1) "aaa" + 2) "ccc" + 3) "bbb" + ``` + + + srem: 删除集合元素 + + spop: 随机返回删除的key + + + sdiff :返回两个集合的不同元素 (哪个集合在前就以哪个集合为标准) + + ```shell + 127.0.0.1:6379> smembers set1 + 1) "ccc" + 2) "bbb" + 127.0.0.1:6379> smembers set2 + 1) "fff" + 2) "rrr" + 3) "bbb" + 127.0.0.1:6379> sdiff set1 set2 + 1) "ccc" + 127.0.0.1:6379> sdiff set2 set1 + 1) "fff" + 2) "rrr" + ``` + + + sinter: 返回两个集合的交集 + + sinterstore: 返回交集结果,存入目标集合 + + ```shell + 127.0.0.1:6379> sinterstore set3 set1 set2 + (integer) 1 + 127.0.0.1:6379> smembers set3 + 1) "bbb" + ``` + + + sunion: 取两个集合的并集 + + sunionstore: 取两个集合的并集,并存入目标集合 + + + smove: 将一个集合中的元素移动到另一个集合中 + + scard: 返回集合中的元素个数 + + sismember: 判断某元素是否存在某集合中,0代表否 1代表是 + + srandmember: 随机返回一个元素 + + ```shell + 127.0.0.1:6379> srandmember set1 1 + 1) "bbb" + 127.0.0.1:6379> srandmember set1 2 + 1) "ccc" + 2) "bbb" + ``` ++ 应用场景 + + + 对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;微信点赞,微信抽奖小程序 + + 另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好,可能认识的人等功能。 + +### zset + +__和集合对象相比,有序集合对象是有序的。与列表使用索引下表作为排序依据不同,有序集合为每一个元素设置一个分数(score)作为排序依据。__ + ++ 编码 + + + 有序集合的编码可以使ziplist或者skiplist + + + ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。 + + skiplist编码的依序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表 + + ``` + typedef struct zset{ + //跳跃表 + zskiplist *zsl; + //字典 + dict *dice; + }zset + 字典的键保存元素的值,字典的值保存元素的分值,跳跃表节点的object属性保存元素的成员,跳跃表节点的score属性保存元素的分值。这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。 + ``` + + + 编码转换 + + + 当有序结合对象同时满足以下两个条件时,对象使用ziplist编码,否则使用skiplist编码 + + 保存的元素数量小于128 + + 保存的所有元素长度都小于64字节 + ++ 常用命令 + + + zrem: 删除集合中名称为key的元素member + + zincrby: 以指定值去自动递增 + + zcard: 查看元素集合的个数 + + zcount: 返回score在给定区间中的数量 + + ```shell + 127.0.0.1:6379> zrange zset 0 -1 + 1) "one" + 2) "three" + 3) "two" + 4) "four" + 5) "five" + 6) "six" + 127.0.0.1:6379> zcard zset + (integer) 6 + 127.0.0.1:6379> zcount zset 1 4 + (integer) 4 + ``` + + + zrangebyscore: 找到指定区间范围的数据进行返回 + + ```shell + 127.0.0.1:6379> zrangebyscore zset 0 4 withscores + 1) "one" + 2) "1" + 3) "three" + 4) "2" + 5) "two" + 6) "2" + 7) "four" + 8) "4" + ``` + + + zremrangebyrank zset from to: 删除索引 + + ```shell + 127.0.0.1:6379> zrange zset 0 -1 + 1) "one" + 2) "three" + 3) "two" + 4) "four" + 5) "five" + 6) "six" + 127.0.0.1:6379> zremrangebyrank zset 1 3 + (integer) 3 + 127.0.0.1:6379> zrange zset 0 -1 + 1) "one" + 2) "five" + 3) "six" + + ``` + + + zremrangebyscore zset from to: 删除指定序号 + + ```shell + 127.0.0.1:6379> zrange zset 0 -1 withscores + 1) "one" + 2) "1" + 3) "five" + 4) "5" + 5) "six" + 6) "6" + 127.0.0.1:6379> zremrangebyscore zset 3 6 + (integer) 2 + 127.0.0.1:6379> zrange zset 0 -1 withscores + 1) "one" + 2) "1" + ``` + + + zrank: 返回排序索引 (升序之后再找索引) + + zrevrank: 返回排序索引 (降序之后再找索引) + ++ 应用场景 + + + 对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等。 + +### hash + +__hash对象的键是一个字符串类型,值是一个键值对集合__ + ++ 编码 + + + hash对象的编码可以是ziplist或者hashtable + + 当使用ziplist,也就是压缩列表作为底层实现时,新增的键值是保存到压缩列表的表尾。 + + hashtable 编码的hash表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对。__Redis中的字典相当于Java里面的HashMap,内部实现也差不多类似,都是通过“数组+链表”的链地址法来解决哈希冲突的,这样的结构吸收了两种不同数据结构的优点。__ + + + 编码转换 + + 当同时满足下面两个条件使用ziplist编码,否则使用hashtable编码 + + 列表保存元素个数小于512个 + + 每个元素长度小于64字节 + ++ hash是一个String类型的field和value之间的映射表 + ++ Hash特别适合存储对象 + ++ 所存储的成员较少时数据存储为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht + ++ Hash命令详解 + + + hset/hget + + + hset hashname hashkey hashvalue + + hget hashname hashkey + + ```shell + 127.0.0.1:6379> hset user id 1 + (integer) 1 + 127.0.0.1:6379> hset user name z3 + (integer) 1 + 127.0.0.1:6379> hset user add shanxi + (integer) 1 + 127.0.0.1:6379> hget user id + "1" + 127.0.0.1:6379> hget user name + "z3" + 127.0.0.1:6379> hget user add + "shanxi" + + ``` + + + + + hmset/hmget + + + hmset hashname hashkey1hashvalue1 hashkey2 hashvalue2 hashkey3 hashvalue3 + + hget hashname hashkey1 hashkey2 hashkey3 + + ```shell + 127.0.0.1:6379> hmset user id 1 name z3 add shanxi + OK + 127.0.0.1:6379> hmget user id name add + 1) "1" + 2) "z3" + 3) "shanxi" + ``` + + + hsetnx/hgetnx + + + hincrby/hdecrby + + ```shell + 127.0.0.1:6379> hincrby user2 id 3 + (integer) 6 + 127.0.0.1:6379> hget user2 id + "6" + ``` + + + hexist 判断是否存在key,不存在返回0 + + ```shell + 127.0.0.1:6379> hget user2 id + "6" + ``` + + + hlen 返回hash集合里所有的键值数 + + ```shell + 127.0.0.1:6379> hmset user3 id 3 name w5 + OK + 127.0.0.1:6379> hlen user3 + (integer) 2 + ``` + + + hdel :删除指定的hash的key + + hkeys 返回hash里所有的字段 + + hvals 返回hash里所有的value + + hgetall:返回hash集合里所有的key和value + + ```shell + 127.0.0.1:6379> hgetall user3 + 1) "id" + 2) "3" + 3) "name" + 4) "w3" + 5) "add" + 6) "beijing" + ``` + ++ 优点 + + + 同类数据归类整合存储,方便数据管理,比如单个用户的所有商品都放在一个hash表里面。 + + 相比string操作消耗内存cpu更小 + ++ 缺点 + + + hash结构的存储消耗要高于单个字符串 + + 过期功能不能使用在field上,只能用在key上 + + redis集群架构不适合大规模使用 + ++ 应用场景 + + + 对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息。 + + 存放商品信息,实现购物车 + +## 内存回收和内存共享 + +``` +typedef struct redisObject{ + //类型 + unsigned type:4; + //编码 + unsigned encoding:4; + //指向底层数据结构的指针 + void *ptr; + //引用计数 + int refcount; + //记录最后一次被程序访问的时间 + unsigned lru:22; + +}robj +``` + ++ 内存回收 + __因为c语言不具备自动内存回收功能,当将redisObject对象作为数据库的键或值而不是作为参数存储时其生命周期是非常长的,为了解决这个问题,Redis自己构建了一个内存回收机制,通过redisobject结构中的refcount实现.这个属性会随着对象的使用状态而不断变化。__ + 1. 创建一个新对象,属性初始化为1 + 2. 对象被一个新程序使用,属性refcount加1 + 3. 对象不再被一个程序使用,属性refcount减1 + 4. 当对象的引用计数值变为0时,对象所占用的内存就会被释放 ++ 内存共享 + __refcount属性除了能实现内存回收以外,还能实现内存共享__ + 1. 将数据块的键的值指针指向一个现有值的对象 + 2. 将被共享的值对象引用refcount加1 + Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为o(1),对于普通字符串,判断复杂度为o(n);而对于哈希,列表,集合和有序集合,判断的复杂度为o(n^2).虽然共享的对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象。 + + + diff --git "a/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" "b/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" new file mode 100644 index 00000000000..451c91bd028 --- /dev/null +++ "b/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" @@ -0,0 +1,392 @@ +> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis + + + + + +# 一、跳跃表简介 + +跳跃表(skiplist)是一种随机化的数据结构,由 **William Pugh** 在论文[《Skip lists: a probabilistic alternative to balanced trees》](https://www.cl.cam.ac.uk/teaching/0506/Algorithms/skiplists.pdf)中提出,是一种可以于平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下完成,以下是一个典型的跳跃表例子: + + + +我们在上一篇中提到了 Redis 的五种基本结构中,有一个叫做 **有序列表 zset** 的数据结构,它类似于 Java 中的 **SortedSet** 和 **HashMap** 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 **排序** 的目的。 + +它的内部实现就依赖了一种叫做 **「跳跃列表」** 的数据结构。 + +## 为什么使用跳跃表 + +首先,因为 zset 要支持随机的插入和删除,所以它 **不宜使用数组来实现**,关于排序问题,我们也很容易就想到 **红黑树/ 平衡树** 这样的树形结构,为什么 Redis 不使用这样一些结构呢? + +1. **性能考虑:** 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 _(下面详细说)_; +2. **实现考虑:** 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观; + +基于以上的一些考虑,Redis 基于 **William Pugh** 的论文做出一些改进后采用了 **跳跃表** 这样的结构。 + +## 本质是解决查找问题 + +我们先来看一个普通的链表结构: + + + +我们需要这个链表按照 score 值进行排序,这也就意味着,当我们需要添加新的元素时,我们需要定位到插入点,这样才可以继续保证链表是有序的,通常我们会使用 **二分查找法**,但二分查找是有序数组的,链表没办法进行位置定位,我们除了遍历整个找到第一个比给定数据大的节点为止 _(时间复杂度 O(n))_ 似乎没有更好的办法。 + +但假如我们每相邻两个节点之间就增加一个指针,让指针指向下一个节点,如下图: + + + +这样所有新增的指针连成了一个新的链表,但它包含的数据却只有原来的一半 _(图中的为 3,11)_。 + +现在假设我们想要查找数据时,可以根据这条新的链表查找,如果碰到比待查找数据大的节点时,再回到原来的链表中进行查找,比如,我们想要查找 7,查找的路径则是沿着下图中标注出的红色指针所指向的方向进行的: + + + +这是一个略微极端的例子,但我们仍然可以看到,通过新增加的指针查找,我们不再需要与链表上的每一个节点逐一进行比较,这样改进之后需要比较的节点数大概只有原来的一半。 + +利用同样的方式,我们可以在新产生的链表上,继续为每两个相邻的节点增加一个指针,从而产生第三层链表: + + + +在这个新的三层链表结构中,我们试着 **查找 13**,那么沿着最上层链表首先比较的是 11,发现 11 比 13 小,于是我们就知道只需要到 11 后面继续查找,**从而一下子跳过了 11 前面的所有节点。** + +可以想象,当链表足够长,这样的多层链表结构可以帮助我们跳过很多下层节点,从而加快查找的效率。 + +## 更进一步的跳跃表 + +**跳跃表 skiplist** 就是受到这种多层链表结构的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 _O(logn)_。 + +但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的 2:1 的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点 _(也包括新插入的节点)_ 重新进行调整,这会让时间复杂度重新蜕化成 _O(n)_。删除数据也有同样的问题。 + +**skiplist** 为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 **为每个节点随机出一个层数(level)**。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。为了表达清楚,下图展示了如何通过一步步的插入操作从而形成一个 skiplist 的过程: + + + +从上面的创建和插入的过程中可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,**插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整**,这就降低了插入操作的复杂度。 + +现在我们假设从我们刚才创建的这个结构中查找 23 这个不存在的数,那么查找路径会如下图: + + + +# 二、跳跃表的实现 + +Redis 中的跳跃表由 `server.h/zskiplistNode` 和 `server.h/zskiplist` 两个结构定义,前者为跳跃表节点,后者则保存了跳跃节点的相关信息,同之前的 `集合 list` 结构类似,其实只有 `zskiplistNode` 就可以实现了,但是引入后者是为了更加方便的操作: + +```c +/* ZSETs use a specialized version of Skiplists */ +typedef struct zskiplistNode { + // value + sds ele; + // 分值 + double score; + // 后退指针 + struct zskiplistNode *backward; + // 层 + struct zskiplistLevel { + // 前进指针 + struct zskiplistNode *forward; + // 跨度 + unsigned long span; + } level[]; +} zskiplistNode; + +typedef struct zskiplist { + // 跳跃表头指针 + struct zskiplistNode *header, *tail; + // 表中节点的数量 + unsigned long length; + // 表中层数最大的节点的层数 + int level; +} zskiplist; +``` + +正如文章开头画出来的那张标准的跳跃表那样。 + +## 随机层数 + +对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数,源码在 `t_zset.c/zslRandomLevel(void)` 中被定义: + +```c +int zslRandomLevel(void) { + int level = 1; + while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) + level += 1; + return (level图片来自:https://simplesnippets.tech/exception-handling-in-java-part-1/
+ + + + +图片来自:https://chercher.tech/java-programming/exceptions-java
+ +在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 **Throwable 类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 + +**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 + +这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。 + +**Exception(异常):是程序本身可以处理的异常**。 {
+ P create(String firstName, String lastName);
+}
+```
+
+这里我们使用构造函数引用来将他们关联起来,而不是手动实现一个完整的工厂:
+
+```java
+PersonFactory