├── README.md ├── cs-basics ├── consensus │ ├── gossip.md │ ├── pbft.md │ └── raft.md ├── network │ └── network.pdf └── operating-system │ └── os.pdf ├── database ├── mysql │ └── base.md └── redis │ └── base.md ├── golang ├── deep │ ├── channel.md │ ├── context.md │ ├── gc.md │ ├── gmp.md │ ├── interface.md │ ├── map.md │ ├── memory_distribution.md │ ├── reflect.md │ ├── slice.md │ └── unsafe.md └── useful_package.md ├── system-design └── security │ ├── advantages&disadvantages-of-jwt.md │ ├── basis-of-authority-certification.md │ ├── casbin-intro.md │ ├── images │ ├── jwt.drawio │ ├── jwt.svg │ ├── session-cookie.drawio │ ├── session-cookie.svg │ ├── sso.drawio │ └── sso.svg │ ├── jwt-intro.md │ └── sso-intro.md └── tools ├── docker ├── docker-compose.md └── docker.md └── git-intro.md /README.md: -------------------------------------------------------------------------------- 1 | # GolangGuide 无__忧😃 2 | 汇总了一些关于Golang相关的资料提供查看,后续会继续补充完善,欢迎大家star~:smiley: 3 | 4 | **感谢:公众号【 脑子进煎鱼了】,【码农桃花源】,【小林coding】以及JavaGuide(github Guide哥)** 5 | 6 | | Golang | 计算机基础 | 数据库 | 开发框架 | 中间件 | 微服务 | 系统设计 | 开发工具 | 7 | | :----------: | :------------: | :--------: | :----------: | :------------: | :--------: | :-----------: | :--------: | 8 | | [📝](#Golang) | [💻](#计算机基础) | [💾](#数据库) | [🔲](#开发框架) | [✉️](#中间件) | [🎰](#微服务) | [🔬](#系统设计) | [🔧](#开发工具) | 9 | 10 | 11 | 12 | ## Golang 📝 13 | ### 面试题 14 | 15 | 1. [Go 面试题: new 和 make 是什么,差异在哪?](https://mp.weixin.qq.com/s/tZg3zmESlLmefAWdTR96Tg) 16 | 2. [Go 群友提问:Goroutine 数量控制在多少合适,会影响 GC 和调度?](https://mp.weixin.qq.com/s/uWP2X6iFu7BtwjIv5H55vw) 17 | 3. [Go 群友提问:学习 defer 时很懵逼,这道不会做!](https://mp.weixin.qq.com/s/lELMqKho003h0gfKkZxhHQ) 18 | 4. [Go 面试题:Go interface 的一个 “坑” 及原理分析](https://mp.weixin.qq.com/s/vNACbdSDxC9S0LOAr7ngLQ) 19 | 5. [Go 群友提问:进程、线程都有 ID,为什么 Goroutine 没有 ID?](https://mp.weixin.qq.com/s/qFAtgpbAsHSPVLuo3PYIhg) 20 | 6. [ Go 面试题:GMP 模型,为什么要有 P?](https://mp.weixin.qq.com/s/an7dml9NLOhqOZjEGLdEEw) 21 | 7. [Go 面试题:Go 结构体是否可以比较,为什么?](https://mp.weixin.qq.com/s/HScH6nm3xf4POXVk774jUA) 22 | 8. [Go 面试题:单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?](https://mp.weixin.qq.com/s/h27GXmfGYVLHRG3Mu_8axw) 23 | 9. [Go 群友提问:你知道 Go 结构体和结构体指针调用有什么区别吗?](https://mp.weixin.qq.com/s/g-D_eVh-8JaIoRne09bJ3Q) 24 | 10. [跟读者聊 Goroutine 泄露的 N 种方法](https://mp.weixin.qq.com/s/ql01K1nOnEZpdbp--6EDYw) 25 | 11. [详解 Go 程序的启动流程,你知道 g0,m0 是什么吗?](https://mp.weixin.qq.com/s/YK-TD3bZGEgqC0j-8U6VkQ) 26 | 12. [用 Go struct 不能犯的一个低级错误!](https://mp.weixin.qq.com/s/K5B2ItkzOb4eCFLxZI5Wvw) 27 | 13. [嗯,你觉得 Go 在什么时候会抢占 P?](https://mp.weixin.qq.com/s/WAPogwLJ2BZvrquoKTQXzg) 28 | 14. [Go 面试官:什么是协程,协程和线程的区别和联系?](https://mp.weixin.qq.com/s/vW5n_JWa3I-Qopbx4TmIgQ) 29 | 15. [用 Go map 要注意这 1 个细节,避免依赖他!](https://mp.weixin.qq.com/s/MzAktbjNyZD0xRVTPRKHpw) 30 | 16. [为什么 Go map 和 slice 是非线性安全的?](https://mp.weixin.qq.com/s/TzHvDdtfp0FZ9y1ndqeCRw) 31 | 17. [一口气搞懂 Go sync.map 所有知识点](https://mp.weixin.qq.com/s/8aufz1IzElaYR43ccuwMyA) 32 | 18. [Go 面试官问我如何实现面向对象?](https://mp.weixin.qq.com/s/2x4Sajv7HkAjWFPe4oD96g) 33 | 19. [Go 是传值还是传引用?](https://mp.weixin.qq.com/s/qsxvfiyZfRCtgTymO9LBZQ) 34 | 20. [回答我,停止 Goroutine 有几种方法?](https://mp.weixin.qq.com/s/tN8Q1GRmphZyAuaHrkYFEg) 35 | 36 | ### 深度解析 37 | 38 | 1. [Go语言深度解析之slice](golang/deep/slice.md) 39 | 2. [Go语言深度解析之map](golang/deep/map.md) 40 | 3. [Go语言深度解析之channel](golang/deep/channel.md) 41 | 4. [Go语言深度解析之context](golang/deep/context.md) 42 | 5. [Go语言深度解析之unsafe](golang/deep/unsafe.md) 43 | 6. [Go语言深度解析之interface](golang/deep/interface.md) 44 | 7. [Go语言深度解析之reflect](golang/deep/reflect.md) 45 | 8. [Go语言深度解析之内存分配](golang/deep/memory_distribution.md) 46 | 9. [Go语言深度解析之垃圾回收机制](golang/deep/gc.md) 47 | 10. [Go语言深度解析之GPM调度器](golang/deep/gmp.md) 48 | 49 | 50 | ## 计算机基础 💻 51 | ### 操作系统 52 | 53 | - [图解操作系统](cs-basics/operating-system/os.pdf) 54 | 55 | ### 网络 56 | 57 | - [图解计算机网络](cs-basics/network/network.pdf) 58 | 59 | ### 算法 ⌛️ 60 | 61 | - [《剑指offer》](https://leetcode-cn.com/study-plan/lcof/) 62 | - 常见共识算法 63 | - [Raft协议](cs-basics/consensus/raft.md) 64 | - [PBFT协议](cs-basics/consensus/pbft.md) 65 | - [Gossip协议](cs-basics/consensus/gossip.md) 66 | 67 | ## 数据库 💾 68 | ### MySQL 69 | 70 | - [MySQL基础](database/mysql/base.md) 71 | - [图解MySQL](https://www.xiaolincoding.com/mysql/) 72 | 73 | ### Redis 74 | 75 | - [Redis基础](middleware/redis/base.md) 76 | - [图解Redis](https://www.xiaolincoding.com/redis/) 77 | 78 | ## 开发框架 🔲 79 | ### Gin 80 | 81 | - [Go Gin 系列一:Go 介绍与环境安装](https://mp.weixin.qq.coam/s?__biz=MzUxMDI4MDc1NA==&mid=2247483714&idx=1&sn=0b536199884cb45a1316c77998895baf&chksm=f904141fce739d0978e02147507dc29fadee2e19ac312d34a3190062ae40e62a490fc58df6ae&scene=178&cur_album_id=1383459655464337409#rd) 82 | - [Go Gin 系列二:初始化项目及公共库](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483807&idx=1&sn=9c7aede4f675f2de49ddc08ab1a95a71&chksm=f90414c2ce739dd4b8711c0043286fba9744b8d9c86c75c7ac7750d28cd2fed43f749eb5de99&scene=178&cur_album_id=1383459655464337409#rd) 83 | - [Go Gin 系列三:开发标签模块](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483807&idx=2&sn=513f8e5620db9cc37fea62fe6ff69796&chksm=f90414c2ce739dd4ccc217360b50618c085ec2327e4149dfbc1d136566ef6543dadd80b1e20e&scene=178&cur_album_id=1383459655464337409#rd) 84 | - [Go Gin 系列四:开发文章模块](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483807&idx=3&sn=d24c23a03579f9ab662826c15174e3f4&chksm=f90414c2ce739dd42a4829099cc1229b51f4770d887f55a5995c584d0015d32fc8b9fe16d751&scene=178&cur_album_id=1383459655464337409#rd) 85 | - [Go Gin 系列五:使用 JWT 进行身份校验](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483807&idx=4&sn=fae0d5ec098860038bb4de5c45d5d624&chksm=f90414c2ce739dd4b6fb2356afef5304057a49cbf527951000da107456ad07e87d1e69b32370&scene=178&cur_album_id=1383459655464337409#rd) 86 | - [Go Gin 系列六:编写一个简单的文件日志](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483807&idx=5&sn=dbfc85b5a612a364f323de4703ae98ec&chksm=f90414c2ce739dd484a2c0583c424e59104809da9304ad8d23a85d9b4ff42f3e7d7f146d3930&scene=178&cur_album_id=1383459655464337409#rd) 87 | - [Go Gin 系列七:优雅的重启服务](https://github.com/gravityblast/fresh) 88 | - [Go Gin 系列八:为它加上Swagger](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483807&idx=7&sn=b73f0fd0ee14cdb43bc28ab6cb7c5644&chksm=f90414c2ce739dd43173eaec770dba45e04417a0849b676a0fa12af8e45a3db69e19eab3ab04&scene=178&cur_album_id=1383459655464337409#rd) 89 | - [Go Gin 系列九:将Golang应用部署到Docker](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483807&idx=8&sn=b2827c18847397e6d1d37bfe49b2065f&chksm=f90414c2ce739dd4061203ea791b35846a3ecb0aa40680783676fb3e4a39115bda9abe14fbf0&scene=178&cur_album_id=1383459655464337409#rd) 90 | - [Go Gin 系列十:定制 GORM Callbacks](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483819&idx=1&sn=90a68030b7d3f40b5ccfb9f91ce571d7&chksm=f90414f6ce739de092938728fe189e8d7b490aecaa19dddaa2c1c43eab971df29df0c37aa04a&scene=178&cur_album_id=1383459655464337409#rd) 91 | - [Go Gin 系列十一:Cron定时任务](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483819&idx=2&sn=a85e39912a709d22dc3529ea9bdc3322&chksm=f90414f6ce739de02d20484b3368476a4ecf19c0b1e38f8e263703432af3c1776365d096c12e&scene=178&cur_album_id=1383459655464337409#rd) 92 | - [Go Gin 系列十二:优化配置结构及实现图片上传](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483819&idx=3&sn=e76373b6bd530a552f08472d4987854e&chksm=f90414f6ce739de07dc82412e9c7e684a5058921253d1541b58e6ae205301be2fe782df9d6d6&scene=178&cur_album_id=1383459655464337409#rd) 93 | - [Go Gin 系列十三:优化应用结构和实现Redis缓存](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483819&idx=4&sn=e6f85aa6196688198f3514e1efbbbeca&chksm=f90414f6ce739de0570a358c84023373a4021ed9e74bbaf7eec7c931e61a2c6c292bacae399d&scene=178&cur_album_id=1383459655464337409#rd) 94 | - [Go Gin 系列十四:实现导出、导入 Excel](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483819&idx=5&sn=780affae40072df28ae6f6e4e226fdd8&chksm=f90414f6ce739de08b373523ea53b11575c64fd2db8ee04ee9237b9d24c93c8f6a0153918afd&scene=178&cur_album_id=1383459655464337409#rd) 95 | - [Go Gin 系列十五:生成二维码、合并海报](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483819&idx=6&sn=57f8d9031249f61d039477b11d62612f&chksm=f90414f6ce739de0e0c36a5ad3784e2ebd82e7a8941805d162dbd660e54fe169cd87573b7f34&scene=178&cur_album_id=1383459655464337409#rd) 96 | - [Go Gin 系列十六:在图片上绘制文字](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483819&idx=7&sn=1929b2cf09de3ec6222281def551a901&chksm=f90414f6ce739de04400958b1f4aebbd331715914b03efad26204ac8ba284d59d89f3af86099&scene=178&cur_album_id=1383459655464337409#rd) 97 | - [Go Gin 系列十七:用Nginx部署Go应用](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483819&idx=8&sn=c64f86744121ba7f4c2f7b8539de8b7d&chksm=f90414f6ce739de012bbdef88a31e18332a21ea24ba773ef676d5d1ba7e3ab7ebed941aca5c1&scene=178&cur_album_id=1383459655464337409#rd) 98 | 99 | ## 中间件 ✉️ 100 | ### Kafka 101 | - ... 102 | 103 | ### ElasticSearch 104 | - ... 105 | 106 | ## 微服务 🎰 107 | ### gRPC 108 | 109 | - [gRPC及相关介绍](https://mp.weixin.qq.com/s/bbHqWqtmk_k3-X_1XEDEJw) 110 | - [gRPC Client and Server](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483721&idx=2&sn=5fab143b3cd50209fafc658aaba7c0e9&chksm=f9041414ce739d023611ac6ff38dbfe81d48591ab24ba37eefb3fe6cb121e89dd46fa2fbb1a9&cur_album_id=1383472721040064512&scene=189#rd) 111 | - [gRPC Streaming, Client and Server](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483721&idx=3&sn=b61db0379afd96e0149c279564d8efea&chksm=f9041414ce739d02c1554318a6e86942a0450266f27360913882860f24bc59268d315142f79b&cur_album_id=1383472721040064512&scene=189#rd) 112 | - [gRPC TLS 证书认证](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483719&idx=1&sn=34b3a6a6fd63106a4c369b3a0eaef330&chksm=f904141ace739d0cc5ecd1f40ed03688934a380fd5006ffd10947e45638277b0fcd197ab7ff8&scene=178&cur_album_id=1383472721040064512#rd) 113 | - [gRPC 基于 CA 的 TLS 证书认证](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483719&idx=2&sn=e8208b347f8a38c98fd4f5986bd0df4a&chksm=f904141ace739d0c7106280b5332832353cd1022204089ab46bab6c5b95c8687f34c13a755b3&scene=178&cur_album_id=1383472721040064512#rd) 114 | - [gRPC Unary and Stream interceptor](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483718&idx=1&sn=ae0f6ea8111e7e9aeb152a247a333e68&chksm=f904141bce739d0dac96d1e3276fa141069681740a95c390b7c965f4381a14934075aa01d1c3&cur_album_id=1383472721040064512&scene=189#rd) 115 | - [让你的服务同时提供 HTTP 接口](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483718&idx=2&sn=0e592098eb5c1a837db12387fafe5f9c&chksm=f904141bce739d0d98ec188879258dd81c750a0404ba1a38b0c08610e3318b71fe65025e573c&cur_album_id=1383472721040064512&scene=189#rd) 116 | - [gRPC 对 RPC 方法做自定义认证](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483716&idx=1&sn=2b173c55cbe242cafda64a042b30669e&chksm=f9041419ce739d0fb6d4b210dd70962d96a72b1290d5138246c4b236b14ff1a57798f01969ae&cur_album_id=1383472721040064512&scene=189#rd) 117 | - [gRPC 超时控制](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483716&idx=2&sn=60a9d2e9c6a91c369aba0293e8bdb95b&chksm=f9041419ce739d0f0070b5e7bebeb112cd48ea86dbf9e36ad91ffe7943a944d85cf487ef0fb2&cur_album_id=1383472721040064512&scene=189#rd) 118 | - [gRPC + Zipkin 分布式链路追踪](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247483716&idx=3&sn=71c2f616b4bed0af7a6a914e1ee2c1df&chksm=f9041419ce739d0fc3839eaffa7d7075f3be8cda92df241bd3e0e961d7a93b9eafdbf33d2335&cur_album_id=1383472721040064512&scene=189#rd) 119 | - [总结:万字长文 | 从实践到原理,带你参透 gRPC](https://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247484984&idx=1&sn=392e258f24aec08f58c84ccaba96b2ae&chksm=f9041365ce739a73054b01edcf31fdf3590fb403b1b48aa7dbeccc74c568e5b0e8a4e838c65e&scene=178&cur_album_id=1383472721040064512#rd) 120 | 121 | ### ... 122 | 123 | ## 系统设计 🔬 124 | ### 安全 125 | #### 认证授权 126 | 127 | - [认证授权基础概念详解](system-design/security/basis-of-authority-certification.md) 128 | - [JWT基础概念详解及使用](system-design/security/jwt-intro.md) 129 | - [JWT优缺点分析以及常见问题解决方案](system-design/security/advantages%26disadvantages-of-jwt.md) 130 | - [SSO单点登录详解](system-design/security/sso-intro.md) 131 | - [Casbin访问控制详解及使用](system-design/security/casbin-intro.md) 132 | 133 | ## 开发工具 🔧 134 | ### git 135 | 136 | - [git入门](tools/git-intro.md) 137 | ### Docker 138 | 139 | - [docker介绍](tools/docker/docker.md) 140 | - [docker-compose介绍](tools/docker/docker-compose.md) 141 | 142 | ### Kubernetes 143 | - ... 144 | 145 | ### Golang常用第三方库 146 | 147 | - [常用第三方库](golang/useful_package.md) 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /cs-basics/consensus/gossip.md: -------------------------------------------------------------------------------- 1 | # Gossip协议详解 2 | 3 | ## 一.概述 4 | 5 | Gossip 协议适用于去中心化、容忍时延、读多写少的分布式集群场景。Gossip 过程由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。 6 | 7 | Gossip 协议是为了解决分布式环境下监控和事件通知的瓶颈。Gossip 协议中的每个 Agent 会利用 Gossip 协议互相检查在线状态,分担了服务器节点的心跳压力,通过 Gossip 广播的方式发送消息。 8 | 9 | Gossip 协议的最大的好处在于即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。这就允许 Consul 管理的集群规模能横向扩展到数千个节点 10 | 11 | ## 二.Gossip 的特点 12 | 13 | 1. **扩展性**:网络可以允许节点的任意增加和减少,新增加的节点的状态最终会与其他节点一致。 14 | 2. **容错**:网络中任何节点的宕机和重启都不会影响 Gossip 消息的传播,Gossip 协议具有天然的分布式系统容错特性。 15 | 3. **去中心化**:Gossip 协议不要求任何中心节点,所有节点都可以是对等的,任何一个节点无需知道整个网络状况,只要网络是连通的,任意一个节点就可以把消息散播到全网。 16 | 4. **一致性收敛**:Gossip 协议中的消息会以一传十、十传百一样的指数级速度在网络中快速传播,因此系统状态的不一致可以在很快的时间内收敛到一致。消息传播速度达到了 logN。 17 | 5. **简单**:Gossip 协议的过程极其简单,实现起来几乎没有太多复杂性。 18 | 19 | 20 | 21 | ## 三.Gossip 中的通信模式 22 | 23 | Gossip 协议中的两个节点(A、B)之间存在三种通信方式: 24 | 25 | - Push(A 最新): A 将数据 (key,value,version) 及对应的版本号 Push 给 B,B 更新本地数据。 26 | - Pull(B 最新):A 仅将数据 key, version 推送给 B,B 将本地比 A 新的数据(key, value, version)Push back 给 A,A 更新本地。 27 | - Push/Pull(A/B 一样新):比 Pull 多一步,A 更新本地后,再将数据 Push back B,B更新本地。 28 | 29 | 如果把两个节点数据同步一次定义为一个周期,则在一个周期内,Push 需通信 1 次,Pull 需 2 次,Push/Pull 则需 3 次。虽然消息数增加了,但从效果上来讲,Push/Pull 最好,理论上一个周期内可以使两个节点完全一致。 30 | 31 | 32 | 33 | ## 四.Gossip 的缺点 34 | 35 | - 消息延迟:由于 Gossip 协议中,节点只会随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网的,因此使用 Gossip 协议会造成不可避免的消息延迟。不适合用在对实时性要求较高的场景下。 36 | - 消息冗余:Gossip 协议规定,节点会定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤,因此就不可避免的存在消息重复发送给同一节点的情况,造成了消息的冗余,同时也增加了收到消息的节点的处理压力。而且,由于是定期发送,因此,即使收到了消息的节点还会反复收到重复消息,加重了消息的冗余。 -------------------------------------------------------------------------------- /cs-basics/consensus/pbft.md: -------------------------------------------------------------------------------- 1 | # PBFT实用拜占庭容错 2 | 3 | ## 一.概述 4 | 5 | 拜占庭将军问题最早是由 Leslie Lamport 在 1982 年发表的论文**《The Byzantine Generals Problem 》**提出的, 他证明了在将军总数大于 3f ,背叛者为f 或者更少时,忠诚的将军可以达成命令上的一致,即 3f+1<=n 。算法复杂度为 O(nf+1) 。而 Miguel Castro 和 Barbara Liskov 在1999年发表的论文**《 Practical Byzantine Fault Tolerance 》**中首次提出 PBFT算法,该算法容错数量也满足 `3f+1<=n`,也即最大的容错作恶节点数`f=(n-1)/3`。算法复杂度为 O(n2),将系统的复杂度由指数级别降低为多项式级别,使得拜占庭容错算法在实际系统应用中变得可行。 6 | 7 | **那么为什么PBFT算法的容错数量满足3f+1<=n呢?** 8 | 9 | 因为 PBFT 算法的除了需要支持容错故障节点之外,还需要支持**容错作恶节点**。假设集群节点数为 N,有问题的节点为 f。有问题的节点中,可以既是故障节点,也可以是作恶节点,或者只是故障节点或者只是作恶节点。那么会产生以下两种极端情况: 10 | 11 | 1. 这f 个有问题节点既是故障节点,又是作恶节点,那么根据少数服从多数的原则,集群里正常节点只需要比f个节点再多一个节点,即 f+1 个节点,确节点的数量就会比故障节点数量多,那么集群就能达成共识,即总节点数为f+(f+1)=n,也就是说这种情况支持的最大容错节点数量是 (n-1)/2。 12 | 2. 故障节点和作恶节点都是不同的节点。那么就会有 f 个作恶节点和 f 个故障节点,当发现节点是作恶节点后,会被集群排除在外,剩下 f 个故障节点,那么根据少数服从多数的原则,集群里正常节点只需要比f个节点再多一个节点,即 f+1 个节点,确节点的数量就会比故障节点数量多,那么集群就能达成共识。所以,所有类型的节点数量加起来就是 f+1 个正常节点,f个故障节点和f个作恶节点,即 3f+1=n。 13 | 14 | 结合上述两种情况,因此PBFT算法支持的最大容错节点数量是(n-1)/3。 15 | 16 | ## 二.PBFT共识算法流程 17 | 18 | **角色划分** 19 | 20 | - **Client:**客户端节点,负责发送交易请求。 21 | 22 | - **Primary**: 主节点,负责将交易打包成区块和区块共识,每轮共识过程中有且仅有一个Primary节点。 23 | 24 | - **Replica**: 副本节点,负责区块共识,每轮共识过程中有多个Replica节点,每个Replica节点的处理过程类似。 25 | 26 | 其中,Primary和Replica节点都属于共识节点。 27 | 28 | **算法流程** 29 | 30 | PBFT 算法的基本流程主要有以下四步: 31 | 32 | 1. 客户端发送请求给主节点 33 | 2. 主节点广播请求给其它节点,节点执行PBFT算法的**三阶段共识流程**。 34 | 3. 节点处理完三阶段流程后,返回消息给客户端。 35 | 4. 客户端收到来自 f+1 个节点的相同消息后,代表共识已经正确完成。 36 | 37 |  38 | 39 | 算法的核心三个阶段分别是 `pre-prepare` 阶段(预准备阶段),`prepare` 阶段(准备阶段), `commit` 阶段(提交阶段)。图中的C代表客户端,0,1,2,3 代表节点的编号,其中0 是主节点primary,打×的3代表可能是故障节点或者是作恶节点,这里表现的行为就是对其它节点的请求无响应。整个过程大致是如下: 40 | 41 | 首先,客户端向主节点0发起请求`<> `其中t是时间戳,o表示操作,c是这个client,主节点收到客户端请求,会向其它节点发送 pre-prepare 消息,其它节点就收到了pre-prepare 消息,就开始了这个核心三阶段共识过程了。 42 | 43 | - **Pre-prepare 阶段**:副本节点replica收到 pre-prepare 消息后,会有两种选择,一种是接受,一种是不接受。**什么时候才不接受主节点发来的 pre-prepare 消息呢?**一种典型的情况就是如果一个replica节点接受到了一条 pre-prepare 消息`<,m>`,其中,**v 代表视图编号(视图的编号是什么意思呢?比如当前主节点为 A,视图编号为 1,如果主节点换成 B,那么视图编号就为 2)**,**n代表序号(主节点收到客户端的每个请求都以一个编号来标记)**,**d代表消息摘要**,**m代表原始消息数据**。消息里的 v 和 n 在之前收到里的消息是曾经出现过的,但是 d 和 m 却和之前的消息不一致,或者请求编号n不在高低水位之间,这时候就会拒绝请求。拒绝的逻辑就是主节点不会发送两条具有相同的 v 和 n ,但 d 和 m 却不同的消息。 44 | 45 | Replia节点接收到pre-prepare消息,进行以下消息验证: 46 | 47 | 1. 消息m的签名合法性,并且消息摘要d和消息m相匹配:d=hash(m) 48 | 2. 节点当前处于视图v中 49 | 3. 节点当前在同一个(view v ,sequence n)上没有其它pre-prepare消息,即不存在另外一个m'和对应的d' ,d'=hash(m') 50 | 4. h<=n<=H,H和h代表序号n的高低水位。 51 | 52 | - **Prepare 阶段**:当前节点同意请求后会向其它节点发送 prepare 消息` `同时将消息记录到**log**中,其中i用于表示当前节点的身份。同一时刻不是只有一个节点在进行这个过程,可能有 n 个节点也在进行这个过程。因此节点是有可能收到其它节点发送的 prepare 消息的,当前节点i验证这些prepare消息和自己发出的prepare消息的v,n,d三个数据是否都是一致的。验证通过之后,当前节点i将prepared(m,v,n) 设置为true,**prepared(m,v,n) 代表共识节点认为在(v,n)中针对消息m的Prepare阶段是否已经完成**。在一定时间范围内,如果收到超过 **2f** 个其他节点的prepare 消息,就代表 prepare 阶段已经完成。最后共识节点i发送commit消息并进入Commit阶段。 53 | 54 | - **Commit 阶段**:当前节点i接收到2f个来自其他共识节点的commit消息``同时将该消息插入**log**中(算上自己的共有2f+1个),验证这些commit消息和自己发的commit消息的v,n,d三个数据都是一致后,共识节点将committed-local(m,v,n)设置为true,**committed-local(m,v,n)代表共识节点确定消息m已经在整个系统中得到至少2f+1个节点的共识,而这保证了至少有f+1个non-faulty节点已经对消息m达成共识**。于是节点就会执行请求,写入数据。 55 | 56 | 处理完毕后,节点会返回消息`<>`给客户端,当客户端收集到f+1个消息后,共识完成,这就是PBFT算法的全部流程。 57 | 58 | ## 三.垃圾回收 59 | 60 | 根据前面的算法部分可以发现,我们需要不断地往log中插入消息,在`view change`时恢复需要用到。于是log很快就会变得很占内存,这时候需要有一种方式清理掉无用的log。当某一request已经被f+1个正常节点执行完毕后,并当view change可以向其他节点证明当前状态的正确性,与该request相关的message就可以删除了。 61 | 62 | 每执行一个request就产生一次证明效率过于低下,论文中是每处理一定的request后产生一次证明。也就是当request的序号`n % C ( 某 一 定 值 ) =0`时,产生一个checkpoint,节点i多播消息`<>`给其他节点,当节点接收2f+1个消息时,该checkpoint变为stable checkpoint,也就是这2f+1个节点可以证明该状态的正确性,同时可以删除序号≤n的消息相关的log信息和checkpoint信息。 63 | 64 | **什么是 checkpoint 呢?** checkpoint 就是当前节点处理的**最新请求序号**。前文已经提到主节点收到请求是会给请求记录编号的。比如一个节点正在共识的一个请求编号是101,那么对于这个节点,它的 checkpoint 就是101。 65 | 66 | **什么是 stable checkpoint (稳定检查点)呢**?stable checkpoint 就是大部分节点 (2f+1个) 已经共识完成的最大请求序号。比如系统有 4 个节点,三个节点都已经共识完了的请求编号是 213 ,那么这个 213 就是 stable checkpoint 了,也就可以删除213 号之前的记录了。 67 | 68 | **什么是高低水位呢?**低水位就是stable checkpoint的序号n,高水位是stable checkpoint的序号n + K,其中K是定值,一般是C(上面提及到的某一定值)的整数倍。 69 | 70 | ## 四.视图更换(view change) 71 | 72 | 正常情况下,client将request发给一个主节点primary,然后主节点将request多播到其他节点replica,进行一个view。然而当**主节点出错或成为恶意节点**时,就需要进行`视图更换(view change)`,也就是选择(轮换法)下一个replica节点作为主节点,视图编号v进行+1操作,共识过程进入下一个view。 73 | 74 |  75 | 76 | 如图所示, view change 会有三个阶段,分别是 `view-change` , `view-change-ack` 和 `new-view `阶段。replica节点认为主节点primary有问题时,会向其它节点发送 view-change 消息`<> `其中: 77 | 78 | - v:上一个视图编号 79 | - n:节点i的stable checkpoint的编号 80 | - C:2f+1个节点的有效checkpoint信息的集合 81 | - P:节点i中的上一个视图中编号大于n并且达到prepared状态的请求消息的集合 82 | - i:节点的编号 83 | 84 | 当前存活的节点编号最小的节点将成为新的主节点。当新的主节点收到 2f 个其它节点的 view-change 消息,则证明有足够多人的节点认为主节点有问题,于是就会向其它节点广播 new-view 消息`<>` 85 | 86 | 其中: 87 | 88 | - v:上一个视图编号 89 | 90 | - V:新的主节点接收到的有效的视图编号为v+1的view-change消息集合 91 | - O:pre-prepare消息的集合。假设 O 集合里消息的编号范围:(min~max),则 Min 为 V 集合最小的 stable checkpoint , Max 为 V 集合中最大序号的 prepare 消息。最后一步执行 O 集合里的 pre-preapare 消息,每条消息会有两种情况: 如果 max-min>0,则产生消息 <> ;如果 max-min=0,则产生消息 <> 92 | 93 | 注意:replica节点不会发起 new-view 事件。对于主节点,发送 new-view 消息后会继续执行上个视图未处理完的请求,从 pre-prepare 阶段开始。其它节点验证 new-view 消息通过后,就会处理主节点发来的 pre-prepare 消息,这时执行的过程就是前面描述的PBFT过程。到这时,正式进入 v+1 (视图编号加1)的时代了。 94 | 95 | ## 五.优缺点 96 | 97 | ### **优点:** 98 | 99 | - 通信复杂度O(n2),解决了原始拜占庭容错(BFT)算法效率不高的问题,将算法复杂度由指数级降低到多项式级,使得拜占庭容错算法在实际系统应用中变得可行。 100 | - 首次提出在**异步网络环境下使用状态机副本复制协议**,该算法可以工作在异步环境中,并且通过优化在早期算法的基础上把响应性能提升了一个数量级以上。作者使用这个算法实现了拜占庭容错的网络文件系(NFS),性能测试证明了该系统仅比无副本复制的标准NFS慢了3%。 101 | - 使用了加密技术来防止欺骗攻击和重播攻击,以及检测被破坏的消息。消息包含了公钥签名(RSA算法)、消息验证编码(MAC)和无碰撞哈希函数生成的消息摘要(message digest)。 102 | 103 | ### **缺点:** 104 | 105 | - 仅仅适用于permissioned systems (联盟链/私有链)。 106 | - 通信复杂度过高,可拓展性比较低,一般的系统在达到100左右的节点个数时,性能下降非常快。 107 | - PBFT在网络不稳定的情况下延迟很高。 -------------------------------------------------------------------------------- /cs-basics/consensus/raft.md: -------------------------------------------------------------------------------- 1 | # Raft共识算法 2 | 3 | ## 一.背景 4 | 5 | 拜占庭将军问题是分布式领域**最复杂、最严格的容错模型**。但在日常工作中使用的分布式系统面对的问题不会那么复杂,更多的是计算机故障挂掉了,或者网络通信问题而没法传递信息,这种情况**不考虑计算机之间互相发送恶意信息**,极大简化了系统对容错的要求,最主要的是达到一致性。 6 | 7 | 所以将拜占庭将军问题根据常见的工作上的问题进行简化:**假设将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成一致性决定?** 8 | 9 | 对于这个简化后的问题,有许多解决方案,第一个被证明的共识算法是 Paxos,由拜占庭将军问题的作者 Leslie Lamport 在1990年提出,但因为 Paxos 难懂,难实现,所以斯坦福大学的教授Diego Ongaro 和 John Ousterhout在2014年发表了论文**《In Search of an Understandable Consensus Algorithm》**,其中提到了新的分布式协议 `Raft`。与 Paxos 相比,Raft 有着基本相同运行效率,但是更容易理解,也更容易被用在系统开发上。 10 | 11 | ## 二.概述 12 | 13 | Raft实现一致性的机制是这样的:首先选择一个leader全权负责管理日志复制,leader从客户端接收log entries(日志条目),将它们复制给集群中的其它机器,然后负责告诉其它机器什么时候将日志应用于它们的状态机。举个例子,leader可以在无需询问其它server的情况下决定把新entries放在哪个位置,数据永远是从leader流向其它机器(leader的强一致性)。一个leader可以fail或者与其他机器失去连接,这种情形下会有新的leader被选举出来。 14 | 15 | 在任何时刻,每个server节点有三种状态:`leader`,`candidate`,`follower`。 16 | 17 | - leader:作为客户端的接收者,接收客户端发送的日志复制请求,并将日志信息复制到 follower 节点中,维持网络各个节点的账本状态。 18 | - candidate:在leader 选举阶段存在的状态,通过任期号term和票数进行领导人身份竞争,获胜者将成为下一任期的领导人。 19 | - follower:作为leader 节点发送日志复制请求的接收者,与leader节点通信,接收账本信息,并确认账本信息的有效性,完成日志信息的提交和存储。 20 | 21 | 正常运行时,只有一个leader,其余全是follower。follower是被动的:它们不主动提出请求,只是响应leader和candidate的请求。leader负责处理所有客户端请求(如果客户端先连接某个follower,该follower要负责把它重定向到leader)。candidate状态用于选举领导节点。下图展示了这些状态以及它们之间的转化: 22 | 23 |  24 | 25 | Raft将时间分解成任意长度的`terms`,如下图所示: 26 | 27 |  28 | 29 | terms有连续单调递增的编号,每个term开始于选举,这一阶段每个candidate都试图成为leader。如果一个candidate选举成功,它就在该term剩余周期内履行leader职责。在某种情形下,可能出现选票分散,没有选出leader的情况,这时新的term立即开始。**Raft确保在任何term都只可能存在一个leader**。term在Raft用作逻辑时钟,servers可以利用term判断一些过时的信息:比如过时的leader。每台server都存储当前term号,它随时间单调递增。term号可以在任何server通信时改变:如果某个server节点的当前term号小于其它servers,那么这台server必须更新它的term号,保持一致;如果一个candidate或者leader发现自己的term过期,则降级成follower;如果某个server节点收到一个过时的请求(拥有过时的term号),它会拒绝该请求。 30 | 31 | Raft servers使用RPC交互,基本的一致性算法只需要两种RPC。`RequestVote RPCs`由candidate在选举阶段发起。`AppendEntries RPCs`在leader复制数据时发起,leader在和follower做心跳时也用该RPC。servers发起一个RPC,如果没得到响应,则需要不断重试。另外,发起RPC是并行的。 32 | 33 | ## 三.具体共识流程 34 | 35 | raft算法大致可以划分为两个阶段,即`Leader Selection`和`Log Relocation`,同时使用强一致性来减少需要考虑的状态。 36 | 37 | ### 3.1 Leader Selection 38 | 39 | Raft使用`heartbeat`(心跳机制)来触发选举。当server节点启动时,初始状态都是follower。每一个server都有一个定时器,超时时间为`election timeout`(**时间长度一般为150ms~300ms**),如果某server没有超时的情况下收到来自leader或者candidate的任何RPC,则定时器**重启**,如果超时,它就开始一次选举。leader给followers发RPC要么复制日志,要么就是用来告诉followers自己是leader,不用选举的心跳(告诉followers对状态机应用日志的消息夹杂在心跳中)。如果某个candidate获得了**超过半数**节点的选票(自己投了自己),它就赢得了选举成为新leader。 40 | 41 | 上述的具体过程如下: 42 | 43 | ➢ 初始状态下集群中的所有节点都处于 follower 状态。 44 | 45 |  46 | 47 | ➢ 某一时刻,其中的一个 follower 由于没有收到 leader 的 heartbeat 率先发生 election timeout 进而发起选举。 48 | 49 |  50 | 51 | ➢ 只要集群中超过半数的节点接受投票,candidate 节点将成为即切换 leader 状态。 52 | 53 |  54 | 55 | ➢ 成为 leader 节点之后,leader 将定时向 follower 节点同步日志并发送 heartbeat。 56 | 57 |  58 | 59 | **如果leader节点出现了故障,那怎么办?** 60 | 61 | 下面将说明当集群中的 leader 节点不可用时,raft 集群是如何应对的。 62 | 63 | ➢ 一般情况下,leader 节点定时发送 heartbeat 到 follower 节点。 64 | 65 |  66 | 67 | ➢ 由于某些异常导致 leader 不再发送 heartbeat ,或 follower 无法收到 heartbeat 。 68 | 69 |  70 | 71 | ➢ 当某一 follower 发生election timeout 时,其状态变更为 candidate,并向其他 follower 发起投票。 72 | 73 |  74 | 75 | ➢ 当超过半数的 follower 接受投票后,这一节点将成为新的 leader,leader 的任期号term加1并开始向 follower 同步日志。 76 | 77 |  78 | 79 | ➢ 当一段时间之后,如果之前的 leader 再次加入集群,则两个 leader 比较彼此的任期号,任期号低的leader将切换自己的状态为follower。 80 | 81 |  82 | 83 | ➢ 较早前 leader 中不一致的日志将被清除,并与现有 leader 中的日志保持一致。 84 |  85 | 86 | 还有第三种可能性就是candidate既没选举成功也没选举失败:如果多个follower同时成为candidate去拉选票,导致选票分散,任何candidate都没拿到大多数选票,这种情况下Raft使用超时机制`election timeout`来解决。所以同时出现多个candidate的可能性不大,即使机缘巧合同时出现了多个candidate导致选票分散,那么它们就等待自己的election timeout超时,重新开始一次新选举,实验也证明这个机制在选举过程中收敛速度很快。 87 | 88 | ### 3.2 Log Relocation 89 | 90 | 在 raft 集群中,所有日志都必须首先提交至 leader 节点。leader 在每个 heartbeat 向 follower 发送AppendEntries RPC同步日志,follower如果发现没问题,复制成功后会给leader一个表示成功的ACK,leader收到超过半数的ACK后应用该日志,返回客户端执行结果。若 follower 节点宕机、运行缓慢或者丢包,则 leader 节点会不断重试AppendEntries RPC,直到所有 follower 节点最终都复制所有日志条目。 91 | 92 | 上述的具体过程如下: 93 | 94 | ➢ 首先有一条 uncommitted 的日志条目提交至 leader 节点。 95 | 96 |  97 | 98 | ➢ 在下一个 heartbeat,leader 将此条目复制给所有的 follower。 99 | 100 |  101 | 102 | ➢ 当大多数节点记录此条目之后,leader 节点认定此条目有效,将此条目设定为已提交并存储于本地磁盘。 103 | 104 |  105 | 106 |  107 | 108 | ➢ 在下一个 heartbeat,leader 通知所有 follower 提交这一日志条目并存储于各自的磁盘内。 109 | 110 |  111 | 112 | **Network Partition 情况下进行复制日志:** 113 | 114 | 由于网络的隔断,造成集群中多数的节点在一段时间内无法访问到 leader 节点。按照 raft 共识算法,没有 leader 的那一组集群将会通过选举投票出新的 leader,甚至会在两个集群内产生不一致的日志条目。在集群重新完整连通之后,原来的 leader 仍会按照 raft 共识算法从步进数更高的 leader 同步日志并将自己切换为 follower。 115 | 116 | ➢ 集群的理想状态。 117 | 118 |  119 | 120 | ➢ 网络间隔造成大多数的节点无法访问 leader 节点。 121 | 122 |  123 | 124 | ➢ 新的日志条目添加到 leader 中。 125 | 126 |  127 | 128 | ➢ leader 节点将此条日志同步至能够访问到 leader 的节点。 129 | 130 |  131 | 132 | ➢ follower 确认日志被记录,但是确认记录日志的 follower 数量没有超过集群节点的半数,leader 节点并不将此条日志存档。 133 | 134 |  135 | 136 | ➢ 在被隔断的这部分节点,在 election timeout 之后,followers 中产生 candidate 并发起选举。 137 | 138 |  139 | 140 | ➢ 多数节点接受投票之后,candidate 成为 leader。 141 | 142 |  143 | 144 | ➢ 一个日志条目被添加到新的 leader并复制给新 leader 的 follower。 145 | 146 |  147 | 148 | ➢ 多数节点确认之后,leader 将日志条目提交并存储。 149 | 150 |  151 | 152 | ➢ 在下一个 heartbeat,leader 通知 follower 各自提交并保存在本地磁盘。 153 | 154 |  155 | 156 | ➢ 经过一段时间之后,集群重新连通到一起,集群中出现两个 leader 并且存在不一致的日志条目。 157 | 158 |  159 | 160 | ➢ 新的 leader 在下一次 heartbeat timeout 时向所有的节点发送一次 heartbeat。 161 | 162 |  163 | 164 | ➢ leader 在收到任期号term更高的 leader heartbeat 时放弃 leader 地位并切换到 follower 状态。 165 |  166 | 167 | ➢ 此时leader同步未被复制的日志条目给所有的 follower。 168 | 169 |  170 | 171 | 通过这种方式,只要集群中有效连接的节点超过总数的一半,集群将一直以这种规则运行下去并始终确保各个节点中的数据始终一致。 172 | 173 | > **参考:** 174 | > 175 | > - [共识算法:Raft](https://www.jianshu.com/p/8e4bbe7e276c) 176 | > - [Raft原理动画](http://thesecretlivesofdata.com/raft/) 177 | > - Ongaro D, Ousterhout J. In search of an understandable consensus algorithm[C]// USENIX Annual Technical Conference. [s.l.]: USENIX. 2014: 305-319. -------------------------------------------------------------------------------- /cs-basics/network/network.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmk-c/GolangGuide/f31c71d88da07256492133c7b9686021ee4f0ec0/cs-basics/network/network.pdf -------------------------------------------------------------------------------- /cs-basics/operating-system/os.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmk-c/GolangGuide/f31c71d88da07256492133c7b9686021ee4f0ec0/cs-basics/operating-system/os.pdf -------------------------------------------------------------------------------- /database/redis/base.md: -------------------------------------------------------------------------------- 1 | # Redis 基础 2 | 3 |  4 | 5 | ## 1.Redis的数据类型 6 | 7 | Redis支持五种数据类型:`string`(字符串),`hash`(哈希),`list`(列表),`set`(集合)及`zset`(sorted set:有序集合)。 8 | 9 | ### string(字符串) 10 | 11 | string 是 redis 最基本的类型,你可以理解成与 [Memcached](https://baike.baidu.com/item/memcached/1625373?fr=aladdin) 一模一样的类型,一个 key 对应一个 value。 12 | 13 | **string 类型是二进制安全的**。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。 14 | 15 | string 类型是 Redis 最基本的数据类型,**string 类型的值最大能存储 512MB**。 16 | 17 | #### 实战场景: 18 | 19 | 1. **缓存功能**:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。 20 | 2. **计数器**:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。 21 | 3. **共享用户Session**:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。 22 | 23 | #### 示例: 24 | 25 |  26 | 27 | 示例中使用了 Redis 的 `SET key value`命令添加了一组键值对,`GET key value` 命令用来获取该组键值对。 28 | 29 | --- 30 | 31 | ### hash (哈希) 32 | 33 | 是一个map,指值本身又是一种键值对结构,如 value={{field1,value1},......,{fieldN,valueN}} 34 | 35 |  36 | 37 | #### 实战场景: 38 | 39 | 1. 缓存: 更直观,相比string更节省空间,hash维护缓存信息,如用户信息,视频信息等。 40 | 41 | #### 示例: 42 | 43 |  44 | 45 | 示例中我们使用了 Redis的`HSET key field1 value1 [value2]`命令(redis:4.0中弃用了HMSET),设置了三个 **field-value** 对, `HGET key field`命令获取对应 field 对应的 **value**。`HGETALL key`命令可以获取整个key的信息。**每个 hash 可以存储 232 -1个键值对**。 46 | 47 | --- 48 | 49 | ### list(列表) 50 | 51 | Redis 列表是简单的字符串列表(双端队列实现),按照插入顺序排序。你可以添加一个元素到列表的头部(左边lpush)或者尾部(右边rpush)。 52 | 53 |  54 | 55 | #### 使用列表的技巧: 56 | 57 | - lpush+lpop=Stack(栈) 58 | - lpush+rpop=Queue(队列) 59 | - lpush+ltrim=Capped Collection(有限集合) 60 | - lpush+brpop=Message Queue(消息队列) 61 | 62 | #### 实战场景: 63 | 64 | 1. **消息队列**:Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过lpush命令从左边插入数据,多个数据消费者,可以使用brpop命令阻塞的“抢”列表尾部的数据。 65 | 2. **文章列表或者数据分页展示的应用**:比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。 66 | 67 | #### 示例: 68 | 69 |  70 | 71 | 示例使用了redis的`LPUSH key value`命令向列表的左端插入了三组数据,`LRANGE key start stop`命令展示key中的索引从start到stop的全部value信息。`LPOP key`和`RPOP key`分别从列表的左右两端删除value值。队列中最大的成员数为 232 - 1 72 | 73 | --- 74 | 75 | ### set(集合) 76 | 77 | Redis 的 Set 是 string 类型的无序集合。 78 | 79 | 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 80 | 81 | 集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中 1. 不允许有重复的元素,2.集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集。 82 | 83 | 84 |  85 | 86 | #### 实战场景: 87 | 88 | 1. 标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。 89 | 2. 点赞,或点踩,收藏等,可以放到set中实现 90 | 91 | #### 示例: 92 | 93 |  94 | 95 | 示例使用了redis的`SADD key value`命令向集合中添加了四组键值对,但是set集合是无序,不重复的集合,在使用`SMEMBERS key`命令查看集合中全部数据时,只能看到唯一元素。集合中最大的成员数为 232 - 1 96 | 97 | --- 98 | 99 | ### zset(有序集合) 100 | 101 | Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。 102 | 103 | 不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。 104 | 105 | zset的成员是唯一的,但分数(score)却可以重复,就和一个班里的同学学号不能重复,但考试成绩可以相同。 106 | 107 | 108 |  109 | 110 | #### 实战场景: 111 | 112 | 1. **排行榜**:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。 113 | 2. **带权重的队列**:比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。 114 | 115 | #### 示例: 116 | 117 |  118 | 119 | 示例使用了redis的`ZADD key score value`创建了四组用户的薪水值,并用`ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]`命令按薪水的高低对用户进行升序排序。 120 | 121 | ## 2.Redis keys 命令 122 | 123 | 下表给出了与 Redis 键相关的基本命令: 124 | 125 | | 序号 | 命令及描述 | 126 | | :--- | :----------------------------------------------------------- | 127 | | 1 | [DEL key](https://www.runoob.com/redis/keys-del.html) 该命令用于在 key 存在时删除 key。不存在的key会被忽略,返回被删除key的数量。 | 128 | | 2 | [DUMP key](https://www.runoob.com/redis/keys-dump.html) 序列化给定 key ,并返回被序列化的值。 | 129 | | 3 | [EXISTS key](https://www.runoob.com/redis/keys-exists.html) 检查给定 key 是否存在。 | 130 | | 4 | [EXPIRE key seconds](https://www.runoob.com/redis/keys-expire.html) 为给定 key 设置过期时间,key过期后将被删除不可再使用,以秒计。 | 131 | | 5 | [EXPIREAT key timestamp](https://www.runoob.com/redis/keys-expireat.html) EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 | 132 | | 6 | [PEXPIRE key milliseconds](https://www.runoob.com/redis/keys-pexpire.html) 设置 key 的过期时间以毫秒计。 | 133 | | 7 | [PEXPIREAT key milliseconds-timestamp](https://www.runoob.com/redis/keys-pexpireat.html) 设置 key 过期时间的时间戳(unix timestamp) 以毫秒计。 | 134 | | 8 | [KEYS pattern](https://www.runoob.com/redis/keys-keys.html) 查找所有符合给定模式( pattern)的 key 。`KEYS *`返回所有存在的key | 135 | | 9 | [MOVE key db](https://www.runoob.com/redis/keys-move.html) 将当前数据库的 key 移动到给定的数据库 db 当中。 | 136 | | 10 | [PERSIST key](https://www.runoob.com/redis/keys-persist.html) 移除 key 的过期时间,key 将持久保持。 | 137 | | 11 | [PTTL key](https://www.runoob.com/redis/keys-pttl.html) 以毫秒为单位返回 key 的剩余的过期时间。 | 138 | | 12 | [TTL key](https://www.runoob.com/redis/keys-ttl.html) 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以秒为单位,返回 key 的剩余生存时间。 | 139 | | 13 | [RANDOMKEY](https://www.runoob.com/redis/keys-randomkey.html) 从当前数据库中随机返回一个 key 。 | 140 | | 14 | [RENAME key newkey](https://www.runoob.com/redis/keys-rename.html) 修改 key 的名称。 | 141 | | 15 | [RENAMENX key newkey](https://www.runoob.com/redis/keys-renamenx.html) 仅当 newkey 不存在时,将 key 改名为 newkey 。 | 142 | | 16 | [SCAN cursor \[MATCH pattern\] \[COUNT count\]](https://www.runoob.com/redis/keys-scan.html) 迭代数据库中的数据库键。 | 143 | | 17 | [TYPE key](https://www.runoob.com/redis/keys-type.html) 返回 key 所储存的值的类型。 | 144 | 145 | ## 3.Redis 字符串命令 146 | 147 | 下表列出了常用的 redis 字符串命令: 148 | 149 | | 序号 | 命令及描述 | 150 | | :--- | :----------------------------------------------------------- | 151 | | 1 | [SET key value](https://www.runoob.com/redis/strings-set.html) 设置指定 key 的值 | 152 | | 2 | [GET key](https://www.runoob.com/redis/strings-get.html) 获取指定 key 的值。 | 153 | | 3 | [GETRANGE key start end](https://www.runoob.com/redis/strings-getrange.html) 返回 key 中字符串值的子字符,字符串的截取范围由 start 和 end 两个偏移量决定(包括 start 和 end 在内)。GETRANGE key 0 -1表示全部字符串 | 154 | | 4 | [GETSET key value](https://www.runoob.com/redis/strings-getset.html) 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 | 155 | | 5 | [GETBIT key offset](https://www.runoob.com/redis/strings-getbit.html) 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。 | 156 | | 6 | [MGET key1 key2..](https://www.runoob.com/redis/strings-mget.html) 获取所有(一个或多个)给定 key 的值。 | 157 | | 7 | [SETBIT key offset value](https://www.runoob.com/redis/strings-setbit.html) 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。 | 158 | | 8 | [SETEX key seconds value](https://www.runoob.com/redis/strings-setex.html) 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。 | 159 | | 9 | [SETNX key value](https://www.runoob.com/redis/strings-setnx.html) Redis Setnx(**SET** if **N**ot e**X**ists) 命令在指定的 key 不存在时,为 key 设置指定的值。。 | 160 | | 10 | [SETRANGE key offset value](https://www.runoob.com/redis/strings-setrange.html) 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。 | 161 | | 11 | [STRLEN key](https://www.runoob.com/redis/strings-strlen.html) 返回 key 所储存的字符串值的长度。 | 162 | | 12 | [MSET key value \[key value ...](https://www.runoob.com/redis/strings-mset.html) 同时设置一个或多个 key-value 对。 | 163 | | 13 | [MSETNX key value \[key value ...](https://www.runoob.com/redis/strings-msetnx.html) 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 | 164 | | 14 | [PSETEX key milliseconds value](https://www.runoob.com/redis/strings-psetex.html) 这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。 | 165 | | 15 | [INCR key](https://www.runoob.com/redis/strings-incr.html) 将 key 中储存的数字值增一。 | 166 | | 16 | [INCRBY key increment](https://www.runoob.com/redis/strings-incrby.html) 将 key 所储存的值加上给定的增量值(increment) 。 | 167 | | 17 | [INCRBYFLOAT key increment](https://www.runoob.com/redis/strings-incrbyfloat.html) 将 key 所储存的值加上给定的浮点增量值(increment) 。 | 168 | | 18 | [DECR key](https://www.runoob.com/redis/strings-decr.html) 将 key 中储存的数字值减一。 | 169 | | 19 | [DECRBY key decrement](https://www.runoob.com/redis/strings-decrby.html) key 所储存的值减去给定的减量值(decrement) 。 | 170 | | 20 | [APPEND key value](https://www.runoob.com/redis/strings-append.html) 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。 | 171 | 172 | ## 4.Redis hash 命令 173 | 174 | 下表列出了 redis hash 基本的相关命令: 175 | 176 | | 序号 | 命令及描述 | 177 | | :--- | :----------------------------------------------------------- | 178 | | 1 | [HDEL key field1 \[field2\]](https://www.runoob.com/redis/hashes-hdel.html) 删除一个或多个哈希表字段 | 179 | | 2 | [HEXISTS key field](https://www.runoob.com/redis/hashes-hexists.html) 查看哈希表 key 中,指定的字段是否存在。 | 180 | | 3 | [HGET key field](https://www.runoob.com/redis/hashes-hget.html) 获取存储在哈希表中指定字段的值。 | 181 | | 4 | [HGETALL key](https://www.runoob.com/redis/hashes-hgetall.html) 获取在哈希表中指定 key 的所有字段和值 | 182 | | 5 | [HINCRBY key field increment](https://www.runoob.com/redis/hashes-hincrby.html) 为哈希表 key 中的指定字段的整数值加上增量 increment 。增量也可以为负数,相当于对指定字段进行减法操作。对一个储存字符串值的字段执行 HINCRBY 命令将造成一个错误。如果指定的字段不存在,那么在执行命令前,字段的值被初始化为 0 。 | 183 | | 6 | [HINCRBYFLOAT key field increment](https://www.runoob.com/redis/hashes-hincrbyfloat.html) 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。 | 184 | | 7 | [HKEYS key](https://www.runoob.com/redis/hashes-hkeys.html) 获取所有哈希表中的字段 | 185 | | 8 | [HLEN key](https://www.runoob.com/redis/hashes-hlen.html) 获取哈希表中字段的数量 | 186 | | 11 | [HSET key field value](https://www.runoob.com/redis/hashes-hset.html) 将哈希表 key 中的字段 field 的值设为 value 。如果字段是哈希表中的一个新建字段,并且值设置成功,返回 1 。 如果哈希表中域字段已经存在且旧值已被新值覆盖,返回 0 。 | 187 | | 12 | [HSETNX key field value](https://www.runoob.com/redis/hashes-hsetnx.html) 只有在字段 field 不存在时,设置哈希表字段的值。 | 188 | | 13 | [HVALS key](https://www.runoob.com/redis/hashes-hvals.html) 获取哈希表中所有值。 | 189 | | 14 | [HSCAN key cursor \[MATCH pattern\] \[COUNT count\]](https://www.runoob.com/redis/hashes-hscan.html) 迭代哈希表中的键值对。 | 190 | 191 | ## 5.Redis 列表命令 192 | 193 | 下表列出了列表相关的基本命令: 194 | 195 | | 序号 | 命令及描述 | 196 | | :--- | :----------------------------------------------------------- | 197 | | 1 | [BLPOP key1 \[key2\] timeout](https://www.runoob.com/redis/lists-blpop.html) 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | 198 | | 2 | [BRPOP key1 \[key2\] timeout](https://www.runoob.com/redis/lists-brpop.html) 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | 199 | | 3 | [BRPOPLPUSH source destination timeout](https://www.runoob.com/redis/lists-brpoplpush.html) 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | 200 | | 4 | [LINDEX key index](https://www.runoob.com/redis/lists-lindex.html) 通过索引获取列表中的元素 | 201 | | 5 | [LINSERT key BEFORE or AFTER pivot value](https://www.runoob.com/redis/lists-linsert.html) 在列表的元素前或者后插入元素 | 202 | | 6 | [LLEN key](https://www.runoob.com/redis/lists-llen.html) 获取列表长度 | 203 | | 7 | [LPOP key](https://www.runoob.com/redis/lists-lpop.html) 移出并获取列表的第一个元素 | 204 | | 8 | [LPUSH key value1 \[value2\]](https://www.runoob.com/redis/lists-lpush.html) 将一个或多个值插入到列表头部 | 205 | | 9 | [LPUSHX key value](https://www.runoob.com/redis/lists-lpushx.html) 将一个值插入到已存在的列表头部 | 206 | | 10 | [LRANGE key start stop](https://www.runoob.com/redis/lists-lrange.html) 获取列表指定范围内的元素.。 | 207 | | 11 | [LREM key count value](https://www.runoob.com/redis/lists-lrem.html) Lrem 根据参数 count 的值,移除列表中与参数 VALUE 相等的元素。若count>0,则从表头开始向表尾搜索,移除与value相等的值,个数为count个;若count<0,则从表尾开始向表头搜索删除;若count=0,则删除与value相等的全部元素。 | 208 | | 12 | [LSET key index value](https://www.runoob.com/redis/lists-lset.html) 通过索引设置列表元素的值 | 209 | | 13 | [LTRIM key start stop](https://www.runoob.com/redis/lists-ltrim.html) Redis Ltrim 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。下标 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 | 210 | | 14 | [RPOP key](https://www.runoob.com/redis/lists-rpop.html) 移除列表的最后一个元素,返回值为移除的元素。 | 211 | | 15 | [RPOPLPUSH source destination](https://www.runoob.com/redis/lists-rpoplpush.html) 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 | 212 | | 16 | [RPUSH key value1 \[value2\]](https://www.runoob.com/redis/lists-rpush.html) 在列表中添加一个或多个值 | 213 | | 17 | [RPUSHX key value](https://www.runoob.com/redis/lists-rpushx.html) 为已存在的列表添加值 | 214 | 215 | ## 6.Redis 集合命令 216 | 217 | 下表列出了 Redis 集合基本命令: 218 | 219 | | 序号 | 命令及描述 | 220 | | :--- | :----------------------------------------------------------- | 221 | | 1 | [SADD key member1 \[member2\]](https://www.runoob.com/redis/sets-sadd.html) 向集合添加一个或多个成员 | 222 | | 2 | [SCARD key](https://www.runoob.com/redis/sets-scard.html) 获取集合的成员数 | 223 | | 3 | [SDIFF key1 \[key2\]](https://www.runoob.com/redis/sets-sdiff.html) 返回第一个集合与其他集合之间的差异。 | 224 | | 4 | [SDIFFSTORE destination key1 \[key2\]](https://www.runoob.com/redis/sets-sdiffstore.html) 返回给定所有集合的`差集`并存储在 destination 中 | 225 | | 5 | [SINTER key1 \[key2\]](https://www.runoob.com/redis/sets-sinter.html) 返回给定所有集合的交集 | 226 | | 6 | [SINTERSTORE destination key1 \[key2\]](https://www.runoob.com/redis/sets-sinterstore.html) 返回给定所有集合的`交集`并存储在 destination 中 | 227 | | 7 | [SISMEMBER key member](https://www.runoob.com/redis/sets-sismember.html) 判断 member 元素是否是集合 key 的成员 | 228 | | 8 | [SMEMBERS key](https://www.runoob.com/redis/sets-smembers.html) 返回集合中的所有成员 | 229 | | 9 | [SMOVE source destination member](https://www.runoob.com/redis/sets-smove.html) 将 member 元素从 source 集合移动到 destination 集合 | 230 | | 10 | [SPOP key](https://www.runoob.com/redis/sets-spop.html) 移除并返回集合中的一个随机元素 | 231 | | 11 | [SRANDMEMBER key \[count\]](https://www.runoob.com/redis/sets-srandmember.html) 返回集合中一个或多个随机数 | 232 | | 12 | [SREM key member1 \[member2\]](https://www.runoob.com/redis/sets-srem.html) 移除集合中一个或多个成员 | 233 | | 13 | [SUNION key1 \[key2\]](https://www.runoob.com/redis/sets-sunion.html) 返回所有给定集合的并集 | 234 | | 14 | [SUNIONSTORE destination key1 \[key2\]](https://www.runoob.com/redis/sets-sunionstore.html) 所有给定集合的并集存储在 destination 集合中 | 235 | | 15 | [SSCAN key cursor \[MATCH pattern\] \[COUNT count\]](https://www.runoob.com/redis/sets-sscan.html) 迭代集合中的元素 | 236 | 237 | ## 7.Redis 有序集合命令 238 | 239 | 下表列出了 redis 有序集合的基本命令: 240 | 241 | | 序号 | 命令及描述 | 242 | | :--- | :----------------------------------------------------------- | 243 | | 1 | [ZADD key score1 member1 \[score2 member2\]](https://www.runoob.com/redis/sorted-sets-zadd.html) 向有序集合添加一个或多个成员,或者更新已存在成员的分数 | 244 | | 2 | [ZCARD key](https://www.runoob.com/redis/sorted-sets-zcard.html) 获取有序集合的成员数 | 245 | | 3 | [ZCOUNT key min max](https://www.runoob.com/redis/sorted-sets-zcount.html) 计算在有序集合中指定区间分数的成员数 | 246 | | 4 | [ZINCRBY key increment member](https://www.runoob.com/redis/sorted-sets-zincrby.html) 有序集合中对指定成员的分数加上增量 increment | 247 | | 5 | [ZINTERSTORE destination numkeys key \[key ...\]](https://www.runoob.com/redis/sorted-sets-zinterstore.html) 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中 | 248 | | 6 | [ZLEXCOUNT key min max](https://www.runoob.com/redis/sorted-sets-zlexcount.html) 在有序集合中计算指定字典区间内成员数量 | 249 | | 7 | [ZRANGE key start stop \[WITHSCORES\]](https://www.runoob.com/redis/sorted-sets-zrange.html) 通过索引区间返回有序集合指定区间内的成员 | 250 | | 8 | [ZRANGEBYLEX key min max \[LIMIT offset count\]](https://www.runoob.com/redis/sorted-sets-zrangebylex.html) 通过字典区间返回有序集合的成员 | 251 | | 9 | [ZRANGEBYSCORE key min max \[WITHSCORES\] \[LIMIT\]](https://www.runoob.com/redis/sorted-sets-zrangebyscore.html)` 通过分数返回`有序集合指定区间内的成员,有序集成员按分数值递增(从小到大)次序排列。 | 252 | | 10 | [ZRANK key member](https://www.runoob.com/redis/sorted-sets-zrank.html) 返回有序集合中指定成员的索引 | 253 | | 11 | [ZREM key member \[member ...\]](https://www.runoob.com/redis/sorted-sets-zrem.html) 移除有序集合中的一个或多个成员 | 254 | | 12 | [ZREMRANGEBYLEX key min max](https://www.runoob.com/redis/sorted-sets-zremrangebylex.html) 移除有序集合中给定的字典区间的所有成员 | 255 | | 13 | [ZREMRANGEBYRANK key start stop](https://www.runoob.com/redis/sorted-sets-zremrangebyrank.html) 移除有序集合中给定的排名区间的所有成员 | 256 | | 14 | [ZREMRANGEBYSCORE key min max](https://www.runoob.com/redis/sorted-sets-zremrangebyscore.html) 移除有序集合中给定的分数区间的所有成员 | 257 | | 15 | [ZREVRANGE key start stop \[WITHSCORES\]](https://www.runoob.com/redis/sorted-sets-zrevrange.html) 返回有序集中指定区间内的成员,通过索引,分数从高到低 | 258 | | 16 | [ZREVRANGEBYSCORE key max min \[WITHSCORES\]](https://www.runoob.com/redis/sorted-sets-zrevrangebyscore.html) 返回有序集中指定分数区间内的成员,分数从高到低排序 | 259 | | 17 | [ZREVRANK key member](https://www.runoob.com/redis/sorted-sets-zrevrank.html) 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 | 260 | | 18 | [ZSCORE key member](https://www.runoob.com/redis/sorted-sets-zscore.html) 返回有序集中,成员的分数值 | 261 | | 19 | [ZUNIONSTORE destination numkeys key \[key ...\]](https://www.runoob.com/redis/sorted-sets-zunionstore.html) 计算给定的一个或多个有序集的并集,并存储在新的 key 中 | 262 | | 20 | [ZSCAN key cursor \[MATCH pattern\] \[COUNT count\]](https://www.runoob.com/redis/sorted-sets-zscan.html) 迭代有序集合中的元素(包括元素成员和元素分值) | 263 | 264 | ## 补充 265 | 266 | redis 默认有0-15总共16个库,默认在0库 267 | 268 | 1. **选择DB0库**:select 0 269 | 2. **查看所选择库的大小**:dbsize 270 | 3. **删除特定的redis库的所有Key**:flushdb -------------------------------------------------------------------------------- /golang/deep/gc.md: -------------------------------------------------------------------------------- 1 | # Go语言深度解析之垃圾回收机制 2 | 3 | ## 常见的垃圾回收方法 4 | 5 | 1. 引用计数(reference counting) 6 | 7 | 对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0是回收该对象。 8 | 9 | - 优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。 10 | - 缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。 11 | - 代表语言:Python、PHP、Swift 12 | 13 | 2. 标记清除(mark and sweep) 14 | 15 | 该方法分为两步:1.从根变量来遍历所有被引用的对象进行标记,2.对未标记对象进行回收。优点:解决了引用计数的缺点。但每次垃圾回收的时候都会暂停所有正常运行的代码`STW`(stop the world)导致**卡顿**,所以后面有mark and sweep的变种方法——**三色标记法**(golang采用的垃圾回收算法),用来缓解性能问题。 16 | 17 | 3. 分代收集(generation) 18 | 19 | jvm使用的垃圾回收算法。在面向对象编程语言中,绝大多数对象的生命周期非常短,分代收集的基本思想是,将堆划分为两个或多个称为代(generation)的空间,新创建的对象存放在称为**新生代**,随着垃圾回收的重复执行,生命周期较长的对象会被提升到老年代中。然后分成针对新生代和老年代的垃圾回收方式。 20 | 21 | ## Golang中的垃圾回收 22 | 23 | > 记住:当前Golang使用的垃圾回收机制是**三色标记法**配合**写屏障**和**辅助GC**,三色标记法是**标记-清除法**的一种增强版本。 24 | 25 | 简单的说,垃圾回收的核心就是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用了(即未被引用),把未被引用的内存回收掉,以供后续内存分配时使用。 26 | 27 | 前面介绍内存分配时,介绍过`span`数据结构,`span`中维护了一个个内存块,并由一个位图`allocBits`表示每个内存块的分配情况。在span数据结构中还有另一个位图`gcmarkBits`用于标记内存块被引用情况。 28 | 29 |  30 | 31 | 如上图所示,`allocBits`记录了每块内存分配情况,而`gcmarkBits`记录了每块内存标记情况。标记阶段对每块内存进行标记,有对象引用的的内存标记为1(如图中灰色所示),没有引用到的保持默认为0。 32 | 33 | allocBits和gcmarkBits数据结构是完全一样的,标记结束就是内存回收,回收时将allocBits指向gcmarkBits,则代表标记过的才是存活的,gcmarkBits则会在下次标记时重新分配内存,非常的巧妙。 34 | 35 | ### 三色标记 36 | 37 | 三色标记法将对象的颜色分为了灰、黑、白,三种: 38 | 39 | - 灰色:对象已被标记,但这个对象包含的子对象未标记 40 | - 黑色:对象已被标记,且这个对象包含的子对象也已标记(gcmarkBits对应的位为1,该对象不会在本次GC中被清理) 41 | - 白色:对象未被标记(gcmarkBits对应的位为0,该对象将会在本次GC中被清理) 42 | 43 | 例如,当前内存中有A~F一共6个对象,根对象a,b本身为栈上分配的局部变量,根对象a、b分别引用了对象A、B, 而B对象又引用了对象D,则GC开始前各对象的状态如下图所示: 44 | 45 |  46 | 47 | 初始状态下所有对象都是白色的。 48 | 49 | 接着开始扫描根对象a、b: 50 | 51 |  52 | 53 | 由于根对象引用了对象A、B,那么A、B变为灰色对象,接下来就开始分析灰色对象,分析A时,A没有引用其他对象很快就转入黑色,B引用了D,则B转入黑色的同时还需要将D转为灰色,进行接下来的分析。如下图所示: 54 | 55 |  56 | 57 | 上图中灰色对象只有D,由于D没有引用其他对象,所以D转入黑色。标记过程结束: 58 | 59 |  60 | 61 | 最终,黑色的对象会被保留下来,白色对象会被回收掉。 62 | 63 | **下面总结一下三色标记法的流程:** 64 | 65 | 1. GC的根对象会被标记为灰色(当前运行的进程所需的对象); 66 | 2. 从当前灰色集合中获取对象,将该对象引用到的对象标记为灰色,自身则标记为黑色; 67 | 3. 重复步骤2,直到没有灰色集合可以标记为止; 68 | 4. 对剩下没有标记的白色对象进行回收(表示GC 根对象不可达); 69 | 70 | ### 三色标记存在的问题 71 | 72 | 因为go支持并行GC,GC的扫描和go代码可以同时运行,这样带来的问题是GC扫描的过程中go代码有可能改变了对象的依赖树。因此三色标记也会存在一些问题: 73 | 74 | 1. **多标 - 浮动垃圾问题** 75 | 76 | 假设 E 已经被标记过了(变成灰色了),此时 D 和 E 断开了引用,按理来说对象 E/F/G 应该被回收的,但是因为 E 已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即**本轮 GC 不会回收这部分内存**。 77 | 78 | 这部分本应该回收 但是没有回收到的内存,被称之为“浮动垃圾”。过程如下图所示: 79 | 80 |  81 | 82 | 2. **漏标 - 悬挂指针问题** 83 | 84 | 当 GC 线程已经遍历到 E 变成灰色,D变成黑色时,灰色 E 断开引用白色 G ,黑色 D 引用了白色 G。此时切回 GC 线程继续跑,因为 E 已经没有对 G 的引用了,所以不会将 G 放到灰色集合。尽管因为 D 重新引用了 G,但因为 D 已经是黑色了,不会再重新做遍历处理。 85 | 86 | 最终导致的结果是:G 会一直停留在白色集合中,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的,这也是 Go 需要在 GC 时解决的问题。过程如下图所示: 87 | 88 |  89 | 90 | 为了避免这个问题,go在GC的标记阶段会启用**写屏障**(Write Barrier),写屏障类似一种开关,在GC的特定时机开启,开启后指针传递时会把指针标记,即本轮不回收,下次GC时再确定。 91 | 92 | 同时为了防止内存分配过快,在GC执行过程中,如果goroutine需要分配内存,那么这个goroutine会参与一部分GC的工作,这个机制叫做**辅助GC(Mutator Assist)** 93 | 94 | ### 垃圾回收触发时机 95 | 96 | 1. 内存分配量达到阀值 97 | 98 | 每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。 99 | 100 | ``` 101 | 阀值 = 上次GC内存分配量 * 内存增长率 102 | ``` 103 | 104 | 内存增长率由环境变量`GOGC`控制,默认为100,即每当内存扩大一倍时启动GC。 105 | 106 | 2. 定期触发GC 107 | 108 | 默认情况下,最长2分钟触发一次GC,这个间隔在`src/runtime/proc.go:forcegcperiod`变量中被声明: 109 | 110 | ```go 111 | // forcegcperiod is the maximum time in nanoseconds between garbage 112 | // collections. If we go this long without a garbage collection, one 113 | // is forced to run. 114 | // 115 | // This is a variable for testing purposes. It normally doesn't change. 116 | var forcegcperiod int64 = 2 * 60 * 1e9 117 | ``` 118 | 119 | 3. 手动触发 120 | 121 | 程序代码中也可以使用`runtime.GC()`来手动触发GC。这主要用于GC性能测试和统计。 122 | 123 | ## 总结golang GC的垃圾回收阶段 124 | 125 | GC 相关的代码在`runtime/mgc.go`文件下。通过注释介绍我们可以知道 GC 一共分为4个阶段: 126 | 127 | 1. 准备阶段:STW,初始化标记任务,启用写屏障 128 | 2. 标记阶段 GCMark:标记存活对象,并发与用户代码执行,保持只占用25%CPU 129 | 3. 标记终止阶段 GCMarkTermination:STW,关闭写屏障 130 | 4. 清扫阶段 GCOff:回收白色对象,并发与用户代码执行 131 | 132 | > Reference: 133 | > 134 | > 1. [Go语言——垃圾回收GC](https://www.jianshu.com/p/8b0c0f7772da) 135 | > 2. [Go语言GC实现原理及源码分析](https://juejin.cn/post/6941768640265977886) 136 | > 3. [《Go专家编程》Go 垃圾回收原理](https://my.oschina.net/renhc/blog/2244717) 137 | > 4. [**golang gc**](http://yangxikun.github.io/golang/2019/12/22/golang-gc.html) -------------------------------------------------------------------------------- /golang/deep/gmp.md: -------------------------------------------------------------------------------- 1 | # Go语言深度解析之GPM调度器 2 | 3 | ## 预前补充 4 | 5 | 先来了解一下进程,线程和Goroutine 6 | 7 | 在仅支持进程的操作系统中,进程是拥有资源和独立调度的基本单位。在引入线程的操作系统中,**线程是独立调度的基本单位,进程是资源拥有的基本单位**。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。 8 | 9 | 线程创建、管理、调度等采用的方式称为**线程模型**。线程模型一般分为以下三种: 10 | 11 | - 内核级线程(Kernel Level Thread)模型 12 | - 用户级线程(User Level Thread)模型 13 | - 两级线程模型,也称混合型线程模型 14 | 15 | **三大线程模型最大差异就在于用户级线程与内核调度实体KSE(KSE,Kernel Scheduling Entity)之间的对应关系。**KSE是Kernel Scheduling Entity的缩写,其是**可被操作系统内核调度器调度的对象实体**,是操作系统**内核的最小调度单元**,可以简单理解为内核级线程。 16 | 17 | **用户级线程即协程**,由应用程序创建与管理,协程必须与内核级线程绑定之后才能执行。**线程由 CPU 调度是抢占式的,协程由用户态调度是协作式的,一个协程让出 CPU 后,才执行下一个协程**。 18 | 19 | ### 内核级线程模型 20 | 21 |  22 | 23 | **内核级线程模型中用户线程与内核调度实体KS是一对一关系(1 : 1)**。**线程的创建、销毁、切换工作都是有内核完成的**。应用程序不参与线程的管理工作,只能调用内核级线程编程接口(应用程序创建一个新线程或撤销一个已有线程时,都会进行一个系统调用)。每个用户线程都会被绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程都将离开系统。 24 | 25 | 操作系统调度器管理、调度并分派这些线程。运行时库为每个用户级线程请求一个内核级线程。操作系统的内存管理和调度子系统必须要考虑到数量巨大的用户级线程。操作系统为每个线程创建上下文。进程的每个线程在资源可用时都可以被指派到处理器内核。 26 | 27 | 内核级线程模型有如下优点: 28 | 29 | - 在多处理器系统中,内核能够并行执行同一进程内的多个线程 30 | - 如果进程中的一个线程被阻塞,不会阻塞其他线程,是能够切换同一进程内的其他线程继续执行 31 | - 当一个线程阻塞时,内核根据选择可以运行另一个进程的线程,而用户空间实现的线程中,运行时系统始终运行自己进程中的线程 32 | 33 | 缺点: 34 | 35 | - 线程的创建与删除都需要CPU参与,成本大 36 | 37 | ### 用户级线程模型 38 | 39 |  40 | 41 | **用户线程模型中的用户线程与内核线程实体KSE是多对一关系(N : 1)**。**线程的创建、销毁以及线程之间的协调、同步等工作都是在用户态完成**,具体来说就是由应用程序的线程库来完成。**内核对这些是无感知的,内核此时的调度都是基于进程的**。线程的并发处理从宏观来看,任意时刻每个进程只能够有一个线程在运行,且只有一个处理器内核会被分配给该进程。 42 | 43 | 从上图中可以看出来:库调度器从进程的多个线程中选择一个线程,然后该线程和该进程允许的一个内核线程关联起来。内核线程将被操作系统调度器指派到处理器内核。用户级线程是一种”多对一”的线程映射 44 | 45 | 用户级线程有如下优点: 46 | 47 | - 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多, 因为保存线程状态的过程和调用程序都只是本地过程 48 | - 线程能够利用的表空间和堆栈空间比内核级线程多 49 | 50 | 缺点: 51 | 52 | - 线程发生I/O或页面故障引起的阻塞时,如果调用阻塞系统调用则内核由于不知道有多线程的存在,而会阻塞整个进程从而阻塞所有线程, 因此同一进程中只能同时有一个线程在运行 53 | - 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用 54 | 55 | ### 混合线程模型 56 | 57 |  58 | 59 | **混合线程模型中用户线程与内核线程实体KSR是多对多关系(N : M)**。两级线程模型充分吸收上面两种模型的优点,尽量规避缺点。其线程创建在用户空间中完成,线程的调度和同步也在应用程序中进行。一个应用程序中的多个用户级线程被绑定到一些(小于或等于用户级线程的数目)内核级线程上。 60 | 61 | ### Go的线程模型 62 | 63 | **Golang在底层实现了混合型线程模型**。M即系统线程,由系统调用产生,一个M关联一个KSE,即两级线程模型中的内核级线程。G为Groutine,即两级线程模型的应用级线程。M与G的关系是N:M。 64 | 65 |  66 | 67 | ## GMP模型 68 | 69 | 70 | 71 | GMP图 72 | 73 | 基于**没有什么是加一个中间层不能解决的**思路,golang在原有的`GM`模型的基础上加入了一个调度器`P`,于是就有了现在的`GMP`模型。 74 | 75 | 76 | 77 | GMP模型 78 | 79 | 1. **全局队列**:存放等待运行的 `G`。 80 | 81 | 2. **P 的本地队列**:同全局队列类似,存放的也是等待运行的 `G`,存的数量有限,**不超过 256 个**。新建 `G`时,`G`优先加入到 P 的本地队列,如果本地队列满了,则会把**本地队列中一半的 G 移动到全局队列**。 82 | 83 | 3. **P 列表**:所有的` P` 都在程序启动时创建,并保存在数组中,最多有 **GOMAXPROCS(默认是CPU的核数)** 个。 84 | 85 | 4. **M**:`M`想运行任务就得获取 `P`,从 P 的本地队列获取 `G`,**访问本地队列不用加锁**。 86 | 87 | 如果P 的本地队列为空,`M` 会尝试从全局队列拿一批`G` 放到 P 的本地队列。 88 | 89 | 如果全局协程队列为空,`M`会从 其他`P` 的本地队列**偷一半,采用Work Stealing算法**放到自己 P 的本地队列。 90 | 91 | `M` 运行 `G`,`G` 执行之后,`M` 会从 `P `获取下一个 `G`,不断重复下去。 92 | 93 | ### 调度的生命周期 94 | 95 |  96 | 97 | - `M0` 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了 98 | - `G0` 是每次启动一个 M 都会第一个创建的 gourtine,G0 仅用于负责调度的 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0 99 | 100 | 上面生命周期流程说明: 101 | 102 | - runtime 创建最初的线程 m0 和 goroutine g0,并把两者进行关联(g0.m = m0) 103 | - 调度器初始化:设置M最大数量,P个数,栈和内存出事,以及创建 GOMAXPROCS个P 104 | - 示例代码中的 main 函数是 main.main,runtime 中也有 1 个 main 函数 ——runtime.main,代码经过编译后,runtime.main 会调用 main.main,程序启动时会为 runtime.main 创建 goroutine,称它为 main goroutine 吧,然后把 main goroutine 加入到 P 的本地队列。 105 | - 启动 m0,m0 已经绑定了 P,会从 P 的本地队列获取 G,获取到 main goroutine。 106 | - G 拥有栈,M 根据 G 中的栈信息和调度信息设置运行环境 107 | - M 运行 G 108 | - G 退出,再次回到 M 获取可运行的 G,这样重复下去,直到 main.main 退出,runtime.main 执行 Defer 和 Panic 处理,或调用 runtime.exit 退出程序。 109 | 110 | ### 调度的流程状态 111 | 112 |  113 | 114 | 从上图我们可以看出来: 115 | 116 | - 每个P有个局部队列,局部队列保存待执行的goroutine(流程2),当M绑定的P的的局部队列已经满了之后就会把goroutine放到全局队列(流程2-1) 117 | - 每个P和一个M绑定,M是真正的执行P中goroutine的实体(流程3),M从绑定的P中的局部队列获取G来执行 118 | - 当M绑定的P的局部队列为空时,M会从全局队列获取到本地队列来执行G(流程3.1),当从全局队列中没有获取到可执行的G时候,M会从其他P的局部队列中偷取G来执行(流程3.2),这种从其他P偷的方式称为**work stealing** 119 | - 当G因系统调用(syscall)阻塞时会阻塞M,此时P会和M解绑即**hand off**,并寻找新的idle的M,若没有idle的M就会新建一个M(流程5.1)。 120 | - 当G因channel或者network I/O阻塞时,不会阻塞M,M会寻找其他runnable的G;当阻塞的G恢复后会重新进入runnable进入P队列等待执行(流程5.3) 121 | 122 | ### 调度过程中阻塞 123 | 124 | GMP模型的阻塞可能发生在下面几种情况: 125 | 126 | - I/O,select 127 | - block on syscall 128 | - channel 129 | - 等待锁 130 | - runtime.Gosched() 131 | 132 | #### 用户态阻塞 133 | 134 | 当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有runnable的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为runnable,尝试加入G2所在P的runnext,然后再是P的Local队列和Global队列。 135 | 136 | #### 系统调用阻塞 137 | 138 | 当G被阻塞在某个系统调用上时,此时G会阻塞在_Gsyscall状态,M也处于 block on syscall 状态,此时的M可被抢占调度:执行该G的M会与P解绑,而P则尝试与其它idle的M绑定,继续执行其它G。如果没有其它idle的M,但P的Local队列中仍然有G需要执行,则创建一个新的M;当系统调用完成后,G会重新尝试获取一个idle的P进入它的Local队列恢复执行,如果没有idle的P,G会被标记为runnable加入到Global队列。 139 | 140 | ### GMP内部结构 141 | 142 | #### G的结构 143 | 144 | ```go 145 | type g struct { 146 | stack stack // g自己的栈 147 | m *m // 隶属于哪个M 148 | sched gobuf // 保存了g的现场,goroutine切换时通过它来恢复 149 | atomicstatus uint32 // G的运行状态 150 | goid int64 151 | schedlink guintptr // 下一个g, g链表 152 | preempt bool //抢占标记 153 | lockedm muintptr // 锁定的M,g中断恢复指定M执行 154 | gopc uintptr // 创建该goroutine的指令地址 155 | startpc uintptr // goroutine 函数的指令地址 156 | } 157 | ``` 158 | 159 | G的状态有以下9种: 160 | 161 | | 状态 | 值 | 含义 | 162 | | ----------------- | ---- | ------------------------------------------------------------ | 163 | | _Gidle | 0 | 刚刚被分配,还没有进行初始化。 | 164 | | _Grunnable | 1 | 已经在运行队列中,还没有执行用户代码。 | 165 | | _Grunning | 2 | 不在运行队列里中,已经可以执行用户代码,此时已经分配了 M 和 P。 | 166 | | _Gsyscall | 3 | 正在执行系统调用,此时分配了 M。 | 167 | | _Gwaiting | 4 | 在运行时被阻止,没有执行用户代码,也不在运行队列中,此时它正在某处阻塞等待中。 | 168 | | _Gmoribund_unused | 5 | 尚未使用,但是在 gdb 中进行了硬编码。 | 169 | | _Gdead | 6 | 尚未使用,这个状态可能是刚退出或是刚被初始化,此时它并没有执行用户代码,有可能有也有可能没有分配堆栈。 | 170 | | _Genqueue_unused | 7 | 尚未使用。 | 171 | | _Gcopystack | 8 | 正在复制堆栈,并没有执行用户代码,也不在运行队列中。 | 172 | 173 | #### M的结构 174 | 175 | ```go 176 | type m struct { 177 | g0 *g // g0, 每个M都有自己独有的g0 178 | 179 | curg *g // 当前正在运行的g 180 | p puintptr // 隶属于哪个P 181 | nextp puintptr // 当m被唤醒时,首先拥有这个p 182 | id int64 183 | spinning bool // 是否处于自旋 184 | 185 | park note 186 | alllink *m // on allm 187 | schedlink muintptr // 下一个m, m链表 188 | mcache *mcache // 内存分配 189 | lockedg guintptr // 和 G 的lockedm对应 190 | freelink *m // on sched.freem 191 | } 192 | 复制代码 193 | ``` 194 | 195 | #### P的内部结构 196 | 197 | ```go 198 | type p struct { 199 | id int32 200 | status uint32 // P的状态 201 | link puintptr // 下一个P, P链表 202 | m muintptr // 拥有这个P的M 203 | mcache *mcache 204 | 205 | // P本地runnable状态的G队列,无锁访问 206 | runqhead uint32 207 | runqtail uint32 208 | runq [256]guintptr 209 | 210 | runnext guintptr // 一个比runq优先级更高的runnable G 211 | 212 | // 状态为dead的G链表,在获取G时会从这里面获取 213 | gFree struct { 214 | gList 215 | n int32 216 | } 217 | 218 | gcBgMarkWorker guintptr // (atomic) 219 | gcw gcWork 220 | 221 | } 222 | 复制代码 223 | ``` 224 | 225 | P有以下5种状态: 226 | 227 | | 状态 | 值 | 含义 | 228 | | --------- | ---- | ------------------------------------------------------------ | 229 | | _Pidle | 0 | 刚刚被分配,还没有进行进行初始化。 | 230 | | _Prunning | 1 | 当 M 与 P 绑定调用 acquirep 时,P 的状态会改变为 _Prunning。 | 231 | | _Psyscall | 2 | 正在执行系统调用。 | 232 | | _Pgcstop | 3 | 暂停运行,此时系统正在进行 GC,直至 GC 结束后才会转变到下一个状态阶段。 | 233 | | _Pdead | 4 | 废弃,不再使用。 | 234 | 235 | #### 调度器的内部结构 236 | 237 | ```go 238 | type schedt struct { 239 | 240 | lock mutex 241 | 242 | midle muintptr // 空闲M链表 243 | nmidle int32 // 空闲M数量 244 | nmidlelocked int32 // 被锁住的M的数量 245 | mnext int64 // 已创建M的数量,以及下一个M ID 246 | maxmcount int32 // 允许创建最大的M数量 247 | nmsys int32 // 不计入死锁的M数量 248 | nmfreed int64 // 累计释放M的数量 249 | 250 | pidle puintptr // 空闲的P链表 251 | npidle uint32 // 空闲的P数量 252 | 253 | runq gQueue // 全局runnable的G队列 254 | runqsize int32 // 全局runnable的G数量 255 | 256 | // Global cache of dead G's. 257 | gFree struct { 258 | lock mutex 259 | stack gList // Gs with stacks 260 | noStack gList // Gs without stacks 261 | n int32 262 | } 263 | 264 | // freem is the list of m's waiting to be freed when their 265 | // m.exited is set. Linked through m.freelink. 266 | freem *m 267 | } 268 | ``` 269 | 270 | ## 为什么要有P 271 | 272 | 如果是想实现本地队列、Work Stealing 算法,那为什么不直接在 M 上加呢,M 也照样可以实现类似的功能。为什么又要再多加一个组件P? 273 | 274 | 结合 `M`的定位来看,若这么做,有以下问题。 275 | 276 | - 一般来讲,`M` 的数量都会多于 `P`。像在 golang 中,**M 的数量最大限制是 10000**,**P 的默认数量的 CPU 核数**。另外由于` M` 的属性,也就是如果存在系统阻塞调用,阻塞了`M`,又不够用的情况下,`M` 会不断增加。 277 | - `M `不断增加的话,如果本地队列挂载在 `M` 上,那就意味着本地队列也会随之增加。这显然是不合理的,因为本地队列的管理会变得复杂,且 `Work Stealing` 性能会大幅度下降。 278 | - `M` 被系统调用阻塞后,我们是期望把他既有未执行的任务分配给其他继续运行的,而不是一阻塞就导致全部停止。 279 | 280 | 因此使用 M 是不合理的,那么引入新的组件 `P`,把本地队列关联到 `P` 上,就能很好的解决这个问题: 281 | 282 | - 每个 `P` 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。 283 | - 每个 `P` 相对的平衡上,在 GMP 模型中也实现了 `Work Stealing `算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 `G` 来运行,减少空转,提高了资源利用率。 284 | 285 | > 参考: 286 | > 287 | > - [动图图解!GMP模型里为什么要有P?背后的原因让人暖心](https://mp.weixin.qq.com/s/O_GPwa71zqcpIkNdlkWYnQ) 288 | > - [Golang并发调度的GMP模型](https://juejin.cn/post/6886321367604527112) 289 | > - [再见 Go 面试官:GMP 模型,为什么要有 P?](https://mp.weixin.qq.com/s/an7dml9NLOhqOZjEGLdEEw) -------------------------------------------------------------------------------- /golang/deep/map.md: -------------------------------------------------------------------------------- 1 | # Go语言深度解析之map 2 | 3 | ## map是什么 4 | 5 | `map`在计算机科学里,被称为相关数组、map、符号表或者字典,是由一组 `` 对组成的抽象数据结构,并且同一个 key 只会出现一次。 6 | 7 | 和 map 相关的操作主要是: 8 | 9 | 1. 增加一个 k-v 对 —— add or insert; 10 | 2. 删除一个 k-v 对 —— remove or delete; 11 | 3. 修改某个 k 对应的 v —— update; 12 | 4. 查询某个 k 对应的 v —— query; 13 | 14 | 简单说就是最基本的 `增删查改`。 15 | 16 | map的底层结构是`hmap`(即hashmap的缩写),**核心元素是一个由若干个桶(`bucket`,结构为`bmap`)组成的数组**,下面是hmap的结构体: 17 | 18 | ```go 19 | // go 1.14 src/runtime/map.go:114 20 | // A header for a Go map. 21 | type hmap struct { 22 | count int // 元素个数,使用len(map)时返回该值 23 | flags uint8 24 | B uint8 // 说明包含2^B个buckets,bucket中存储了key-value 25 | noverflow uint16 // 溢出的buckets近似数; 26 | hash0 uint32 // hash种子 27 | 28 | buckets unsafe.Pointer // 指向buckets数组,大小为 2^B,如果元素个数为0,就为 nil. 29 | oldbuckets unsafe.Pointer // 扩容的时候,buckets 长度会是 oldbuckets 的两倍 30 | nevacuate uintptr // 指示扩容进度,小于此地址的 buckets 迁移完成 31 | 32 | extra *mapextra // 用于扩展 33 | } 34 | ``` 35 | 36 | buckets 是一个指针,最终它指向的是一个结构体`bmap`: 37 | 38 | ```go 39 | // sgo 1.14 src/runtime/map.go 40 | // A bucket for a Go map. 41 | type bmap struct { 42 | tophash [bucketCnt]uint8 43 | } 44 | ``` 45 | 46 | 但这只是表面的结构,编译期间会给它动态地创建一个新的结构: 47 | 48 | ```go 49 | type bmap struct { 50 | topbits [8]uint8 // 根据key计算出来的hash值的 高8位 来决定key到底落入桶内的哪个位置 51 | keys [8]keytype // 存储key的数组 52 | values [8]valuetype // 存储vlaue的数组 53 | pad uintptr 54 | overflow uintptr // 指向扩容bucket的指针 55 | } 56 | ``` 57 | 58 | 59 | 60 | 注意到 key 和 value 是各自放在一起的,并不是 `key/value/key/value/...` 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding 字段,节省内存空间。 61 | 62 | **举个例子**,有这样一个类型的 map: 63 | 64 | ``` 65 | map[int64]int8 66 | ``` 67 | 68 | 如果按照 `key/value/key/value/...` 这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,这种形式 `key/key/.../value/value/...`,则只需要在最后添加 padding。 69 | 70 | 每个 bucket 设计成最多只能放 `8个key-value对`,如果有第 9 个 key-value 落入当前的 bucket,那就需要再构建一个 bucket ,通过 `overflow` 指针连接起来。 71 | 72 | **下面来用一个整体的图表示map的结构:** 73 | 74 | 75 | 76 | 当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,**会把 bmap 标记为不含指针**,这样可以避免 gc 时扫描整个 hmap。但是,我们看 bmap 其实有一个 overflow 的字段,是指针类型的,破坏了 bmap 不含指针的设想,这时会把 overflow 移动到 extra 字段来。 77 | 78 | ```go 79 | // go 1.14 src/runtime/map.go 80 | type mapextra struct { 81 | // overflow contains overflow buckets for hmap.buckets. 82 | // oldoverflow contains overflow buckets for hmap.oldbuckets. 83 | overflow *[]*bmap 84 | oldoverflow *[]*bmap 85 | 86 | // nextOverflow holds a pointer to a free overflow bucket. 87 | nextOverflow *bmap 88 | } 89 | ``` 90 | 91 | ## map的操作 92 | 93 | ### 创建 94 | 95 | ```go 96 | ageMp := make(map[string]int) 97 | // 指定 map 长度 98 | ageMp := make(map[string]int, 8) 99 | // 创建并初始化内容 100 | ageMp := map[string]int{“id”:1,"age":22} 101 | // ageMp 为 nil,不能向其添加元素,会直接panic: assign to entry in nil map. 因此需要初始化 102 | var ageMp map[string]int 103 | ``` 104 | 105 | **注意:**map的声明的时候默认值是**nil** ,此时进行取值,返回的是**对应类型的零值**(不存在也是返回零值)。 106 | 107 | ```go 108 | var m map[int]bool 109 | v, ok := m[1] 110 | fmt.Println(v, ok) // false false 111 | ``` 112 | 113 | ### 插入&删除&更新&查询 114 | 115 | ```go 116 | // 插入 117 | m[1] = "hello world" 118 | 119 | // 删除,key不存在则啥也不干 120 | delete(m, 1) 121 | 122 | // 更新 123 | m[1] = "Hello World" 124 | 125 | // 查询,key不存在返回value类型的零值 有三种查询方式 126 | i := m[1] 127 | i, ok := m[1] 128 | _, ok := m[1] 129 | ``` 130 | 131 | ### 遍历 132 | 133 | map本身是**无序的**,在遍历的时候并不会按照你传入的顺序,进行传出。 134 | 135 | ```go 136 | package main 137 | 138 | import ( 139 | "fmt" 140 | "sort" 141 | ) 142 | 143 | func main() { 144 | m := make(map[int]string) 145 | //插入数据 146 | for i := 0; i < 50; i++ { 147 | m[i] = fmt.Sprintf("用户%v", i) 148 | } 149 | 150 | //正常遍历 map中的内容是无序的 151 | for k, v := range m { 152 | fmt.Println(k, v) 153 | } 154 | 155 | fmt.Println("============== 分割线 ================") 156 | 157 | //若想要获得map中的有序值 158 | //可以先用一个slice保存key值 159 | var key []int 160 | for k := range m { 161 | key = append(key, k) 162 | } 163 | 164 | //再用sort包进行排序 165 | sort.Ints(key) 166 | 167 | //最后再遍历,此时就是有序的了 168 | for k := range key { 169 | fmt.Println(k, m[k]) 170 | } 171 | } 172 | ``` 173 | 174 | ### 函数传参 175 | 176 | Golang中是没有引用传递的,均为值传递。这意味着传递的是数据的拷贝。那么map本身是**引用类型**,作为形参或返回参数的时候,传递的是**值的拷贝,而值是地址**,**扩容**时也**不会改变**这个地址。 177 | 178 | ```go 179 | package main 180 | 181 | import "fmt" 182 | 183 | func main() { 184 | var m map[int]int 185 | m = make(map[int]int, 1) 186 | fmt.Printf("m 原始的地址是:%p\n", m) 187 | changeM(m) 188 | fmt.Printf("m 改变后地址是:%p\n", m) 189 | fmt.Println("m 长度是", len(m)) 190 | fmt.Println("m 参数是", m) 191 | } 192 | 193 | // 改变map的函数 194 | func changeM(m map[int]int) { 195 | fmt.Printf("m 函数开始时地址是:%p\n", m) 196 | var max = 5 197 | for i := 0; i < max; i++ { 198 | m[i] = 2 199 | } 200 | fmt.Printf("m 在函数返回前地址是:%p\n", m) 201 | } 202 | ``` 203 | 204 | 结果: 205 | 206 | ``` 207 | m 原始地址是:0xc42007a180 208 | m 函数开始时地址是:0xc42007a180 209 | m 在函数返回前地址是:0xc42007a180 210 | m 改变后地址是:0xc42007a180 211 | m 长度是 5 212 | m 参数是 map[3:2 4:2 0:2 1:2 2:2] 213 | ``` 214 | 215 | ## map的hash计算 216 | 217 | 在第一小节中我们知道bmap中存储的是key-value值,那么具体key是分配到哪个bucket呢?也就是bmap中的tophash是如何计算? 218 | 219 | 具体实现: 220 | 221 | ```go 222 | // go 1.14 src/runtime/map.go 223 | func tophash(hash uintptr) uint8 { 224 | top := uint8(hash >> (sys.PtrSize*8 - 8)) 225 | if top < minTopHash { 226 | top += minTopHash 227 | } 228 | return top 229 | } 230 | ``` 231 | 232 | key 经过哈希计算后得到哈希值,共 64 个 bit 位(64位机)计算它到底要落在哪个桶时,**只会用到最后 B 个 bit 位**。如果 B = 5,那么桶的数量,也就是 buckets 数组的长度是 25 = 32。 233 | 234 | **例如**,现在有一个 key 经过哈希函数计算后,得到的哈希结果是: 235 | 236 | ``` 237 | 10010111 | 000011110110110010001111001010100010010110010101010 │ 00110 238 | ``` 239 | 240 | 用最后的 5 个 bit 位,也就是 `00110`,值为 6,也就是 **6 号桶**。这个操作实际上就是取余操作,但是取余开销太大,所以代码实现上用的**位操作**代替。 241 | 242 | 再**用哈希值的高 8 位,找到此 key 在 bucket 中的位置**,这是在寻找已有的 key。最开始桶内还没有 key,新加入的 key 会找到第一个空位放入。 243 | 244 | buckets 编号就是桶编号,当两个不同的 key 落在同一个桶中,也就是发生了哈希冲突。冲突的解决手段是用链表法。这样,在查找某个 key 时,先找到对应的桶,再去遍历 bucket 中的 key。 245 | 246 | 247 | 248 | 上图中,假定 B = 5,所以 bucket 总数就是 25 = 32。首先计算出待查找 key 的哈希,使用低 5 位 `00110`,找到对应的 6 号 bucket,使用高 8 位 `10010111`,对应十进制 151,在 6 号 bucket 中寻找 tophash 值(HOB hash)为 151 的 key,找到了 2 号槽位,这样整个查找过程就结束了。 249 | 250 | **如果在 bucket 中没找到,并且 overflow 不为空,还要继续去 overflow bucket 中寻找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket**。 251 | 252 | 具体来看源码: 253 | 254 | ```go 255 | // go 1.14 src/runtime.go 256 | func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { 257 | // …… 258 | // 如果 h 什么都没有,返回零值 259 | if h == nil || h.count == 0 { 260 | return unsafe.Pointer(&zeroVal[0]) 261 | } 262 | // 写和读冲突 263 | if h.flags&hashWriting != 0 { 264 | throw("concurrent map read and map write") 265 | } 266 | // 不同类型 key 使用的 hash 算法在编译期确定 267 | alg := t.key.alg 268 | // 计算哈希值,并且加入 hash0 引入随机性 269 | hash := alg.hash(key, uintptr(h.hash0)) 270 | // 比如 B=5,那 m 就是31,二进制是全 1 271 | // 求 bucket num 时,将 hash 与 m 相与, 272 | // 达到 bucket num 由 hash 的低 8 位决定的效果 273 | m := uintptr(1)<>= 1 283 | } 284 | // 求出 key 在老的 map 中的 bucket 位置 285 | oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize))) 286 | // 如果 oldb 没有搬迁到新的 bucket 287 | // 那就在老的 bucket 中寻找 288 | if !evacuated(oldb) { 289 | b = oldb 290 | } 291 | } 292 | // 计算出高 8 位的 hash 293 | // 相当于右移 56 位,只取高8位 294 | top := uint8(hash >> (sys.PtrSize*8 - 8)) 295 | // 增加一个 minTopHash 296 | if top < minTopHash { 297 | top += minTopHash 298 | } 299 | for { 300 | // 遍历 8 个 bucket 301 | for i := uintptr(0); i < bucketCnt; i++ { 302 | // tophash 不匹配,继续 303 | if b.tophash[i] != top { 304 | continue 305 | } 306 | // tophash 匹配,定位到 key 的位置 307 | k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 308 | // key 是指针 309 | if t.indirectkey { 310 | // 解引用 311 | k = *((*unsafe.Pointer)(k)) 312 | } 313 | // 如果 key 相等 314 | if alg.equal(key, k) { 315 | // 定位到 value 的位置 316 | v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 317 | // value 解引用 318 | if t.indirectvalue { 319 | v = *((*unsafe.Pointer)(v)) 320 | } 321 | return v 322 | } 323 | } 324 | // bucgoket 找完(还没找到),继续到 overflow bucket 里找 325 | b = b.overflow(t) 326 | // overflow bucket 也找完了,说明没有目标 key 327 | // 返回零值 328 | if b == nil { 329 | return unsafe.Pointer(&zeroVal[0]) 330 | } 331 | } 332 | } 333 | ``` 334 | 335 | ## map的扩容 336 | 337 | 使用哈希表的目的就是要快速查找到目标 key,然而,随着向 map 中添加的 key 越来越多,key 发生碰撞的概率也越来越大。bucket 中的 8 个 cell 会被逐渐塞满,查找、插入、删除 key 的效率也会越来越低。最理想的情况是一个 bucket 只装一个 key,这样,就能达到 `O(1)` 的效率,但这样空间消耗太大,用空间换时间的代价太高。 338 | 339 | Go 语言采用一个 bucket 里装载 8 个 key,定位到某个 bucket 后,还需要再定位到具体的 key,这实际上又用了时间换空间。 340 | 341 | 当然,这样做,要有一个度,不然所有的 key 都落在了同一个 bucket 里,直接退化成了链表,各种操作的效率直接降为 O(n),是不行的。 342 | 343 | 因此,需要有一个指标来衡量前面描述的情况,这就是 `装载因子`。Go 源码里这样定义 `装载因子`: 344 | 345 | ``` 346 | loadFactor := count / (2^B) 347 | ``` 348 | 349 | count 就是 map 的元素个数,2^B 表示 bucket 数量。 350 | 351 | **再来说触发 map 扩容的时机**:在向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容: 352 | 353 | 1. 装载因子超过阈值,源码里定义的阈值是 6.5。 354 | 355 | 2. overflow 的 bucket 数量过多: 356 | 357 | 当 B 小于 15,也就是 bucket 总数 2^B 小于 2^15 时,如果 overflow 的 bucket 数量超过 2^B; 358 | 359 | 当 B >= 15,也就是 bucket 总数 2^B 大于等于 2^15,如果 overflow 的 bucket 数量超过 2^15。 360 | 361 | 对应扩容条件的源码如下: 362 | 363 | ```go 364 | // src/runtime/hashmap.go/mapassign 365 | // 触发扩容时机 366 | if !h.growing() && (overLoadFactor(int64(h.count), h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { 367 | hashGrow(t, h) 368 | } 369 | // 装载因子超过 6.5 370 | func overLoadFactor(count int64, B uint8) bool { 371 | return count >= bucketCnt && float32(count) >= loadFactor*float32((uint64(1)<= uint16(1)<= 1<<15 379 | } 380 | ``` 381 | 382 | **解释一下:** 383 | 384 | 第 1 点:我们知道,每个 bucket 有 8 个空位,在没有溢出,且所有的桶都装满了的情况下,装载因子算出来的结果是 8。因此当装载因子超过 6.5 时,表明很多 bucket 都快要装满了,查找效率和插入效率都变低了。在这个时候进行扩容是有必要的。 385 | 386 | 第 2 点:是对第 1 点的补充。就是说在装载因子比较小的情况下,这时候 map 的查找和插入效率也很低,而第 1 点识别不出来这种情况。表面现象就是计算装载因子的分子比较小,即 map 里元素总数少,但是 bucket 数量多(真实分配的 bucket 数量多,包括大量的 overflow bucket)。 387 | 388 | ## 并发中的map 389 | 390 | ### 安全性 391 | 392 | 举例证明并发中的map是不安全的: 393 | 394 | ```go 395 | package main 396 | 397 | import ( 398 | "fmt" 399 | "time" 400 | ) 401 | 402 | func main() { 403 | var m = map[int]int{1: 1} 404 | 405 | go func() { 406 | for i := 0; i < 1000; i++ { 407 | m[i] = i 408 | } 409 | }() 410 | 411 | go func() { 412 | for i := 0; i < 1000; i++ { 413 | m[i] = i 414 | } 415 | }() 416 | 417 | time.Sleep(3 * time.Second) 418 | fmt.Println(m) 419 | 420 | } 421 | ``` 422 | 423 | 会发现有这样的报错: 424 | 425 | ```go 426 | fatal error: concurrent map read and map write 427 | ``` 428 | 429 | **根本原因就是:并发的去读写map结构的数据了。** 430 | 431 | ### 处理方案&优缺点 432 | 433 | 解决方案就是加锁: 434 | 435 | ```go 436 | package main 437 | 438 | import ( 439 | "fmt" 440 | "sync" 441 | "time" 442 | ) 443 | 444 | func main() { 445 | var m = map[int]int{1: 1} 446 | var rw sync.RWMutex 447 | go func() { 448 | for i := 0; i < 1000; i++ { 449 | rw.Lock() 450 | m[i] = i 451 | rw.Unlock() 452 | } 453 | }() 454 | 455 | go func() { 456 | for i := 0; i < 1000; i++ { 457 | rw.Lock() 458 | m[1] = i 459 | rw.Unlock() 460 | } 461 | }() 462 | 463 | time.Sleep(3 * time.Second) 464 | fmt.Println(m) 465 | } 466 | ``` 467 | 468 | > 优点:实现简单粗暴,好理解 469 | > 缺点:锁的粒度为整个map,存在优化空间 470 | > 适用场景:all 471 | 472 | ### 官方处理方案 & 优缺点 473 | 474 | 在程序设计中,想增加运行的速度,那么必然要有另外的牺牲,很容易想到“空间换时间”的方案: 475 | 476 | ```go 477 | package main 478 | 479 | import ( 480 | "fmt" 481 | "sync" 482 | "time" 483 | ) 484 | 485 | func main() { 486 | m := sync.Map{} 487 | m.Store(1, 1) //初始值 488 | go func() { 489 | for i := 0; i < 1000; i++ { 490 | m.Store(i, i) 491 | } 492 | }() 493 | 494 | go func() { 495 | for i := 0; i < 1000; i++ { 496 | m.Store(i, i) 497 | } 498 | }() 499 | 500 | time.Sleep(3 * time.Second) 501 | fmt.Println(m.Load(1)) //根据键取出值 502 | } 503 | ``` 504 | 505 | 运行完呢,会发现,其实是不会报错的。因为sync.Map里头已经实现了一套加锁的机制,让你更方便地使用map。 506 | 507 | **sync.Map的原理介绍**:sync.Map里头有两个map一个是**专门用于读**的`read map`,另一个是才是**提供读写**的`dirty map`;优先读read map,若不存在则加锁穿透读dirty map,同时记录一个未从read map读到的计数,当计数到达一定值,就将read map用dirty map进行覆盖。 508 | 509 | > 优点:是官方推荐的;通过空间换时间的方式;读写分离; 510 | > 缺点:不适用于大量写的场景,这样会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。 511 | > 适用场景:大量读,少量写 512 | 513 | ## 总结 514 | 515 | - Go 语言中,通过哈希查找表实现 map,用链表法解决哈希冲突。 516 | - 通过 key 的哈希值将 key 散落到不同的桶中,每个桶中有 8 个 cell。哈希值的低位决定桶序号,高位标识同一个桶中的不同 key。 517 | - 当向桶中添加了很多 key,造成元素过多,或者溢出桶太多,就会触发扩容。扩容分为等量扩容和 2 倍容量扩容。扩容后,原来一个 bucket 中的 key 一分为二,会被重新分配到两个桶中。 518 | - 扩容过程是渐进的,主要是防止一次扩容需要搬迁的 key 数量过多,引发性能问题。触发扩容的时机是增加了新元素,bucket 搬迁的时机则发生在赋值、删除期间,每次最多搬迁两个 bucket。 519 | 520 | - 注意并发条件下的map和sync.Map 521 | 522 | 523 | 524 | > 参考: 525 | > 526 | > - [深度解密Go语言之map](https://mp.weixin.qq.com/s/2CDpE5wfoiNXm1agMAq4wA) 527 | > - [golang map源码详解 (juejin.cn)](https://juejin.cn/post/6844903517530882061) 528 | > - [由浅入深聊聊Golang的map_咖啡色的羊驼-CSDN博客_golang map](https://blog.csdn.net/u011957758/article/details/82846609) -------------------------------------------------------------------------------- /golang/deep/memory_distribution.md: -------------------------------------------------------------------------------- 1 | # Go语言深度解析之内存分配 2 | 3 | **Go语言内置运行时(就是runtime),抛弃了传统的内存分配方式,改为自主管理**。这样可以自主地实现更好的内存使用模式,比如内存池、预分配等等。这样,不会每次内存分配都需要进行系统调用。 4 | 5 | Golang运行时的内存分配算法主要源自 Google 为 C 语言开发的`TCMalloc算法`,全称`Thread-Caching Malloc`。核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。 6 | 7 | ## 基础概念 8 | 9 | Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。 10 | 11 | 申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB大小。 12 | 13 |  14 | 15 | `arena区域`就是我们所谓的**堆区**,**Go动态分配的内存都是在这个区域**,它把内存分割成`8KB`大小的页,一些页组合起来称为`mspan`。 16 | 17 | `bitmap区域`标识`arena`区域哪些地址保存了对象,并且用`4bit`标志位表示对象是否包含指针、`GC`标记信息。`bitmap`中一个`byte`大小的内存对应`arena`区域中4个指针大小(指针大小为 8B )的内存,所以`bitmap`区域的大小是`512GB/(4*8B)=16GB`。 18 | 19 |  20 | 21 |  22 | 23 | 从上图其实还可以看到bitmap的高地址部分指向arena区域的低地址部分,也就是说bitmap的地址是由高地址向低地址增长的。 24 | 25 | `spans区域`存放`mspan`(也就是一些`arena`分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以`spans`区域的大小就是`512GB/8KB*8B=512MB`。除以8KB是计算`arena`区域的页数,而最后乘以8是计算`spans`区域所有指针的大小。创建`mspan`的时候,按页填充对应的`spans`区域,在回收`object`时,根据地址很容易就能找到它所属的`mspan`。 26 | 27 | ## 内存管理单元 28 | 29 | `mspan`:Go中内存管理的基本单元,是由一片连续的`8KB`的页组成的大块内存。注意,这里的页和操作系统本身的页并不是一回事,它一般是操作系统页大小的几倍。一句话概括:`mspan`是一个包含起始地址、`mspan`规格、页的数量等内容的双端链表。 30 | 31 | 每个`mspan`按照它自身的属性`Size Class`的大小分割成若干个`object`,每个`object`可存储一个对象。并且会使用一个位图来标记其尚未使用的`object`。属性`Size Class`决定`object`大小,而`mspan`只会分配给和`object`尺寸大小接近的对象,当然,对象的大小要小于`object`大小。还有一个概念:`Span Class`,它和`Size Class`的含义差不多, 32 | 33 | ```go 34 | Size_Class = Span_Class / 2 35 | ``` 36 | 37 | 这是因为其实每个 `Size Class`有两个`mspan`,也就是有两个`Span Class`。其中一个分配给含有指针的对象,另一个分配给不含有指针的对象。这会给垃圾回收机制带来利好,之后的文章再谈。 38 | 39 | 如下图,`mspan`由一组连续的页组成,按照一定大小划分成`object`。 40 | 41 |  42 | 43 | Go1.9.2里`mspan`的`Size Class`共有67种,每种`mspan`分割的object大小是8*2n的倍数,这个是写死在代码里的: 44 | 45 | ```go 46 | // path: /usr/local/go/src/runtime/sizeclasses.go 47 | 48 | const _NumSizeClasses = 67 49 | 50 | var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768} 51 | ``` 52 | 53 | 根据`mspan`的`Size Class`可以得到它划分的`object`大小。 比如`Size Class`等于3,`object`大小就是32B。 32B大小的object可以存储对象大小范围在17B~32B的对象。而对于微小对象(小于16B),分配器会将其进行合并,将几个对象分配到同一个`object`中。 54 | 55 | 数组里最大的数是32768,也就是32KB,超过此大小就是大对象了,它会被特别对待,这个稍后会再介绍。顺便提一句,类型`Size Class`为0表示大对象,它实际上直接由堆内存分配,而小对象都要通过`mspan`来分配。 56 | 57 | 对于mspan来说,它的`Size Class`会决定它所能分到的页数,这也是写死在代码里的: 58 | 59 | ```go 60 | // path: /usr/local/go/src/runtime/sizeclasses.go 61 | 62 | const _NumSizeClasses = 67 63 | 64 | var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4} 65 | ``` 66 | 67 | 比如当我们要申请一个`object`大小为`32B`的`mspan`的时候,在class_to_size里对应的索引是3,而索引3在`class_to_allocnpages`数组里对应的页数就是1。 68 | 69 | `mspan`结构体定义: 70 | 71 | ```go 72 | // path: /usr/local/go/src/runtime/mheap.go 73 | 74 | type mspan struct { 75 | //链表前向指针,用于将span链接起来 76 | next *mspan 77 | 78 | //链表前向指针,用于将span链接起来 79 | prev *mspan 80 | 81 | // 起始地址,也即所管理页的地址 82 | startAddr uintptr 83 | 84 | // 管理的页数 85 | npages uintptr 86 | 87 | // 块个数,表示有多少个块可供分配 88 | nelems uintptr 89 | 90 | //分配位图,每一位代表一个块是否已分配 91 | allocBits *gcBits 92 | 93 | // 已分配块的个数 94 | allocCount uint16 95 | 96 | // class表中的class ID,和Size Classs相关 97 | spanclass spanClass 98 | 99 | // class表中的对象大小,也即块大小 100 | elemsize uintptr 101 | } 102 | ``` 103 | 104 | 我们将`mspan`放到更大的视角来看: 105 | 106 |  107 | 108 | 上图可以看到有两个`S`指向了同一个`mspan`,因为这两个`S`指向的`P`是同属一个`mspan`的。所以,通过`arena`上的地址可以快速找到指向它的`S`,通过`S`就能找到`mspan`,回忆一下前面我们说的`mspan`区域的每个指针对应一页。 109 | 110 | 假设最左边第一个`mspan`的`Size Class`等于10,根据前面的`class_to_size`数组,得出这个`msapn`分割的`object`大小是144B,算出可分配的对象个数是`8KB/144B=56.89`个,取整56个,所以会有一些内存浪费掉了,Go的源码里有所有`Size Class`的`mspan`浪费的内存的大小;再根据`class_to_allocnpages`数组,得到这个`mspan`只由1个`page`组成;假设这个`mspan`是分配给无指针对象的,那么`spanClass`等于20。 111 | 112 | `startAddr`直接指向`arena`区域的某个位置,表示这个`mspan`的起始地址,`allocBits`指向一个位图,每位代表一个块是否被分配了对象;`allocCount`则表示总共已分配的对象个数。 113 | 114 | 这样,左起第一个`mspan`的各个字段参数就如下图所示: 115 | 116 |  117 | 118 | ## 内存管理组件 119 | 120 | 内存分配由内存分配器完成。分配器由3种组件构成:`mcache`, `mcentral`, `mheap`。 121 | 122 | ### mcache 123 | 124 | `mcache`:每个工作线程都会绑定一个mcache,本地缓存可用的`mspan`资源,这样就可以直接给Goroutine分配,因为不存在多个Goroutine竞争的情况,所以不会消耗锁资源。 125 | 126 | `mcache`的结构体定义: 127 | 128 | ```go 129 | //path: /usr/local/go/src/runtime/mcache.go 130 | 131 | type mcache struct { 132 | alloc [numSpanClasses]*mspan 133 | } 134 | 135 | numSpanClasses = _NumSizeClasses << 1 136 | ``` 137 | 138 | `mcache`用`Span Classes`作为索引管理多个用于分配的`mspan`,它包含所有规格的`mspan`。它是`_NumSizeClasses`的2倍,也就是`67*2=134`,为什么有一个两倍的关系,前面我们提到过:为了加速之后内存回收的速度,数组里一半的`mspan`中分配的对象不包含指针,另一半则包含指针。 139 | 140 | 对于无指针对象的`mspan`在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象。 后面的垃圾回收文章会再讲到,这次先到这里。 141 | 142 |  143 | 144 | [ 145 | ](https://user-images.githubusercontent.com/7698088/54191324-a86bfd00-44f0-11e9-9039-3b64d39036d9.png)`mcache`在初始化的时候是没有任何`mspan`资源的,在使用过程中会动态地从`mcentral`申请,之后会缓存下来。当对象小于等于32KB大小时,使用`mcache`的相应规格的`mspan`进行分配。 146 | 147 | ### mcentral 148 | 149 | `mcentral`:为所有`mcache`提供切分好的`mspan`资源。每个`central`保存一种特定大小的全局`mspan`列表,包括已分配出去的和未分配出去的。 每个`mcentral`对应一种`mspan`,而`mspan`的种类导致它分割的`object`大小不同。当工作线程的`mcache`中没有合适(也就是特定大小的)的`mspan`时就会从`mcentral`获取。 150 | 151 | `mcentral`被所有的工作线程共同享有,存在多个Goroutine竞争的情况,因此会消耗锁资源。结构体定义: 152 | 153 | ```go 154 | //path: /usr/local/go/src/runtime/mcentral.go 155 | 156 | type mcentral struct { 157 | // 互斥锁 158 | lock mutex 159 | 160 | // 规格 161 | sizeclass int32 162 | 163 | // 尚有空闲object的mspan链表 164 | nonempty mSpanList 165 | 166 | // 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表 167 | empty mSpanList 168 | 169 | // 已累计分配的对象个数 170 | nmalloc uint64 171 | } 172 | ``` 173 | 174 |  175 | 176 | `empty`表示这条链表里的`mspan`都被分配了`object`,或者是已经被`cache`取走了的`mspan`,这个`mspan`就被那个工作线程独占了。而`nonempty`则表示有空闲对象的`mspan`列表。每个`central`结构体都在`mheap`中维护。 177 | 178 | 简单说下`mcache`从`mcentral`获取和归还`mspan`的流程: 179 | 180 | - 获取 181 | 加锁;从`nonempty`链表找到一个可用的`mspan`;并将其从`nonempty`链表删除;将取出的`mspan`加入到`empty`链表;将`mspan`返回给工作线程;解锁。 182 | - 归还 183 | 加锁;将`mspan`从`empty`链表删除;将`mspan`加入到`nonempty`链表;解锁。 184 | 185 | ### mheap 186 | 187 | `mheap`:代表Go程序持有的所有堆空间,Go程序使用一个`mheap`的全局对象`_mheap`来管理堆内存。 188 | 189 | 当`mcentral`没有空闲的`mspan`时,会向`mheap`申请。而`mheap`没有资源时,会向操作系统申请新内存。`mheap`主要用于大对象的内存分配,以及管理未切割的`mspan`,用于给`mcentral`切割成小对象。 190 | 191 | 同时我们也看到,`mheap`中含有所有规格的`mcentral`,所以,当一个`mcache`从`mcentral`申请`mspan`时,只需要在独立的`mcentral`中使用锁,并不会影响申请其他规格的`mspan`。 192 | 193 | `mheap`结构体定义: 194 | 195 | ```go 196 | //path: /usr/local/go/src/runtime/mheap.go 197 | 198 | type mheap struct { 199 | lock mutex 200 | 201 | // spans: 指向mspans区域,用于映射mspan和page的关系 202 | spans []*mspan 203 | 204 | // 指向bitmap首地址,bitmap是从高地址向低地址增长的 205 | bitmap uintptr 206 | 207 | // 指示arena区首地址 208 | arena_start uintptr 209 | 210 | // 指示arena区已使用地址位置 211 | arena_used uintptr 212 | 213 | // 指示arena区末地址 214 | arena_end uintptr 215 | 216 | central [67*2]struct { 217 | mcentral mcentral 218 | pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte 219 | } 220 | } 221 | ``` 222 | 223 |  224 | 225 | 上图我们看到,bitmap和arena_start指向了同一个地址,这是因为bitmap的地址是从高到低增长的,所以他们指向的内存位置相同。 226 | 227 | ## 内存分配流程 228 | 229 | 上一篇文章[《Golang之变量去哪儿》](https://www.cnblogs.com/qcrao-2018/p/10453260.html)中我们提到了,变量是在栈上分配还是在堆上分配,是由逃逸分析的结果决定的。通常情况下,编译器是倾向于将变量分配到栈上的,因为它的开销小,最极端的就是”zero garbage”,所有的变量都会在栈上分配,这样就不会存在内存碎片,垃圾回收之类的东西。 230 | 231 | Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。 232 | 233 | 大体上的分配流程: 234 | 235 | - > 32KB 的对象,直接从mheap上分配; 236 | 237 | - <=16B 的对象使用mcache的tiny分配器分配; 238 | 239 | - (16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配; 240 | 241 | - 如果mcache没有相应规格大小的mspan,则向mcentral申请 242 | - 如果mcentral没有相应规格大小的mspan,则向mheap申请 243 | - 如果mheap中也没有合适大小的mspan,则向操作系统申请 244 | 245 | ## 总结 246 | 247 | Go语言的内存分配非常复杂,它的一个原则就是能复用的一定要复用。源码很难追,后面可能会再来一篇关于内存分配的源码阅读相关的文章。简单总结一下本文吧。 248 | 249 | 文章从一个比较粗的角度来看Go的内存分配,并没有深入细节。一般而言,了解它的原理,到这个程度也可以了。 250 | 251 | - Go在程序启动时,会向操作系统申请一大块内存,之后自行管理。 252 | - Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan可以分配特定大小的object。 253 | - mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存。 254 | - 极小对象会分配在一个object中,以节省资源,使用tiny分配器分配内存;一般小对象通过mspan分配内存;大对象则直接由mheap分配内存。 255 | 256 | > Reference: 257 | > 258 | > 1. [图解Go语言内存分配](https://qcrao.com/2019/03/13/graphic-go-memory-allocation/) 259 | -------------------------------------------------------------------------------- /golang/deep/reflect.md: -------------------------------------------------------------------------------- 1 | # Go语言深度解析之reflect 2 | 3 | ## 什么是反射 4 | 5 | 直接看维基百科上的定义: 6 | 7 | > 在计算机科学中,反射是指计算机程序在运行时(Run time)可以**访问、检测和修改它本身状态或行为的一种能力**。打个比方:反射就是程序在运行的时候能够“观察”并且修改自己的行为。 8 | 9 | 那我就要问个问题了:不用反射就不能在运行时访问、检测和修改它本身的状态和行为吗? 10 | 11 | 问题的回答,其实要首先理解什么叫访问、检测和修改它本身状态或行为,它的本质是什么? 12 | 13 | 实际上,它的本质是程序在运行期探知对象的类型信息和内存结构,不用反射能行吗?可以的!使用汇编语言,直接和内层打交道,什么信息不能获取?但是,当编程迁移到高级语言上来之后,就不行了!就只能通过`反射`来达到此项技能。 14 | 15 | 不同语言的反射模型不尽相同,有些语言还不支持反射。《Go 语言圣经》中是这样定义反射的: 16 | 17 | > Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。 18 | 19 | ## 为什么要用反射 20 | 21 | 需要反射的 2 个常见场景: 22 | 23 | 1. 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。 24 | 2. 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。 25 | 26 | 在讲反射的原理以及如何用之前,还是说几点不使用反射的理由: 27 | 28 | 1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。 29 | 2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。 30 | 3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。 31 | 32 | ## 反射是如何实现的 33 | 34 | 上一篇文章讲到了 `interface`,它是 Go 语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。 35 | 36 | Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。 37 | 38 | #### types 和 interface 39 | 40 | Go 语言中,每个变量都有一个静态类型,在编译阶段就确定了的,比如 `int, float64, []int` 等等。注意,这个类型是声明时候的类型,不是底层数据类型。 41 | 42 | Go 官方博客里就举了一个例子: 43 | 44 | ```go 45 | type MyInt int // 别名 46 | 47 | var i int 48 | var j MyInt 49 | ``` 50 | 51 | 尽管 i,j 的底层类型都是 int,但我们知道,他们是不同的静态类型,除非进行类型转换,否则,i 和 j 不能同时出现在等号两侧。j 的静态类型就是 `MyInt`。 52 | 53 | 反射主要与 interface{} 类型相关。前面一篇关于 interface 相关的文章已经探讨过 interface 的底层结构,这里再来复习一下。 54 | 55 | ```go 56 | type iface struct { 57 | tab *itab 58 | data unsafe.Pointer 59 | } 60 | 61 | type itab struct { 62 | inter *interfacetype 63 | _type *_type 64 | link *itab 65 | hash uint32 66 | bad bool 67 | inhash bool 68 | unused [2]byte 69 | fun [1]uintptr 70 | } 71 | ``` 72 | 73 | 其中 `itab` 由具体类型 `_type` 以及 `interfacetype` 组成。`_type` 表示具体类型,而 `interfacetype` 则表示具体类型实现的接口类型。 74 | 75 |  76 | 77 | 实际上,iface 描述的是非空接口,它包含方法;与之相对的是 `eface`,描述的是空接口,不包含任何方法,Go 语言里有的类型都 `“实现了”` 空接口。 78 | 79 | ```go 80 | type eface struct { 81 | _type *_type 82 | data unsafe.Pointer 83 | } 84 | ``` 85 | 86 | 相比 `iface`,`eface` 就比较简单了。只维护了一个 `_type` 字段,表示空接口所承载的具体的实体类型。`data` 描述了具体的值。 87 | 88 |  89 | 90 | 先明确一点:接口变量可以存储任何实现了接口定义的所有方法的变量。 91 | 92 | Go 语言中最常见的就是 `Reader` 和 `Writer` 接口: 93 | 94 | ```go 95 | type Reader interface { 96 | Read(p []byte) (n int, err error) 97 | } 98 | 99 | type Writer interface { 100 | Write(p []byte) (n int, err error) 101 | } 102 | ``` 103 | 104 | 接下来,就是接口之间的各种转换和赋值了: 105 | 106 | ```go 107 | var r io.Reader 108 | tty, err := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0) 109 | if err != nil { 110 | return nil, err 111 | } 112 | r = tty 113 | ``` 114 | 115 | 首先声明 `r` 的类型是 `io.Reader`,注意,这是 `r` 的静态类型,此时它的动态类型为 `nil`,并且它的动态值也是 `nil`。 116 | 117 | 之后,`r = tty` 这一语句,将 `r` 的动态类型变成 `*os.File`,动态值则变成非空,表示打开的文件对象。这时,r 可以用``对来表示为: ``。 118 | 119 |  120 | 121 | 注意看上图,此时虽然 `fun` 所指向的函数只有一个 `Read` 函数,其实 `*os.File` 还包含 `Write` 函数,也就是说 `*os.File` 其实还实现了 `io.Writer` 接口。因此下面的断言语句可以执行: 122 | 123 | ```go 124 | var w io.Writer 125 | w = r.(io.Writer) 126 | ``` 127 | 128 | 之所以用断言,而不能直接赋值,是因为 `r` 的静态类型是 `io.Reader`,并没有实现 `io.Writer` 接口。断言能否成功,看 `r` 的动态类型是否符合要求。 129 | 130 | 这样,w 也可以表示成 ``,仅管它和 `r` 一样,但是 w 可调用的函数取决于它的静态类型 `io.Writer`,也就是说它只能有这样的调用形式: `w.Write()` 。`w` 的内存形式如下图: 131 | 132 |  133 | 134 | 和 `r` 相比,仅仅是 `fun` 对应的函数变了:`Read -> Write`。 135 | 136 | 最后,再来一个赋值: 137 | 138 | ```go 139 | var empty interface{} 140 | empty = w 141 | ``` 142 | 143 | 由于 `empty` 是一个空接口,因此所有的类型都实现了它,w 可以直接赋给它,不需要执行断言操作。 144 | 145 |  146 | 147 | 从上面的三张图可以看到,interface 包含三部分信息:`_type` 是类型信息,`*data` 指向实际类型的实际值,`itab` 包含实际类型的信息,包括大小、包路径,还包含绑定在类型上的各种方法(图上没有画出方法),补充一下关于 os.File 结构体的图: 148 | 149 |  150 | 151 | ## 反射的基本函数 152 | 153 | reflect 包里定义了一个接口和一个结构体,即 `reflect.Type` 和 `reflect.Value`,它们提供很多函数来获取存储在接口里的类型信息。 154 | 155 | `reflect.Type` 主要提供关于类型相关的信息,所以它和 `_type` 关联比较紧密;`reflect.Value` 则结合 `_type` 和 `data` 两者,因此程序员可以获取甚至改变类型的值。 156 | 157 | reflect 包中提供了两个基础的关于反射的函数来获取上述的接口和结构体: 158 | 159 | ```go 160 | func TypeOf(i interface{}) Type 161 | func ValueOf(i interface{}) Value 162 | ``` 163 | 164 | `TypeOf` 函数用来提取一个接口中值的类型信息。由于它的输入参数是一个空的 `interface{}`,调用此函数时,实参会先被转化为 `interface{}`类型。这样,实参的类型信息、方法集、值信息都存储到 `interface{}` 变量里了。 165 | 166 | 看下源码: 167 | 168 | ```go 169 | func TypeOf(i interface{}) Type { 170 | eface := *(*emptyInterface)(unsafe.Pointer(&i)) 171 | return toType(eface.typ) 172 | } 173 | ``` 174 | 175 | 这里的 `emptyInterface` 和上面提到的 `eface` 是一回事(字段名略有差异,字段是相同的),且在不同的源码包:前者在 `reflect` 包,后者在 `runtime` 包。 `eface.typ` 就是动态类型。 176 | 177 | ```go 178 | type emptyInterface struct { 179 | typ *rtype 180 | word unsafe.Pointer 181 | } 182 | ``` 183 | 184 | 至于 `toType` 函数,只是做了一个类型转换: 185 | 186 | ```go 187 | func toType(t *rtype) Type { 188 | if t == nil { 189 | return nil 190 | } 191 | return t 192 | } 193 | ``` 194 | 195 | 注意,返回值 `Type` 实际上是一个接口,定义了很多方法,用来获取类型相关的各种信息,而 `*rtype` 实现了 `Type` 接口。 196 | 197 | ```go 198 | type Type interface { 199 | // 所有的类型都可以调用下面这些函数 200 | 201 | // 此类型的变量对齐后所占用的字节数 202 | Align() int 203 | 204 | // 如果是 struct 的字段,对齐后占用的字节数 205 | FieldAlign() int 206 | 207 | // 返回类型方法集里的第 `i` (传入的参数)个方法 208 | Method(int) Method 209 | 210 | // 通过名称获取方法 211 | MethodByName(string) (Method, bool) 212 | 213 | // 获取类型方法集里导出的方法个数 214 | NumMethod() int 215 | 216 | // 类型名称 217 | Name() string 218 | 219 | // 返回类型所在的路径,如:encoding/base64 220 | PkgPath() string 221 | 222 | // 返回类型的大小,和 unsafe.Sizeof 功能类似 223 | Size() uintptr 224 | 225 | // 返回类型的字符串表示形式 226 | String() string 227 | 228 | // 返回类型的类型值 229 | Kind() Kind 230 | 231 | // 类型是否实现了接口 u 232 | Implements(u Type) bool 233 | 234 | // 是否可以赋值给 u 235 | AssignableTo(u Type) bool 236 | 237 | // 是否可以类型转换成 u 238 | ConvertibleTo(u Type) bool 239 | 240 | // 类型是否可以比较 241 | Comparable() bool 242 | 243 | // 下面这些函数只有特定类型可以调用 244 | // 如:Key, Elem 两个方法就只能是 Map 类型才能调用 245 | 246 | // 类型所占据的位数 247 | Bits() int 248 | 249 | // 返回通道的方向,只能是 chan 类型调用 250 | ChanDir() ChanDir 251 | 252 | // 返回类型是否是可变参数,只能是 func 类型调用 253 | // 比如 t 是类型 func(x int, y ... float64) 254 | // 那么 t.IsVariadic() == true 255 | IsVariadic() bool 256 | 257 | // 返回内部子元素类型,只能由类型 Array, Chan, Map, Ptr, or Slice 调用 258 | Elem() Type 259 | 260 | // 返回结构体类型的第 i 个字段,只能是结构体类型调用 261 | // 如果 i 超过了总字段数,就会 panic 262 | Field(i int) StructField 263 | 264 | // 返回嵌套的结构体的字段 265 | FieldByIndex(index []int) StructField 266 | 267 | // 通过字段名称获取字段 268 | FieldByName(name string) (StructField, bool) 269 | 270 | // FieldByNameFunc returns the struct field with a name 271 | // 返回名称符合 func 函数的字段 272 | FieldByNameFunc(match func(string) bool) (StructField, bool) 273 | 274 | // 获取函数类型的第 i 个参数的类型 275 | In(i int) Type 276 | 277 | // 返回 map 的 key 类型,只能由类型 map 调用 278 | Key() Type 279 | 280 | // 返回 Array 的长度,只能由类型 Array 调用 281 | Len() int 282 | 283 | // 返回类型字段的数量,只能由类型 Struct 调用 284 | NumField() int 285 | 286 | // 返回函数类型的输入参数个数 287 | NumIn() int 288 | 289 | // 返回函数类型的返回值个数 290 | NumOut() int 291 | 292 | // 返回函数类型的第 i 个值的类型 293 | Out(i int) Type 294 | 295 | // 返回类型结构体的相同部分 296 | common() *rtype 297 | 298 | // 返回类型结构体的不同部分 299 | uncommon() *uncommonType 300 | } 301 | ``` 302 | 303 | 可见 `Type` 定义了非常多的方法,通过它们可以获取类型的一切信息,大家一定要完整的过一遍上面所有的方法。 304 | 305 | 注意到 `Type` 方法集的倒数第二个方法 `common` 306 | 返回的 `rtype`类型,它和上一篇文章讲到的 `_type` 是一回事,而且源代码里也注释了:两边要保持同步: 307 | 308 | ```go 309 | // rtype must be kept in sync with ../runtime/type.go:/^type._type. 310 | type rtype struct { 311 | size uintptr 312 | ptrdata uintptr 313 | hash uint32 314 | tflag tflag 315 | align uint8 316 | fieldAlign uint8 317 | kind uint8 318 | alg *typeAlg 319 | gcdata *byte 320 | str nameOff 321 | ptrToThis typeOff 322 | } 323 | ``` 324 | 325 | 所有的类型都会包含 `rtype` 这个字段,表示各种类型的公共信息;另外,不同类型包含自己的一些独特的部分。 326 | 327 | 比如下面的 `arrayType` 和 `chanType` 都包含 `rytpe`,而前者还包含 slice,len 等和数组相关的信息;后者则包含 `dir` 表示通道方向的信息。 328 | 329 | ```go 330 | // arrayType represents a fixed array type. 331 | type arrayType struct { 332 | rtype `reflect:"array"` 333 | elem *rtype // array element type 334 | slice *rtype // slice type 335 | len uintptr 336 | } 337 | 338 | // chanType represents a channel type. 339 | type chanType struct { 340 | rtype `reflect:"chan"` 341 | elem *rtype // channel element type 342 | dir uintptr // channel direction (ChanDir) 343 | } 344 | ``` 345 | 346 | 注意到,`Type` 接口实现了 `String()` 函数,满足 `fmt.Stringer` 接口,因此使用 `fmt.Println` 打印的时候,输出的是 `String()` 的结果。另外,`fmt.Printf()` 函数,如果使用 `%T` 来作为格式参数,输出的是 `reflect.TypeOf` 的结果,也就是动态类型。例如: 347 | 348 | ```go 349 | fmt.Printf("%T", 3) // int 350 | ``` 351 | 352 | 讲完了 `TypeOf` 函数,再来看一下 `ValueOf` 函数。返回值 `reflect.Value` 表示 `interface{}` 里存储的实际变量,它能提供实际变量的各种信息。相关的方法常常是需要结合类型信息和值信息。例如,如果要提取一个结构体的字段信息,那就需要用到 _type (具体到这里是指 structType) 类型持有的关于结构体的字段信息、偏移信息,以及 `*data` 所指向的内容 —— 结构体的实际值。 353 | 354 | 源码如下: 355 | 356 | ```go 357 | func ValueOf(i interface{}) Value { 358 | if i == nil { 359 | return Value{} 360 | } 361 | 362 | // …… 363 | return unpackEface(i) 364 | } 365 | 366 | // 分解 eface 367 | func unpackEface(i interface{}) Value { 368 | e := (*emptyInterface)(unsafe.Pointer(&i)) 369 | 370 | t := e.typ 371 | if t == nil { 372 | return Value{} 373 | } 374 | 375 | f := flag(t.Kind()) 376 | if ifaceIndir(t) { 377 | f |= flagIndir 378 | } 379 | return Value{t, e.word, f} 380 | } 381 | ``` 382 | 383 | 从源码看,比较简单:将先将 `i` 转换成 `*emptyInterface` 类型, 再将它的 `typ` 字段和 `word` 字段以及一个标志位字段组装成一个 `Value` 结构体,而这就是 `ValueOf` 函数的返回值,它包含类型结构体指针、真实数据的地址、标志位。 384 | 385 | Value 结构体定义了很多方法,通过这些方法可以直接操作 Value 字段 ptr 所指向的实际数据: 386 | 387 | ```go 388 | // 设置切片的 len 字段,如果类型不是切片,就会panic 389 | func (v Value) SetLen(n int) 390 | 391 | // 设置切片的 cap 字段 392 | func (v Value) SetCap(n int) 393 | 394 | // 设置字典的 kv 395 | func (v Value) SetMapIndex(key, val Value) 396 | 397 | // 返回切片、字符串、数组的索引 i 处的值 398 | func (v Value) Index(i int) Value 399 | 400 | // 根据名称获取结构体的内部字段值 401 | func (v Value) FieldByName(name string) Value 402 | 403 | // …… 404 | ``` 405 | 406 | `Value` 字段还有很多其他的方法。例如: 407 | 408 | ```go 409 | // 用来获取 int 类型的值 410 | func (v Value) Int() int64 411 | 412 | // 用来获取结构体字段(成员)数量 413 | func (v Value) NumField() int 414 | 415 | // 尝试向通道发送数据(不会阻塞) 416 | func (v Value) TrySend(x reflect.Value) bool 417 | 418 | // 通过参数列表 in 调用 v 值所代表的函数(或方法 419 | func (v Value) Call(in []Value) (r []Value) 420 | 421 | // 调用变参长度可变的函数 422 | func (v Value) CallSlice(in []Value) []Value 423 | ``` 424 | 425 | 不一一列举了,反正是非常多。可以去 `src/reflect/value.go` 去看看源码,搜索 `func (v Value)` 就能看到。 426 | 427 | 另外,通过 `Type()` 方法和 `Interface()` 方法可以打通 `interface`、`Type`、`Value` 三者。Type() 方法也可以返回变量的类型信息,与 reflect.TypeOf() 函数等价。Interface() 方法可以将 Value 还原成原来的 interface。 428 | 429 |  430 | 431 | 总结一下:`TypeOf()` 函数返回一个接口,这个接口定义了一系列方法,利用这些方法可以获取关于类型的所有信息; `ValueOf()` 函数返回一个结构体变量,包含类型信息以及实际值。 432 | 433 | 用一张图来串一下: 434 | 435 |  436 | 437 | 上图中,`rtye` 实现了 `Type` 接口,是所有类型的公共部分。emptyface 结构体和 eface 其实是一个东西,而 rtype 其实和 _type 是一个东西,只是一些字段稍微有点差别,比如 emptyface 的 word 字段和 eface 的 data 字段名称不同,但是数据型是一样的。 438 | 439 | ## 反射的三大定律 440 | 441 | 根据 Go 官方关于反射的博客,反射有三大定律: 442 | 443 | > 1. Reflection goes from interface value to reflection object. 444 | 445 | > 1. Reflection goes from reflection object to interface value. 446 | 447 | > 1. To modify a reflection object, the value must be settable. 448 | 449 | 第一条是最基本的:反射是一种检测存储在 `interface` 中的类型和值机制。这可以通过 `TypeOf` 函数和 `ValueOf` 函数得到。 450 | 451 | 第二条实际上和第一条是相反的机制,它将 `ValueOf` 的返回值通过 `Interface()` 函数反向转变成 `interface` 变量。 452 | 453 | 前两条就是说 `接口型变量` 和 `反射类型对象` 可以相互转化,反射类型对象实际上就是指的前面说的 `reflect.Type` 和 `reflect.Value`。 454 | 455 | 第三条不太好懂:如果需要操作一个反射变量,那么它必须是可设置的。反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。 456 | 457 | 举一个经典例子: 458 | 459 | ```go 460 | var x float64 = 3.4 461 | v := reflect.ValueOf(x) 462 | v.SetFloat(7.1) // Error: will panic. 463 | ``` 464 | 465 | 执行上面的代码会产生 panic,原因是反射变量 `v` 不能代表 `x` 本身,为什么?因为调用 `reflect.ValueOf(x)` 这一行代码的时候,传入的参数在函数内部只是一个拷贝,是值传递,所以 `v` 代表的只是 `x` 的一个拷贝,因此对 `v` 进行操作是被禁止的。 466 | 467 | 可设置是反射变量 `Value` 的一个性质,但不是所有的 `Value` 都是可被设置的。 468 | 469 | 就像在一般的函数里那样,当我们想改变传入的变量时,使用指针就可以解决了。 470 | 471 | ```go 472 | var x float64 = 3.4 473 | p := reflect.ValueOf(&x) 474 | fmt.Println("type of p:", p.Type()) 475 | fmt.Println("settability of p:", p.CanSet()) 476 | ``` 477 | 478 | 输出是这样的: 479 | 480 | ``` 481 | type of p: *float64 482 | settability of p: false 483 | ``` 484 | 485 | `p` 还不是代表 `x`,`p.Elem()` 才真正代表 `x`,这样就可以真正操作 `x` 了: 486 | 487 | ```go 488 | v := p.Elem() 489 | v.SetFloat(7.1) 490 | fmt.Println(v.Interface()) // 7.1 491 | fmt.Println(x) // 7.1 492 | ``` 493 | 494 | 关于第三条,记住一句话:如果想要操作原变量,反射变量 `Value` 必须要 hold 住原变量的地址才行。 495 | 496 | ## 总结 497 | 498 | Go 作为一门静态语言,相比 Python 等动态语言,在编写过程中灵活性会受到一定的限制。但是通过接口加反射实现了类似于动态语言的能力:可以在程序运行时动态地捕获甚至改变类型的信息和值。 499 | 500 | Go 语言的反射实现的基础是类型,或者说是 interface,当我们使用反射特性时,实际上用到的就是存储在 interface 变量中的和类型相关的信息,也就是常说的 `` 对。 501 | 502 | 只有 interface 才有反射的说法。 503 | 504 | 反射在 reflect 包中实现,涉及到两个相关函数: 505 | 506 | ``` 507 | func TypeOf ( i interface{} ) Type 508 | func ValueOf ( i interface{} ) Value 509 | ``` 510 | 511 | Type 是一个接口,定义了很多相关方法,用于获取类型信息。Value 则持有类型的具体值。Type、Value、Interface 三者间通过函数 TypeOf,ValueOf,Interface 进行相互转换。 512 | 513 | 最后温习一下反射三大定律: 514 | 515 | 1. 反射将接口变量转换成反射对象 Type 和 Value; 516 | 2. 反射可以通过反射对象 Value 还原成原先的接口变量; 517 | 3. 反射可以用来修改一个变量的值,前提是这个值可以被修改。 518 | 519 | 520 | 521 | > Reference: 522 | > 523 | > 1. [深度解密Go语言之反射](https://qcrao.com/2019/05/07/dive-into-go-reflection/) 524 | -------------------------------------------------------------------------------- /golang/deep/slice.md: -------------------------------------------------------------------------------- 1 | # Go语言深度解析之slice 2 | 3 | ## slice是什么 4 | 5 | `slice` 翻译成中文就是`切片`,它和`数组(array)`很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。 6 | 7 | 了解 slice 的本质,最简单的方法就是看它的源代码: 8 | 9 | ```go 10 | // go 1.14 src/runtime/slice.go 11 | type slice struct { 12 | array unsafe.Pointer // 元素指针 指向底层数组 13 | len int // 长度 表示切片可用元素的个数 14 | cap int // 容量 底层数组的元素个数,一般>=长度 15 | } 16 | ``` 17 | 18 | 19 | 20 | 可以看到slice共有三个属性,每个属性的解释如上所示,其中在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。需要注意的是,**底层数组是可以被多个 slice 同时指向的**,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。 21 | 22 | ## slice的创建 23 | 24 | 创建 slice 的方式有以下几种: 25 | 26 | | 序号 | 方式 | 代码示例 | 27 | | ---- | ------------------ | ---------------------------------------------------- | 28 | | 1 | 直接声明 | `var slice []int` | 29 | | 2 | new | `slice := *new([]int)` | 30 | | 3 | 字面量 | `slice := []int{1,2,3,4,5}` | 31 | | 4 | make | `slice := make([]int, 5, 10)` | 32 | | 5 | 从切片或数组“截取” | `slice := array[1:5]` 或 `slice := sourceSlice[1:5]` | 33 | 34 | 其中第一种方式和第二种方式创建出来的切片其实是一种`nil slice`,与之对应的还有一种`empty slice`。 35 | 36 | 区别如下(官方建议使用`nil slice`): 37 | 38 | | 创建方式 | nil切片 | 空切片 | 39 | | ------------- | -------------------- | ----------------------- | 40 | | 方式一 | var s1 []int | var s2 = []int{} | 41 | | 方式二 | var s3 = *new([]int) | var s4 = make([]int, 0) | 42 | | 长度 | 0 | 0 | 43 | | 容量 | 0 | 0 | 44 | | 和 `nil` 比较 | `true` | `false` | 45 | 46 | ```go 47 | package main 48 | 49 | import "fmt" 50 | 51 | func main() { 52 | var s1 []int 53 | var s2 = *new([]int) 54 | var s3 = make([]int, 0) 55 | var s4 = []int{} 56 | fmt.Printf("s1's address is %p\n", s1) 57 | fmt.Printf("s2's address is %p\n", s2) 58 | fmt.Printf("s3's address is %p\n", s3) 59 | fmt.Printf("s4's address is %p\n", s4) 60 | fmt.Printf("s1 equal nil? %v\n", s1 == nil) 61 | fmt.Printf("s2 equal nil? %v\n", s2 == nil) 62 | fmt.Printf("s3 equal nil? %v\n", s3 == nil) 63 | fmt.Printf("s4 equal nil? %v\n", s4 == nil) 64 | } 65 | ``` 66 | 67 | ``` 68 | 结果如下: 69 | s1's address is 0x0 70 | s2's address is 0x0 71 | s3's address is 0x5a6d48 72 | s4's address is 0x5a6d48 73 | s1 equal nil? true 74 | s2 equal nil? true 75 | s3 equal nil? false 76 | s4 equal nil? false 77 | ``` 78 | 79 | 所有的空切片`empty slice`的数据指针都指向**同一个地址`0x5a6d48`**。 80 | 81 | 下面来看看“截取”方式: 82 | 83 | 第一种: 84 | 85 | ```go 86 | data := [...]int{0,1,2,3,4,5,6,7,8,9} 87 | slice := data[2:4] //data[low,high] 88 | ``` 89 | 90 | 对 `data` 使用2个索引值,截取出新的`slice`。这里 `data` 可以是数组或者 slice。`low` 是最低索引值,为闭区间,也就是说第一个元素是 data 位于 `low` 索引处的元素(这里为元素`2`);而 `high` 则是开区间,表示最后一个元素只能是索引 `high-1` 处的元素(这里为元素`3`),新的slice的长度计算方式为`high-low`(这里长度为4-2=2),而容量则是从当前low索引往后的元素个数(这里容量为8)。 91 | 92 | ```go 93 | package main 94 | 95 | import "fmt" 96 | 97 | func main() { 98 | data1 := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 99 | slice := data1[2:4] 100 | fmt.Printf("slice is %v\n", slice) 101 | fmt.Printf("slice' length = %v\n", len(slice)) 102 | fmt.Printf("slice' cap = %v\n", cap(slice)) 103 | } 104 | ``` 105 | 106 | ``` 107 | 结果: 108 | slice is [2 3] 109 | slice' length = 2 110 | slice' cap = 8 111 | ``` 112 | 113 | 第二种: 114 | 115 | ```go 116 | data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 117 | slice := data[2:4:6] // data[low, high, max] 其中max >= high >= low 118 | ``` 119 | 120 | 对 `data` 使用3个索引值,多出了一个`max`索引,其中`low`和`high`的作用相同,可以计算出slice的长度为`high-low`(这里长度同样为4-2=2),而`max`也是开区间,而最大容量则只能是索引 `max-1` 处的元素(这里为`5`),slice容量的计算方式为`max-low`(这里容量为6-2=4)。 121 | 122 | ```go 123 | package main 124 | 125 | import "fmt" 126 | 127 | func main() { 128 | data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 129 | slice := data[2:4:6] 130 | fmt.Printf("slice is %v\n", slice) 131 | fmt.Printf("slice' length = %v\n", len(slice)) 132 | fmt.Printf("slice' cap = %v\n", cap(slice)) 133 | } 134 | ``` 135 | 136 | ``` 137 | 结果: 138 | slice is [2 3] 139 | slice' length = 2 140 | slice' cap = 4 141 | ``` 142 | 143 | 注意: 144 | 145 | - 当 `high == low` 时,新 `slice` 为空。 146 | 147 | - 还有一点,`high` 和 `max` 必须在旧数组或者旧切片的容量范围内。 148 | 149 | **这里引入一道题,对“截取”做一个好好的回顾:** 150 | 151 | ```go 152 | package main 153 | import "fmt" 154 | func main() { 155 | slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 156 | 157 | s1 := slice[2:5] 158 | s2 := s1[2:6:7] 159 | 160 | s2 = append(s2, 100) 161 | s2 = append(s2, 200) 162 | 163 | s1[2] = 20 164 | 165 | fmt.Println(s1) 166 | fmt.Println(s2) 167 | fmt.Println(slice) 168 | } 169 | ``` 170 | 171 | ``` 172 | 结果: 173 | [2 3 20] 174 | [4 5 6 7 100 200] 175 | [0 1 2 3 20 5 6 7 100 9] 176 | ``` 177 | 178 | 来分析一遍代码,初始状态如下: 179 | 180 | ```go 181 | slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 182 | s1 := slice[2:5] 183 | s2 := s1[2:6:7] 184 | ``` 185 | 186 | `s1` 从 `slice`的索引 [2,5),长度为3,容量默认到数组结尾,为8。 `s2` 从 `s1`的索引 [2,6),容量大小从[2,7),为5。 187 | 188 | 189 | 190 | 191 | 192 | 接着,向 `s2` 尾部追加一个元素 100: 193 | 194 | ```go 195 | s2 = append(s2, 100) 196 | ``` 197 | 198 | `s2` 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 `s1` 都可以看得到。 199 | 200 | 201 | 202 | 203 | 204 | 再次向 `s2` 追加元素200: 205 | 206 | ```go 207 | s2 = append(s2, 100) 208 | ``` 209 | 210 | 这时,`s2` 的容量不够用,该扩容了。于是,`s2` 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 `append` 带来的再一次扩容,`s2` 会在此次扩容的时候多留一些 `buffer`,将新的容量将扩大为原始容量的`2倍`(具体见下小节分析),也就是10了。 211 | 212 | 213 | 214 | 215 | 216 | 最后,修改 `s1` 索引为2位置的元素: 217 | 218 | ```go 219 | s1[2] = 20 220 | ``` 221 | 222 | 这次只会影响原始数组相应位置的元素。它影响不到 `s2` ,因为不属于同一个底层数组了。 223 | 224 | 225 | 226 | 227 | 228 | 最后打印 `s1` 的时候,只会打印出 `s1` 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。 229 | 230 | ## slice的append操作 231 | 232 | 在2节最后一段提到了append操作,**下面让我们来看看slice的append操作到底做了什么**: 233 | 234 | 先来看看 `append` 函数的原型: 235 | 236 | ```go 237 | func append(slice []Type, elems ...Type) []Type 238 | ``` 239 | 240 | append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 `...` 传入 slice,直接追加一个切片。 241 | 242 | ```go 243 | slice = append(slice, elem1, elem2) 244 | slice = append(slice, anotherSlice...) 245 | ``` 246 | 247 | 使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 `len-1` 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。 248 | 249 | 这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 `slice` 的容量是留了一定的 `buffer` 的。否则,每次添加元素的时候,都会发生迁移,成本太高。 250 | 251 | 新 slice 预留的 `buffer` 大小是有一定规律的。网上大多数的文章都是这样描述的: 252 | 253 | > 当原 slice 容量小于 `1024` 的时候,新 slice 容量变成原来的 `2` 倍;原 slice 容量超过 `1024`,新 slice 容量变成原来的`1.25`倍。 254 | 255 | **这句话是错误的!** 256 | 257 | **举个例子:** 258 | 259 | ```go 260 | package main 261 | 262 | import "fmt" 263 | 264 | func main() { 265 | s := []int{1,2} 266 | s = append(s,4,5,6) 267 | fmt.Printf("len=%d, cap=%d",len(s),cap(s)) 268 | } 269 | ``` 270 | 271 | ``` 272 | 结果: 273 | len=5,cap=6 274 | ``` 275 | 276 | 如果按网上各种文章中总结的那样:**原 slice 长度小于 1024 的时候,容量每次增加 1 倍。添加元素 4 的时候,容量变为4;添加元素 5 的时候不变;添加元素 6 的时候容量增加 1 倍,变成 8。** 277 | 278 | 那上面代码的运行结果就是: 279 | 280 | ``` 281 | len=5, cap=8 282 | ``` 283 | 284 | 这是错误的,下面来看看源码: 285 | 286 | ```go 287 | // go 1.14 src/runtime/slice.go:76 288 | //growslice():它被传递给slice元素类型,旧的slice和所需的新的最小容量,并返回一个至少具有该容量的新slice,并将旧数据复制到其中。 289 | func growslice(et *_type, old slice, cap int) slice { 290 | //... 291 | newcap := old.cap 292 | doublecap := newcap + newcap 293 | //如果新的容量大于旧的两倍,则直接扩容到新的容量 294 | if cap > doublecap { 295 | newcap = cap 296 | } else { 297 | // 当新的容量不大于旧的两倍 298 | // 如果旧长度小于1024,那扩容到旧的两倍 299 | if old.len < 1024 { 300 | newcap = doublecap 301 | } else { 302 | //否则扩容到旧的1.25倍 303 | for 0 < newcap && newcap < cap { 304 | newcap += newcap / 4 305 | } 306 | //... 307 | } 308 | 309 | //... 310 | //内存对齐 311 | capmem = roundupsize(uintptr(newcap) * sys.PtrSize) 312 | newcap = int(capmem / sys.PtrSize) 313 | } 314 | ``` 315 | 316 | 如果只看前半部分,现在网上各种文章里说的 `newcap` 的规律是对的。现实是,后半部分还对 `newcap` 作了一个`内存对齐roundupsize`,这个**和内存分配策略相关**。 317 | 318 | > 进行内存对齐之后,新 slice 的容量是要 `大于等于` 旧slice 容量的 `2倍`或者`1.25倍`。 319 | 320 | **例子分析:** 321 | 322 | `growslice`函数的参数依次是 `元素的类型`,`旧slice`,`新slice最小求的容量`。 323 | 324 | 例子中 `s` 原来只有 2 个元素,`len` 和 `cap` 都为 2,append了三个元素后,长度变为 3,容量最小要变成 5,即调用 `growslice` 函数时,传入的第三个参数应该为 5。即 cap=5。而一方面,`doublecap` 是原 slice容量的 2 倍,等于 4。满足第一个 if 条件,所以 `newcap` 变成了 5。 325 | 326 | 接着调用了 `roundupsize` 函数,传入` size=40`。(代码中`sys.PtrSize`是指一个指针的大小,在64位机上是8,即传入5*8) 327 | 328 | ```go 329 | // go 1.14 src/runtime/internal/sys/stubs.go: 8 330 | //拿 64 系统来说,0 取反之后右移 63 位的结果是 1,然后 4 的二进制表示是 0100,左移 1 位的结果是 1000,结果为 8。 331 | const PtrSize = 4 << (^uintptr(0) >> 63) 332 | ``` 333 | 334 | 下面来看看roundupsize源码: 335 | 336 | ```go 337 | // go 1.14 src/runtime/msize.go:13 338 | func roundupsize(size uintptr) uintptr { 339 | if size < _MaxSmallSize { 340 | if size <= smallSizeMax-8 { 341 | return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) 342 | } else { 343 | return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]]) 344 | } 345 | } 346 | //... 347 | } 348 | 349 | 其中: 350 | const _MaxSmallSize = 32768 351 | const smallSizeMax = 1024 352 | const smallSizeDiv = 8 353 | ``` 354 | 355 | 传入size=40后,很明显,最后返回 356 | 357 | ```go 358 | class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]] 359 | ``` 360 | 361 | 这是 `Go` 源码中有关内存分配的两个 `slice`。`class_to_size`通过 `spanClass`获取 `span`划分的 `object`大小。而 `size_to_class8` 表示通过 `size` 获取它的 `spanClass`。 362 | 363 | ```go 364 | // go 1.14 src/runtime/sizeclasses.go 365 | var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768} 366 | 367 | var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31} 368 | ``` 369 | 370 | 我们传进去的 `size` 等于 40。所以 `(size+smallSizeDiv-1)/smallSizeDiv = 5`;获取 `size_to_class8` 数组中索引为 `5` 的元素为 `4`;获取 `class_to_size` 中索引为 `4` 的元素为 `48`。 371 | 372 | 最终,新的 slice 的容量为 `6`: 373 | 374 | ``` 375 | newcap = int(capmem / sys.PtrSize) // 48 / 8 = 6 376 | ``` 377 | 378 | ## 为什么 nil slice 可以直接 append 379 | 380 | 其实 `nil slice` 或者 `empty slice` 都是可以通过调用 append 函数来获得底层数组的扩容。最终都是调用 `mallocgc` 来向 Go 的内存管理器申请到一块内存,然后再赋给原来的`nil slice` 或 `empty slice`,然后摇身一变,成为“真正”的 `slice` 了。 381 | 382 | ## 传 slice 和 slice 指针有什么区别 383 | 384 | 当 slice 作为函数参数时,就是一个普通的结构体。其实很好理解:若直接传 slice,在调用者看来,实参 slice 并不会被函数中的操作改变;若传的是 slice 的指针,在调用者看来,是会被改变原 slice 的。 385 | 386 | 值的注意的是,不管传的是 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。为什么能改变底层数组的数据?很好理解:**底层数据在 slice 结构体里是一个指针,仅管 slice 结构体自身不会被改变,也就是说底层数据地址不会被改变。 但是通过指向底层数据的指针,可以改变切片的底层数据**。 387 | 388 | 通过 slice 的 array 字段就可以拿到数组的地址。在代码里,是直接通过类似 `s[i]=10` 这种操作改变 slice 底层数组元素值。 389 | 390 | 将切片通过参数传递给函数,其实质是复制了slice结构体对象,两个slice结构体的字段值均相等。正常情况下,由于函数内slice结构体的array和函数外slice结构体的array指向的是同一底层数组,所以当对底层数组中的数据做修改时,两者均会受到影响。 391 | 392 | 但是存在这样的问题:如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改。 393 | 394 | ## 总结 395 | 396 | - 切片是对底层数组的一个抽象,描述了它的一个片段。 397 | - 切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。 398 | - 多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。 399 | - `append` 函数会在切片容量不够的情况下,调用 `growslice` 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。 400 | - 扩容策略并不是简单的扩为原切片容量的 `2` 倍或 `1.25` 倍,还有**内存对齐的操作**。扩容后的容量 >= 原容量的 `2` 倍或 `1.25` 倍。 401 | - 当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。 402 | 403 | 404 | 405 | > 参考: 406 | > 407 | > [深度解密Go语言之Slice](https://mp.weixin.qq.com/s/MTZ0C9zYsNrb8wyIm2D8BA) 408 | > 409 | > [切片传递与指针传递的区别_zuiyijiangnan的博客-CSDN博客_切片传递](https://blog.csdn.net/zuiyijiangnan/article/details/112673446) -------------------------------------------------------------------------------- /golang/deep/unsafe.md: -------------------------------------------------------------------------------- 1 | # Go语言深度解析之unsafe 2 | 3 | ## 什么是unsafe 4 | 5 | unsafe 库让 golang 可以像C语言一样操作计算机内存,但这并不是golang推荐使用的,能不用尽量不用,就像它的名字所表达的一样,它绕过了golang的内存安全原则,是不安全的,容易使你的程序出现莫名其妙的问题,不利于程序的扩展与维护。 6 | 7 | 先简单介绍下Golang指针类型: 8 | 9 | 1. `*类型`:普通指针,用于传递对象地址,不能进行指针运算。 10 | 2. `unsafe.Pointer`:通用指针类型,用于转换不同类型的指针,不能进行指针运算。 11 | 3. `uintptr`:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象,**uintptr 类型的目标会被回收**。 12 | 13 | unsafe.Pointer 可以和 普通指针 进行相互转换。 14 | 15 | unsafe.Pointer 可以和 uintptr 进行相互转换。 16 | 17 | 也就是说 **unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算**。 18 | 19 |  20 | 21 | unsafe底层源码如下: 22 | 23 | 两个类型: 24 | 25 | ```go 26 | // go 1.14 src/unsafe/unsafe.go 27 | type ArbitraryType int 28 | type Pointer *ArbitraryType 29 | ``` 30 | 31 | 三个函数: 32 | 33 | ```go 34 | func Sizeof(x ArbitraryType) uintptr 35 | func Offsetof(x ArbitraryType) uintptr 36 | func Alignof(x ArbitraryType) uintptr 37 | ``` 38 | 39 | 通过分析发现,这三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。 40 | 41 | 1. `Sizeof` **返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小**。例如,对于一个指针,函数返回的大小为 8 字节(64位机上),一个 slice 的大小则为 slice header 的大小。 42 | 2. `Offsetof`返回变量指定属性的偏移量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的属性当作参数。 43 | 3. `Alignof`返回变量对齐字节数量 44 | 45 | ## unsafe包的操作 46 | 47 | ### 大小Sizeof 48 | 49 | unsafe.Sizeof函数返回的就是uintptr类型的值,表示所占据的字节数(表达式,即值的大小): 50 | 51 | ```go 52 | package main 53 | 54 | import ( 55 | "fmt" 56 | "reflect" 57 | "unsafe" 58 | ) 59 | 60 | func main() { 61 | var a int32 62 | var b = &a 63 | fmt.Println(reflect.TypeOf(unsafe.Sizeof(a))) // uintptr 64 | fmt.Println(unsafe.Sizeof(a)) // 4 65 | fmt.Println(reflect.TypeOf(b).Kind()) // ptr 66 | fmt.Println(unsafe.Sizeof(b)) // 8 67 | } 68 | ``` 69 | 70 | 对于 `a`来说,它是`int32`类型,在内存中占4个字节,而对于`b`来说,是`*int32`类型,即底层为`ptr`指针类型,在64位机下占8字节。 71 | 72 | ### 偏移Offsetof 73 | 74 | 对于一个结构体,通过 Offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。 75 | 76 | 这里有一个内存分配相关的事实:**结构体会被分配一块连续的内存,结构体的地址也代表了第一个字段的地址。** 77 | 78 | 举个例子: 79 | 80 | ```go 81 | package main 82 | 83 | import ( 84 | "fmt" 85 | "unsafe" 86 | ) 87 | 88 | type user struct { 89 | id int32 90 | name string 91 | age byte 92 | } 93 | 94 | func main() { 95 | var u = user{ 96 | id: 1, 97 | name: "xiaobai", 98 | age: 22, 99 | } 100 | fmt.Println(u) 101 | fmt.Println(unsafe.Offsetof(u.id)) // 0 id在结构体user中的偏移量,也是结构体的地址 102 | fmt.Println(unsafe.Offsetof(u.name)) // 8 103 | fmt.Println(unsafe.Offsetof(u.age)) // 24 104 | 105 | // 根据偏移量修改字段的值 比如将id字段改为1001 106 | // 因为结构体的地址相当于第一个字段id的地址 107 | // 直接用unsafe包自带的Pointer获取id指针 108 | id := (*int)(unsafe.Pointer(&u)) 109 | *id = 1001 110 | 111 | // 更加相对于id字段的偏移量获取name字段的地址并修改其内容 112 | // 需要用到uintptr进行指针运算 然后再利用unsafe.Pointer这个媒介将uintptr类型转换成一般的指针类型*string 113 | name := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + unsafe.Offsetof(u.name))) 114 | *name = "花花" 115 | 116 | // 同理更改age字段 117 | age := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + unsafe.Offsetof(u.age))) 118 | *age = 33 119 | 120 | fmt.Println(u) 121 | } 122 | ``` 123 | 124 | ### 对齐Alignof 125 | 126 | 要了解这个函数,你需要了解`数据对齐`。简单的说,它让数据结构在内存中以某种的布局存放,使该数据的读取性能能够更加的快速。 127 | 128 | CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度。 129 | 130 | #### 普通字段的对齐值 131 | 132 | ```go 133 | fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true))) 134 | fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0))) 135 | fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0))) 136 | fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0))) 137 | fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0))) 138 | fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY")) 139 | fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{})) 140 | ``` 141 | 142 | 输出结果: 143 | 144 | ```text 145 | bool align: 1 146 | int32 align: 4 147 | int8 align: 1 148 | int64 align: 8 149 | byte align: 1 150 | string align: 8 151 | map align: 8 152 | ``` 153 | 154 | 在 Go 中可以调用 `unsafe.Alignof` 来返回相应类型的对齐系数。通过观察输出结果,可得知基本都是 2n,最大也`不会超过 8`。这是因为我们的**64位编译器默认对齐系数是 8**,因此最大值不会超过这个数。 155 | 156 | #### 对齐规则 157 | 158 | 1. **结构体的成员变量**,第一个成员变量的偏移量为 0。往后的每个成员变量的`对齐值=min(编译器默认对齐长度,当前成员变量类型的长度)`。其`偏移量=对齐值xN` 159 | 2. **结构体本身**,`对齐值=最小整数倍{max(编译器默认对齐长度,结构体的所有成员变量类型中的最大长度)}`, 160 | 161 | 结合以上两点,可得知若编译器默认对齐长度超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的 162 | 163 | #### 结构体的对齐值 164 | 165 | 下面来看一下结构体的对齐: 166 | 167 | ```go 168 | type part struct { 169 | a bool // 1 170 | b int32 // 4 171 | c int8 // 1 172 | d int64 // 8 173 | e byte // 1 174 | } 175 | 176 | func main() { 177 | var p part 178 | fmt.Println(unsafe.Sizeof(p)) // 32 179 | } 180 | ``` 181 | 182 | 按照普通字段(结构体内成员变量)的对齐方式,我们可以计算得出,这个结构体的大小占`1+4+1+8+1=15`个字节,但是用`unsafe.Sizeof`计算发现`part结构体`占`32`字节,是不是有点惊讶😮 183 | 184 | 这里面就涉及到了内存对齐,下面我们来分析一下: 185 | 186 | | 成员变量 | 类型 | 偏移量 | 自身占用 | 187 | | ---------- | ----- | ------ | -------- | 188 | | a | bool | 0 | 1 | 189 | | 数据对齐 | - | 1 | 3 | 190 | | b | int32 | 4 | 4 | 191 | | c | int8 | 8 | 1 | 192 | | 数据对齐 | - | 9 | 7 | 193 | | d | in64 | 16 | 8 | 194 | | e | byte | 24 | 1 | 195 | | 数据对齐 | - | 25 | 7 | 196 | | 总占用大小 | - | - | 32 | 197 | 198 | - 对于变量a而言 199 | 200 | 类型是bool;大小/对齐值本身为1字节;偏移量为0,**占用了第0位**;此时内存中表示为`a` 201 | 202 | - 对于变量b而言 203 | 204 | 类型是int32;大小/对齐值本身为4字节;根据对齐规则一,偏移量必须为对齐值4的整数倍,故这里的偏移量为4,**占用了第4 ~ 7位**,则**第1 ~ 3位用padding字节填充**;此时内存中表示为`a---|bbbb`,(`|`只起到分隔作用,表示方便一些) 205 | 206 | - 对于变量c而言 207 | 208 | 类型是int8;大小/对齐值本身为1字节;当前偏移量为8,无需扩充,**占用了第8位**;此时内存中表示为`a---|bbbb|c` 209 | 210 | - 对于变量d而言 211 | 212 | 类型是int64;大小/对齐值本身为8字节;根据对齐规则一,偏移量必须为对齐值8的整数倍,故当前偏移量为16,**占用了第16 ~ 23位**,则**第9 ~ 15为用padding字节填充**;此时内存中表示为`a---|bbbb|c---|----|dddd|dddd` 213 | 214 | - 对于变量e而言 215 | 216 | 类型是byte;大小/对齐值本身为1字节;当前偏移量为24,无需扩充,**占用了第24位**;此时内存中表示为`a---|bbbb|c---|----|dddd|dddd|e` 217 | 218 | 这里计算后,发现总共占用25字节,哪里又来的32字节呢?😳 219 | 220 | 再让我们回顾一下对齐原则的第二点,**结构体本身**,`对齐值=最小整数倍{max(编译器默认对齐长度,结构体的所有成员变量类型中的最大长度)}` 221 | 222 | 1. 这里编译器默认对齐长度为8字节(64位机) 223 | 224 | 2. 结构体中所有成员变量类型的最大长度为int64,8字节 225 | 3. 取二者最大数的最小整数倍作为对齐值,我们算的part结构体大小为25字节,不是8字节的整数倍,故还需要填充到32字节。 226 | 227 | 综上,part结构体在内存中表示为`a---|bbbb|c---|----|dddd|dddd|e----|----` 228 | 229 | #### 扩展 230 | 231 | 让我们改变一下part结构体中字段的顺序看看(part结构体完全相同) 232 | 233 | ```go 234 | type part struct { 235 | a bool // 1 236 | c int8 // 1 237 | e byte // 1 238 | b int32 //4 239 | d int64 // 8 240 | } 241 | 242 | func main() { 243 | var p part 244 | fmt.Println(unsafe.Sizeof(p)) // 16 245 | } 246 | ``` 247 | 248 | 这时候再用`unsafe.Sizeof`查看会发现,part结构体的内存占用只有`16`字节,瞬间减少了一半的内存空间,大家可以按照前面的步骤分析一下~ 249 | 250 | 这里建议在构建结构体时,**按照字段大小的升序进行排序,会减少一点的内存空间**。 251 | 252 | #### 反射包的对齐方法 253 | 254 | 反射包也有某些方法可用于计算对齐值: 255 | 256 | ```go 257 | unsafe.Alignof(w)等价于reflect.TypeOf(w).Align 258 | unsafe.Alignof(w.i)等价于reflect.Typeof(w.i).FieldAlign() 259 | ``` 260 | 261 | ## 总结 262 | 263 | - unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。 264 | 265 | - unsafe 包定义了 Pointer 和三个函数: 266 | 267 | ```go 268 | type ArbitraryType int 269 | type Pointer *ArbitraryType 270 | 271 | func Sizeof(x ArbitraryType) uintptr 272 | func Offsetof(x ArbitraryType) uintptr 273 | func Alignof(x ArbitraryType) uintptr 274 | ``` 275 | 276 | 通过三个函数可以获取变量的大小、偏移、对齐等信息。 277 | 278 | - uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。 279 | 280 | - 通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。 281 | 282 | 283 | 284 | > 参考: 285 | > 286 | > - [深度解密Go语言之unsafe](https://mp.weixin.qq.com/s/OO-kwB4Fp_FnCaNXwGJoEw) 287 | > - [unsafe包的学习和使用 - 离地最远的星 - 博客园 (cnblogs.com)](https://www.cnblogs.com/hualou/p/12070155.html) 288 | > - [在 Go 中恰到好处的内存对齐 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/53413177) -------------------------------------------------------------------------------- /golang/useful_package.md: -------------------------------------------------------------------------------- 1 | # Golang常用到的库 2 | 3 | | 库 | 地址 | 4 | | ------------------------------------------------------ | ------------------------------------------------------------ | 5 | | 数据库操作,需要装驱动 | https://github.com/go-sql-driver/mysql https://github.com/jinzhu/gorm | 6 | | 搜索es | https://github.com/olivere/elastic | 7 | | rocketmq操作 | https://github.com/apache/rocketmq-client-go/v2 | 8 | | rabbitmq 操作 | https://github.com/streadway/amqp | 9 | | redis 操作 | https://github.com/go-redis/redis https://github.com/gomodule/redigo | 10 | | etcd 操作 | [github.com/coreos/etcd/clientv3](https://pkg.go.dev/go.etcd.io/etcd/clientv3) | 11 | | kafka | https://github.com/Shopify/sarama https://github.com/bsm/sarama-cluste | 12 | | pprof | https://github.com/gin-contrib/pprof | 13 | | testfiy测试库 | https://github.com/stretchr/testify | 14 | | excel 操作 | https://github.com/360EntSecGroup-Skylar/excelize | 15 | | 可以操作任何数据的包 比方sting 转换类型啊 方便编写代码 | https://github.com/Unknwon/com | 16 | | 表单验证器 | https://github.com/go-playground/validator | 17 | | goroutine池 | https://github.com/panjf2000/ants | 18 | | golang的爬虫框架colly | https://github.com/gocolly/colly | 19 | | 命令行程序框架 cobra | https://github.com/spf13/cobra | 20 | | 配置读取viper | https://github.com/spf13/viper | 21 | | go key/value存储 | https://github.com/etcd-io/bbolt | 22 | | golang的绘图库go-echart | https://github.com/go-echarts/go-echarts/v2 | 23 | | golangweb项目热更新 进入项目目录下:fresh启动 | https://github.com/pilu/fresh | 24 | | 轻量级的协程池 | https://github.com/ivpusic/grpool | 25 | | 打印go的详细数据结构 | https://github.com/davecgh/go-spew | 26 | | jwt鉴权 | https://github.com/dgrijalva/jwt-go | 27 | | 拼音 | https://github.com/go-ego/gpy | 28 | | 分词 | https://github.com/go-ego/gse | 29 | | 搜索 | https://github.com/go-ego/riot | 30 | | session | https://github.com/gorilla/sessions | 31 | | 路由 | https://github.com/gorilla/mux | 32 | | websocket | https://github.com/gorilla/websocket | 33 | | Action handler | https://github.com/gorilla/handlers | 34 | | csrf | https://github.com/gorilla/csrf | 35 | | context | https://github.com/gorilla/context | 36 | | 过滤html标签 | https://github.com/grokify/html-strip-tags-go | 37 | | 可配置的HTML标签过滤 | https://github.com/microcosm-cc/bluemonday | 38 | | 根据IP获取地理位置信息 | https://github.com/ipipdotnet/ipdb-go | 39 | | html转markdown | https://github.com/jaytaylor/html2text | 40 | | goroutine 本地存储 | https://github.com/jtolds/gls | 41 | | 彩色输出 | https://github.com/mgutz/ansi | 42 | | 表格打印 | https://github.com/olekukonko/tablewriter | 43 | | reflect 更高效的反射API | https://github.com/modern-go/reflect2 | 44 | | 可取消的goroutine | https://github.com/modern-go/concurrent | 45 | | 深度拷贝 | https://github.com/mohae/deepcopy | 46 | | 安全的类型转换包 | https://github.com/spf13/cast | 47 | | 从文本中提取链接 | https://github.com/mvdan/xurls | 48 | | 字符串格式处理(驼峰转换) | https://godoc.org/github.com/naoina/go-stringutil | 49 | | 文本diff实现 | https://github.com/pmezard/go-difflib | 50 | | uuid相关 | https://github.com/satori/go.uuid https://github.com/snluu/uuid | 51 | | 去除UTF编码中的BOM | https://github.com/ssor/bom | 52 | | 图片缩放 | https://github.com/nfnt/resize | 53 | | 生成 mock server | https://github.com/otokaze/mock | 54 | | go 性能上报到influxdb | https://github.com/rcrowley/go-metrics | 55 | | go zookeeper客户端 | https://github.com/samuel/go-zookeeper | 56 | | go thrift | https://github.com/samuel/go-thrift | 57 | | go 性能上报到influxdb | https://github.com/rcrowley/go-metrics | 58 | | go 性能上报到prometheus | https://github.com/deathowl/go-metrics-prometheus | 59 | | ps utils | https://github.com/shirou/gopsutil | 60 | | 小数处理 | https://github.com/shopspring/decimal | 61 | | 结构化日志处理(json) | https://github.com/sirupsen/logrus | 62 | | grpc操作 | https://google.golang.org/grpc https://github.com/golang/protobuf/protoc-gen-go | 63 | | ceph私有云库 | https://gopkg.in/amz.v1/aws https://gopkg.in/amz.v1/s3 | 64 | | 阿里云oss库 | https://github.com/aliyun/aliyun-oss-go-sdk/oss | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /system-design/security/advantages&disadvantages-of-jwt.md: -------------------------------------------------------------------------------- 1 | 在 [JWT基础概念详解及使用](./jwt-intro.md)这篇文章中,我介绍了: 2 | 3 | - 什么是 JWT? 4 | - JWT 由哪些部分组成? 5 | - 如何基于 JWT 进行身份验证? 6 | - JWT 如何防止 Token 被篡改? 7 | - 如何加强 JWT 的安全性? 8 | 9 | 这篇文章,我们一起探讨一下 JWT 身份认证的优缺点以及常见问题的解决办法。 10 | 11 | ## JWT 的优势 12 | 13 | 相比于 Session 认证的方式来说,使用 JWT 进行身份认证主要有下面 4 个优势。 14 | 15 | ### 无状态 16 | 17 | JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。 18 | 19 | 不过,也正是由于 JWT 的无状态,也导致了它最大的缺点:**不可控!** 20 | 21 | 就比如说,我们想要在 JWT 有效期内废弃一个 JWT 或者更改它的权限的话,并不会立即生效,通常需要等到有效期过后才可以。再比如说,当用户 Logout 的话,JWT 也还有效。除非,我们在后端增加额外的处理逻辑比如将失效的 JWT 存储起来,后端先验证 JWT 是否有效再进行处理。具体的解决办法,我们会在后面的内容中详细介绍到,这里只是简单提一下。 22 | 23 | ### 有效避免了 CSRF 攻击 24 | 25 | **CSRF(Cross Site Request Forgery)** 一般被翻译为 **跨站请求伪造**,属于网络攻击领域范围。相比于 SQL 脚本注入、XSS 等安全攻击方式,CSRF 的知名度并没有它们高。但是,它的确是我们开发系统时必须要考虑的安全隐患。就连业内技术标杆 Google 的产品 Gmail 也曾在 2007 年的时候爆出过 CSRF 漏洞,这给 Gmail 的用户造成了很大的损失。 26 | 27 | **那么究竟什么是跨站请求伪造呢?** 简单来说就是用你的身份去做一些不好的事情(发送一些对你不友好的请求比如恶意转账)。 28 | 29 | 举个简单的例子:小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了 10000 元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。 30 | 31 | ```html 32 | 科学理财,年盈利率过万 33 | ``` 34 | 35 | CSRF 攻击需要依赖 Cookie ,Session 认证中 Cookie 中的 `SessionID` 是由浏览器发送到服务端的,只要发出请求,Cookie 就会被携带。借助这个特性,即使黑客无法获取你的 `SessionID`,只要让你误点攻击链接,就可以达到攻击效果。 36 | 37 | 另外,并不是必须点击链接才可以达到攻击效果,很多时候,只要你打开了某个页面,CSRF 攻击就会发生。 38 | 39 | ```html 40 | 41 | ``` 42 | 43 | **那为什么 JWT 不会存在这种问题呢?** 44 | 45 | 一般情况下我们使用 JWT 的话,在我们登录成功获得 JWT 之后,一般会选择存放在 localStorage 中。前端的每一个请求后续都会附带上这个 JWT,整个过程压根不会涉及到 Cookie。因此,即使你点击了非法链接发送了请求到服务端,这个非法请求也是不会携带 JWT 的,所以这个请求将是非法的。 46 | 47 | 总结来说就一句话:**使用 JWT 进行身份验证不需要依赖 Cookie ,因此可以避免 CSRF 攻击。** 48 | 49 | 不过,这样也会存在 XSS 攻击的风险。为了避免 XSS 攻击,你可以选择将 JWT 存储在标记为`httpOnly` 的 Cookie 中。但是,这样又导致了你必须自己提供 CSRF 保护,因此,实际项目中我们通常也不会这么做。 50 | 51 | 常见的避免 XSS 攻击的方式是过滤掉请求中存在 XSS 攻击风险的可疑字符串。 52 | 53 | ### 适合移动端应用 54 | 55 | 使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 `SessionId`),所以不适合移动端。 56 | 57 | 但是,使用 JWT 进行身份认证就不会存在这种问题,因为只要 JWT 可以被客户端存储就能够使用,而且 JWT 还可以跨语言使用。 58 | 59 | ### 单点登录友好 60 | 61 | 使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 JWT 进行认证的话, JWT 被保存在客户端,不会存在这些问题。 62 | 63 | ## JWT 身份认证常见问题及解决办法 64 | 65 | ### 注销登录等场景下 JWT 还有效 66 | 67 | 与之类似的具体相关场景有: 68 | 69 | - 退出登录; 70 | - 修改密码; 71 | - 服务端修改了某个用户具有的权限或者角色; 72 | - 用户的帐户被封禁/删除; 73 | - 用户被服务端强制注销; 74 | - 用户被踢下线; 75 | - ...... 76 | 77 | 这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 JWT 认证的方式就不好解决了。我们也说过了,JWT 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。 78 | 79 | 那我们如何解决这个问题呢?查阅了很多资料,我简单总结了下面 4 种方案: 80 | 81 | **1、将 JWT 存入内存数据库** 82 | 83 | 将 JWT 存入 DB 中,Redis 内存数据库在这里是不错的选择。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 发送请求都要先从 DB 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。 84 | 85 | **2、黑名单机制** 86 | 87 | 和上面的方式类似,使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 **黑名单** 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。 88 | 89 | 前两种方案的核心在于将有效的 JWT 存储起来或者将指定的 JWT 拉入黑名单。 90 | 91 | 虽然这两种方案都违背了 JWT 的无状态原则,但是一般实际项目中我们通常还是会使用这两种方案。 92 | 93 | **3、修改密钥 (Secret)** : 94 | 95 | 我们为每个用户都创建一个专属密钥,如果我们想让某个 JWT 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大: 96 | 97 | - 如果服务是分布式的,则每次发出新的 JWT 时都必须在多台机器同步密钥。为此,你需要将密钥存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。 98 | - 如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。 99 | 100 | **4、保持令牌的有效期限短并经常轮换** 101 | 102 | 很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。 103 | 104 | 另外,对于修改密码后 JWT 还有效问题的解决还是比较容易的。说一种我觉得比较好的方式:**使用用户的密码的哈希值对 JWT 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。** 105 | 106 | ### JWT 的续签问题 107 | 108 | JWT 有效期一般都建议设置的不太长,那么 JWT 过期后如何认证,如何实现动态刷新 JWT,避免用户经常需要重新登录? 109 | 110 | 我们先来看看在 Session 认证中一般的做法:**假如 Session 的有效期 30 分钟,如果 30 分钟内用户有访问,就把 Session 有效期延长 30 分钟。** 111 | 112 | JWT 认证的话,我们应该如何解决续签问题呢?查阅了很多资料,我简单总结了下面 4 种方案: 113 | 114 | **1、类似于 Session 认证中的做法** 115 | 116 | 这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。 117 | 118 | **2、每次请求都返回新 JWT** 119 | 120 | 这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。 121 | 122 | **3、JWT 有效期设置到半夜** 123 | 124 | 这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。 125 | 126 | **4、用户登录返回两个 JWT** 127 | 128 | 第一个是 accessJWT ,它的过期时间 JWT 本身的过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。 129 | 130 | 这种方案的不足是: 131 | 132 | - 需要客户端来配合; 133 | - 用户注销的时候需要同时保证两个 JWT 都无效; 134 | - 重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况(可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT)。 135 | 136 | ## 总结 137 | 138 | JWT 其中一个很重要的优势是无状态,但实际上,我们想要在实际项目中合理使用 JWT 的话,也还是需要保存 JWT 信息。 139 | 140 | JWT 也不是银弹,也有很多缺陷,具体是选择 JWT 还是 Session 方案还是要看项目的具体需求。万万不可尬吹 JWT,而看不起其他身份认证方案。 141 | 142 | 另外,不用 JWT 直接使用普通的 Token(随机生成,不包含具体的信息) 结合 Redis 来做身份认证也是可以的。我在 [「优质开源项目推荐」](https://javaguide.cn/open-source-project/)的第 8 期推荐过的 [Sa-Token](https://github.com/dromara/sa-JWT) 这个项目是一个比较完善的 基于 JWT 的身份认证解决方案,支持自动续签、踢人下线、账号封禁、同端互斥登录等功能,感兴趣的朋友可以看看。 143 | 144 |  145 | 146 | ## 参考 147 | 148 | - JWT 超详细分析:https://learnku.com/articles/17883 149 | - How to log out when using JWT:https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6 150 | - CSRF protection with JSON Web JWTs:https://medium.com/@agungsantoso/csrf-protection-with-json-web-JWTs-83e0f2fcbcc 151 | - Invalidating JSON Web JWTs:https://stackoverflow.com/questions/21978658/invalidating-json-web-JWTs 152 | -------------------------------------------------------------------------------- /system-design/security/basis-of-authority-certification.md: -------------------------------------------------------------------------------- 1 | ## 认证 (Authentication) 和授权 (Authorization)的区别是什么? 2 | 3 | 这是一个绝大多数人都会混淆的问题。首先先从读音上来认识这两个名词,很多人都会把它俩的读音搞混,所以我建议你先先去查一查这两个单词到底该怎么读,他们的具体含义是什么。 4 | 5 | 说简单点就是: 6 | 7 | - **认证 (Authentication):** 你是谁 8 | - **授权 (Authorization):** 你有权限干什么 9 | 10 | 稍微正式点的说法就是 : 11 | 12 | - **Authentication(认证)** 是验证您的身份的凭据(例如用户名/用户 ID 和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。 13 | - **Authorization(授权)** 发生在 **Authentication(认证)** 之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如 admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有 14 | 15 | 认证 : 16 | 17 |  18 | 19 | 授权: 20 | 21 |  22 | 23 | 这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。 24 | 25 | ## RBAC 模型了解吗? 26 | 27 | 系统权限控制最常采用的访问控制模型就是 **RBAC 模型** 。 28 | 29 | **什么是 RBAC 呢?** 30 | 31 | RBAC 即基于角色的权限访问控制(Role-Based Access Control)。这是一种通过角色关联权限,角色同时又关联用户的授权的方式。 32 | 33 | 简单地说:一个用户可以拥有若干角色,每一个角色又可以被分配若干权限,这样就构造成“用户-角色-权限” 的授权模型。在这种模型中,用户与角色、角色与权限之间构成了多对多的关系,如下图: 34 |  35 | 36 | **在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。** 37 | 38 | 本系统的权限设计相关的表如下(一共 5 张表,2 张用户建立表之间的联系): 39 | 40 |  41 | 42 | 通过这个权限模型,我们可以创建不同的角色并为不同的角色分配不同的权限范围(菜单)。 43 | 44 |  45 | 46 | 通常来说,如果系统对于权限控制要求比较严格的话,一般都会选择使用 RBAC 模型来做权限控制。 47 | 48 | ## 什么是 Cookie ? Cookie 的作用是什么? 49 | 50 |  51 | 52 | `Cookie` 和 `Session` 都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。 53 | 54 | 维基百科是这样定义 `Cookie` 的: 55 | 56 | > `Cookies` 是某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。 57 | 58 | 简单来说: **`Cookie` 存放在客户端,一般用来保存用户信息**。 59 | 60 | 下面是 `Cookie` 的一些应用案例: 61 | 62 | 1. 我们在 `Cookie` 中保存已经登录过的用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了。除此之外,`Cookie` 还能保存用户首选项,主题和其他设置信息。 63 | 2. 使用 `Cookie` 保存 `SessionId` 或者 `Token` ,向后端发送请求的时候带上 `Cookie`,这样后端就能取到 `Session` 或者 `Token` 了。这样就能记录用户当前的状态了,因为 HTTP 协议是无状态的。 64 | 3. `Cookie` 还可以用来记录和分析用户行为。举个简单的例子你在网上购物的时候,因为 HTTP 协议是没有状态的,如果服务器想要获取你在某个页面的停留状态或者看了哪些商品,一种常用的实现方式就是将这些信息存放在 `Cookie` 65 | 4. ...... 66 | 67 | **原理**:客户端首次向服务器发送请求时,服务器会为其分配cookie并存放到响应消息的特定字段中,客户端收到响应消息会解析并存储该字段,在下次再发送请求时就会在请求头部带上这个字段,因此服务器就能区分不同的客户端,以及客户端配置的各种偏好要求 68 | 69 | ## Cookie 和 Session 有什么区别? 70 | 71 | **`Session` 的主要作用就是通过服务端记录用户的状态。** 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 `Session` 之后就可以标识这个用户并且跟踪这个用户了。 72 | 73 | `Cookie` 数据保存在客户端(浏览器端),`Session` 数据保存在服务器端。相对来说 `Session` 安全性更高。如果使用 `Cookie` 的一些敏感信息不要写入 `Cookie` 中,最好能将 `Cookie` 信息加密然后使用到的时候再去服务器端解密。 74 | 75 | **那么,如何使用 `Session` 进行身份验证?** 76 | 77 | ## 如何使用 Session-Cookie 方案进行身份验证? 78 | 79 | 很多时候我们都是通过 `Session ID` 来实现特定的用户,`Session ID` 一般会选择存放在 Redis 中。举个例子: 80 | 81 | 1. 用户成功登陆系统,然后返回给客户端具有 `SessionID` 的 `Cookie` 。 82 | 2. 当用户向后端发起请求的时候会把 `Session ID` 带上,这样后端就知道你的身份状态了。 83 | 84 | 关于这种认证方式更详细的过程如下: 85 |  86 | 87 | 1. 用户使用账号密码请求登录; 88 | 2. 服务器验证用户身份后,创建session并分配唯一标识session ID,记录用户的认证状态,并将session ID放在响应头中返回给客户端(这一步中,可以使用服务端私钥对其进行签名,非必须); 89 | 3. 客户端解析响应头,将服务端分配的session ID保存在本地cookie中,下一次请求时会在请求头中附带cookie信息; 90 | 4. 服务端接收请求时解析头部的session ID,与本地保存的session进行对比校验请求的合法性。 91 | 92 | 使用 `Session` 的时候需要注意下面几个点: 93 | 94 | - 依赖 `Session` 的关键业务一定要确保客户端开启了 `Cookie`。 95 | - 注意 `Session` 的过期时间。 96 | 97 | ## 多服务器节点下 Session-Cookie 方案如何做? 98 | 99 | Session-Cookie 方案在单体环境是一个非常好的身份认证方案。但是,当服务器水平拓展成多节点时,Session-Cookie 方案就要面临挑战了。 100 | 101 | 举个例子:假如我们部署了两份相同的服务 A,B,用户第一次登陆的时候 ,Nginx 通过负载均衡机制将用户请求转发到 A 服务器,此时用户的 Session 信息保存在 A 服务器。结果,用户第二次访问的时候 Nginx 将请求路由到 B 服务器,由于 B 服务器没有保存 用户的 Session 信息,导致用户需要重新进行登陆。 102 | 103 | **我们应该如何避免上面这种情况的出现呢?** 104 | 105 | 有几个方案可供大家参考: 106 | 107 | 1. 某个用户的所有请求都通过特性的哈希策略分配给同一个服务器处理。这样的话,每个服务器都保存了一部分用户的 Session 信息。服务器宕机,其保存的所有 Session 信息就完全丢失了。 108 | 2. 每一个服务器保存的 Session 信息都是互相同步的,也就是说每一个服务器都保存了全量的 Session 信息。每当一个服务器的 Session 信息发生变化,我们就将其同步到其他服务器。这种方案成本太大,并且,节点越多时,同步成本也越高。 109 | 3. 单独使用一个所有服务器都能访问到的数据节点(比如缓存)来存放 Session 信息。为了保证高可用,数据节点尽量要避免是单点。 110 | 111 | ## 如果没有 Cookie 的话 Session 还能用吗? 112 | 113 | 这是一道经典的面试题! 114 | 115 | 一般是通过 `Cookie` 来保存 `Session ID` ,假如你使用了 `Cookie` 保存 `Session ID` 的方案的话, 如果客户端禁用了 `Cookie`,那么 `Session` 就无法正常工作。 116 | 117 | 但是,并不是没有 `Cookie` 之后就不能用 `Session` 了,比如你可以将 `SessionID` 放在请求的 `url` 里面`https://golangguide.cn/?Session_id=xxx` 。这种方案的话可行,但是安全性和用户体验感降低。当然,为了你也可以对 `Session ID` 进行一次加密之后再传入后端。 118 | 119 | ## 为什么 Cookie 无法防止 CSRF 攻击,而 Token 可以? 120 | 121 | **CSRF(Cross Site Request Forgery)** 一般被翻译为 **跨站请求伪造** 。那么什么是 **跨站请求伪造** 呢?说简单用你的身份去发送一些对你不友好的请求。举个简单的例子: 122 | 123 | 小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了 10000 元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。 124 | 125 | ```html 126 | 科学理财,年盈利率过万> 127 | ``` 128 | 129 | 上面也提到过,进行 `Session` 认证的时候,我们一般使用 `Cookie` 来存储 `SessionId`,当我们登陆后后端生成一个 `SessionId` 放在 Cookie 中返回给客户端,服务端通过 Redis 或者其他存储工具记录保存着这个 `SessionId`,客户端登录以后每次请求都会带上这个 `SessionId`,服务端通过这个 `SessionId` 来标示你这个人。如果别人通过 `Cookie` 拿到了 `SessionId` 后就可以代替你的身份访问系统了。 130 | 131 | `Session` 认证中 `Cookie` 中的 `SessionId` 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。 132 | 133 | 但是,我们使用 `Token` 的话就不会存在这个问题,在我们登录成功获得 `Token` 之后,一般会选择存放在 `localStorage` (浏览器本地存储)中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 `Token`,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 `Token` 的,所以这个请求将是非法的。 134 | 135 |  136 | 137 | 需要注意的是:不论是 `Cookie` 还是 `Token` 都无法避免 **跨站脚本攻击(Cross Site Scripting)XSS** 。 138 | 139 | > 跨站脚本攻击(Cross Site Scripting)缩写为 CSS 但这会与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆。因此,有人将跨站脚本攻击缩写为 XSS。 140 | 141 | XSS 中攻击者会用各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本盗用信息比如 `Cookie` 。 142 | 143 | 推荐阅读:[如何防止 CSRF 攻击?—美团技术团队](https://tech.meituan.com/2018/10/11/fe-security-csrf.html) 144 | 145 | ## 什么是 JWT?JWT 由哪些部分组成? 146 | 147 | [JWT基础概念详解及使用](./jwt-intro.md) 148 | 149 | ## 如何基于 JWT 进行身份验证? 如何防止 JWT 被篡改? 150 | 151 | [JWT基础概念详解及使用](./jwt-intro.md) 152 | 153 | ## 什么是 SSO? 154 | 155 | SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后,我们同时也成功登陆京东的京东超市、京东国际、京东生鲜等子系统。 156 | 157 |  158 | 159 | 160 | 161 | ## SSO 有什么好处? 162 | 163 | - **用户角度** :用户能够做到一次登录多次使用,无需记录多套用户名和密码,省心。 164 | - **系统管理员角度** : 管理员只需维护好一个统一的账号中心就可以了,方便。 165 | - **新系统开发角度:** 新系统开发时只需直接对接统一的账号中心即可,简化开发流程,省时。 166 | 167 | ## 如何设计实现一个 SSO 系统? 168 | 169 | [SSO单点登录详解](./sso-intro.md) 170 | 171 | ## 什么是 OAuth 2.0? 172 | 173 | OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。而 OAuth 2.0 是对 OAuth 1.0 的完全重新设计,OAuth 2.0 更快,更容易实现,OAuth 1.0 已经被废弃。详情请见:[rfc6749](https://tools.ietf.org/html/rfc6749)。 174 | 175 | 实际上它就是一种授权机制,它的最终目的是为第三方应用颁发一个有时效性的令牌 Token,使得第三方应用能够通过该令牌获取相关的资源。 176 | 177 | OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了第三方登录的时候一般就是使用的 OAuth 2.0 协议。 178 | 179 | 另外,现在 OAuth 2.0 也常见于支付场景(微信支付、支付宝支付)和开发平台(微信开放平台、阿里开放平台等等)。 180 | 181 | 下图是 [Slack OAuth 2.0 第三方登录](https://api.slack.com/legacy/oauth)的示意图: 182 | 183 |  184 | 185 | **推荐阅读:** 186 | 187 | - [OAuth 2.0 的一个简单解释](http://www.ruanyifeng.com/blog/2019/04/oauth_design.html) 188 | - [10 分钟理解什么是 OAuth 2.0 协议](https://deepzz.com/post/what-is-oauth2-protocol.html) 189 | - [OAuth 2.0 的四种方式](http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html) 190 | - [GitHub OAuth 第三方登录示例教程](http://www.ruanyifeng.com/blog/2019/04/github-oauth.html) 191 | 192 | ## 参考 193 | 194 | - https://blog.csdn.net/weixin_35016347/article/details/108776512 195 | - Introduction to JSON Web Tokens:https://jwt.io/introduction 196 | - JSON Web Token Claims:https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-claims 197 | -------------------------------------------------------------------------------- /system-design/security/casbin-intro.md: -------------------------------------------------------------------------------- 1 | ## [什么是Casbin?](https://casbin.org/docs/zh-CN/overview) 2 | 权限管理在几乎每个系统中都是必备的模块。如果项目开发每次都要实现一次权限管理,无疑会浪费开发时间,增加开发成本。因此,casbin库出现了。**casbin是一个强大、高效的访问控制库。支持常用的多种访问控制模型,如ACL/RBAC/ABAC等**。可以实现灵活的访问权限控制。同时,casbin支持多种编程语言,Go/Java/Node/PHP/Python/.NET/Rust。我们只需要一次学习,多处运用。 3 | 4 | Casbin 可以: 5 | 1. 支持自定义请求的格式,默认的请求格式为{subject, object, action}。 6 | 2. 具有访问控制模型model和策略policy两个核心概念。 7 | 3. 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。 8 | 4. 支持内置的超级用户 例如:root 或 administrator。超级用户4可以执行任何操作而无需显式的权限声明。 9 | 5. 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo* 10 | 11 | Casbin 不能: 12 | 1. 身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。 13 | 2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。 14 | 15 | ## 工作原理 16 | 在 Casbin 中, 访问控制模型被抽象为基于 PERM (Policy, Effect, Request, Matcher) 的一个文件。 因此,切换或升级项目的授权机制与修改配置一样简单。 您可以通过组合可用的模型来定制您自己的访问控制模型。 例如,您可以在一个model中结合RBAC角色和ABAC属性,并共享一组policy规则。 17 | 18 | PERM模式由四个基础(政策Policy、效果Effect、请求Request、匹配Matcher)组成,描述了资源与用户之间的关系。 19 | 20 | ### 请求 21 | 定义请求参数。基本请求是一个元组对象,至少需要**主体(访问实体)、对象(访问资源) 和动作(访问方式)** 22 | 23 | 例如,一个请求可能长这样: r={sub,obj,act} 24 | 25 | 它实际上定义了我们应该提供访问控制匹配功能的参数名称和顺序。 26 | 27 | ### 策略 28 | 定义访问策略模式。事实上,它是在政策规则文件中定义字段的名称和顺序。 29 | 30 | 例如: p={sub, obj, act} 或 p={sub, obj, act, eft} 31 | 32 | 注⚠️:如果未定义eft (policy result,只包含两种结果allow/deny),则策略文件中的结果字段将不会被读取, 和匹配的策略结果将默认被允许。 33 | 34 | ### 匹配器 35 | 匹配请求和政策的规则。 36 | 37 | 例如: m = r.sub == p.sub && r.act == p.act && r.obj == p.obj 这个简单和常见的匹配规则意味着如果请求的参数(访问实体,访问资源和访问方式)匹配, 如果可以在策略中找到资源和方法,那么策略结果(p.eft)便会返回。 策略的结果将保存在 p.eft 中。 38 | 39 | ### 效果 40 | 它可以被理解为一种模型,在这种模型中,对匹配结果再次作出逻辑组合判断。 41 | 42 | 例如: e = some (where (p.eft == allow)) 43 | 44 | 这句话意味着,如果匹配的策略结果有一些是允许的,那么最终结果为真。 45 | 46 | 让我们看看另一个示例: e = some (where (p.eft == allow)) && !some(where (p.eft == deny) 此示例组合的逻辑含义是:如果有符合允许结果的策略且没有符合拒绝结果的策略, 结果是为真。 换言之,当匹配策略均为允许(没有任何否认)是为真(更简单的是,既允许又同时否认,拒绝就具有优先地位)。 47 | 48 | Policy effect中只有以下几种类型 49 |  50 | 51 | Casbin中最基本、最简单的model是ACL。ACL中的model 配置为: 52 | 53 | ```casbin 54 | # 在线编辑器 55 | https://casbin.org/en/editor 56 | 57 | # Request definition 58 | [request_definition] 59 | r = sub, obj, act 60 | 61 | # Policy definition 62 | [policy_definition] 63 | p = sub, obj, act 64 | 65 | # Policy effect 66 | [policy_effect] 67 | e = some(where (p.eft == allow)) 68 | 69 | # Matchers 70 | [matchers] 71 | m = r.sub == p.sub && r.obj == p.obj && r.act == p.act 72 | ``` 73 | 74 | ## 角色域 75 | role_definiton 角色域 76 | - g = _ , _ 表示以角色为基础 77 | - g = _ , _ , _ 表示以域为基础(多商户模式) 78 | 79 | Casbin中RABC(Role-Based Access Control,基于角色的访问控制)的model 配置为: 80 | ```casbin 81 | # 在线编辑器 82 | https://casbin.org/en/editor 83 | 84 | # 请求入参(实体、资源、方法) 85 | [request_definition] 86 | r = sub, obj, act 87 | 88 | # 策略(实体、资源、方法) 89 | [policy_definition] 90 | p = sub, obj, act 91 | 92 | # 相比ACL 多了一个角色域 93 | [role_definition] 94 | g = _, _ # 这里的意思是g收两个参数 g=用户,角色 95 | 96 | # 经过下面的匹配规则匹配后的bool值中是否有一条等于allow的 97 | [policy_effect] 98 | e = some(where (p.eft == allow)) 99 | 100 | # 这里的g 有点类似转换器的意思 r.sub被p.sub这个角色替换 101 | [matchers] 102 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 103 | ``` 104 | 105 | 多商户模型的model 配置为: 106 | ```casbin 107 | # 在线编辑器 108 | https://casbin.org/en/editor 109 | 110 | [request_definition] 111 | r = sub, dom, obj, act 112 | 113 | [policy_definition] 114 | p = sub, dom, obj, act 115 | 116 | [role_definition] 117 | g = _, _, _ 118 | 119 | [policy_effect] 120 | e = some(where (p.eft == allow)) 121 | 122 | [matchers] 123 | m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act 124 | ``` 125 | 126 | ## casbin应用于golang中 127 | 第一种:使用配置文件的方式 128 | 其中,model.conf中的文件内容如下: 129 | ```conf 130 | [request_definition] 131 | r = sub, obj, act 132 | 133 | [policy_definition] 134 | p = sub, obj, act 135 | 136 | [policy_effect] 137 | e = some(where (p.eft == allow)) 138 | 139 | [matchers] 140 | m = r.sub == p.sub && r.obj == p.obj && r.act == p.act 141 | ``` 142 | policy.csv中的文件内容如下: 143 | ```csv 144 | p,zhangsan,data1,read 145 | p,zhangsan,data1,write 146 | ``` 147 | 开始使用 148 | ```golang 149 | # 引入casbin包 150 | go get github.com/casbin/casbin/v2 151 | ``` 152 | ```golang 153 | package main 154 | 155 | import ( 156 | "fmt" 157 | "github.com/casbin/casbin/v2" 158 | "log" 159 | ) 160 | 161 | func main() { 162 | // 1.创建模型和policy 使用配置文件的放松 163 | e, err := casbin.NewEnforcer("./model.conf", "./policy.csv") 164 | 165 | sub := "alice" // 想要访问资源的用户 166 | obj := "data1" // 将要被访问的资源 167 | act := "read" // 用户对资源实施的操作 168 | 169 | // 2.进行检查 170 | ok, err := e.Enforce(sub, obj, act) 171 | 172 | if err != nil { 173 | // 处理错误 174 | log.Fatal("err=>", err) 175 | return 176 | } 177 | 178 | if ok { 179 | // 允许 alice 读取 data1 180 | fmt.Println("匹配成功") 181 | } else { 182 | // 拒绝请求,抛出异常 183 | fmt.Println("匹配失败") 184 | } 185 | } 186 | ``` 187 | 第二种:将模型和策略存在数据库中 采用gorm映射 需要使用适配器 188 | ```golang 189 | package main 190 | 191 | import ( 192 | "fmt" 193 | "github.com/casbin/casbin/v2" 194 | gormadapter "github.com/casbin/gorm-adapter/v3" 195 | _ "github.com/go-sql-driver/mysql" 196 | "log" 197 | ) 198 | 199 | func main() { 200 | // 将策略policy保存到数据库中 201 | a, _ := gormadapter.NewAdapter("mysql", "root:rootroot@tcp(127.0.0.1:3306)/casbin", true) 202 | e, _ := casbin.NewEnforcer("./base/demo_casbin_2/model.conf", a) 203 | 204 | // 添加一个策略 重复策略只添加一次 205 | added, err := e.AddPolicy("zhangsan", "data1", "read") 206 | if err != nil { 207 | return 208 | } 209 | 210 | if added { 211 | fmt.Println("添加策略入库成功") 212 | } 213 | 214 | // Load the policy from DB. 215 | e.LoadPolicy() 216 | 217 | // Check the permission. 218 | ok, err := e.Enforce("alice", "data1", "read") 219 | if err != nil { 220 | // 处理错误 221 | log.Fatal("err=>", err) 222 | return 223 | } 224 | 225 | if ok { 226 | fmt.Println("匹配成功") 227 | } else { 228 | fmt.Println("匹配失败") 229 | } 230 | 231 | // Modify the policy. 即增删改查policy 232 | // e.AddPolicy(...) 233 | // e.RemovePolicy(...) 234 | 235 | // Save the policy back to DB. 236 | e.SavePolicy() 237 | } 238 | 239 | ``` 240 | match匹配规则自定义函数 241 | ```golang 242 | package main 243 | 244 | import ( 245 | "fmt" 246 | "github.com/casbin/casbin/v2" 247 | gormadapter "github.com/casbin/gorm-adapter/v3" 248 | _ "github.com/go-sql-driver/mysql" 249 | ) 250 | 251 | func main() { 252 | // 将策略policy保存到数据库中 253 | a, _ := gormadapter.NewAdapter("mysql", "root:rootroot@tcp(127.0.0.1:3306)/casbin", true) // Your driver and data source. 254 | e, _ := casbin.NewEnforcer("./base/demo_casbin_3/model.conf", a) 255 | 256 | // 在casbin的执行者(enforcer)中添加这个自定义匹配函数 257 | e.AddFunction("my_func", KeyMatchFunc) 258 | 259 | // Load the policy from DB. 260 | e.LoadPolicy() 261 | 262 | // Check the permission. 263 | ok, _ := e.Enforce("zhangsan", "data1", "read") 264 | if ok { 265 | fmt.Println("匹配成功") 266 | } else { 267 | fmt.Println("匹配失败") 268 | } 269 | } 270 | 271 | // KeyMatch 自定义匹配函数 272 | func KeyMatch(key1 string, key2 string) bool { 273 | // 简单写一下 274 | return key1 == key2 275 | } 276 | 277 | // 使用interface{}封装 278 | func KeyMatchFunc(args ...interface{}) (interface{}, error) { 279 | name1 := args[0].(string) 280 | name2 := args[1].(string) 281 | 282 | return (bool)(KeyMatch(name1, name2)), nil 283 | } 284 | 285 | ``` 286 | 此时的model中的match匹配规则为: 287 | ```conf 288 | [request_definition] 289 | r = sub, obj, act 290 | 291 | [policy_definition] 292 | p = sub, obj, act 293 | 294 | [policy_effect] 295 | e = some(where (p.eft == allow)) 296 | 297 | # 可以看到自定义的匹配函数my_func 298 | [matchers] 299 | m = r.sub == p.sub && my_func(r.obj,p.obj) && r.act == p.act 300 | ``` 301 | 302 | 303 | ## 参考 304 | - Casbin官方文档 - https://casbin.org/docs/zh-CN/overview -------------------------------------------------------------------------------- /system-design/security/images/jwt.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /system-design/security/images/session-cookie.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /system-design/security/images/session-cookie.svg: -------------------------------------------------------------------------------- 1 | 客户端客户端1.已发送登录信息(用户id、密码)1.已发送登录信息(用户id、密码)2.发送包含Session ID的CookieSet-Cookie: xxxxx2.发送包含Session ID的Cookie...客户端客户端向客户端发放Session ID,记录认证状态向客户端发放Session ID,记录认证状态2.发送包含Session ID的CookieSet-Cookie: xxxxx2.发送包含Session ID的Cookie...通过验证Session ID来判定对方是否是真实用户通过验证Session ID...Viewer does not support full SVG 1.1 -------------------------------------------------------------------------------- /system-design/security/images/sso.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /system-design/security/images/sso.svg: -------------------------------------------------------------------------------- 1 | 京东超市京东超市京东国际京东国际京东生鲜京东生鲜............SSOSSOViewer does not support full SVG 1.1 -------------------------------------------------------------------------------- /system-design/security/jwt-intro.md: -------------------------------------------------------------------------------- 1 | ## 什么是 JWT? 2 | 3 | JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。 4 | 5 | JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。 6 | 7 | 可以看出,**JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则** 。 8 | 9 | 并且, 使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。 10 | 11 | 我在 [JWT 优缺点分析](./advantages&disadvantages-of-jwt.md)这篇文章中有详细介绍到使用 JWT 做身份认证的优势和劣势。 12 | 13 | 下面是 [RFC 7519](https://tools.ietf.org/html/rfc7519) 对 JWT 做的较为正式的定义。 14 | 15 | > JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. ——[JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) 16 | 17 | ## JWT 由哪些部分组成? 18 | 19 |  20 | 21 | JWT 本质上就是一组字串,通过(`.`)切分成三个为 Base64 编码的部分: 22 | 23 | - **Header** : 描述 JWT 的元数据,定义了生成签名的算法以及 `Token` 的类型。 24 | - **Payload** : 用来存放实际需要传递的数据 25 | - **Signature(签名)** :服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。 26 | 27 | JWT 通常是这样的:`xxxxx.yyyyy.zzzzz`。 28 | 29 | 示例: 30 | 31 | ``` 32 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. 33 | eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. 34 | SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 35 | ``` 36 | 37 | 你可以在 [jwt.io](https://jwt.io/) 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。 38 | 39 | Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。 40 | 41 |  42 | 43 | ### Header 44 | 45 | Header 通常由两部分组成: 46 | 47 | - `typ`(Type):令牌类型,也就是 JWT。 48 | - `alg`(Algorithm) :签名算法,比如 HS256。 49 | 50 | 示例: 51 | 52 | ```json 53 | { 54 | "alg": "HS256", 55 | "typ": "JWT" 56 | } 57 | ``` 58 | 59 | JSON 形式的 Header 被转换成 Base64 编码,成为 JWT 的第一部分。 60 | 61 | ### Payload 62 | 63 | Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。 64 | 65 | Claims 分为三种类型: 66 | 67 | - **Registered Claims(注册声明)** :预定义的一些声明,建议使用,但不是强制性的。 68 | - **Public Claims(公有声明)** :JWT 签发方可以自定义的声明,但是为了避免冲突,应该在 [IANA JSON Web Token Registry](https://www.iana.org/assignments/jwt/jwt.xhtml) 中定义它们。 69 | - **Private Claims(私有声明)** :JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景使用。 70 | 71 | 下面是一些常见的注册声明: 72 | 73 | - `iss`(issuer):JWT 签发方。 74 | - `iat`(issued at time):JWT 签发时间。 75 | - `sub`(subject):JWT 主题。 76 | - `aud`(audience):JWT 接收方。 77 | - `exp`(expiration time):JWT 的过期时间。 78 | - `nbf`(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。 79 | - `jti`(JWT ID):JWT 唯一标识。 80 | 81 | 示例: 82 | 83 | ```json 84 | { 85 | "uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a", 86 | "sub": "1234567890", 87 | "name": "John Doe", 88 | "exp": 15323232, 89 | "iat": 1516239022, 90 | "scope": ["admin", "user"] 91 | } 92 | ``` 93 | 94 | Payload 部分默认是不加密的,**一定不要将隐私信息存放在 Payload 当中!!!** 95 | 96 | JSON 形式的 Payload 被转换成 Base64 编码,成为 JWT 的第二部分。 97 | 98 | ### Signature 99 | 100 | Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。 101 | 102 | 这个签名的生成需要用到: 103 | 104 | - Header + Payload。 105 | - 存放在服务端的密钥(一定不要泄露出去)。 106 | - 签名算法。 107 | 108 | 签名的计算公式如下: 109 | 110 | ``` 111 | HMACSHA256( 112 | base64UrlEncode(header) + "." + 113 | base64UrlEncode(payload), 114 | secret) 115 | ``` 116 | 117 | 算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(`.`)分隔,这个字符串就是 JWT 。 118 | 119 | ## golang中是如何使用JWT的? 120 | ### JWT加密 121 | ``` golang 122 | package main 123 | 124 | import ( 125 | "fmt" 126 | "github.com/dgrijalva/jwt-go" // 需要导入这个包 127 | "log" 128 | "time" 129 | ) 130 | 131 | /** 132 | jwt是一种后端不做存储的前端身份验证工具 133 | 分为三部分:Header Payload Signature 134 | */ 135 | 136 | type MyClaim struct { 137 | UserName string `json:"user_name"` 138 | jwt.StandardClaims 139 | } 140 | 141 | var ( 142 | TokenSecret = []byte("wo_shi_zmk") // token加密的key 143 | ) 144 | 145 | func main() { 146 | /*jwt加密部分*/ 147 | 148 | // Create the Claims 149 | claims := MyClaim{ 150 | UserName: "Krade", 151 | StandardClaims: jwt.StandardClaims{ 152 | ExpiresAt: time.Now().Add(time.Hour * 2).Unix(), // 过期时间 常见的字段 这里设置为两小时后 153 | Issuer: "zmk", // 签发者 154 | }, 155 | } 156 | 157 | // 带有参数的生成 通常使用NewWithClaims(加密算法,结构体MapClaims) 因为我们可以通过匿名结构体来实现Claims接口 从而可以携带自己的参数 158 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 159 | 160 | // 签名Signature 头部Header Payload 161 | fmt.Println("token中的内容为=>", token) // { 0x14000128180 map[alg:HS256 typ:JWT] {Krade { 15000 0 zmk 0 }} false} 162 | 163 | // 对token传给前端时进行加密 这里的signedString就可以给前端使用了 前端存在浏览器缓存中 请求后端时放在头部携带中丢回来 164 | signedString, err := token.SignedString(TokenSecret) 165 | if err != nil { 166 | log.Fatalf("加密失败[%v]", err) 167 | return 168 | } 169 | fmt.Println("加密输出的内容为", signedString) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJLcmFkZSIsImV4cCI6MTY2ODUyNzcxMiwiaXNzIjoiem1rIn0.0H2T_iQqN8Xzh7jWIm8kbATnbDvaMqCd4EnTWcyid0k 170 | } 171 | 172 | ``` 173 | 174 | ### JWT解析 175 | ```golang 176 | package main 177 | 178 | import ( 179 | "fmt" 180 | "github.com/dgrijalva/jwt-go" 181 | "log" 182 | ) 183 | 184 | type MyClaim struct { 185 | UserName string `json:"user_name"` 186 | jwt.StandardClaims 187 | } 188 | 189 | var ( 190 | TokenSecret = []byte("wo_shi_zmk") // token加密的key 191 | ) 192 | 193 | func main() { 194 | /*jwt解密部分*/ 195 | 196 | // 待解密的字符串 197 | tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJLcmFkZSIsImV4cCI6MTY2ODUyNzcxMiwiaXNzIjoiem1rIn0.0H2T_iQqN8Xzh7jWIm8kbATnbDvaMqCd4EnTWcyid0k" 198 | 199 | // 解析jwt 200 | // 先使用指定key 将字符串解密成Claim结构体 201 | token, err := jwt.ParseWithClaims(tokenString, &MyClaim{}, func(token *jwt.Token) (interface{}, error) { 202 | return TokenSecret, nil 203 | }) 204 | 205 | if err != nil { 206 | log.Fatalln("解析失败", err) 207 | return 208 | } 209 | 210 | fmt.Printf("解析出来的内容为:%v\n 后端需要使用的是jwt的claim:%v\n", token, token.Claims) // &{eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJLcmFkZSIsImV4cCI6MTY2ODUyNzcxMiwiaXNzIjoiem1rIn0.0H2T_iQqN8Xzh7jWIm8kbATnbDvaMqCd4EnTWcyid0k 0x1400012[alg:HS256 typ:JWT] 0x1400015c000 0H2T_iQqN8Xzh7jWIm8kbATnbDvaMqCd4EnTWcyid0k true} 211 | 212 | // 对claim做个断言 token.Valid经过验证 213 | if claim, ok := token.Claims.(*MyClaim); ok && token.Valid { 214 | fmt.Println("UserName=>", claim.UserName) 215 | } 216 | 217 | } 218 | 219 | ``` 220 | 221 | ## 如何基于 JWT 进行身份验证? 222 | 223 | 在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。 224 | 225 |  226 | 227 | 简化后的步骤如下: 228 | 229 | 1. 用户向服务器发送用户名、密码以及验证码用于登陆系统。 230 | 2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。 231 | 3. 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。 232 | 4. 服务端检查 JWT 并从中获取用户相关信息。 233 | 234 | 两点建议: 235 | 236 | 1. 建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。 237 | 2. 请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的 `Authorization` 字段中(`Authorization: Bearer Token`) 238 | 239 | ## 如何防止 JWT 被篡改? 240 | 241 | 有了签名之后,即使 JWT 被泄露或者解惑,黑客也没办法同时篡改 Signature 、Header 、Payload。 242 | 243 | 这是为什么呢?因为服务端拿到 JWT 之后,会解析出其中包含的 Header、Payload 以及 Signature 。服务端会根据 Header、Payload、密钥再次生成一个 Signature。拿新生成的 Signature 和 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改。 244 | 245 | 不过,如果服务端的秘钥也被泄露的话,黑客就可以同时篡改 Signature 、Header 、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。 246 | 247 | **密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。** 248 | 249 | ## 如何加强 JWT 的安全性? 250 | 251 | 1. 使用安全系数高的加密算法。 252 | 2. 使用成熟的开源库,没必要造轮子。 253 | 3. JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。 254 | 4. 一定不要将隐私信息存放在 Payload 当中。 255 | 5. 密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。 256 | 6. Payload 要加入 `exp` (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。 257 | 7. ...... -------------------------------------------------------------------------------- /system-design/security/sso-intro.md: -------------------------------------------------------------------------------- 1 | > 本文授权转载自 : https://ken.io/note/sso-design-implement 作者:ken.io 2 | 3 | ## SSO 介绍 4 | 5 | ### 什么是 SSO? 6 | 7 | SSO 英文全称 Single Sign On,单点登录。SSO 是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 8 | 9 | 例如你登录网易账号中心(https://reg.163.com/ )之后访问以下站点都是登录状态。 10 | 11 | - 网易直播 [https://v.163.com](https://v.163.com/) 12 | - 网易博客 [https://blog.163.com](https://blog.163.com/) 13 | - 网易花田 [https://love.163.com](https://love.163.com/) 14 | - 网易考拉 [https://www.kaola.com](https://www.kaola.com/) 15 | - 网易 Lofter [http://www.lofter.com](http://www.lofter.com/) 16 | 17 | ### SSO 有什么好处? 18 | 19 | 1. **用户角度** :用户能够做到一次登录多次使用,无需记录多套用户名和密码,省心。 20 | 2. **系统管理员角度** : 管理员只需维护好一个统一的账号中心就可以了,方便。 21 | 3. **新系统开发角度:** 新系统开发时只需直接对接统一的账号中心即可,简化开发流程,省时。 22 | 23 | ## SSO 设计与实现 24 | 25 | 本篇文章也主要是为了探讨如何设计&实现一个 SSO 系统 26 | 27 | 以下为需要实现的核心功能: 28 | 29 | - 单点登录 30 | - 单点登出 31 | - 支持跨域单点登录 32 | - 支持跨域单点登出 33 | 34 | ### 核心应用与依赖 35 | 36 |  37 | 38 | | 应用/模块/对象 | 说明 | 39 | | ----------------- | ----------------------------------- | 40 | | 前台站点 | 需要登录的站点 | 41 | | SSO 站点-登录 | 提供登录的页面 | 42 | | SSO 站点-登出 | 提供注销登录的入口 | 43 | | SSO 服务-登录 | 提供登录服务 | 44 | | SSO 服务-登录状态 | 提供登录状态校验/登录信息查询的服务 | 45 | | SSO 服务-登出 | 提供用户注销登录的服务 | 46 | | 数据库 | 存储用户账户信息 | 47 | | 缓存 | 存储用户的登录信息,通常使用 Redis | 48 | 49 | ### 用户登录状态的存储与校验 50 | 51 | 常见的 Web 框架对于 Session 的实现都是生成一个 SessionId 存储在浏览器 Cookie 中。然后将 Session 内容存储在服务器端内存中,这个 [ken.io](https://ken.io/) 在之前[Session 工作原理](https://ken.io/note/session-principle-skill)中也提到过。整体也是借鉴这个思路。 52 | 53 | 用户登录成功之后,生成 AuthToken 交给客户端保存。如果是浏览器,就保存在 Cookie 中。如果是手机 App 就保存在 App 本地缓存中。本篇主要探讨基于 Web 站点的 SSO。 54 | 55 | 用户在浏览需要登录的页面时,客户端将 AuthToken 提交给 SSO 服务校验登录状态/获取用户登录信息 56 | 57 | 对于登录信息的存储,建议采用 Redis,使用 Redis 集群来存储登录信息,既可以保证高可用,又可以线性扩充。同时也可以让 SSO 服务满足负载均衡/可伸缩的需求。 58 | 59 | | 对象 | 说明 | 60 | | --------- | ------------------------------------------------------------ | 61 | | AuthToken | 直接使用 UUID/GUID 即可,如果有验证 AuthToken 合法性需求,可以将 UserName+时间戳加密生成,服务端解密之后验证合法性 | 62 | | 登录信息 | 通常是将 UserId,UserName 缓存起来 | 63 | 64 | ### 用户登录/登录校验 65 | 66 | **登录时序图** 67 | 68 |  69 | 70 | 按照上图,用户登录后 AuthToken 保存在 Cookie 中。 domain=test.com 71 | 浏览器会将 domain 设置成 .test.com, 72 | 73 | 这样访问所有 \*.test.com 的 web 站点,都会将 AuthToken 携带到服务器端。 74 | 然后通过 SSO 服务,完成对用户状态的校验/用户登录信息的获取 75 | 76 | **登录信息获取/登录状态校验** 77 | 78 |  79 | 80 | ### 用户登出 81 | 82 | 用户登出时要做的事情很简单: 83 | 84 | 1. 服务端清除缓存(Redis)中的登录状态 85 | 2. 客户端清除存储的 AuthToken 86 | 87 | **登出时序图** 88 | 89 |  90 | 91 | ### 跨域登录、登出 92 | 93 | 前面提到过,核心思路是客户端存储 AuthToken,服务器端通过 Redis 存储登录信息。由于客户端是将 AuthToken 存储在 Cookie 中的。所以跨域要解决的问题,就是如何解决 Cookie 的跨域读写问题。 94 | 95 | 解决跨域的核心思路就是: 96 | 97 | - 登录完成之后通过回调的方式,将 AuthToken 传递给主域名之外的站点,该站点自行将 AuthToken 保存在当前域下的 Cookie 中。 98 | - 登出完成之后通过回调的方式,调用非主域名站点的登出页面,完成设置 Cookie 中的 AuthToken 过期的操作。 99 | 100 | **跨域登录(主域名已登录)** 101 | 102 |  103 | 104 | **跨域登录(主域名未登录)** 105 | 106 |  107 | 108 | **跨域登出** 109 | 110 |  111 | 112 | ## 说明 113 | 114 | - 关于方案 :这次设计方案更多是提供实现思路。如果涉及到 APP 用户登录等情况,在访问 SSO 服务时,增加对 APP 的签名验证就好了。当然,如果有无线网关,验证签名不是问题。 115 | - 关于时序图:时序图中并没有包含所有场景,只列举了核心/主要场景,另外对于一些不影响理解思路的消息能省就省了。 116 | -------------------------------------------------------------------------------- /tools/docker/docker-compose.md: -------------------------------------------------------------------------------- 1 | # Docker-compose容器编排技术 2 | 3 |  4 | 5 | 项目地址: [https://github.com/docker/compose](https://github.com/docker/compose ) 6 | 7 | Compose 是用来定义和运行一个或多个容器应用的`工具`。 8 | 9 | Compose 可以`简化`容器镜像的建立及容器的运行。 10 | 11 | Compose 使用python语言开发,非常适合在`单机环境`里部署一个或多个容器,并自动把多个容器`互相关联`起来。 12 | 13 | Compose 还是Docker`三剑客`之一。 14 | 15 | ### 本质 16 | 通过docker-api来与docker-server进行交互的。 17 | 18 | ### 两个重要概念 19 | - **服务**(***service***):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。 20 | 21 | - **项目**(***project***):由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yml中定义。 22 | 23 | ### 安装docker 24 | ```bash 25 | # 安装依赖 26 | yum install -y yum-utils device-mapper-persistent-data lvm2 27 | 28 | # yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 29 | 30 | # 指定docker社区版的镜像源 31 | yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 32 | 33 | yum-config-manager --enable docker-ce-edge 34 | 35 | yum-config-manager --enable docker-ce-test 36 | 37 | # 安装docker社区版 38 | yum install -y docker-ce 39 | 40 | # 启动docker 41 | systemctl start docker 42 | 43 | # 查看docker版本 44 | docker --version 45 | 46 | # 开机启动 47 | chkconfig docker on 48 | ``` 49 | 50 | ### 安装compose 51 | ```bash 52 | # 官方下载地址 53 | curl -L https://github.com/docker/compose/releases/download/1.23.0-rc2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose 54 | # 国内下载地址 55 | curl -L https://get.daocloud.io/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose 56 | # 如果没有curl命令则可以安装curl: 57 | yum instal -y curl 58 | chmod +x /usr/local/bin/docker-compose 59 | docker-compose version 60 | ``` 61 | 62 | ### 环境变量 63 | ```bash 64 | # 环境变量可以用来配置 Compose 的行为,以DOCKER_开头的变量和用来配置 Docker 命令行客户端的使用一样。 65 | 66 | COMPOSE_PROJECT_NAME 67 | 68 | COMPOSE_FILE 69 | 70 | DOCKER_HOST 71 | 72 | DOCKER_TLS_VERIFY 73 | 74 | DOCKER_CERT_PATH 75 | ``` 76 | 77 | ### docker-compose.yaml 78 | 整理的不全,详细请看 [官方文档](https://docs.docker.com/compose/compose-file/compose-file-v2/) 79 | ```yaml 80 | # 例子docker-compose.yml(不能正常运行,仅供参考) 81 | version: '2' #文档版本 82 | 83 | networks: 84 | helloworld: #一个叫helloworld的网络 85 | driver: bridge #使用桥接模式驱动网络 86 | 87 | services: 88 | web: 89 | image: hello-world #指定服务的镜像名称或者ID,如果镜像不在本地,compose会尝试拉取 90 | restart: always #表示总是重新启动容器 91 | net: "bridge" #网络采取桥接模式 92 | ports: 93 | - "80:80" 94 | depends_on: #用于解决依赖,启动先后的问题 95 | - redis 96 | networks: 97 | - helloworld 98 | dns: 99 | - 8.8.8.8 100 | - 9.9.9.9 101 | links: 102 | - web 103 | extra_hosts: 104 | - "redis:192.168.1.100" 105 | 106 | redis: 107 | image: redis 108 | container_name: redis 109 | commond: redis-server.sh ./redis.conf 110 | volumes: 111 | - ~/data:/data:rw 112 | expose: #暴露端口,但不映射到宿主机 113 | - "6379" 114 | networks: 115 | - helloworld 116 | ``` 117 | 118 | 1. image 119 | - 是指定服务的镜像名称或镜像ID。如果镜像在本地不存在,Compose将会尝试拉取镜像。 120 | ``` 121 | services: 122 | web: 123 | image: hello-world 124 | ``` 125 | 126 | 2. ==build== 127 | - 服务除了可以基于指定的镜像,还可以基于一份`Dockerfile`,在使用up启动时执行构建任务,构建标签是build,可以指定Dockerfile所在文件夹的路径。Compose将会利用Dockerfile自动构建镜像,然后使用镜像启动服务容器。 128 | ```yaml 129 | # 也可以是相对路径,只要上下文确定就可以读取到Dockerfile。 130 | build: /path/to/build/dir 131 | ``` 132 | ```yaml 133 | # 设定上下文根目录,然后以该目录为准指定Dockerfile。 134 | build: ./dir 135 | ``` 136 | ```yaml 137 | # build都是一个目录,如果要指定Dockerfile文件需要在build标签的子级标签中使用dockerfile标签指定。 138 | # 如果同时指定image和build两个标签,那么Compose会构建镜像并且把镜像命名为image值指定的名字。 139 | build: 140 | context: ../ 141 | dockerfile: path/of/Dockerfile 142 | ``` 143 | 3. context 144 | - context选项可以是Dockerfile的文件路径,也可以是到链接到git仓库的url,当提供的值是相对路径时,被解析为相对于撰写文件的路径,此目录也是发送到Docker守护进程的context 145 | ```yaml 146 | build: 147 | context: ./dir 148 | ``` 149 | 150 | 4. dockerfile 151 | - 使用dockerfile文件来构建,必须指定构建路径 152 | ```yaml 153 | build: 154 | context: . 155 | dockerfile: Dockerfile-alternate 156 | ``` 157 | 158 | 5. commond 159 | - 使用command可以覆盖容器启动后默认执行的命令。 160 | ```yaml 161 | command: bundle exec thin -p 3000 162 | ``` 163 | 164 | 6. container_name 165 | - Compose的容器名称格式是:<项目名称><服务名称><序号> 166 | - 可以自定义项目名称、服务名称,但如果想完全控制容器的命名,可以使用标签指定: 167 | ```yaml 168 | container_name: app 169 | ``` 170 | 171 | 7. depends_on 172 | - 在使用Compose时,最大的好处就是少打启动命令,但一般项目容器启动的顺序是有要求的,如果直接从上到下启动容器,必然会因为容器依赖问题而启动失败。例如在没启动数据库容器的时候启动应用容器,应用容器会因为找不到数据库而退出。depends_on标签用于解决容器的依赖、启动先后的问题 173 | ```yaml 174 | version: '2' 175 | services: 176 | web: 177 | build: . 178 | depends_on: 179 | - db 180 | - redis 181 | redis: 182 | image: redis 183 | db: 184 | image: postgres 185 | ``` 186 | 187 | 8. PID 188 | - 将PID模式设置为主机PID模式,跟主机系统共享进程命名空间。容器使用pid标签将能够访问和操纵其他容器和宿主机的名称空间。 189 | ```yaml 190 | pid: "host" 191 | ``` 192 | 193 | 9. ports 194 | - ports用于映射端口的标签。 195 | - 使用HOST:CONTAINER格式或者只是指定容器的端口,宿主机会随机映射端口。 196 | ```yaml 197 | ports: 198 | - "3000" 199 | - "8000:8000" #宿主机端口:容器端口 200 | - "49100:22" 201 | - "127.0.0.1:8001:8001" 202 | # 当使用HOST:CONTAINER格式来映射端口时,如果使用的容器端口小于60可能会得到错误得结果,因为YAML将会解析xx:yy这种数字格式为60进制。所以建议采用字符串格式。 203 | ``` 204 | 205 | 10. extra_hosts 206 | - 添加主机名的标签,会在/etc/hosts文件中添加一些记录。 207 | ```yaml 208 | extra_hosts: 209 | - "somehost:162.242.195.82" 210 | - "otherhost:50.31.209.229" 211 | ``` 212 | 213 | 11. volumes 214 | - 挂载一个目录或者一个已存在的数据卷容器,可以直接使用 [HOST:CONTAINER]格式,或者使用[HOST:CONTAINER:ro]格式,后者对于容器来说,数据卷是只读的,可以有效保护宿主机的文件系统。 215 | Compose的数据卷指定路径可以是相对路径,使用 . 或者 .. 来指定相对目录。 216 | 数据卷的格式可以是下面多种形式 217 | ```yaml 218 | volumes: 219 | # 只是指定一个路径,Docker 会自动在创建一个数据卷(这个路径是容器内部的)。 220 | - /var/lib/mysql 221 | # 使用绝对路径挂载数据卷 222 | - /opt/data:/var/lib/mysql 223 | # 以 Compose 配置文件为中心的相对路径作为数据卷挂载到容器。 224 | - ./cache:/tmp/cache 225 | # 使用用户的相对路径(~/ 表示的目录是 /home/<用户目录>/ 或者 /root/)。 226 | - ~/configs:/etc/configs/:ro 227 | # 已经存在的命名的数据卷。 228 | - datavolume:/var/lib/mysql 229 | # 如果不使用宿主机的路径,可以指定一个volume_driver。 230 | # volume_driver: mydriver 231 | ``` 232 | 233 | 12. volumes_from 234 | - 从另一个服务或容器挂载其数据卷: 235 | ```yaml 236 | volumes_from: 237 | - service_name 238 | - container_name 239 | ``` 240 | 241 | 13. dns 242 | - 自定义DNS服务器。可以是一个值,也可以是一个列表。 243 | ```yaml 244 | dns:8.8.8.8 245 | dns: 246 | - 8.8.8.8 247 | - 9.9.9.9 248 | ``` 249 | 250 | 14. expose 251 | - 暴露端口,但不映射到宿主机,只允许能被连接的服务访问。仅可以指定内部端口为参数,如下所示: 252 | ```yaml 253 | expose: 254 | - "3000" 255 | - "8000" 256 | ``` 257 | 258 | 15. links 259 | - 链接到其它服务中的容器。使用服务名称(同时作为别名),或者“服务名称:服务别名”(如 SERVICE:ALIAS),例如: 260 | ```yaml 261 | links: 262 | - db 263 | - db:database 264 | - redis 265 | ``` 266 | 267 | 16. net 268 | - 设置网络模式。 269 | ```text 270 | net: "bridge" 271 | net: "none" 272 | net: "host" 273 | ``` 274 | 275 | 17. env_file 276 | - 环境变量文件,可以在yml文件引用这些变量 277 | ```yaml 278 | env_file: .env 279 | env_file: 280 | - ./common.env 281 | - ./apps/web.env 282 | - /opt/runtime_opts.env 283 | ``` 284 | 285 | 18. extends 286 | - 扩展当前文件或其他文件中的另一个服务(可选覆盖配置)。 287 | ```yaml 288 | extends: 289 | file: common.yml 290 | service: webapp 291 | ``` 292 | 293 | 19. restart 294 | - no是默认的重启策略,它在任何情况下都不会重启容器。当always总是指定时,容器总是重新启动。如果退出码表明出现了on-failure错误,则on-failure策略将重新启动容器。 295 | ```yaml 296 | restart: no 297 | restart: always 298 | restart: on-failure 299 | restart: unless-stopped 300 | ``` 301 | 302 | 20. environment 303 | - 用于设置环境变量 304 | ```yaml 305 | environment: 306 | - xxx 307 | - yyy 308 | ``` 309 | 310 | 311 | ### 命令帮助 312 | ```bash 313 | docker-compose --h 314 | ``` 315 | ```text 316 | Define and run multi-container applications with Docker. 317 | 318 | Usage: 319 | docker-compose [-f ...] [options] [COMMAND] [ARGS...] 320 | docker-compose -h|--help 321 | 322 | Options: 323 | -f, --file FILE Specify an alternate compose file(指定yml文件的名字) 324 | (default: docker-compose.yml) 325 | -p, --project-name NAME Specify an alternate project name 326 | (default: directory name) 327 | --verbose Show more output 328 | --log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) 329 | --no-ansi Do not print ANSI control characters 330 | -v, --version Print version and exit 331 | -H, --host HOST Daemon socket to connect to 332 | 333 | --tls Use TLS; implied by --tlsverify 334 | --tlscacert CA_PATH Trust certs signed only by this CA 335 | --tlscert CLIENT_CERT_PATH Path to TLS certificate file 336 | --tlskey TLS_KEY_PATH Path to TLS key file 337 | --tlsverify Use TLS and verify the remote 338 | --skip-hostname-check Don't check the daemon's hostname against the 339 | name specified in the client certificate 340 | --project-directory PATH Specify an alternate working directory 341 | (default: the path of the Compose file) 342 | --compatibility If set, Compose will attempt to convert keys 343 | in v3 files to their non-Swarm equivalent 344 | 345 | Commands: 346 | build Build or rebuild services 347 | bundle Generate a Docker bundle from the Compose file 348 | config Validate and view the Compose file 349 | create Create services 350 | down Stop and remove containers, networks, images, and volumes 351 | events Receive real time events from containers 352 | exec Execute a command in a running container 353 | help Get help on a command 354 | images List images 355 | kill Kill containers 356 | logs View output from containers 357 | pause Pause services 358 | port Print the public port for a port binding 359 | ps List containers 360 | pull Pull service images 361 | push Push service images 362 | restart Restart services 363 | rm Remove stopped containers 364 | run Run a one-off command 365 | scale Set number of containers for a service 366 | start Start services 367 | stop Stop services 368 | top Display the running processes 369 | unpause Unpause services 370 | up Create and start containers 371 | version Show the Docker-Compose version information 372 | ``` -------------------------------------------------------------------------------- /tools/git-intro.md: -------------------------------------------------------------------------------- 1 | ## 版本控制 2 | 3 | ### 什么是版本控制 4 | 5 | 版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。 除了项目源代码,你可以对任何类型的文件进行版本控制。 6 | 7 | ### 为什么要版本控制 8 | 9 | 有了它你就可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态,你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。 10 | 11 | ### 本地版本控制系统 12 | 13 | 许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会改名加上备份时间以示区别。 这么做唯一的好处就是简单,但是特别容易犯错。 有时候会混淆所在的工作目录,一不小心会写错文件或者覆盖意想外的文件。 14 | 15 | 为了解决这个问题,人们很久以前就开发了许多种本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异。 16 | 17 |  18 | 19 | ### 集中化的版本控制系统 20 | 21 | 接下来人们又遇到一个问题,如何让在不同系统上的开发者协同工作? 于是,集中化的版本控制系统(Centralized Version Control Systems,简称 CVCS)应运而生。 22 | 23 | 集中化的版本控制系统都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。 24 | 25 |  26 | 27 | 这么做虽然解决了本地版本控制系统无法让在不同系统上的开发者协同工作的诟病,但也还是存在下面的问题: 28 | 29 | - **单点故障:** 中央服务器宕机,则其他人无法使用;如果中心数据库磁盘损坏又没有进行备份,你将丢失所有数据。本地版本控制系统也存在类似问题,只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险。 30 | - **必须联网才能工作:** 受网络状况、带宽影响。 31 | 32 | ### 分布式版本控制系统 33 | 34 | 于是分布式版本控制系统(Distributed Version Control System,简称 DVCS)面世了。 Git 就是一个典型的分布式版本控制系统。 35 | 36 | 这类系统,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。 37 | 38 |  39 | 40 | 分布式版本控制系统可以不用联网就可以工作,因为每个人的电脑上都是完整的版本库,当你修改了某个文件后,你只需要将自己的修改推送给别人就可以了。但是,在实际使用分布式版本控制系统的时候,很少会直接进行推送修改,而是使用一台充当“中央服务器”的东西。这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 41 | 42 | 分布式版本控制系统的优势不单是不必联网这么简单,后面我们还会看到 Git 极其强大的分支管理等功能。 43 | 44 | ## 认识 Git 45 | 46 | ### Git 简史 47 | 48 | Linux 内核项目组当时使用分布式版本控制系统 BitKeeper 来管理和维护代码。但是,后来开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux 内核社区免费使用 BitKeeper 的权力。 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds)基于使用 BitKeeper 时的经验教训,开发出自己的版本系统,而且对新的版本控制系统做了很多改进。 49 | 50 | ### Git 与其他版本管理系统的主要区别 51 | 52 | Git 在保存和对待各种信息的时候与其它版本控制系统有很大差异,尽管操作起来的命令形式非常相近,理解这些差异将有助于防止你使用中的困惑。 53 | 54 | 下面我们主要说一个关于 Git 与其他版本管理系统的主要差别:**对待数据的方式**。 55 | 56 | **Git采用的是直接记录快照的方式,而非差异比较。我后面会详细介绍这两种方式的差别。** 57 | 58 | 大部分版本控制系统(CVS、Subversion、Perforce、Bazaar 等等)都是以文件变更列表的方式存储信息,这类系统**将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。** 59 | 60 | 具体原理如下图所示,理解起来其实很简单,每当我们提交更新一个文件之后,系统都会记录这个文件做了哪些更新,以增量符号Δ(Delta)表示。 61 | 62 |  63 | 64 | **我们怎样才能得到一个文件的最终版本呢?** 65 | 66 | 很简单,高中数学的基本知识,我们只需要将这些原文件和这些增加进行相加就行了。 67 | 68 | **这种方式有什么问题呢?** 69 | 70 | 比如我们的增量特别特别多的话,如果我们要得到最终的文件是不是会耗费时间和性能。 71 | 72 | Git 不按照以上方式对待或保存数据。 反之,Git 更像是把数据看作是对小型文件系统的一组快照。 每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。 为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 **快照流**。 73 | 74 |  75 | 76 | ### Git 的三种状态 77 | 78 | Git 有三种状态,你的文件可能处于其中之一: 79 | 80 | 1. **已提交(committed)**:数据已经安全的保存在本地数据库中。 81 | 2. **已修改(modified)**:已修改表示修改了文件,但还没保存到数据库中。 82 | 3. **已暂存(staged)**:表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。 83 | 84 | 由此引入 Git 项目的三个工作区域的概念:**Git 仓库(.git directory)**、**工作目录(Working Directory)** 以及 **暂存区域(Staging Area)** 。 85 | 86 |  87 | 88 | 89 | **基本的 Git 工作流程如下:** 90 | 91 | 1. 在工作目录中修改文件。 92 | 2. 暂存文件,将文件的快照放入暂存区域。 93 | 3. 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。 94 | 95 | ## Git 使用快速入门 96 | 97 | ### 获取 Git 仓库 98 | 99 | 有两种取得 Git 项目仓库的方法。 100 | 101 | 1. 在现有目录中初始化仓库: 进入项目目录运行 `git init` 命令,该命令将创建一个名为 `.git` 的子目录。 102 | 2. 从一个服务器克隆一个现有的 Git 仓库: `git clone [url]` 自定义本地仓库的名字: `git clone [url] directoryname` 103 | 104 | ### 记录每次更新到仓库 105 | 106 | 1. **检测当前文件状态** : `git status` 107 | 2. **提出更改(把它们添加到暂存区**):`git add filename ` (针对特定文件)、`git add *`(所有文件)、`git add *.txt`(支持通配符,所有 .txt 文件) 108 | 3. **忽略文件**:`.gitignore` 文件 109 | 4. **提交更新:** `git commit -m "代码提交信息"` (每次准备提交前,先用 `git status` 看下,是不是都已暂存起来了, 然后再运行提交命令 `git commit`) 110 | 5. **跳过使用暂存区域更新的方式** : `git commit -a -m "代码提交信息"`。 `git commit` 加上 `-a` 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 `git add` 步骤。 111 | 6. **移除文件** :`git rm filename` (从暂存区域移除,然后提交。) 112 | 7. **对文件重命名** :`git mv README.md README`(这个命令相当于`mv README.md README`、`git rm README.md`、`git add README` 这三条命令的集合) 113 | 114 | ### 一个好的 Git 提交消息 115 | 一个好的 Git 提交消息如下: 116 | 117 | 标题行:用这一行来描述和解释你的这次提交 118 | 119 | 主体部分可以是很少的几行,来加入更多的细节来解释提交,最好是能给出一些相关的背景或者解释这个提交能修复和解决什么问题。 120 | 121 | 主体部分当然也可以有几段,但是一定要注意换行和句子不要太长。因为这样在使用 "git log" 的时候会有缩进比较好看。 122 | 123 | 提交的标题行描述应该尽量的清晰和尽量的一句话概括。这样就方便相关的 Git 日志查看工具显示和其他人的阅读。 124 | 125 | ### 推送改动到远程仓库 126 | 127 | - 如果你还没有克隆现有仓库,并欲将你的仓库连接到某个远程服务器,你可以使用如下命令添加:`git remote add origin ` ,比如我们要让本地的一个仓库和 Github 上创建的一个仓库关联可以这样`git remote add origin https://github.com/Snailclimb/test.git` 128 | - 将这些改动提交到远端仓库:`git push origin master` (可以把 *master* 换成你想要推送的任何分支) 129 | 130 | 如此你就能够将你的改动推送到所添加的服务器上去了。 131 | 132 | ### 远程仓库的移除与重命名 133 | 134 | - 将 test 重命名为 test1:`git remote rename test test1` 135 | - 移除远程仓库 test1:`git remote rm test1` 136 | 137 | ### 查看提交历史 138 | 139 | 在提交了若干更新,又或者克隆了某个项目之后,你也许想回顾下提交历史。 完成这个任务最简单而又有效的工具是 `git log` 命令。`git log` 会按提交时间列出所有的更新,最近的更新排在最上面。 140 | 141 | **可以添加一些参数来查看自己希望看到的内容:** 142 | 143 | 只看某个人的提交记录: 144 | 145 | ```shell 146 | git log --author=bob 147 | ``` 148 | 149 | ### 撤销操作 150 | 151 | 有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有 `--amend` 选项的提交命令尝试重新提交: 152 | 153 | ```shell 154 | git commit --amend 155 | ``` 156 | 157 | 取消暂存的文件 158 | 159 | ```shell 160 | git reset filename 161 | ``` 162 | 163 | 撤消对文件的修改: 164 | 165 | ```shell 166 | git checkout -- filename 167 | ``` 168 | 169 | 假如你想丢弃你在本地的所有改动与提交,可以到服务器上获取最新的版本历史,并将你本地主分支指向它: 170 | 171 | ```shell 172 | git fetch origin 173 | git reset --hard origin/master 174 | ``` 175 | 176 | 177 | ### 分支 178 | 179 | 分支是用来将特性开发绝缘开来的。在你创建仓库的时候,*master* 是“默认”的分支。在其他分支上进行开发,完成后再将它们合并到主分支上。 180 | 181 | 我们通常在开发新功能、修复一个紧急 bug 等等时候会选择创建分支。单分支开发好还是多分支开发好,还是要看具体场景来说。 182 | 183 | 创建一个名字叫做 test 的分支 184 | 185 | ```shell 186 | git branch test 187 | ``` 188 | 189 | 切换当前分支到 test(当你切换分支的时候,Git 会重置你的工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。 Git 会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样) 190 | 191 | ```shell 192 | git checkout test 193 | ``` 194 | 195 |  196 | 197 | 198 | 你也可以直接这样创建分支并切换过去(上面两条命令的合写) 199 | 200 | ```shell 201 | git checkout -b feature_x 202 | ``` 203 | 204 | 切换到主分支 205 | 206 | ```shell 207 | git checkout master 208 | ``` 209 | 210 | 合并分支(可能会有冲突) 211 | 212 | ```shell 213 | git merge test 214 | ``` 215 | 216 | 把新建的分支删掉 217 | 218 | ```shell 219 | git branch -d feature_x 220 | ``` 221 | 222 | 将分支推送到远端仓库(推送成功后其他人可见): 223 | 224 | ```shell 225 | git push origin 226 | ``` 227 | 228 | ## 推荐 229 | 230 | **在线演示学习工具:** 231 | 232 | 「补充,来自[issue729](https://github.com/Snailclimb/JavaGuide/issues/729)」Learn Git Branching https://oschina.gitee.io/learn-git-branching/ 。该网站可以方便的演示基本的git操作,讲解得明明白白。每一个基本命令的作用和结果。 233 | 234 | **推荐阅读:** 235 | 236 | - [Git - 简明指南](https://rogerdudler.github.io/git-guide/index.zh.html) 237 | - [图解Git](https://marklodato.github.io/visual-git-guide/index-zh-cn.html) 238 | - [猴子都能懂得Git入门](https://backlog.com/git-tutorial/cn/intro/intro1_1.html) 239 | - https://git-scm.com/book/en/v2 240 | - [Generating a new SSH key and adding it to the ssh-agent](https://help.github.com/en/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) 241 | - [一个好的 Git 提交消息,出自 Linus 之手](https://github.com/torvalds/subsurface-for-dirk/blob/a48494d2fbed58c751e9b7e8fbff88582f9b2d02/README#L88) 242 | --------------------------------------------------------------------------------