From 84d6d0a2595eecba50b2ffb030f92a1eccdbb6ab Mon Sep 17 00:00:00 2001 From: Zhang Peng Date: Tue, 14 Jan 2020 23:56:55 +0800 Subject: [PATCH] update docs --- .editorconfig | 4 +- .gitignore | 2 + README.md | 2 +- docs/.nojekyll | 0 docs/README.md | 2 +- docs/book.json | 69 +++++ docs/coverpage.md | 4 +- docs/index.html | 404 ++++++++----------------- docs/nosql/redis/redis-ops.md | 6 +- docs/package.json | 38 ++- docs/sidebar.md | 2 + docs/sql/h2.md | 88 +++--- docs/sql/middleware/flyway.md | 16 +- docs/sql/postgresql.md | 4 +- docs/sql/sql-interview.md | 544 +++++++++++++++++++++------------- prettier.config.js | 4 +- 16 files changed, 627 insertions(+), 562 deletions(-) delete mode 100644 docs/.nojekyll create mode 100644 docs/book.json diff --git a/.editorconfig b/.editorconfig index c7ca29b..d72a75e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ root = true [*] end_of_line = lf indent_size = 2 -indent_style = tab +indent_style = space max_line_length = 120 charset = utf-8 trim_trailing_whitespace = true @@ -19,7 +19,7 @@ insert_final_newline = true [*.{bat, cmd}] end_of_line = crlf -[*.{java, groovy, kt, sh}] +[*.{java, gradle, groovy, kt, sh}] indent_size = 4 [*.md] diff --git a/.gitignore b/.gitignore index b887576..4a99239 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ hs_err_pid* # maven plugin temp files .flattened-pom.xml +package-lock.json # ------------------------------- javascript ------------------------------- @@ -47,6 +48,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* bundle*.js +book.pdf # ------------------------------- intellij ------------------------------- diff --git a/README.md b/README.md index 72ded1b..7b08fb6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 数据库教程 -> 数据库经验总结 +> 💾 **db-tutorial** 是一个数据库教程。 > > - 🔁 项目同步维护:[Github](https://github.com/dunwu/db-tutorial/) | [Gitee](https://gitee.com/turnon/db-tutorial/) > - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/db-tutorial/) | [Gitee Pages](https://turnon.gitee.io/db-tutorial/) diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/docs/README.md b/docs/README.md index d5775f8..809cd85 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # 数据库教程 -> 数据库经验总结 +> 💾 **db-tutorial** 是一个数据库教程。 > > - 🔁 项目同步维护:[Github](https://github.com/dunwu/db-tutorial/) | [Gitee](https://gitee.com/turnon/db-tutorial/) > - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/db-tutorial/) | [Gitee Pages](https://turnon.gitee.io/db-tutorial/) diff --git a/docs/book.json b/docs/book.json new file mode 100644 index 0000000..50f37ec --- /dev/null +++ b/docs/book.json @@ -0,0 +1,69 @@ +{ + "gitbook": "3.2.2", + "title": "db-tutorial", + "language": "zh-hans", + "root": "./", + "structure": { + "summary": "sidebar.md" + }, + "links": { + "sidebar": { + "db-tutorial": "https://github.com/dunwu/db-tutorial" + } + }, + "plugins": [ + "-lunr", + "-search", + "advanced-emoji@^0.2.2", + "anchor-navigation-ex@1.0.10", + "anchors@^0.7.1", + "edit-link@^2.0.2", + "expandable-chapters-small@^0.1.7", + "github@^2.0.0", + "search-plus@^0.0.11", + "simple-page-toc@^0.1.1", + "splitter@^0.0.8", + "tbfed-pagefooter@^0.0.1" + ], + "pluginsConfig": { + "anchor-navigation-ex": { + "showLevel": false, + "associatedWithSummary": true, + "multipleH1": true, + "mode": "float", + "isRewritePageTitle": false, + "float": { + "showLevelIcon": false, + "level1Icon": "fa fa-hand-o-right", + "level2Icon": "fa fa-hand-o-right", + "level3Icon": "fa fa-hand-o-right" + }, + "pageTop": { + "showLevelIcon": false, + "level1Icon": "fa fa-hand-o-right", + "level2Icon": "fa fa-hand-o-right", + "level3Icon": "fa fa-hand-o-right" + } + }, + "edit-link": { + "base": "https://github.com/dunwu/db-tutorial/blob/master/docs", + "label": "编辑此页面" + }, + "github": { + "url": "https://github.com/dunwu" + }, + "simple-page-toc": { + "maxDepth": 4, + "skipFirstH1": true + }, + "sharing": { + "weibo": true, + "all": ["weibo"] + }, + "tbfed-pagefooter": { + "copyright": "Copyright © Zhang Peng 2017", + "modify_label": "该文件上次修订时间:", + "modify_format": "YYYY-MM-DD HH:mm:ss" + } + } +} diff --git a/docs/coverpage.md b/docs/coverpage.md index fd94ab9..2ff88fc 100644 --- a/docs/coverpage.md +++ b/docs/coverpage.md @@ -1,7 +1,7 @@ -
+
# DB Tutorial -> 数据库教程 +> **db-tutorial** 是一个数据库教程。 [开始阅读](README.md) diff --git a/docs/index.html b/docs/index.html index 68c4016..8d3e790 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,290 +1,140 @@ - - - DB Tutorial - - - - - - - + + +
正在加载...
- .cover-main a:hover { - text-align: center; - background-image: -webkit-linear-gradient(left, #ffdcb4, #b96972 25%, #e88a57 50%, #804170 75%, #a596cd); - -webkit-text-fill-color: transparent; - -webkit-background-clip: text; - -webkit-background-size: 200% 100%; - -webkit-animation: masked-animation 1.5s infinite linear; - } + + - /* content 样式内容 */ - .content a, - .sidebar a, - .sidebar ul li a, - .sidebar ul li a strong { - color: #399ab2 !important; - text-decoration: none !important; - } + - .content a:hover, - .sidebar a:hover, - .sidebar ul li a:hover, - .sidebar ul li a strong:hover { - color: #fe4165 !important; - text-decoration: underline !important; - } + + + - .sidebar h2 span { - font-size: 18px; - color: #399ab2 !important; - text-decoration: none !important; - } + + + + + + + - .sidebar h2 span:hover { - color: #fe4165 !important; - text-decoration: underline !important; - } - - .sidebar .sidebar-nav { - padding-left: 20px; - } - - .content h1 :hover, - .content h2 :hover, - .content h3 :hover, - .content h4 :hover { - text-align: center; - background-image: -webkit-linear-gradient(left, #ffdcb4, #b96972 25%, #e88a57 50%, #804170 75%, #a596cd); - -webkit-text-fill-color: transparent; - -webkit-background-clip: text; - -webkit-background-size: 200% 100%; - -webkit-animation: masked-animation 1.5s infinite linear; - font-family: '微软雅黑', serif; - font-weight: bold; - } - - @-webkit-keyframes masked-animation { - 0% { - background-position: 0 0; - } - 100% { - background-position: -100% 0; - } - } - - .markdown-section h1, - .content h1 a, - .content h1 span { - color: #399ab2 !important; - font-size: 30px; - text-shadow: 2px 2px 5px grey; - } - - .content h2 a, - .content h2 span { - color: #60497c !important; - font-size: 26px; - text-shadow: 2px 2px 5px grey; - } - - .content h3 a, - .content h3 span { - color: #346093 !important; - font-size: 22px; - text-shadow: 2px 2px 5px grey; - } - - .content h4 a, - .content h4 span { - font-size: 18px; - color: #78943a; - text-shadow: 2px 2px 5px grey; - } - - - - - - -
正在加载...
- - - - - - - - - - - - - + + + diff --git a/docs/nosql/redis/redis-ops.md b/docs/nosql/redis/redis-ops.md index 3ea46a4..34078ed 100644 --- a/docs/nosql/redis/redis-ops.md +++ b/docs/nosql/redis/redis-ops.md @@ -209,7 +209,7 @@ Redis 3.0 后支持集群模式。 `Redis` 集群一般由 **多个节点** 组成,节点数量至少为 `6` 个,才能保证组成 **完整高可用** 的集群。 -
+![img](https://user-gold-cdn.xitu.io/2019/10/10/16db5250b0d1c392?w=1467&h=803&f=png&s=43428) 理想情况当然是所有节点各自在不同的机器上,首先于资源,本人在部署 Redis 集群时,只得到 3 台服务器。所以,我计划每台服务器部署 2 个 Redis 节点。 @@ -401,9 +401,9 @@ S: b6d70f2ed78922b1dcb7967ebe1d05ad9157fca8 127.0.0.3:6386 > > 搬迁两张 cheat sheet 图,原址:https://www.cheatography.com/tasjaevan/cheat-sheets/redis/ -
+![img](https://user-gold-cdn.xitu.io/2019/10/10/16db5250b0b8ea57?w=2230&h=2914&f=png&s=246433) -
+![img](https://user-gold-cdn.xitu.io/2019/10/10/16db5250b0e9ba3c?w=2229&h=2890&f=png&s=192997) ## 压力测试 diff --git a/docs/package.json b/docs/package.json index 211e351..04634f3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,9 +1,33 @@ { - "name": "db-tutorial", - "version": "1.0.0", - "scripts": { - "start": "docsify serve ./ --port 4000" - }, - "dependencies": {}, - "devDependencies": {} + "name": "db-tutorial", + "author": "Zhang Peng", + "homepage": "http://dunwu.github.io/db-tutorial", + "repository": { + "type": "git", + "url": "git@github.com:dunwu/db-tutorial.git" + }, + "scripts": { + "start": "docsify serve ./ --port 4000", + "clean": "rimraf _book", + "install": "gitbook install", + "serve": "gitbook serve", + "build": "npm run clean & gitbook build", + "pdf": "gitbook pdf ." + }, + "dependencies": { + "gitbook-plugin-advanced-emoji": "^0.2.2", + "gitbook-plugin-anchor-navigation-ex": "^1.0.10", + "gitbook-plugin-anchors": "^0.7.1", + "gitbook-plugin-edit-link": "^2.0.2", + "gitbook-plugin-expandable-chapters-small": "^0.1.7", + "gitbook-plugin-github": "^2.0.0", + "gitbook-plugin-search-plus": "0.0.11", + "gitbook-plugin-simple-page-toc": "^0.1.2", + "gitbook-plugin-splitter": "0.0.8", + "gitbook-plugin-tbfed-pagefooter": "0.0.1" + }, + "devDependencies": { + "gh-pages": "^2.1.1", + "rimraf": "^3.0.0" + } } diff --git a/docs/sidebar.md b/docs/sidebar.md index 9e133a8..1c7b5a0 100644 --- a/docs/sidebar.md +++ b/docs/sidebar.md @@ -1,3 +1,5 @@ +# db-tutorial + - [关系型数据库](sql/README.md) - [关系型数据库面试题](sql/sql-interview.md) - [关系型数据库基本原理](sql/sql-theory.md) diff --git a/docs/sql/h2.md b/docs/sql/h2.md index 05dcd02..30ebc60 100644 --- a/docs/sql/h2.md +++ b/docs/sql/h2.md @@ -27,11 +27,11 @@ H2 允许用户通过浏览器接口方式访问 SQL 数据库。 2. 启动方式:在 bin 目录下,双击 jar 包;执行 `java -jar h2*.jar`;执行脚本:`h2.bat` 或 `h2.sh`。 3. 在浏览器中访问:http://localhost:8082,应该可以看到下图中的页面: -

+
![img](http://dunwu.test.upcdn.net/cs/database/h2/h2-console.png!zp)
点击 **Connect** ,可以进入操作界面: -

+
![img](http://dunwu.test.upcdn.net/cs/database/h2/h2-console-02.png!zp)
操作界面十分简单,不一一细说。 @@ -263,184 +263,184 @@ java -jar h2-1.3.168.jar -web -webPort 8090 -browser ### SELECT -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-a3f90c0d1f1f3437.png)
### INSERT -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-6a92ae4362c3468a.png)
### UPDATE -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-dddf0e26995d46c3.png)
### DELETE -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-96e72023445a6fd6.png)
### BACKUP -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-6267894d24fab47f.png)
### EXPLAIN -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-bbed6bb69f998b7a.png)
7、MERGE -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-bd021648431d12a7.png)
### RUNSCRIPT 运行 sql 脚本文件 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-d6fe03eff0037e14.png)
### SCRIPT 根据数据库创建 sql 脚本 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-9ba7547ab8bcaeab.png)
### SHOW -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-67449c6cc5cbb8c1.png)
### ALTER #### ALTER INDEX RENAME -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-230bd3f97e185d2f.png)
#### ALTER SCHEMA RENAME -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-797a028938e46ba3.png)
#### ALTER SEQUENCE -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-46f343da1b6c6a29.png)
#### ALTER TABLE -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-7e146a4010f2f357.png)
##### 增加约束 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-4e5605a9c87a79cb.png)
##### 修改列 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-fbc1358c553e6614.png)
##### 删除列 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-dc3b897413700981.png)
##### 删除序列 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-ec83899cb8724966.png)
#### ALTER USER ##### 修改用户名 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-a1e429c0d8ece66c.png)
##### 修改用户密码 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-5b86f98796606e54.png)
#### ALTER VIEW -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-8832ecbc2db63a13.png)
### COMMENT -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-467ce031883f0020.png)
### CREATE CONSTANT -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-1231c83563bfec9c.png)
### CREATE INDEX -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-d66d59bd7803d5c1.png)
### CREATE ROLE -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-7df1dee098e1127b.png)
### CREATE SCHEMA -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-c485123c62c0866e.png)
### CREATE SEQUENCE -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-cc25860776d361ae.png)
### CREATE TABLE -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-36ffc66327df8b5b.png)
### CREATE TRIGGER -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-9a7bfa4425281213.png)
### CREATE USER -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-a1e45e308be6dac3.png)
### CREATE VIEW -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-45c4cd516fd36611.png)
### DROP -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-52a3562d76411811.jpg)
### GRANT RIGHT 给 schema 授权授权 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-750e96ceff00c4ee.png)
给 schema 授权给 schema 授权 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-22cfd65c2ff1eea5.png)
#### 复制角色的权限 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-6cba2f1585fd913b.png)
### REVOKE RIGHT #### 移除授权 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-3f905669cbb331b7.png)
#### 移除角色具有的权限 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-af77f495222f1b30.png)
### ROLLBACK #### 从某个还原点(savepoint)回滚 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-c71a226ac4fff913.png)
#### 回滚事务 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-efb65c504c7d69c2.png)
#### 创建 savepoint -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-feefdc236d4b211d.png)
## 数据类型 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-52296dd53249cdae.png)
### INT Type -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-fe62e3d07eb93d11.png)
## 集群 diff --git a/docs/sql/middleware/flyway.md b/docs/sql/middleware/flyway.md index 6b67e38..8a39092 100644 --- a/docs/sql/middleware/flyway.md +++ b/docs/sql/middleware/flyway.md @@ -38,7 +38,7 @@ (2)对于大多数项目而言,最简单的持续集成场景如下所示: -

+
![img](https://flywaydb.org/assets/balsamiq/Environments.png)
这意味着,我们不仅仅要处理一份环境中的修改,由此会引入一些版本冲突问题: @@ -69,13 +69,13 @@ 最简单的场景是指定 Flyway 迁移到一个空的数据库。 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-bb6e9f39e56ebbda.png)
Flyway 会尝试查找它的 schema 历史表,如果数据库是空的,Flyway 就不再查找,而是直接创建数据库。 现再你就有了一个仅包含一张空表的数据库,默认情况下,这张表叫 _flyway_schema_history_。 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-410eb31c6313b389.png)
这张表将被用于追踪数据库的状态。 @@ -83,17 +83,17 @@ Flyway 会尝试查找它的 schema 历史表,如果数据库是空的,Flywa 这些 **migrations** 将根据他们的版本号进行排序。 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-d36ee07ada4efbcd.png)
任意 migration 应用后,schema 历史表将更新。当元数据和初始状态替换后,可以称之为:迁移到新版本。 Flyway 一旦扫描了文件系统或应用 classpath 下的 migrations,这些 migrations 会检查 schema 历史表。如果它们的版本号低于或等于当前的版本,将被忽略。保留下来的 migrations 是等待的 migrations,有效但没有应用。 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-99a88fea7a31a070.png)
migrations 将根据版本号排序并按序执行。 -

+
![img](http://upload-images.jianshu.io/upload_images/3101171-b444fef6e5c13b71.png)
## 快速上手 @@ -406,7 +406,7 @@ migrations 最常用的编写形式就是 SQL。 为了被 Flyway 自动识别,SQL migrations 的文件命名必须遵循规定的模式: -

+
![img](http://dunwu.test.upcdn.net/cs/database/flyway/sql-migrations.png!zp)
- **Prefix** - `V` 代表 versioned migrations (可配置), `U` 代表 undo migrations (可配置)、 `R` 代表 repeatable migrations (可配置) - **Version** - 版本号通过`.`(点)或`_`(下划线)分隔 (repeatable migrations 不需要) @@ -425,7 +425,7 @@ migrations 最常用的编写形式就是 SQL。 为了被 Flyway 自动识别,JAVA migrations 的文件命名必须遵循规定的模式: -

+
![img](http://dunwu.test.upcdn.net/cs/database/flyway/java-migrations.png!zp)
- **Prefix** - `V` 代表 versioned migrations (可配置), `U` 代表 undo migrations (可配置)、 `R` 代表 repeatable migrations (可配置) - **Version** - 版本号通过`.`(点)或`_`(下划线)分隔 (repeatable migrations 不需要) diff --git a/docs/sql/postgresql.md b/docs/sql/postgresql.md index b2b4693..ae099d3 100644 --- a/docs/sql/postgresql.md +++ b/docs/sql/postgresql.md @@ -4,7 +4,7 @@ > > 关键词:Database, RDBM, psql -

+
![img](http://dunwu.test.upcdn.net/snap/20180920181010182614.png!zp)
@@ -26,7 +26,7 @@ 官方下载页面要求用户选择相应版本,然后动态的给出安装提示,如下图所示: -

+
![img](http://dunwu.test.upcdn.net/snap/20180920181010174348.png!zp)
前 3 步要求用户选择,后 4 步是根据选择动态提示的安装步骤 diff --git a/docs/sql/sql-interview.md b/docs/sql/sql-interview.md index dd52fef..357e3e0 100644 --- a/docs/sql/sql-interview.md +++ b/docs/sql/sql-interview.md @@ -1,22 +1,39 @@ # 关系型数据库面试题 - + -- [1. 概念](#1-概念) -- [2. SQL](#2-sql) -- [3. 索引和约束](#3-索引和约束) -- [4. 事务](#4-事务) -- [5. 锁](#5-锁) -- [6. 分库分表](#6-分库分表) -- [7. 数据库架构设计](#7-数据库架构设计) -- [8. 参考资料](#8-参考资料) -- [9. :door: 传送门](#9-door-传送门) +- [概念](#概念) + - [什么是存储过程?有哪些优缺点?](#什么是存储过程有哪些优缺点) + - [什么是视图?以及视图的使用场景有哪些?](#什么是视图以及视图的使用场景有哪些) +- [SQL](#sql) + - [drop、delete 与 truncate 分别在什么场景之下使用?](#dropdelete-与-truncate-分别在什么场景之下使用) +- [索引和约束](#索引和约束) + - [SQL 约束有哪几种?](#sql-约束有哪几种) + - [超键、候选键、主键、外键分别是什么?](#超键候选键主键外键分别是什么) + - [数据库索引有哪些数据结构?](#数据库索引有哪些数据结构) + - [B-Tree 和 B+Tree 有什么区别?](#b-tree-和-btree-有什么区别) + - [索引原则有哪些?](#索引原则有哪些) +- [事务](#事务) + - [什么是事务?](#什么是事务) + - [数据库事务隔离级别?事务隔离级别分别解决什么问题?](#数据库事务隔离级别事务隔离级别分别解决什么问题) + - [如何解决分布式事务?若出现网络问题或宕机问题,如何解决?](#如何解决分布式事务若出现网络问题或宕机问题如何解决) +- [锁](#锁) + - [数据库的乐观锁和悲观锁是什么?](#数据库的乐观锁和悲观锁是什么) + - [数据库锁有哪些类型?如何实现?](#数据库锁有哪些类型如何实现) +- [分库分表](#分库分表) + - [什么是分库分表](#什么是分库分表) + - [分库分表中间件](#分库分表中间件) + - [分库分表的问题](#分库分表的问题) +- [数据库架构设计](#数据库架构设计) + - [高并发系统数据层面如何设计?](#高并发系统数据层面如何设计) +- [参考资料](#参考资料) +- [:door: 传送门](#door-传送门) -## 1. 概念 +## 一、基本概念 -### 1.1.1. 什么是存储过程?有哪些优缺点? +### 什么是存储过程?有哪些优缺点? **存储过程就像我们编程语言中的函数一样,封装了我们的代码(PLSQL、T-SQL)**。 @@ -44,7 +61,7 @@ END// CALL phelloword() ``` -### 1.1.2. 什么是视图?以及视图的使用场景有哪些? +### 什么是视图?以及视图的使用场景有哪些? 视图是一种基于数据表的一种**虚表** @@ -62,15 +79,15 @@ CALL phelloword() 我们应该做到:**他们想看到什么样的数据,我们就给他们什么样的数据...一方面就能够让他们只关注自己的数据,另一方面,我们也保证数据表一些保密的数据不会泄露出来...** -
+![img](https://user-gold-cdn.xitu.io/2018/3/5/161f3de9b3092439?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 我们在查询数据的时候,常常需要编写非常长的 SQL 语句,几乎每次都要写很长很长....上面已经说了,**视图就是基于查询的一种虚表,也就是说,视图可以将查询出来的数据进行封装。。。那么我们在使用的时候就会变得非常方便**... 值得注意的是:**使用视图可以让我们专注与逻辑,但不提高查询效率** -## 2. SQL +## 二、SQL -### 1.2.1. drop、delete 与 truncate 分别在什么场景之下使用? +### drop、delete 与 truncate 分别在什么场景之下使用? - drop table @@ -98,15 +115,19 @@ CALL phelloword() - **想删除部分数据行时候,用 delete,并且带上 where 子句** - **保留表而删除所有数据的时候用 truncate** -## 3. 索引和约束 +## 三、索引和约束 -### SQL 约束有哪几种? +### 约束 -- NOT NULL: 用于控制字段的内容一定不能为空(NULL)。 -- UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。 -- PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。 -- FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。 -- CHECK: 用于控制字段的值范围。 +> ❓ 常见问题: +> +> - SQL 约束有哪些? + +- `NOT NULL`: 用于控制字段的内容一定不能为空(NULL)。 +- `UNIQUE`: 控件字段内容不能重复,一个表允许有多个 Unique 约束。 +- `PRIMARY KEY`: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。 +- `FOREIGN KEY`: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。 +- `CHECK`: 用于控制字段的值范围。 ### 超键、候选键、主键、外键分别是什么? @@ -115,13 +136,13 @@ CALL phelloword() - **主键(主码):数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合**。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。 - **外键:在一个表中存在的另一个表的主键称此表的外键**。 -### 1.3.1. 数据库索引有哪些数据结构? +### 数据库索引有哪些数据结构? - B-Tree - B+Tree - Hash -#### 1.3.1.1. B-Tree +#### B-Tree 一棵 M 阶的 B-Tree 满足以下条件: @@ -145,7 +166,7 @@ CALL phelloword() 2. 否则,可确定 K 在某个 Key[i]和 Key[i+1]之间,则从 Son[i]所指的子结点继续查找,直到在某结点中查找成功; 3. 或直至找到叶结点且叶结点中的查找仍不成功时,查找过程失败。 -#### 1.3.1.2. B+Tree +#### B+Tree B+Tree 是 B-Tree 的变种: @@ -158,7 +179,7 @@ B+Tree 是 B-Tree 的变种: 由于并不是所有节点都具有相同的域,因此 B+Tree 中叶节点和内节点一般大小不同。这点与 B-Tree 不同,虽然 B-Tree 中不同节点存放的 key 和指针可能数量不一致,但是每个节点的域和上限是一致的,所以在实现中 B-Tree 往往对每个节点申请同等大小的空间。 -##### 1.3.1.2.1. 带有顺序访问指针的 B+Tree +##### 带有顺序访问指针的 B+Tree 一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 的基础上进行了优化,增加了顺序访问指针。 @@ -170,7 +191,7 @@ B+Tree 是 B-Tree 的变种: 这个优化的目的是为了提高区间访问的性能,例如上图中如果要查询 key 为从 18 到 49 的所有数据记录,当找到 18 后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。 -#### 1.3.1.3. Hash +#### Hash Hash 索引只有精确匹配索引所有列的查询才有效。 @@ -187,14 +208,14 @@ Hash 索引只有精确匹配索引所有列的查询才有效。 - 哈希索引只支持等值比较查询,不支持任何范围查询,如 WHERE price > 100。 - 哈希索引有可能出现哈希冲突,出现哈希冲突时,必须遍历链表中所有的行指针,逐行比较,直到找到符合条件的行。 -### 1.3.2. B-Tree 和 B+Tree 有什么区别? +### B-Tree 和 B+Tree 有什么区别? - B+Tree 更适合外部存储(一般指磁盘存储),由于内节点(非叶子节点)不存储 data,所以一个节点可以存储更多的内节点,每个节点能索引的范围更大更精确。也就是说使用 B+Tree 单次磁盘 IO 的信息量相比较 B-Tree 更大,IO 效率更高。 - mysql 是关系型数据库,经常会按照区间来访问某个索引列,B+Tree 的叶子节点间按顺序建立了链指针,加强了区间访问性,所以 B+Tree 对索引列上的区间范围查询很友好。而 B-Tree 每个节点的 key 和 data 在一起,无法进行区间查找。 -### 1.3.3. 索引原则有哪些? +### 索引原则有哪些? -#### 1.3.3.1. 独立的列 +#### 独立的列 如果查询中的列不是独立的列,则数据库不会使用索引。 @@ -207,7 +228,7 @@ SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5; SELECT ... WHERE TO_DAYS(CURRENT_DAT) - TO_DAYS(date_col) <= 10; ``` -#### 1.3.3.2. 前缀索引和索引选择性 +#### 前缀索引和索引选择性 有时候需要索引很长的字符列,这会让索引变得大且慢。 @@ -219,23 +240,23 @@ SELECT ... WHERE TO_DAYS(CURRENT_DAT) - TO_DAYS(date_col) <= 10; 要选择足够长的前缀以保证较高的选择性,同时又不能太长(节约空间)。 -#### 1.3.3.3. 多列索引 +#### 多列索引 不要为每个列创建独立的索引。 -#### 1.3.3.4. 选择合适的索引列顺序 +#### 选择合适的索引列顺序 经验法则:将选择性高的列或基数大的列优先排在多列索引最前列。 但有时,也需要考虑 WHERE 子句中的排序、分组和范围条件等因素,这些因素也会对查询性能造成较大影响。 -#### 1.3.3.5. 聚簇索引 +#### 聚簇索引 聚簇索引不是一种单独的索引类型,而是一种数据存储方式。 聚簇表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 -#### 1.3.3.6. 覆盖索引 +#### 覆盖索引 索引包含所有需要查询的字段的值。 @@ -245,110 +266,130 @@ SELECT ... WHERE TO_DAYS(CURRENT_DAT) - TO_DAYS(date_col) <= 10; - 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。 - 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。 -#### 1.3.3.7. 使用索引扫描来做排序 +#### 使用索引扫描来做排序 索引最好既满足排序,又用于查找行。这样,就可以使用索引来对结果排序。 -#### 1.3.3.8. = 和 in 可以乱序 +#### = 和 in 可以乱序 比如 a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql 的查询优化器会帮你优化成索引可以识别的形式。 -#### 1.3.3.9. 尽量的扩展索引,不要新建索引 +#### 尽量的扩展索引,不要新建索引 比如表中已经有 a 的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。 -## 4. 事务 +## 四、数据库事务 -### 1.4.1. 什么是事务? +> ❓ 常见问题: +> +> - 什么是事务?事务有哪些特性? -事务简单来说:**一个 Session 中所进行所有的操作,要么同时成功,要么同时失败** +### 什么是事务 + +事务简单来说:**一个 Session 中所进行所有的操作,要么同时成功,要么同时失败**。具体来说,事务指的是满足 ACID 特性的一组操作,可以通过 `Commit` 提交一个事务,也可以使用 `Rollback` 进行回滚。 **ACID — 数据库事务正确执行的四个基本要素** -- 原子性(Atomicity) -- 一致性(Consistency) -- 隔离性(Isolation) -- 持久性(Durability) +- 原子性(Atomicity)- 事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。回滚可以用回滚日志(Undo Log)来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 +- 一致性(Consistency)- 数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。 +- 隔离性(Isolation) - 一个事务所做的修改在最终提交以前,对其它事务是不可见的。 +- 持久性(Durability) - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。系统发生奔溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。 **一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易。** -### 1.4.2. 数据库事务隔离级别?事务隔离级别分别解决什么问题? +- 只有满足一致性,事务的执行结果才是正确的。 +- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 +- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 +- 事务满足持久化是为了能应对系统崩溃的情况。 + +> MySQL 默认采用自动提交模式(AUTO COMMIT)。也就是说,如果不显式使用`START TRANSACTION`语句来开始一个事务,那么每个查询操作都会被当做一个事务并自动提交。 + +### 数据库事务隔离 + +> ❓ 常见问题: +> +> - 数据库并发一致性问题有哪些? +> - 数据库事务隔离级别有哪些?事务隔离级别分别解决了什么问题?⭐️ + +数据库并发一致性问题: + +- `丢失修改` - T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。 + +- `脏读` - **一个事务读取到另外一个事务未提交的数据**。T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。 +- `不可重复读` - **一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改**。T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 +- `虚读(幻读)` - **是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。**T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 + +数据库隔离级别: - `未提交读(READ UNCOMMITTED)` - 事务中的修改,即使没有提交,对其它事务也是可见的。 - `提交读(READ COMMITTED)` - 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 - `可重复读(REPEATABLE READ)` - 保证在同一个事务中多次读取同样数据的结果是一样的。 - `可串行化(SERIALIXABLE)` - 强制事务串行执行。 -| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | -| :------: | :--: | :--------: | :----: | -| 未提交读 | YES | YES | YES | -| 提交读 | NO | YES | YES | -| 可重复读 | NO | NO | YES | -| 可串行化 | NO | NO | NO | +数据库隔离级别解决的问题: -- `脏读` - **一个事务读取到另外一个事务未提交的数据** -- `不可重复读` - **一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改** -- `虚读(幻读)` - **是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。** +| 隔离级别 | 脏读 | 不可重复读 | 幻读 | +| :------: | :--: | :--------: | :--: | +| 未提交读 | ❌ | ❌ | ❌ | +| 提交读 | ✔️ | ❌ | ❌ | +| 可重复读 | ✔️ | ✔️ | ❌ | +| 可串行化 | ✔️ | ✔️ | ✔️ | -### 1.4.3. 如何解决分布式事务?若出现网络问题或宕机问题,如何解决? +## 五、数据库锁 -## 5. 锁 +### 乐观锁和悲观锁 -### 1.5.1. 数据库的乐观锁和悲观锁是什么? +> :question: 问题: +> +> - 数据库的乐观锁和悲观锁是什么? +> - 数据库的乐观锁和悲观锁如何实现? 确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性,**乐观锁和悲观锁是并发控制主要采用的技术手段。** - **`悲观锁`** - 假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作 - - **在查询完数据的时候就把事务锁起来,直到提交事务** + - **在查询完数据的时候就把事务锁起来,直到提交事务(COMMIT)** - 实现方式:使用数据库中的锁机制 - **`乐观锁`** - 假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 - **在修改数据的时候把事务锁起来,通过 version 的方式来进行锁定** - 实现方式:使用 version 版本或者时间戳 -### 1.5.2. 数据库锁有哪些类型?如何实现? +### 行级锁和表级锁 -#### 1.5.2.1. 锁粒度 +> ❓ 问题: +> +> - 什么是行级锁和表级锁? +> - 什么时候用行级锁?什么时候用表级锁? + +从数据库的锁粒度来看,MySQL 中提供了两种封锁粒度:行级锁和表级锁。 - **表级锁(table lock)** - 锁定整张表。用户对表进行写操作前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他用户才能获得读锁,读锁之间不会相互阻塞。 - **行级锁(row lock)** - 仅对指定的行记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作。 -InnoDB 行锁是通过给索引上的索引项加锁来实现的。只有通过索引条件检索数据,InnoDB 才使用行级锁;否则,InnoDB 将使用表锁! +应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。 -索引分为主键索引和非主键索引两种,如果一条 sql 语句操作了主键索引,MySQL 就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL 会先锁定该非主键索引,再锁定相关的主键索引。在 UPDATE、DELETE 操作时,MySQL 不仅锁定 WHERE 条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的 next-key locking。 +在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。 -当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。发生死锁后,InnoDB 一般都可以检测到,并使一个事务释放锁回退,另一个获取锁完成事务。 +在 `InnoDB` 中,行锁是通过给索引上的索引项加锁来实现的。**如果没有索引,`InnoDB` 将会通过隐藏的聚簇索引来对记录加锁**。 -#### 1.5.2.2. 读写锁 +### 读写锁 -- 排它锁(Exclusive),简写为 X 锁,又称写锁。 -- 共享锁(Shared),简写为 S 锁,又称读锁。 +> ❓ 问题: +> +> - 什么是读写锁? -有以下两个规定: +- 独享锁(Exclusive),简写为 X 锁,又称写锁。 +- 共享锁(Shared),简写为 S 锁,又称读锁 -- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。 -- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。 +写锁和读锁的关系,简言之:同一时刻,针对同一数据,只要有一个事务在进行写操作,其他事务就不能做任何操作。 -锁的兼容关系如下: +使用方式: -| - | X | S | -| :-: | :-: | :-: | -| X | NO | NO | -| S | NO | YES | - -使用: - -- 排他锁:`SELECT ... FOR UPDATE;` +- 独享锁:`SELECT ... FOR UPDATE;` - 共享锁:`SELECT ... LOCK IN SHARE MODE;` -innodb 下的记录锁(也叫行锁),间隙锁,next-key 锁统统属于排他锁。 +`InnoDB` 下的行锁、间隙锁、next-key 锁统统属于独享锁。 -在 InnoDB 中,行锁是通过给索引上的索引项加锁来实现的。如果没有索引,InnoDB 将会通过隐藏的聚簇索引来对记录加锁。另外,根据针对 sql 语句检索条件的不同,加锁又有以下三种情形需要我们掌握。 - -1. Record lock:对索引项加锁。若没有索引项则使用表锁。 -2. Gap lock:对索引项之间的间隙加锁。 -3. Next-key lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。当利用范围条件而不是相等条件获取排他锁时,innoDB 会给符合条件的所有数据加锁。对于在条件范围内但是不存在的记录,叫做间隙。innoDB 也会对这个间隙进行加锁。另外,使用相等的检索条件时,若指定了本身不存在的记录作为检索条件的值的话,则此值对应的索引项也会加锁。 - -#### 1.5.2.3. 意向锁 +### 意向锁 使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。 @@ -363,118 +404,215 @@ innodb 下的记录锁(也叫行锁),间隙锁,next-key 锁统统属于 各种锁的兼容关系如下: -| - | X | IX | S | IS | -| :-: | :-: | :-: | :-: | :-: | -| X | NO | NO | NO | NO | -| IX | NO | YES | NO | YES | -| S | NO | NO | YES | YES | -| IS | NO | YES | YES | YES | +| - | X | IX | S | IS | +| :--: | :--: | :--: | :--: | :--: | +| X | ❌ | ❌ | ❌ | ❌ | +| IX | ❌ | ✔️ | ❌ | ✔️ | +| S | ❌ | ❌ | ✔️ | ✔️ | +| IS | ❌ | ✔️ | ✔️ | ✔️ | 解释如下: -- 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁; -- S 锁只与 S 锁和 IS 锁兼容,也就是说事务 T 想要对数据行加 S 锁,其它事务可以已经获得对表或者表中的行的 S 锁。 +- 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁; +- 这里兼容关系针对的是表级锁,而表级的 IX 锁和行级的 X 锁兼容,两个事务可以对两个数据行加 X 锁。(事务 T1 想要对数据行 R1 加 X 锁,事务 T2 想要对同一个表的数据行 R2 加 X 锁,两个事务都需要对该表加 IX 锁,但是 IX 锁是兼容的,并且 IX 锁与行级的 X 锁也是兼容的,因此两个事务都能加锁成功,对同一个表中的两个数据行做修改。) -意向锁是 InnoDB 自动加的,不需要用户干预。 +意向锁是 `InnoDB` 自动加的,不需要用户干预。 -## 6. 分库分表 +### MVCC -### 1.6.1. 为什么要分库分表? +> ❓ 常见问题: +> +> 什么是 MVCC? +> +> MVCC 有什么用? -分库分表的基本思想就要把一个数据库切分成多个部分放到不同的数据库(server)上,从而缓解单一数据库的性能问题。 +多版本并发控制(Multi-Version Concurrency Control, MVCC)是 `InnoDB` 存储引擎实现隔离级别的一种具体方式,**用于实现提交读和可重复读这两种隔离级别**。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 -分库分表一定是为了**支撑高并发、数据量大**两个问题的。 +#### 基本思想 -#### 1.6.1.1. 分表 +在数据库锁一节中提到,加锁能解决多个事务同时执行时出现的并发一致性问题。在实际场景中读操作往往多于写操作,因此又引入了读写锁来避免不必要的加锁操作,例如读和读没有互斥关系。读写锁中读和写操作仍然是互斥的,而 MVCC 利用了多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系,这一点和 CopyOnWrite 类似。 -比如你单表都几千万数据了,你确定你能扛住么?绝对不行,**单表数据量太大**,会极大影响你的 sql **执行的性能**,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。 +在 MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据行新增一个版本快照。 -分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。 +脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,这不算是脏读。 -#### 1.6.1.2. 分库 +#### 版本号 -分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。 +- 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 +- 事务版本号 TRX_ID :事务开始时的系统版本号。 -这就是所谓的**分库分表**,为啥要分库分表?你明白了吧。 +#### Undo 日志 + +MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。 + +例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。 + +```sql +INSERT INTO t(id, x) VALUES(1, "a"); +UPDATE t SET x="b" WHERE id=1; +UPDATE t SET x="c" WHERE id=1; +``` + +因为没有使用 `START TRANSACTION` 将上面的操作当成一个事务来执行,根据 MySQL 的 `AUTOCOMMIT` 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。 + +`INSERT`、`UPDATE`、`DELETE` 操作会创建一个日志,并将事务版本号 `TRX_ID` 写入。`DELETE` 可以看成是一个特殊的 `UPDATE`,还会额外将 DEL 字段设置为 1。 + +#### ReadView + +MVCC 维护了一个 `ReadView` 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, ...},还有该列表的最小值 `TRX_ID_MIN` 和 `TRX_ID_MAX`。 + +在进行 `SELECT` 操作时,根据数据行快照的 `TRX_ID` 与 `TRX_ID_MIN` 和 `TRX_ID_MAX` 之间的关系,从而判断数据行快照是否可以使用: + +- `TRX_ID` < `TRX_ID_MIN`,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。 +- `TRX_ID` > `TRX_ID_MAX`,表示该数据行快照是在事务启动之后被更改的,因此不可使用。 +- `TRX_ID_MIN` <= `TRX_ID` <= `TRX_ID_MAX`,需要根据隔离级别再进行判断: + - 提交读:如果 `TRX_ID` 在 `TRX_IDs` 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。 + - 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。 + +在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。 + +#### 快照读与当前读 + +##### 快照读 + +MVCC 的 SELECT 操作是快照中的数据,不需要进行加锁操作。 + +```sql +SELECT * FROM table ...; +``` + +##### 当前读 + +MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。 + +```sql +INSERT; +UPDATE; +DELETE; +``` + +在进行 SELECT 操作时,可以强制指定进行加锁操作。以下第一个语句需要加 S 锁,第二个需要加 X 锁。 + +```sql +SELECT * FROM table WHERE ? lock in share mode; +SELECT * FROM table WHERE ? for update; +``` + +### Next-key 锁 + +Next-Key 锁是 MySQL 的 `InnoDB` 存储引擎的一种锁实现。 + +MVCC 不能解决幻影读问题,Next-Key 锁就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 **MVCC + Next-Key 锁** 可以解决幻读问题。 + +另外,根据针对 SQL 语句检索条件的不同,加锁又有以下三种情形需要我们掌握。 + +- `Record Lock` - **行锁对索引项加锁,若没有索引则使用表锁**。 +- `Gap Lock` - 对索引项之间的间隙加锁。锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。`SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;` +- `Next-key lock` -它是 `Record Lock` 和 `Gap Lock` 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。它锁定一个前开后闭区间,例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间: + +索引分为主键索引和非主键索引两种,如果一条 SQL 语句操作了主键索引,MySQL 就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL 会先锁定该非主键索引,再锁定相关的主键索引。在 `UPDATE`、`DELETE` 操作时,MySQL 不仅锁定 `WHERE` 条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的 `next-key lock`。 + +当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。发生死锁后,`InnoDB` 一般都可以检测到,并使一个事务释放锁回退,另一个获取锁完成事务。 + +## 六、分库分表 + +### 什么是分库分表 + +> ❓ 常见问题: +> +> 什么是分库分表?什么是垂直拆分?什么是水平拆分?什么是 Sharding? +> +> 分库分表是为了解决什么问题? +> +> 分库分表有什么优点? +> +> 分库分表有什么策略? + +分库分表的基本思想就是:把原本完整的数据切分成多个部分,放到不同的数据库或表上。 + +分库分表一定是为了支撑 **高并发、数据量大**两个问题的。 + +#### 垂直切分 + +> **垂直切分**,是 **把一个有很多字段的表给拆分成多个表,或者是多个库上去**。一般来说,会 **将较少的、访问频率较高的字段放到一个表里去**,然后 **将较多的、访问频率较低的字段放到另外一个表里去**。因为数据库是有缓存的,访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。 + +![image-20200114211639899](http://dunwu.test.upcdn.net/snap/image-20200114211639899.png) + +一般来说,满足下面的条件就可以考虑扩容了: + +- Mysql 单库超过 5000 万条记录,Oracle 单库超过 1 亿条记录,DB 压力就很大。 +- 单库超过每秒 2000 个并发时,而一个健康的单库最好保持在每秒 1000 个并发左右,不要太大。 + +在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。 + +#### 水平拆分 + +> **水平拆分** 又称为 **Sharding**,它是将同一个表中的记录拆分到多个结构相同的表中。当 **单表数据量太大** 时,会极大影响 **SQL 执行的性能** 。分表是将原来一张表的数据分布到数据库集群的不同节点上,从而缓解单点的压力。 + +![image-20200114211203589](http://dunwu.test.upcdn.net/snap/image-20200114211203589.png) + +一般来说,**单表有 200 万条数据** 的时候,性能就会相对差一些了,需要考虑分表了。但是,这也要视具体情况而定,可能是 100 万条,也可能是 500 万条,SQL 越复杂,就最好让单表行数越少。 + +#### 分库分表的优点 | # | 分库分表前 | 分库分表后 | | ------------ | ---------------------------- | -------------------------------------------- | -| 并发支撑情况 | MySQL 单机部署,扛不住高并发 | MySQL 从单机到多机,能承受的并发增加了多倍 | -| 磁盘使用情况 | MySQL 单机磁盘容量几乎撑满 | 拆分为多个库,数据库服务器磁盘使用率大大降低 | +| 并发支撑情况 | 单机部署,扛不住高并发 | 从单机到多机,能承受的并发增加了多倍 | +| 磁盘使用情况 | 单机磁盘容量几乎撑满 | 拆分为多个库,数据库服务器磁盘使用率大大降低 | | SQL 执行性能 | 单表数据量太大,SQL 越跑越慢 | 单表数据量减少,SQL 执行效率明显提升 | -### 1.6.2. 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点? +#### 分库分表策略 -这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。 +- 哈希取模:`hash(key) % N`。 + - 优点:可以平均分配每个库的数据量和请求压力(负载均衡)。 + - 缺点:扩容麻烦,需要数据迁移。 +- 范围:可以按照 ID 或时间划分范围。 + - 优点:扩容简单。 + - 缺点:这种策略容易产生热点问题。 +- 映射表:使用单独的一个数据库来存储映射关系。 + - 缺点:存储映射关系的数据库也可能成为性能瓶颈,且一旦宕机,分库分表的数据库就无法工作。所以不建议使用这种策略。 + - 优点:扩容简单,可以解决分布式 ID 问题。 -比较常见的包括: +### 分库分表中间件 -- cobar -- TDDL -- atlas -- sharding-jdbc -- mycat +> ❓ 常见问题: +> +> - 你用过哪些分库分表中间件,简单介绍一下? +> +> - 不同的分库分表中间件各自有什么特性,有什么优缺点? +> +> - 分库分表中间件技术如何选型? -#### 1.6.2.1. cobar +#### 常见的分库分表中间件 -阿里 b2b 团队开发和开源的,属于 proxy 层方案,就是介于应用服务器和数据库服务器之间。应用程序通过 JDBC 驱动访问 cobar 集群,cobar 根据 SQL 和分库规则对 SQL 做分解,然后分发到 MySQL 集群不同的数据库实例上执行。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。 +- [Cobar](https://github.com/alibaba/cobar) - 阿里 b2b 团队开发和开源的,属于 proxy 层方案,就是介于应用服务器和数据库服务器之间。应用程序通过 JDBC 驱动访问 cobar 集群,cobar 根据 SQL 和分库规则对 SQL 做分解,然后分发到 MySQL 集群不同的数据库实例上执行。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。 +- [TDDL](https://github.com/alibaba/tb_tddl) - 淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。 +- [Atlas](https://github.com/Qihoo360/Atlas) - 360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。 +- [sharding-jdbc](https://github.com/dangdangdotcom/sharding-jdbc) - 当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也**可以选择的方案**。 +- [Mycat](http://www.mycat.org.cn/) - 基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。 -#### 1.6.2.2. TDDL +#### 分库分表中间件技术选型 -淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。 +建议使用的是 sharding-jdbc 和 mycat。 -#### 1.6.2.3. atlas +- [sharding-jdbc](https://github.com/dangdangdotcom/sharding-jdbc) 这种 client 层方案的**优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高**,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要**耦合** sharding-jdbc 的依赖。其本质上通过配置多数据源,然后根据设定的分库分表策略,计算路由,将请求发送到计算得到的节点上。 -360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。 - -#### 1.6.2.4. sharding-jdbc - -当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也**可以选择的方案**。 - -#### 1.6.2.5. mycat - -基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。 - -#### 1.6.2.6. 总结 - -综上,现在其实建议考量的,就是 sharding-jdbc 和 mycat,这两个都可以去考虑使用。 - -sharding-jdbc 这种 client 层方案的**优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高**,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要**耦合** sharding-jdbc 的依赖; - -mycat 这种 proxy 层方案的**缺点在于需要部署**,自己运维一套中间件,运维成本高,但是**好处在于对于各个项目是透明的**,如果遇到升级之类的都是自己中间件那里搞就行了。 +- [Mycat](http://www.mycat.org.cn/) 这种 proxy 层方案的**缺点在于需要部署**,自己运维一套中间件,运维成本高,但是**好处在于对于各个项目是透明的**,如果遇到升级之类的都是自己中间件那里搞就行了。 通常来说,这两个方案其实都可以选用,但是我个人建议中小型公司选用 sharding-jdbc,client 层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;但是中大型公司最好还是选用 mycat 这类 proxy 层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护 mycat,然后大量项目直接透明使用即可。 -### 1.6.3. 你们具体是如何对数据库如何进行垂直拆分或水平拆分的? +### 分库分表的问题 -**水平拆分**的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。 +> ❓ 常见问题: +> +> - 分库分表的常见问题有哪些? +> +> +> - 你是如何解决分库分表的问题的? +> +> 下文一一讲解常见分库分表的问题及解决方案。 -
- -**垂直拆分**的意思,就是**把一个有很多字段的表给拆分成多个表**,**或者是多个库上去**。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会**将较少的访问频率很高的字段放到一个表里去**,然后**将较多的访问频率很低的字段放到另外一个表里去**。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。 - -
- -这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。 - -还有**表层面的拆分**,就是分表,将一个表变成 N 个表,就是**让每个表的数据量控制在一定范围内**,保证 SQL 的性能。否则单表数据量越大,SQL 性能就越差。一般是 200 万行左右,不要太多,但是也得看具体你怎么操作,也可能是 500 万,或者是 100 万。你的 SQL 越复杂,就最好让单表行数越少。 - -好了,无论分库还是分表,上面说的那些数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,**中间件可以根据你指定的某个字段值**,比如说 userid,**自动路由到对应的库上去,然后再自动路由到对应的表里去**。 - -你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都 ok 了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。 - -而且这儿还有两种**分库分表的方式**: - -- 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如**时间范围**来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。 -- 或者是按照某个字段 hash 一下均匀分散,这个较为常用。 - -range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。 - -hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。 - -### 1.6.4. 分库分表的常见问题以及解决方案? - -#### 1.6.4.1. 事务问题 +#### 事务问题 方案一:使用分布式事务 @@ -487,17 +625,23 @@ hash 分发,好处在于说,可以平均分配每个库的数据量和请求 - 优点:性能上有优势 - 缺点:需要应用程序在事务控制上做灵活设计。如果使用了 spring 的事务管理,改动起来会面临一定的困难。 -#### 1.6.4.2. 跨节点 Join 的问题 +#### 跨节点 Join 只要是进行切分,跨节点 Join 的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的 id,根据这些 id 发起第二次请求得到关联数据。 -#### 1.6.4.3. 跨节点的 count,order by,group by 以及聚合函数问题 +#### 跨节点的 count,order by,group by 以及聚合函数 这些是一类问题,因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。 解决方案:与解决跨节点 join 问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和 join 不同的是每个节点的查询可以并行执行,因此很多时候它的速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。 -#### 1.6.4.4. ID 唯一性 +业务角度上的解决方案: + +- 如果是在前台应用提供分页,则限定用户只能看前面 n 页,这个限制在业务上也是合理的,一般看后面的分页意义不大(如果一定要看,可以要求用户缩小范围重新查询)。 +- 如果是后台批处理任务要求分批获取数据,则可以加大 page size,比如每次获取 5000 条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。 +- 分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。 + +#### ID 唯一性 一旦数据库被切分到多个物理节点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的 ID 无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得 ID,以便进行 SQL 路由。 @@ -507,45 +651,29 @@ hash 分发,好处在于说,可以平均分配每个库的数据量和请求 - 为每个分片指定一个 ID 范围。 - 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)。 -#### 1.6.4.5. 数据迁移,容量规划,扩容等问题 +#### 数据迁移,容量规划,扩容等问题 来自淘宝综合业务平台团队,它利用对 2 的倍数取余具有向前兼容的特性(如对 4 取余得 1 的数对 2 取余也是 1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了 Sharding 扩容的难度。 -#### 1.6.4.6. 分库数量 +## 七、集群 -分库数量首先和单库能处理的记录数有关,一般来说,Mysql 单库超过 5000 万条记录,Oracle 单库超过 1 亿条记录,DB 压力就很大(当然处理能力和字段数量/访问模式/记录长度有进一步关系)。 +> 这个专题需要根据熟悉哪个数据库而定,但是主流、成熟的数据库都会实现一些基本功能,只是实现方式、策略上有所差异。由于本人较为熟悉 Mysql,所以下面主要介绍 Mysql 系统架构问题。 -#### 1.6.4.7. 跨分片的排序分页 +### 复制机制 -- 如果是在前台应用提供分页,则限定用户只能看前面 n 页,这个限制在业务上也是合理的,一般看后面的分页意义不大(如果一定要看,可以要求用户缩小范围重新查询)。 -- 如果是后台批处理任务要求分批获取数据,则可以加大 page size,比如每次获取 5000 条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。 -- 分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。 +Mysql 支持两种复制:基于行的复制和基于语句的复制。 -### 1.6.5. 如何设计可以动态扩容缩容的分库分表方案? +这两种方式都是在主库上记录二进制日志(binlog),然后在从库上以异步方式更新主库上的日志记录。这意味着:复制过程存在时延,这段时间内,主从数据可能不一致(即最终一致性)。 -### 1.6.6. 有哪些分库分表中间件?各自有什么优缺点?底层实现原理? +主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。 -#### 1.6.6.1. 简单易用的组件: +- **binlog 线程** :负责将主服务器上的数据更改写入二进制文件(binlog)中。 +- **I/O 线程** :负责从主服务器上读取二进制日志文件,并写入从服务器的日志中。 +- **SQL 线程** :负责读取日志并执行 SQL 语句以更新数据。 -- [当当 sharding-jdbc](https://github.com/dangdangdotcom/sharding-jdbc) -- [蘑菇街 TSharding](https://github.com/baihui212/tsharding) +![img](http://dunwu.test.upcdn.net/cs/database/mysql/master-slave.png) -#### 1.6.6.2. 强悍重量级的中间件: - -- [sharding ](https://github.com/go-pg/sharding) -- [TDDL Smart Client 的方式(淘宝)](https://github.com/alibaba/tb_tddl) -- [Atlas(Qihoo 360)](https://github.com/Qihoo360/Atlas) -- [alibaba.cobar(是阿里巴巴(B2B)部门开发)](https://github.com/alibaba/cobar) -- [MyCAT(基于阿里开源的 Cobar 产品而研发)](http://www.mycat.org.cn/) -- [Oceanus(58 同城数据库中间件)](https://github.com/58code/Oceanus) -- [OneProxy(支付宝首席架构师楼方鑫开发)](http://www.cnblogs.com/youge-OneSQL/articles/4208583.html) -- [vitess(谷歌开发的数据库中间件)](https://github.com/youtube/vitess) - -## 7. 数据库架构设计 - -### 1.7.1. 高并发系统数据层面如何设计? - -#### 1.7.1.1. 读写分离的原理 +### 读写分离 主服务器用来处理写操作以及实时性要求比较高的读操作,而从服务器用来处理读操作。 @@ -554,29 +682,17 @@ hash 分发,好处在于说,可以平均分配每个库的数据量和请求 MySQL 读写分离能提高性能的原因在于: - 主从服务器负责各自的读和写,极大程度缓解了锁的争用; -- 从服务器可以配置 MyISAM 引擎,提升查询性能以及节约系统开销; +- 从服务器可以配置 `MyISAM` 引擎,提升查询性能以及节约系统开销; - 增加冗余,提高可用性。 -
- -
+![img](http://dunwu.test.upcdn.net/cs/database/mysql/master-slave-proxy.png) -#### 1.7.1.2. 垂直切分 -按照业务线或功能模块拆分为不同数据库。 -更进一步是服务化改造,将强耦合的系统拆分为多个服务。 - -#### 1.7.1.3. 水平切分 - -- 哈希取模:hash(key) % NUM_DB -- 范围:可以是 ID 范围也可以是时间范围 -- 映射表:使用单独的一个数据库来存储映射关系 - -## 8. 参考资料 +## 参考资料 [数据库面试题(开发者必看)](https://juejin.im/post/5a9ca0d6518825555c1d1acd) -## 9. :door: 传送门 +## :door: 传送门 | [我的 Github 博客](https://github.com/dunwu/blog) | [db-tutorial 首页](https://github.com/dunwu/db-tutorial) | diff --git a/prettier.config.js b/prettier.config.js index eb6bb1f..b9914d2 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -3,5 +3,7 @@ * @see https://prettier.io/docs/en/configuration.html */ module.exports = { - tabWidth: 2, semi: false, singleQuote: true + tabWidth: 2, + semi: false, + singleQuote: true }