From a9edecbbc1a2358d7db75b984d04cea24d8b39e2 Mon Sep 17 00:00:00 2001 From: jint Date: Thu, 26 Dec 2024 11:18:42 +0800 Subject: [PATCH] style: format --- ...50\351\235\242\350\247\243\346\236\220.md" | 32 ++-- ...20\344\270\216\350\247\243\345\206\263.md" | 32 ++-- .../Java \344\270\255\347\232\204 SPI.md" | 2 +- ...4\351\224\201\342\200\235 \344\272\213.md" | 8 +- ...16\347\232\204\346\200\235\350\200\203.md" | 20 +-- ...va\345\220\216\347\253\257\351\242\230.md" | 76 ++++----- ...\346\200\247\302\267InnoDB Buffer Pool.md" | 2 +- ...14\346\255\245\346\234\272\345\210\266.md" | 2 +- ...51\346\272\203\346\201\242\345\244\215.md" | 2 +- ...43\344\272\233\344\272\213\345\204\277.md" | 2 +- ...73\344\273\216\345\244\215\345\210\266.md" | 46 +++--- ...14\346\255\245\345\244\215\345\210\266.md" | 4 +- ...44\272\216GTID\345\244\215\345\210\266.md" | 4 +- ...47\345\210\253\350\257\246\350\247\243.md" | 16 +- ...45\277\227(redolog\345\222\214undolog).md" | 16 +- ...43\347\240\201\345\210\206\344\272\253.md" | 20 +-- ...344\274\230\345\214\226 select count().md" | 2 +- ...01\344\271\220\350\247\202\351\224\201.md" | 4 +- ...01\347\232\204\351\235\242\347\272\261.md" | 153 +++++------------- ...60\346\215\256\345\255\227\345\205\270.md" | 2 +- ...23\345\255\227\347\254\246\351\233\206.md" | 6 +- ...16\347\211\207\346\225\264\347\220\206.md" | 10 +- ...345\212\240\351\224\201\347\232\204SQL.md" | 8 +- ...45\345\277\227\350\257\246\350\247\243.md" | 12 +- ...21\346\216\247\345\210\266\357\274\211.md" | 8 +- ...57\344\273\200\344\271\210\357\274\237.md" | 8 +- ...42\345\274\225\346\234\272\345\210\266.md" | 12 +- ...25\345\261\202\345\256\236\347\216\260.md" | 8 +- ...64\346\200\247\351\227\256\351\242\230.md" | 18 +-- ...20\357\274\210\344\270\212\357\274\211.md" | 8 +- ...20\357\274\210\344\270\213\357\274\211.md" | 2 +- ...20\347\240\201\345\210\206\346\236\220.md" | 8 +- ...25\351\242\230\351\233\206\351\224\246.md" | 8 +- ...\345\210\206\345\270\203\345\274\217ID.md" | 2 +- ...03\344\274\230\345\256\236\350\267\265.md" | 4 +- ...72\344\270\216\347\256\227\346\263\225.md" | 36 ++--- ...46\200\247\347\256\227\346\263\225Raft.md" | 14 +- ...23\345\244\232\345\260\221\357\274\237.md" | 8 +- ...56\347\232\204\345\256\236\350\267\265.md" | 4 +- ...34\346\227\245\345\277\227\357\274\237.md" | 20 +-- ...\347\232\204IM\347\263\273\347\273\237.md" | 8 +- ...21\347\273\234\346\250\241\345\236\213.md" | 18 +-- ...33\347\245\236\351\251\254\357\274\237.md" | 2 +- ...25\351\242\230\350\247\243\347\255\224.md" | 22 +-- ...Zookeeper\343\200\201Nacos\357\274\211.md" | 10 +- ...62\350\247\243 git rebase VS git merge.md" | 4 +- ...ID\347\232\204\346\216\242\347\264\242.md" | 26 +-- ...00\350\207\264\346\200\247\357\274\237.md" | 12 +- ...21\346\216\247\347\232\204\357\274\237.md" | 4 +- ...41\347\220\206\345\256\236\350\267\265.md" | 4 +- ...41\344\270\216\345\256\236\347\216\260.md" | 14 +- ...34\345\203\217\344\274\230\345\214\226.md" | 12 +- ...7\256\227\346\241\206\346\236\266Geode.md" | 16 +- ...55\347\232\204\345\256\236\350\267\265.md" | 10 +- ...71\347\247\260\346\236\266\346\236\204.md" | 18 +-- ...00\344\275\263\345\256\236\350\267\265.md" | 2 +- ...15\345\212\241\346\236\266\346\236\204.md" | 8 +- .../SpringMVC\345\216\237\347\220\206.md" | 2 +- ...15\345\212\241\346\236\266\346\236\204.md" | 6 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 12 +- .../\347\254\25402\350\256\262.md" | 12 +- .../\347\254\25403\350\256\262.md" | 4 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 6 +- .../\347\254\25413\350\256\262.md" | 8 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25417\350\256\262.md" | 8 +- .../\347\254\25418\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 2 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25422\350\256\262.md" | 6 +- .../\347\254\25423\350\256\262.md" | 4 +- .../\347\254\25425\350\256\262.md" | 2 +- .../\347\254\25426\350\256\262.md" | 4 +- .../\347\254\25427\350\256\262.md" | 6 +- .../\347\254\25428\350\256\262.md" | 2 +- .../\347\254\25429\350\256\262.md" | 2 +- .../\347\254\25430\350\256\262.md" | 6 +- .../\347\254\25431\350\256\262.md" | 4 +- .../\347\254\25433\350\256\262.md" | 2 +- .../\347\254\25434\350\256\262.md" | 2 +- .../\347\254\25435\350\256\262.md" | 6 +- .../\347\254\25437\350\256\262.md" | 2 +- .../\347\254\25438\350\256\262.md" | 2 +- .../\347\254\25439\350\256\262.md" | 2 +- .../\347\254\25441\350\256\262.md" | 6 +- .../\347\254\25445\350\256\262.md" | 2 +- .../\347\254\25446\350\256\262.md" | 4 +- .../\347\254\25448\350\256\262.md" | 2 +- .../\347\254\25451\350\256\262.md" | 2 +- .../\347\254\25452\350\256\262.md" | 2 +- .../\347\254\25456\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 12 +- .../\347\254\25401\350\256\262.md" | 8 +- .../\347\254\25402\350\256\262.md" | 20 +-- .../\347\254\25403\350\256\262.md" | 16 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25406\350\256\262.md" | 6 +- .../\347\254\25407\350\256\262.md" | 6 +- .../\347\254\25408\350\256\262.md" | 4 +- .../\347\254\25409\350\256\262.md" | 6 +- .../\347\254\25410\350\256\262.md" | 12 +- .../\347\254\25411\350\256\262.md" | 4 +- .../\347\254\25412\350\256\262.md" | 12 +- .../\347\254\25413\350\256\262.md" | 6 +- .../\347\254\25414\350\256\262.md" | 10 +- .../\347\254\25415\350\256\262.md" | 8 +- .../\347\254\25416\350\256\262.md" | 6 +- .../\347\254\25417\350\256\262.md" | 6 +- .../\347\254\25418\350\256\262.md" | 4 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 6 +- .../\347\254\25421\350\256\262.md" | 6 +- .../\347\254\25424\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 4 +- .../\347\254\25401\350\256\262.md" | 6 +- .../\347\254\25402\350\256\262.md" | 10 +- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 18 +-- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 6 +- .../\347\254\25409\350\256\262.md" | 4 +- .../\347\254\25410\350\256\262.md" | 4 +- .../\347\254\25411\350\256\262.md" | 12 +- .../\347\254\25412\350\256\262.md" | 10 +- .../\347\254\25413\350\256\262.md" | 2 +- .../\347\254\25415\350\256\262.md" | 6 +- .../\347\254\25416\350\256\262.md" | 2 +- .../\347\254\25417\350\256\262.md" | 8 +- .../\347\254\25418\350\256\262.md" | 10 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 10 +- .../\347\254\25421\350\256\262.md" | 10 +- .../\347\254\25422\350\256\262.md" | 4 +- .../\347\254\25424\350\256\262.md" | 6 +- .../\347\254\25426\350\256\262.md" | 8 +- .../\347\254\25427\350\256\262.md" | 2 +- .../\347\254\25428\350\256\262.md" | 10 +- .../\347\254\25430\350\256\262.md" | 2 +- .../\347\254\25431\350\256\262.md" | 8 +- .../\347\254\25433\350\256\262.md" | 18 +-- .../\347\254\25434\350\256\262.md" | 6 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25436\350\256\262.md" | 4 +- .../\347\254\25439\350\256\262.md" | 6 +- .../\347\254\25440\350\256\262.md" | 6 +- .../\347\254\25442\350\256\262.md" | 6 +- .../\347\254\25444\350\256\262.md" | 2 +- .../\347\254\25445\350\256\262.md" | 2 +- .../\347\254\25446\350\256\262.md" | 4 +- .../\347\254\25447\350\256\262.md" | 2 +- .../\347\254\25448\350\256\262.md" | 14 +- .../\347\254\25449\350\256\262.md" | 74 ++++----- .../\347\254\25450\350\256\262.md" | 66 ++++---- .../\347\254\25451\350\256\262.md" | 48 +++--- .../\347\254\25452\350\256\262.md" | 42 ++--- .../\347\254\25453\350\256\262.md" | 38 ++--- .../\347\254\25400\350\256\262.md" | 12 +- .../\347\254\25401\350\256\262.md" | 6 +- .../\347\254\25402\350\256\262.md" | 12 +- .../\347\254\25403\350\256\262.md" | 12 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25406\350\256\262.md" | 10 +- .../\347\254\25407\350\256\262.md" | 8 +- .../\347\254\25408\350\256\262.md" | 22 +-- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25410\350\256\262.md" | 6 +- .../\347\254\25413\350\256\262.md" | 8 +- .../\347\254\25414\350\256\262.md" | 10 +- .../\347\254\25415\350\256\262.md" | 12 +- .../\347\254\25416\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 4 +- .../\347\254\25418\350\256\262.md" | 8 +- .../\347\254\25419\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 10 +- .../\347\254\25421\350\256\262.md" | 6 +- .../\347\254\25422\350\256\262.md" | 8 +- .../\347\254\25423\350\256\262.md" | 14 +- .../\347\254\25424\350\256\262.md" | 4 +- .../\347\254\25425\350\256\262.md" | 10 +- .../\347\254\25426\350\256\262.md" | 14 +- .../\347\254\25427\350\256\262.md" | 10 +- .../\347\254\25428\350\256\262.md" | 22 +-- .../\347\254\25429\350\256\262.md" | 8 +- .../\347\254\25430\350\256\262.md" | 18 +-- .../\347\254\25432\350\256\262.md" | 12 +- .../\347\254\25433\350\256\262.md" | 12 +- .../\347\254\25434\350\256\262.md" | 22 +-- .../\347\254\25435\350\256\262.md" | 34 ++-- .../\347\254\25436\350\256\262.md" | 10 +- .../\347\254\25437\350\256\262.md" | 14 +- .../\347\254\25438\350\256\262.md" | 4 +- .../\347\254\25439\350\256\262.md" | 22 +-- .../\347\254\25440\350\256\262.md" | 4 +- .../\347\254\25441\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 20 +-- .../\347\254\25401\350\256\262.md" | 24 +-- .../\347\254\25402\350\256\262.md" | 14 +- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 10 +- .../\347\254\25411\350\256\262.md" | 8 +- .../\347\254\25412\350\256\262.md" | 22 +-- .../\347\254\25413\350\256\262.md" | 22 +-- .../\347\254\25414\350\256\262.md" | 28 ++-- .../\347\254\25415\350\256\262.md" | 12 +- .../\347\254\25416\350\256\262.md" | 14 +- .../\347\254\25417\350\256\262.md" | 16 +- .../\347\254\25418\350\256\262.md" | 16 +- .../\347\254\25419\350\256\262.md" | 12 +- .../\347\254\25420\350\256\262.md" | 10 +- .../\347\254\25421\350\256\262.md" | 4 +- .../\347\254\25422\350\256\262.md" | 20 +-- .../\347\254\25401\350\256\262.md" | 10 +- .../\347\254\25402\350\256\262.md" | 18 +-- .../\347\254\25403\350\256\262.md" | 14 +- .../\347\254\25404\350\256\262.md" | 10 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25406\350\256\262.md" | 10 +- .../\347\254\25407\350\256\262.md" | 16 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25412\350\256\262.md" | 4 +- .../\347\254\25413\350\256\262.md" | 8 +- .../\347\254\25414\350\256\262.md" | 8 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25417\350\256\262.md" | 14 +- .../\347\254\25418\350\256\262.md" | 6 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 12 +- .../\347\254\25421\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 12 +- .../\347\254\25401\350\256\262.md" | 6 +- .../\347\254\25402\350\256\262.md" | 14 +- .../\347\254\25403\350\256\262.md" | 12 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25409\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 10 +- .../\347\254\25411\350\256\262.md" | 10 +- .../\347\254\25412\350\256\262.md" | 12 +- .../\347\254\25413\350\256\262.md" | 2 +- .../\347\254\25414\350\256\262.md" | 2 +- .../\347\254\25415\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 8 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25419\350\256\262.md" | 8 +- .../\347\254\25420\350\256\262.md" | 8 +- .../\347\254\25421\350\256\262.md" | 10 +- .../\347\254\25422\350\256\262.md" | 8 +- .../\347\254\25423\350\256\262.md" | 6 +- .../\347\254\25424\350\256\262.md" | 10 +- .../\347\254\25425\350\256\262.md" | 2 +- .../\347\254\25426\350\256\262.md" | 14 +- .../\347\254\25427\350\256\262.md" | 4 +- .../\347\254\25428\350\256\262.md" | 4 +- .../\347\254\25429\350\256\262.md" | 12 +- .../\347\254\25430\350\256\262.md" | 4 +- .../\347\254\25431\350\256\262.md" | 16 +- .../\347\254\25432\350\256\262.md" | 2 +- .../\347\254\25433\350\256\262.md" | 2 +- .../\347\254\25434\350\256\262.md" | 8 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25436\350\256\262.md" | 2 +- .../\347\254\25437\350\256\262.md" | 8 +- .../\347\254\25438\350\256\262.md" | 6 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25403\350\256\262.md" | 8 +- .../\347\254\25404\350\256\262.md" | 14 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25409\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25413\350\256\262.md" | 8 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25416\350\256\262.md" | 6 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 4 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25423\350\256\262.md" | 8 +- .../\347\254\25424\350\256\262.md" | 6 +- .../\347\254\25425\350\256\262.md" | 4 +- .../\347\254\25426\350\256\262.md" | 4 +- .../\347\254\25427\350\256\262.md" | 16 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25409\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 4 +- .../\347\254\25412\350\256\262.md" | 2 +- .../\347\254\25416\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 2 +- .../\347\254\25424\350\256\262.md" | 2 +- .../\347\254\25425\350\256\262.md" | 4 +- .../\347\254\25426\350\256\262.md" | 8 +- .../\347\254\25427\350\256\262.md" | 2 +- .../\347\254\25429\350\256\262.md" | 2 +- .../\347\254\25430\350\256\262.md" | 2 +- .../\347\254\25431\350\256\262.md" | 10 +- .../\347\254\25432\350\256\262.md" | 4 +- .../\347\254\25433\350\256\262.md" | 2 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25436\350\256\262.md" | 12 +- .../\347\254\25437\350\256\262.md" | 4 +- .../\347\254\25438\350\256\262.md" | 4 +- .../\347\254\25439\350\256\262.md" | 8 +- .../\347\254\25441\350\256\262.md" | 8 +- .../\347\254\25442\350\256\262.md" | 2 +- .../\347\254\25444\350\256\262.md" | 4 +- .../\347\254\25445\350\256\262.md" | 4 +- .../\347\254\25446\350\256\262.md" | 4 +- .../\347\254\25447\350\256\262.md" | 12 +- .../\347\254\25448\350\256\262.md" | 8 +- .../\347\254\25449\350\256\262.md" | 4 +- .../\347\254\25450\350\256\262.md" | 2 +- .../\347\254\25451\350\256\262.md" | 16 +- .../\347\254\25453\350\256\262.md" | 6 +- .../\347\254\25454\350\256\262.md" | 6 +- .../\347\254\25455\350\256\262.md" | 8 +- .../\347\254\25456\350\256\262.md" | 6 +- .../\347\254\25457\350\256\262.md" | 4 +- .../\347\254\25458\350\256\262.md" | 6 +- .../\347\254\25459\350\256\262.md" | 8 +- .../\347\254\25460\350\256\262.md" | 2 +- .../\347\254\25461\350\256\262.md" | 2 +- .../\347\254\25462\350\256\262.md" | 4 +- .../\347\254\25463\350\256\262.md" | 8 +- .../\347\254\25464\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 12 +- .../\347\254\25402\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 8 +- .../\347\254\25406\350\256\262.md" | 6 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25410\350\256\262.md" | 8 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 4 +- .../\347\254\25413\350\256\262.md" | 4 +- .../\347\254\25414\350\256\262.md" | 4 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25422\350\256\262.md" | 2 +- .../\347\254\25423\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25408\350\256\262.md" | 8 +- .../\347\254\25412\350\256\262.md" | 2 +- .../\347\254\25414\350\256\262.md" | 2 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25416\350\256\262.md" | 2 +- .../\347\254\25417\350\256\262.md" | 8 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 8 +- .../\347\254\25406\350\256\262.md" | 12 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 4 +- .../\347\254\25409\350\256\262.md" | 6 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 8 +- .../\347\254\25413\350\256\262.md" | 6 +- .../\347\254\25414\350\256\262.md" | 14 +- .../\347\254\25416\350\256\262.md" | 10 +- .../\347\254\25417\350\256\262.md" | 4 +- .../\347\254\25418\350\256\262.md" | 4 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 8 +- .../\347\254\25421\350\256\262.md" | 4 +- .../\347\254\25422\350\256\262.md" | 2 +- .../\347\254\25425\350\256\262.md" | 8 +- .../\347\254\25426\350\256\262.md" | 2 +- .../\347\254\25427\350\256\262.md" | 2 +- .../\347\254\25429\350\256\262.md" | 4 +- .../\347\254\25431\350\256\262.md" | 2 +- .../\347\254\25432\350\256\262.md" | 8 +- .../\347\254\25433\350\256\262.md" | 8 +- .../\347\254\25434\350\256\262.md" | 4 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25436\350\256\262.md" | 4 +- .../\347\254\25438\350\256\262.md" | 2 +- .../\347\254\25439\350\256\262.md" | 6 +- .../\347\254\25441\350\256\262.md" | 6 +- .../\347\254\25442\350\256\262.md" | 2 +- .../\347\254\25444\350\256\262.md" | 6 +- .../\347\254\25445\350\256\262.md" | 8 +- .../\347\254\25400\350\256\262.md" | 12 +- .../\347\254\25401\350\256\262.md" | 8 +- .../\347\254\25402\350\256\262.md" | 18 +-- .../\347\254\25403\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 6 +- .../\347\254\25405\350\256\262.md" | 14 +- .../\347\254\25406\350\256\262.md" | 10 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 22 +-- .../\347\254\25409\350\256\262.md" | 24 +-- .../\347\254\25410\350\256\262.md" | 6 +- .../\347\254\25411\350\256\262.md" | 12 +- .../\347\254\25412\350\256\262.md" | 12 +- .../\347\254\25413\350\256\262.md" | 14 +- .../\347\254\25414\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 8 +- .../\347\254\25416\350\256\262.md" | 8 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 4 +- .../\347\254\25421\350\256\262.md" | 8 +- .../\347\254\25423\350\256\262.md" | 6 +- .../\347\254\25424\350\256\262.md" | 4 +- .../\347\254\25426\350\256\262.md" | 6 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25403\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 6 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 4 +- .../\347\254\25423\350\256\262.md" | 2 +- .../\347\254\25429\350\256\262.md" | 2 +- .../\347\254\25430\350\256\262.md" | 6 +- .../\347\254\25431\350\256\262.md" | 6 +- .../\347\254\25432\350\256\262.md" | 6 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25436\350\256\262.md" | 2 +- .../\347\254\25439\350\256\262.md" | 4 +- .../\347\254\25443\350\256\262.md" | 8 +- .../\347\254\25400\350\256\262.md" | 14 +- .../\347\254\25401\350\256\262.md" | 26 +-- .../\347\254\25402\350\256\262.md" | 20 +-- .../\347\254\25403\350\256\262.md" | 20 +-- .../\347\254\25404\350\256\262.md" | 22 +-- .../\347\254\25405\350\256\262.md" | 18 +-- .../\347\254\25406\350\256\262.md" | 12 +- .../\347\254\25407\350\256\262.md" | 26 +-- .../\347\254\25408\350\256\262.md" | 14 +- .../\347\254\25409\350\256\262.md" | 20 +-- .../\347\254\25410\350\256\262.md" | 20 +-- .../\347\254\25411\350\256\262.md" | 20 +-- .../\347\254\25412\350\256\262.md" | 12 +- .../\347\254\25413\350\256\262.md" | 8 +- .../\347\254\25414\350\256\262.md" | 18 +-- .../\347\254\25415\350\256\262.md" | 16 +- .../\347\254\25416\350\256\262.md" | 14 +- .../\347\254\25417\350\256\262.md" | 16 +- .../\347\254\25418\350\256\262.md" | 12 +- .../\347\254\25419\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 12 +- .../\347\254\25421\350\256\262.md" | 10 +- .../\347\254\25422\350\256\262.md" | 4 +- .../\347\254\25423\350\256\262.md" | 4 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 2 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 10 +- .../\347\254\25401\350\256\262.md" | 18 +-- .../\347\254\25402\350\256\262.md" | 6 +- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25406\350\256\262.md" | 10 +- .../\347\254\25407\350\256\262.md" | 14 +- .../\347\254\25408\350\256\262.md" | 6 +- .../\347\254\25409\350\256\262.md" | 10 +- .../\347\254\25410\350\256\262.md" | 12 +- .../\347\254\25411\350\256\262.md" | 8 +- .../\347\254\25412\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 6 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 22 +-- .../\347\254\25402\350\256\262.md" | 12 +- .../\347\254\25403\350\256\262.md" | 8 +- .../\347\254\25404\350\256\262.md" | 8 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 4 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25408\350\256\262.md" | 12 +- .../\347\254\25409\350\256\262.md" | 14 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 34 ++-- .../\347\254\25412\350\256\262.md" | 4 +- .../\347\254\25413\350\256\262.md" | 18 +-- .../\347\254\25414\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 16 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 4 +- .../\347\254\25403\350\256\262.md" | 4 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25406\350\256\262.md" | 8 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 14 +- .../\347\254\25409\350\256\262.md" | 20 +-- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 12 +- .../\347\254\25416\350\256\262.md" | 2 +- .../\347\254\25417\350\256\262.md" | 4 +- .../\347\254\25418\350\256\262.md" | 10 +- .../\347\254\25421\350\256\262.md" | 20 +-- .../\347\254\25424\350\256\262.md" | 4 +- .../\347\254\25426\350\256\262.md" | 6 +- .../\347\254\25427\350\256\262.md" | 2 +- .../\347\254\25428\350\256\262.md" | 2 +- .../\347\254\25430\350\256\262.md" | 2 +- .../\347\254\25433\350\256\262.md" | 4 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25438\350\256\262.md" | 6 +- .../\347\254\25440\350\256\262.md" | 2 +- .../\347\254\25441\350\256\262.md" | 6 +- .../\347\254\25445\350\256\262.md" | 6 +- .../\347\254\25447\350\256\262.md" | 2 +- .../\347\254\25449\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 10 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 4 +- .../\347\254\25403\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 6 +- .../\347\254\25406\350\256\262.md" | 12 +- .../\347\254\25407\350\256\262.md" | 8 +- .../\347\254\25408\350\256\262.md" | 26 +-- .../\347\254\25409\350\256\262.md" | 26 +-- .../\347\254\25410\350\256\262.md" | 10 +- .../\347\254\25411\350\256\262.md" | 22 +-- .../\347\254\25412\350\256\262.md" | 8 +- .../\347\254\25413\350\256\262.md" | 16 +- .../\347\254\25414\350\256\262.md" | 2 +- .../\347\254\25415\350\256\262.md" | 12 +- .../\347\254\25416\350\256\262.md" | 10 +- .../\347\254\25417\350\256\262.md" | 10 +- .../\347\254\25418\350\256\262.md" | 10 +- .../\347\254\25419\350\256\262.md" | 16 +- .../\347\254\25420\350\256\262.md" | 32 ++-- .../\347\254\25421\350\256\262.md" | 6 +- .../\347\254\25400\350\256\262.md" | 6 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 16 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 6 +- .../\347\254\25406\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 6 +- .../\347\254\25408\350\256\262.md" | 10 +- .../\347\254\25409\350\256\262.md" | 4 +- .../\347\254\25410\350\256\262.md" | 18 +-- .../\347\254\25412\350\256\262.md" | 6 +- .../\347\254\25413\350\256\262.md" | 2 +- .../\347\254\25414\350\256\262.md" | 10 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25416\350\256\262.md" | 10 +- .../\347\254\25417\350\256\262.md" | 4 +- .../\347\254\25418\350\256\262.md" | 12 +- .../\347\254\25420\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 12 +- .../\347\254\25401\350\256\262.md" | 28 ++-- .../\347\254\25402\350\256\262.md" | 20 +-- .../\347\254\25403\350\256\262.md" | 22 +-- .../\347\254\25404\350\256\262.md" | 18 +-- .../\347\254\25405\350\256\262.md" | 22 +-- .../\347\254\25406\350\256\262.md" | 18 +-- .../\347\254\25407\350\256\262.md" | 12 +- .../\347\254\25408\350\256\262.md" | 4 +- .../\347\254\25409\350\256\262.md" | 10 +- .../\347\254\25410\350\256\262.md" | 16 +- .../\347\254\25411\350\256\262.md" | 4 +- .../\347\254\25412\350\256\262.md" | 6 +- .../\347\254\25413\350\256\262.md" | 14 +- .../\347\254\25414\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 14 +- .../\347\254\25416\350\256\262.md" | 8 +- .../\347\254\25417\350\256\262.md" | 12 +- .../\347\254\25419\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 6 +- .../\347\254\25401\350\256\262.md" | 12 +- .../\347\254\25402\350\256\262.md" | 6 +- .../\347\254\25403\350\256\262.md" | 26 +-- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 10 +- .../\347\254\25406\350\256\262.md" | 6 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25409\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 10 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 4 +- .../\347\254\25403\350\256\262.md" | 8 +- .../\347\254\25404\350\256\262.md" | 8 +- .../\347\254\25405\350\256\262.md" | 8 +- .../\347\254\25407\350\256\262.md" | 8 +- .../\347\254\25408\350\256\262.md" | 10 +- .../\347\254\25409\350\256\262.md" | 6 +- .../\347\254\25410\350\256\262.md" | 12 +- .../\347\254\25411\350\256\262.md" | 10 +- .../\347\254\25412\350\256\262.md" | 8 +- .../\347\254\25413\350\256\262.md" | 6 +- .../\347\254\25414\350\256\262.md" | 16 +- .../\347\254\25415\350\256\262.md" | 20 +-- .../\347\254\25416\350\256\262.md" | 18 +-- .../\347\254\25417\350\256\262.md" | 12 +- .../\347\254\25418\350\256\262.md" | 18 +-- .../\347\254\25419\350\256\262.md" | 14 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25422\350\256\262.md" | 14 +- .../\347\254\25423\350\256\262.md" | 26 +-- .../\347\254\25424\350\256\262.md" | 18 +-- .../\347\254\25425\350\256\262.md" | 26 +-- .../\347\254\25426\350\256\262.md" | 18 +-- .../\347\254\25427\350\256\262.md" | 24 +-- .../\347\254\25428\350\256\262.md" | 22 +-- .../\347\254\25429\350\256\262.md" | 12 +- .../\347\254\25430\350\256\262.md" | 16 +- .../\347\254\25431\350\256\262.md" | 10 +- .../\347\254\25432\350\256\262.md" | 8 +- .../\347\254\25433\350\256\262.md" | 6 +- .../\347\254\25434\350\256\262.md" | 4 +- .../\347\254\25435\350\256\262.md" | 8 +- .../\347\254\25437\350\256\262.md" | 4 +- .../\347\254\25438\350\256\262.md" | 4 +- .../\347\254\25439\350\256\262.md" | 8 +- .../\347\254\25440\350\256\262.md" | 6 +- .../\347\254\25441\350\256\262.md" | 22 +-- .../\347\254\25442\350\256\262.md" | 10 +- .../\347\254\25444\350\256\262.md" | 10 +- .../\347\254\25402\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 6 +- .../\347\254\25413\350\256\262.md" | 8 +- .../\347\254\25414\350\256\262.md" | 14 +- .../\347\254\25415\350\256\262.md" | 6 +- .../\347\254\25416\350\256\262.md" | 2 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25419\350\256\262.md" | 14 +- .../\347\254\25421\350\256\262.md" | 6 +- .../\347\254\25422\350\256\262.md" | 2 +- .../\347\254\25423\350\256\262.md" | 24 +-- .../\347\254\25424\350\256\262.md" | 8 +- .../\347\254\25425\350\256\262.md" | 12 +- .../\347\254\25426\350\256\262.md" | 12 +- .../\347\254\25427\350\256\262.md" | 8 +- .../\347\254\25429\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 6 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 10 +- .../\347\254\25403\350\256\262.md" | 4 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25407\350\256\262.md" | 6 +- .../\347\254\25408\350\256\262.md" | 6 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25410\350\256\262.md" | 4 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 2 +- .../\347\254\25413\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 6 +- .../\347\254\25417\350\256\262.md" | 4 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25419\350\256\262.md" | 4 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25422\350\256\262.md" | 2 +- .../\347\254\25423\350\256\262.md" | 2 +- .../\347\254\25425\350\256\262.md" | 12 +- .../\347\254\25426\350\256\262.md" | 2 +- .../\347\254\25428\350\256\262.md" | 4 +- .../\347\254\25429\350\256\262.md" | 6 +- .../\347\254\25430\350\256\262.md" | 6 +- .../\347\254\25433\350\256\262.md" | 2 +- .../\347\254\25434\350\256\262.md" | 4 +- .../\347\254\25435\350\256\262.md" | 4 +- .../\347\254\25438\350\256\262.md" | 4 +- .../\347\254\25439\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 6 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25433\350\256\262.md" | 4 +- .../\347\254\25436\350\256\262.md" | 2 +- .../\347\254\25441\350\256\262.md" | 2 +- .../\347\254\25442\350\256\262.md" | 4 +- .../\347\254\25443\350\256\262.md" | 6 +- .../\347\254\25444\350\256\262.md" | 14 +- .../\347\254\25448\350\256\262.md" | 6 +- .../\347\254\25449\350\256\262.md" | 16 +- .../\347\254\25450\350\256\262.md" | 2 +- .../\347\254\25452\350\256\262.md" | 6 +- .../\347\254\25453\350\256\262.md" | 4 +- .../\347\254\25455\350\256\262.md" | 6 +- .../\347\254\25456\350\256\262.md" | 4 +- .../\347\254\25459\350\256\262.md" | 2 +- .../\347\254\25461\350\256\262.md" | 10 +- .../\347\254\25462\350\256\262.md" | 2 +- .../\347\254\25464\350\256\262.md" | 12 +- .../\347\254\25465\350\256\262.md" | 2 +- .../\347\254\25466\350\256\262.md" | 12 +- .../\347\254\25467\350\256\262.md" | 12 +- .../\347\254\25468\350\256\262.md" | 8 +- .../\347\254\25469\350\256\262.md" | 8 +- .../\347\254\25470\350\256\262.md" | 18 +-- .../\347\254\25471\350\256\262.md" | 12 +- .../\347\254\25472\350\256\262.md" | 18 +-- .../\347\254\25473\350\256\262.md" | 10 +- .../\347\254\25474\350\256\262.md" | 10 +- .../\347\254\25475\350\256\262.md" | 16 +- .../\347\254\25476\350\256\262.md" | 10 +- .../\347\254\25477\350\256\262.md" | 12 +- .../\347\254\25478\350\256\262.md" | 32 ++-- .../\347\254\25400\350\256\262.md" | 8 +- .../\347\254\25401\350\256\262.md" | 8 +- .../\347\254\25402\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25407\350\256\262.md" | 8 +- .../\347\254\25408\350\256\262.md" | 4 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 6 +- .../\347\254\25412\350\256\262.md" | 6 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25421\350\256\262.md" | 4 +- .../\347\254\25442\350\256\262.md" | 2 +- .../\347\254\25449\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 22 +-- .../\347\254\25401\350\256\262.md" | 18 +-- .../\347\254\25402\350\256\262.md" | 10 +- .../\347\254\25403\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25406\350\256\262.md" | 24 +-- .../\347\254\25407\350\256\262.md" | 16 +- .../\347\254\25408\350\256\262.md" | 28 ++-- .../\347\254\25409\350\256\262.md" | 26 +-- .../\347\254\25410\350\256\262.md" | 22 +-- .../\347\254\25411\350\256\262.md" | 14 +- .../\347\254\25412\350\256\262.md" | 28 ++-- .../\347\254\25413\350\256\262.md" | 10 +- .../\347\254\25414\350\256\262.md" | 12 +- .../\347\254\25415\350\256\262.md" | 10 +- .../\347\254\25416\350\256\262.md" | 8 +- .../\347\254\25417\350\256\262.md" | 12 +- .../\347\254\25418\350\256\262.md" | 6 +- .../\347\254\25419\350\256\262.md" | 4 +- .../\347\254\25420\350\256\262.md" | 12 +- .../\347\254\25421\350\256\262.md" | 8 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25402\350\256\262.md" | 4 +- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 6 +- .../\347\254\25406\350\256\262.md" | 4 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 6 +- .../\347\254\25411\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 6 +- .../\347\254\25422\350\256\262.md" | 2 +- .../\347\254\25423\350\256\262.md" | 2 +- .../\347\254\25424\350\256\262.md" | 6 +- .../\347\254\25425\350\256\262.md" | 4 +- .../\347\254\25426\350\256\262.md" | 2 +- .../\347\254\25432\350\256\262.md" | 2 +- .../\347\254\25435\350\256\262.md" | 4 +- .../\347\254\25436\350\256\262.md" | 2 +- .../\347\254\25437\350\256\262.md" | 4 +- .../\347\254\25420\350\256\262.md" | 2 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25436\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 8 +- .../\347\254\25406\350\256\262.md" | 28 ++-- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 10 +- .../\347\254\25409\350\256\262.md" | 4 +- .../\347\254\25411\350\256\262.md" | 6 +- .../\347\254\25412\350\256\262.md" | 6 +- .../\347\254\25413\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25416\350\256\262.md" | 10 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 12 +- .../\347\254\25419\350\256\262.md" | 4 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25422\350\256\262.md" | 2 +- .../\347\254\25423\350\256\262.md" | 6 +- .../\347\254\25424\350\256\262.md" | 8 +- .../\347\254\25425\350\256\262.md" | 2 +- .../\347\254\25426\350\256\262.md" | 8 +- .../\347\254\25404\350\256\262.md" | 6 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25414\350\256\262.md" | 2 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 2 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 8 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25402\350\256\262.md" | 4 +- .../\347\254\25406\350\256\262.md" | 2 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 6 +- .../\347\254\25414\350\256\262.md" | 2 +- .../\347\254\25415\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 2 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25422\350\256\262.md" | 6 +- .../\347\254\25400\350\256\262.md" | 18 +-- .../\347\254\25401\350\256\262.md" | 18 +-- .../\347\254\25402\350\256\262.md" | 8 +- .../\347\254\25403\350\256\262.md" | 8 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 6 +- .../\347\254\25406\350\256\262.md" | 10 +- .../\347\254\25407\350\256\262.md" | 6 +- .../\347\254\25408\350\256\262.md" | 6 +- .../\347\254\25409\350\256\262.md" | 6 +- .../\347\254\25411\350\256\262.md" | 12 +- .../\347\254\25412\350\256\262.md" | 8 +- .../\347\254\25413\350\256\262.md" | 4 +- .../\347\254\25414\350\256\262.md" | 6 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25416\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25419\350\256\262.md" | 4 +- .../\347\254\25421\350\256\262.md" | 2 +- .../\347\254\25424\350\256\262.md" | 2 +- .../\347\254\25429\350\256\262.md" | 4 +- .../\347\254\25430\350\256\262.md" | 16 +- .../\347\254\25434\350\256\262.md" | 12 +- .../\347\254\25435\350\256\262.md" | 4 +- .../\347\254\25436\350\256\262.md" | 8 +- .../\347\254\25438\350\256\262.md" | 2 +- .../\347\254\25427\350\256\262.md" | 10 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25402\350\256\262.md" | 56 +++---- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25406\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25409\350\256\262.md" | 4 +- .../\347\254\25410\350\256\262.md" | 4 +- .../\347\254\25411\350\256\262.md" | 6 +- .../\347\254\25413\350\256\262.md" | 2 +- .../\347\254\25414\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 6 +- .../\347\254\25416\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 4 +- .../\347\254\25423\350\256\262.md" | 4 +- .../\347\254\25425\350\256\262.md" | 2 +- .../\347\254\25426\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 8 +- .../\347\254\25404\350\256\262.md" | 8 +- .../\347\254\25405\350\256\262.md" | 12 +- .../\347\254\25406\350\256\262.md" | 6 +- .../\347\254\25407\350\256\262.md" | 8 +- .../\347\254\25408\350\256\262.md" | 12 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25410\350\256\262.md" | 10 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 4 +- .../\347\254\25413\350\256\262.md" | 10 +- .../\347\254\25414\350\256\262.md" | 8 +- .../\347\254\25415\350\256\262.md" | 6 +- .../\347\254\25416\350\256\262.md" | 6 +- .../\347\254\25417\350\256\262.md" | 12 +- .../\347\254\25418\350\256\262.md" | 10 +- .../\347\254\25419\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 10 +- .../\347\254\25421\350\256\262.md" | 8 +- .../\347\254\25422\350\256\262.md" | 6 +- .../\347\254\25423\350\256\262.md" | 12 +- .../\347\254\25424\350\256\262.md" | 2 +- .../\347\254\25425\350\256\262.md" | 6 +- .../\347\254\25426\350\256\262.md" | 4 +- .../\347\254\25427\350\256\262.md" | 14 +- .../\347\254\25428\350\256\262.md" | 2 +- .../\347\254\25430\350\256\262.md" | 4 +- .../\347\254\25431\350\256\262.md" | 24 +-- .../\347\254\25432\350\256\262.md" | 14 +- .../\347\254\25433\350\256\262.md" | 18 +-- .../\347\254\25434\350\256\262.md" | 4 +- .../\347\254\25435\350\256\262.md" | 12 +- .../\347\254\25436\350\256\262.md" | 6 +- .../\347\254\25437\350\256\262.md" | 10 +- .../\347\254\25438\350\256\262.md" | 14 +- .../\347\254\25439\350\256\262.md" | 8 +- .../\347\254\25440\350\256\262.md" | 10 +- .../\347\254\25441\350\256\262.md" | 12 +- .../\347\254\25442\350\256\262.md" | 12 +- .../\347\254\25443\350\256\262.md" | 10 +- .../\347\254\25444\350\256\262.md" | 26 +-- .../\347\254\25445\350\256\262.md" | 10 +- .../\347\254\25446\350\256\262.md" | 14 +- .../\347\254\25447\350\256\262.md" | 16 +- .../\347\254\25448\350\256\262.md" | 4 +- .../\347\254\25449\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 4 +- .../\347\254\25404\350\256\262.md" | 12 +- .../\347\254\25405\350\256\262.md" | 8 +- .../\347\254\25406\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25410\350\256\262.md" | 4 +- .../\347\254\25411\350\256\262.md" | 10 +- .../\347\254\25412\350\256\262.md" | 10 +- .../\347\254\25414\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 8 +- .../\347\254\25416\350\256\262.md" | 14 +- .../\347\254\25417\350\256\262.md" | 8 +- .../\347\254\25418\350\256\262.md" | 6 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 6 +- .../\347\254\25421\350\256\262.md" | 6 +- .../\347\254\25422\350\256\262.md" | 14 +- .../\347\254\25423\350\256\262.md" | 8 +- .../\347\254\25424\350\256\262.md" | 12 +- .../\347\254\25425\350\256\262.md" | 10 +- .../\347\254\25426\350\256\262.md" | 18 +-- .../\347\254\25427\350\256\262.md" | 12 +- .../\347\254\25428\350\256\262.md" | 12 +- .../\347\254\25429\350\256\262.md" | 6 +- .../\347\254\25430\350\256\262.md" | 8 +- .../\347\254\25431\350\256\262.md" | 12 +- .../\347\254\25432\350\256\262.md" | 14 +- .../\347\254\25433\350\256\262.md" | 8 +- .../\347\254\25434\350\256\262.md" | 2 +- .../\347\254\25435\350\256\262.md" | 10 +- .../\347\254\25436\350\256\262.md" | 6 +- .../\347\254\25437\350\256\262.md" | 6 +- .../\347\254\25438\350\256\262.md" | 14 +- .../\347\254\25439\350\256\262.md" | 14 +- .../\347\254\25440\350\256\262.md" | 18 +-- .../\347\254\25441\350\256\262.md" | 6 +- .../\347\254\25442\350\256\262.md" | 8 +- .../\347\254\25443\350\256\262.md" | 4 +- .../\347\254\25444\350\256\262.md" | 10 +- .../\347\254\25401\350\256\262.md" | 10 +- .../\347\254\25402\350\256\262.md" | 8 +- .../\347\254\25403\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 8 +- .../\347\254\25406\350\256\262.md" | 10 +- .../\347\254\25407\350\256\262.md" | 8 +- .../\347\254\25408\350\256\262.md" | 6 +- .../\347\254\25409\350\256\262.md" | 14 +- .../\347\254\25410\350\256\262.md" | 6 +- .../\347\254\25411\350\256\262.md" | 10 +- .../\347\254\25413\350\256\262.md" | 4 +- .../\347\254\25414\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 4 +- .../\347\254\25420\350\256\262.md" | 4 +- .../\347\254\25422\350\256\262.md" | 10 +- .../\347\254\25423\350\256\262.md" | 34 ++-- .../\347\254\25424\350\256\262.md" | 6 +- .../\347\254\25425\350\256\262.md" | 8 +- .../\347\254\25426\350\256\262.md" | 2 +- .../\347\254\25427\350\256\262.md" | 10 +- .../\347\254\25428\350\256\262.md" | 6 +- .../\347\254\25429\350\256\262.md" | 20 +-- .../\347\254\25430\350\256\262.md" | 6 +- .../\347\254\25408\350\256\262.md" | 8 +- .../\347\254\25409\350\256\262.md" | 6 +- .../\347\254\25411\350\256\262.md" | 8 +- .../\347\254\25422\350\256\262.md" | 8 +- .../\347\254\25429\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 10 +- .../\347\254\25401\350\256\262.md" | 18 +-- .../\347\254\25402\350\256\262.md" | 12 +- .../\347\254\25403\350\256\262.md" | 10 +- .../\347\254\25404\350\256\262.md" | 12 +- .../\347\254\25405\350\256\262.md" | 4 +- .../\347\254\25409\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 6 +- .../\347\254\25411\350\256\262.md" | 6 +- .../\347\254\25412\350\256\262.md" | 20 +-- .../\347\254\25413\350\256\262.md" | 18 +-- .../\347\254\25414\350\256\262.md" | 6 +- .../\347\254\25415\350\256\262.md" | 4 +- .../\347\254\25416\350\256\262.md" | 6 +- .../\347\254\25417\350\256\262.md" | 12 +- .../\347\254\25418\350\256\262.md" | 4 +- .../\347\254\25419\350\256\262.md" | 16 +- .../\347\254\25420\350\256\262.md" | 16 +- .../\347\254\25421\350\256\262.md" | 22 +-- .../\347\254\25422\350\256\262.md" | 14 +- .../\347\254\25423\350\256\262.md" | 8 +- .../\347\254\25424\350\256\262.md" | 8 +- .../\347\254\25425\350\256\262.md" | 2 +- .../\347\254\25426\350\256\262.md" | 2 +- .../\347\254\25427\350\256\262.md" | 2 +- .../\347\254\25435\350\256\262.md" | 10 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25402\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25406\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 2 +- .../\347\254\25413\350\256\262.md" | 4 +- .../\347\254\25414\350\256\262.md" | 2 +- .../\347\254\25415\350\256\262.md" | 4 +- .../\347\254\25416\350\256\262.md" | 8 +- .../\347\254\25418\350\256\262.md" | 8 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 6 +- .../\347\254\25421\350\256\262.md" | 10 +- .../\347\254\25422\350\256\262.md" | 12 +- .../\347\254\25423\350\256\262.md" | 8 +- .../\347\254\25424\350\256\262.md" | 6 +- .../\347\254\25425\350\256\262.md" | 4 +- .../\347\254\25428\350\256\262.md" | 6 +- .../\347\254\25429\350\256\262.md" | 4 +- .../\347\254\25430\350\256\262.md" | 6 +- .../\347\254\25433\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 4 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25402\350\256\262.md" | 10 +- .../\347\254\25403\350\256\262.md" | 4 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 8 +- .../\347\254\25406\350\256\262.md" | 10 +- .../\347\254\25407\350\256\262.md" | 8 +- .../\347\254\25408\350\256\262.md" | 4 +- .../\347\254\25409\350\256\262.md" | 6 +- .../\347\254\25410\350\256\262.md" | 6 +- .../\347\254\25411\350\256\262.md" | 4 +- .../\347\254\25412\350\256\262.md" | 4 +- .../\347\254\25414\350\256\262.md" | 2 +- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25416\350\256\262.md" | 6 +- .../\347\254\25417\350\256\262.md" | 4 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25419\350\256\262.md" | 4 +- .../\347\254\25422\350\256\262.md" | 4 +- .../\347\254\25424\350\256\262.md" | 2 +- .../\347\254\25427\350\256\262.md" | 4 +- .../\347\254\25428\350\256\262.md" | 6 +- .../\347\254\25429\350\256\262.md" | 4 +- .../\347\254\25431\350\256\262.md" | 6 +- .../\347\254\25434\350\256\262.md" | 2 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25436\350\256\262.md" | 8 +- .../\347\254\25437\350\256\262.md" | 6 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25409\350\256\262.md" | 4 +- .../\347\254\25411\350\256\262.md" | 6 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 6 +- .../\347\254\25400\350\256\262.md" | 10 +- .../\347\254\25401\350\256\262.md" | 12 +- .../\347\254\25402\350\256\262.md" | 12 +- .../\347\254\25403\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25406\350\256\262.md" | 6 +- .../\347\254\25407\350\256\262.md" | 12 +- .../\347\254\25409\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 4 +- .../\347\254\25411\350\256\262.md" | 12 +- .../\347\254\25412\350\256\262.md" | 18 +-- .../\347\254\25415\350\256\262.md" | 2 +- .../\347\254\25416\350\256\262.md" | 4 +- .../\347\254\25421\350\256\262.md" | 18 +-- .../\347\254\25422\350\256\262.md" | 4 +- .../\347\254\25428\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 10 +- .../\347\254\25401\350\256\262.md" | 18 +-- .../\347\254\25402\350\256\262.md" | 8 +- .../\347\254\25403\350\256\262.md" | 16 +- .../\347\254\25404\350\256\262.md" | 12 +- .../\347\254\25405\350\256\262.md" | 14 +- .../\347\254\25406\350\256\262.md" | 12 +- .../\347\254\25407\350\256\262.md" | 16 +- .../\347\254\25408\350\256\262.md" | 12 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25410\350\256\262.md" | 16 +- .../\347\254\25411\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 10 +- .../\347\254\25413\350\256\262.md" | 14 +- .../\347\254\25414\350\256\262.md" | 20 +-- .../\347\254\25415\350\256\262.md" | 8 +- .../\347\254\25416\350\256\262.md" | 24 +-- .../\347\254\25417\350\256\262.md" | 10 +- .../\347\254\25418\350\256\262.md" | 6 +- .../\347\254\25419\350\256\262.md" | 14 +- .../\347\254\25420\350\256\262.md" | 18 +-- .../\347\254\25421\350\256\262.md" | 6 +- .../\347\254\25422\350\256\262.md" | 20 +-- .../\347\254\25423\350\256\262.md" | 2 +- .../\347\254\25424\350\256\262.md" | 2 +- .../\347\254\25425\350\256\262.md" | 2 +- .../\347\254\25426\350\256\262.md" | 4 +- .../\347\254\25427\350\256\262.md" | 6 +- .../\347\254\25428\350\256\262.md" | 14 +- .../\347\254\25429\350\256\262.md" | 2 +- .../\347\254\25430\350\256\262.md" | 4 +- .../\347\254\25431\350\256\262.md" | 2 +- .../\347\254\25433\350\256\262.md" | 10 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25436\350\256\262.md" | 10 +- .../\347\254\25437\350\256\262.md" | 2 +- .../\347\254\25438\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 6 +- .../\347\254\25402\350\256\262.md" | 8 +- .../\347\254\25403\350\256\262.md" | 16 +- .../\347\254\25404\350\256\262.md" | 10 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 6 +- .../\347\254\25409\350\256\262.md" | 4 +- .../\347\254\25410\350\256\262.md" | 8 +- .../\347\254\25411\350\256\262.md" | 6 +- .../\347\254\25415\350\256\262.md" | 8 +- .../\347\254\25416\350\256\262.md" | 6 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 6 +- .../\347\254\25419\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 6 +- .../\347\254\25423\350\256\262.md" | 6 +- .../\347\254\25425\350\256\262.md" | 2 +- .../\347\254\25426\350\256\262.md" | 10 +- .../\347\254\25427\350\256\262.md" | 2 +- .../\347\254\25428\350\256\262.md" | 8 +- .../\347\254\25430\350\256\262.md" | 2 +- .../\347\254\25431\350\256\262.md" | 6 +- .../\347\254\25432\350\256\262.md" | 2 +- .../\347\254\25433\350\256\262.md" | 4 +- .../\347\254\25434\350\256\262.md" | 2 +- .../\347\254\25435\350\256\262.md" | 2 +- .../\347\254\25437\350\256\262.md" | 2 +- .../\347\254\25438\350\256\262.md" | 2 +- .../\347\254\25440\350\256\262.md" | 4 +- .../\347\254\25400\350\256\262.md" | 6 +- .../\347\254\25401\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 4 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25406\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25411\350\256\262.md" | 6 +- .../\347\254\25413\350\256\262.md" | 4 +- .../\347\254\25417\350\256\262.md" | 4 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25419\350\256\262.md" | 18 +-- .../\347\254\25420\350\256\262.md" | 24 +-- .../\347\254\25421\350\256\262.md" | 4 +- .../\347\254\25422\350\256\262.md" | 14 +- .../\347\254\25423\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 12 +- .../\347\254\25404\350\256\262.md" | 2 +- .../\347\254\25405\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 6 +- .../\347\254\25410\350\256\262.md" | 12 +- .../\347\254\25412\350\256\262.md" | 2 +- .../\347\254\25413\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 10 +- .../\347\254\25402\350\256\262.md" | 2 +- .../\347\254\25403\350\256\262.md" | 4 +- .../\347\254\25404\350\256\262.md" | 10 +- .../\347\254\25405\350\256\262.md" | 12 +- .../\347\254\25406\350\256\262.md" | 12 +- .../\347\254\25407\350\256\262.md" | 16 +- .../\347\254\25408\350\256\262.md" | 12 +- .../\347\254\25409\350\256\262.md" | 24 +-- .../\347\254\25410\350\256\262.md" | 28 ++-- .../\347\254\25411\350\256\262.md" | 22 +-- .../\347\254\25412\350\256\262.md" | 24 +-- .../\347\254\25413\350\256\262.md" | 10 +- .../\347\254\25414\350\256\262.md" | 10 +- .../\347\254\25400\350\256\262.md" | 2 +- .../\347\254\25401\350\256\262.md" | 32 ++-- .../\347\254\25402\350\256\262.md" | 6 +- .../\347\254\25403\350\256\262.md" | 12 +- .../\347\254\25404\350\256\262.md" | 6 +- .../\347\254\25405\350\256\262.md" | 6 +- .../\347\254\25406\350\256\262.md" | 14 +- .../\347\254\25407\350\256\262.md" | 8 +- .../\347\254\25408\350\256\262.md" | 10 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25412\350\256\262.md" | 8 +- .../\347\254\25413\350\256\262.md" | 10 +- .../\347\254\25414\350\256\262.md" | 10 +- .../\347\254\25415\350\256\262.md" | 4 +- .../\347\254\25416\350\256\262.md" | 12 +- .../\347\254\25417\350\256\262.md" | 6 +- .../\347\254\25419\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 8 +- .../\347\254\25401\350\256\262.md" | 8 +- .../\347\254\25406\350\256\262.md" | 2 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25409\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 16 +- .../\347\254\25402\350\256\262.md" | 10 +- .../\347\254\25403\350\256\262.md" | 6 +- .../\347\254\25404\350\256\262.md" | 12 +- .../\347\254\25405\350\256\262.md" | 28 ++-- .../\347\254\25406\350\256\262.md" | 12 +- .../\347\254\25407\350\256\262.md" | 20 +-- .../\347\254\25408\350\256\262.md" | 20 +-- .../\347\254\25409\350\256\262.md" | 14 +- .../\347\254\25410\350\256\262.md" | 4 +- .../\347\254\25411\350\256\262.md" | 10 +- .../\347\254\25412\350\256\262.md" | 20 +-- .../\347\254\25413\350\256\262.md" | 26 +-- .../\347\254\25414\350\256\262.md" | 2 +- .../\347\254\25415\350\256\262.md" | 8 +- .../\347\254\25416\350\256\262.md" | 10 +- .../\347\254\25417\350\256\262.md" | 24 +-- .../\347\254\25418\350\256\262.md" | 20 +-- .../\347\254\25419\350\256\262.md" | 6 +- .../\347\254\25420\350\256\262.md" | 8 +- .../\347\254\25400\350\256\262.md" | 4 +- .../\347\254\25408\350\256\262.md" | 2 +- .../\347\254\25410\350\256\262.md" | 4 +- .../\347\254\25412\350\256\262.md" | 12 +- .../\347\254\25414\350\256\262.md" | 12 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 2 +- .../\347\254\25421\350\256\262.md" | 8 +- .../\347\254\25424\350\256\262.md" | 2 +- .../\347\254\25400\350\256\262.md" | 22 +-- .../\347\254\25401\350\256\262.md" | 4 +- .../\347\254\25402\350\256\262.md" | 8 +- .../\347\254\25403\350\256\262.md" | 2 +- .../\347\254\25404\350\256\262.md" | 8 +- .../\347\254\25405\350\256\262.md" | 10 +- .../\347\254\25406\350\256\262.md" | 10 +- .../\347\254\25407\350\256\262.md" | 6 +- .../\347\254\25408\350\256\262.md" | 8 +- .../\347\254\25409\350\256\262.md" | 8 +- .../\347\254\25410\350\256\262.md" | 10 +- .../\347\254\25411\350\256\262.md" | 10 +- .../\347\254\25412\350\256\262.md" | 16 +- .../\347\254\25413\350\256\262.md" | 12 +- .../\347\254\25414\350\256\262.md" | 6 +- .../\347\254\25415\350\256\262.md" | 8 +- .../\347\254\25417\350\256\262.md" | 2 +- .../\347\254\25418\350\256\262.md" | 4 +- .../\347\254\25419\350\256\262.md" | 2 +- .../\347\254\25420\350\256\262.md" | 4 +- .../\347\254\25407\350\256\262.md" | 4 +- .../\347\254\25415\350\256\262.md" | 4 +- .../\347\254\25416\350\256\262.md" | 6 +- .../\347\254\25402\350\256\262.md" | 2 +- .../\347\254\25408\350\256\262.md" | 2 +- 1278 files changed, 5158 insertions(+), 5227 deletions(-) diff --git "a/docs/Article/Java/AQS \344\270\207\345\255\227\345\233\276\346\226\207\345\205\250\351\235\242\350\247\243\346\236\220.md" "b/docs/Article/Java/AQS \344\270\207\345\255\227\345\233\276\346\226\207\345\205\250\351\235\242\350\247\243\346\236\220.md" index 404263f0a..1b79cd50d 100644 --- "a/docs/Article/Java/AQS \344\270\207\345\255\227\345\233\276\346\226\207\345\205\250\351\235\242\350\247\243\346\236\220.md" +++ "b/docs/Article/Java/AQS \344\270\207\345\255\227\345\233\276\346\226\207\345\205\250\351\235\242\350\247\243\346\236\220.md" @@ -51,7 +51,7 @@ ### 线程一加锁成功 -如果同时有 **三个线程** 并发抢占锁,此时 **线程一** 抢占锁成功, **线程二** 和 **线程三** 抢占锁失败,具体执行流程如下: +如果同时有 **三个线程** 并发抢占锁,此时 **线程一** 抢占锁成功,**线程二** 和 **线程三** 抢占锁失败,具体执行流程如下: ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDJjNWQyZWIyYw.jfif) @@ -80,7 +80,7 @@ static final class NonfairSync extends Sync { } ``` -这里使用的 **ReentrantLock 非公平锁** ,线程进来直接利用 `CAS` 尝试抢占锁,如果抢占成功 `state` 值回被改为 1,且设置对象独占锁线程为当前线程。如下所示: +这里使用的 **ReentrantLock 非公平锁**,线程进来直接利用 `CAS` 尝试抢占锁,如果抢占成功 `state` 值回被改为 1,且设置对象独占锁线程为当前线程。如下所示: ```java protected final boolean compareAndSetState(int expect, int update) { @@ -93,7 +93,7 @@ protected final void setExclusiveOwnerThread(Thread thread) { ### 线程二抢占锁失败 -我们按照真实场景来分析, **线程一** 抢占锁成功后,`state` 变为 1, **线程二** 通过 `CAS` 修改 `state` 变量必然会失败。此时 `AQS` 中 `FIFO`(First In First Out 先进先出) 队列中数据如图所示: +我们按照真实场景来分析,**线程一** 抢占锁成功后,`state` 变为 1,**线程二** 通过 `CAS` 修改 `state` 变量必然会失败。此时 `AQS` 中 `FIFO`(First In First Out 先进先出) 队列中数据如图所示: ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDMwYjUyNTZkZQ.jfif) @@ -136,7 +136,7 @@ final boolean nonfairTryAcquire(int acquires) { 如果 `state` 为 0,则执行 `CAS` 操作,尝试更新 `state` 值为 1,如果更新成功则代表当前线程加锁成功。 -以 **线程二** 为例,因为 **线程一** 已经将 `state` 修改为 1,所以 **线程二** 通过 `CAS` 修改 `state` 的值不会成功。加锁失败。 **线程二** 执行 `tryAcquire ()` 后会返回 false,接着执行 `addWaiter (Node.EXCLUSIVE)` 逻辑,将自己加入到一个 `FIFO` 等待队列中,代码实现如下: +以 **线程二** 为例,因为 **线程一** 已经将 `state` 修改为 1,所以 **线程二** 通过 `CAS` 修改 `state` 的值不会成功。加锁失败。**线程二** 执行 `tryAcquire ()` 后会返回 false,接着执行 `addWaiter (Node.EXCLUSIVE)` 逻辑,将自己加入到一个 `FIFO` 等待队列中,代码实现如下: `java.util.concurrent.locks.AbstractQueuedSynchronizer.addWaiter()`: @@ -262,13 +262,13 @@ private Node addWaiter(Node mode) { } ``` -此时等待队列的 `tail` 节点指向 **线程二** ,进入 `if` 逻辑后,通过 `CAS` 指令将 `tail` 节点重新指向 **线程三** 。接着 **线程三** 调用 `enq ()` 方法执行入队操作,和上面 **线程二** 执行方式是一致的,入队后会修改 **线程二** 对应的 `Node` 中的 `waitStatus=SIGNAL`。最后 **线程三** 也会被挂起。此时等待队列的数据如图: +此时等待队列的 `tail` 节点指向 **线程二**,进入 `if` 逻辑后,通过 `CAS` 指令将 `tail` 节点重新指向 **线程三** 。接着 **线程三** 调用 `enq ()` 方法执行入队操作,和上面 **线程二** 执行方式是一致的,入队后会修改 **线程二** 对应的 `Node` 中的 `waitStatus=SIGNAL`。最后 **线程三** 也会被挂起。此时等待队列的数据如图: ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDJjOGU1ZTI2OA-1590419214352.jfif) ### 线程一释放锁 -现在来分析下释放锁的过程,首先是 **线程一** 释放锁,释放锁后会唤醒 `head` 节点的后置节点,也就是我们现在的 **线程二** ,具体操作流程如下: +现在来分析下释放锁的过程,首先是 **线程一** 释放锁,释放锁后会唤醒 `head` 节点的后置节点,也就是我们现在的 **线程二**,具体操作流程如下: ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDMzMmRmNmZkNQ.jfif) @@ -345,7 +345,7 @@ private void unparkSuccessor(Node node) { ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDM0MzkzYzU4Zg-1590419228808.jfif) -此时 **线程二** 被唤醒, **线程二** 接着之前被 `park` 的地方继续执行,继续执行 `acquireQueued ()` 方法。 +此时 **线程二** 被唤醒,**线程二** 接着之前被 `park` 的地方继续执行,继续执行 `acquireQueued ()` 方法。 ### 线程二唤醒继续加锁 @@ -383,7 +383,7 @@ final boolean acquireQueued(final Node node, int arg) { ### 线程二释放锁 / 线程三加锁 -当 **线程二** 释放锁时,会唤醒被挂起的 **线程三** ,流程和上面大致相同,被唤醒的 **线程三** 会再次尝试加锁,具体代码可以参考上面内容。具体流程图如下: +当 **线程二** 释放锁时,会唤醒被挂起的 **线程三**,流程和上面大致相同,被唤醒的 **线程三** 会再次尝试加锁,具体代码可以参考上面内容。具体流程图如下: ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDNhMWEyNmU5NA.jfif) @@ -393,13 +393,13 @@ final boolean acquireQueued(final Node node, int arg) { ### 公平锁实现原理 -上面所有的加锁场景都是基于 **非公平锁** 来实现的, **非公平锁** 是 `ReentrantLock` 的默认实现,那我们接着来看一下 **公平锁** 的实现原理,这里先用一张图来解释 **公平锁** 和 **非公平锁** 的区别: **非公平锁** 执行流程: +上面所有的加锁场景都是基于 **非公平锁** 来实现的,**非公平锁** 是 `ReentrantLock` 的默认实现,那我们接着来看一下 **公平锁** 的实现原理,这里先用一张图来解释 **公平锁** 和 **非公平锁** 的区别: **非公平锁** 执行流程: ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDNjMjExMjgzOQ.jfif) -这里我们还是用之前的线程模型来举例子,当 **线程二** 释放锁的时候,唤醒被挂起的 **线程三** , **线程三** 执行 `tryAcquire ()` 方法使用 `CAS` 操作来尝试修改 `state` 值,如果此时又来了一个 **线程四** 也来执行加锁操作,同样会执行 `tryAcquire ()` 方法。 +这里我们还是用之前的线程模型来举例子,当 **线程二** 释放锁的时候,唤醒被挂起的 **线程三**,**线程三** 执行 `tryAcquire ()` 方法使用 `CAS` 操作来尝试修改 `state` 值,如果此时又来了一个 **线程四** 也来执行加锁操作,同样会执行 `tryAcquire ()` 方法。 -这种情况就会出现竞争, **线程四** 如果获取锁成功, **线程三** 仍然需要待在等待队列中被挂起。这就是所谓的 **非公平锁** , **线程三** 辛辛苦苦排队等到自己获取锁,却眼巴巴的看到 **线程四** 插队获取到了锁。 **公平锁** 执行流程: +这种情况就会出现竞争,**线程四** 如果获取锁成功,**线程三** 仍然需要待在等待队列中被挂起。这就是所谓的 **非公平锁**,**线程三** 辛辛苦苦排队等到自己获取锁,却眼巴巴的看到 **线程四** 插队获取到了锁。**公平锁** 执行流程: ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDNjMzQ2MmQ2YQ.jfif) @@ -473,7 +473,7 @@ public final boolean hasQueuedPredecessors() { 在第一个红框处,例如 **线程一** 执行完成,此时 head 已经有值,而还未执行 `tail=head` 的时候,此时 **线程二** 判断 `head != tail` 成立。而接着 **线程一** 执行完第二个红框处,此时 `tail = node`,但是并未将 `head.next` 指向 `node`。而这时 **线程二** 就会得到 `head.next == null` 成立,直接返回 true。这种情况代表有节点正在做入队操作。 -如果 `head.next` 不为空,那么接着判断 `head.next` 节点是否为当前线程,如果不是则返回 false。大家要记清楚,返回 false 代表 FIFO 队列中没有等待获取锁的节点,此时线程可以直接尝试获取锁,如果返回 true 代表有等待线程,当前线程如要入队排列,这就是体现 **公平锁** 的地方。 **非公平锁** 和 **公平锁** 的区别: **非公平锁** 性能高于 **公平锁** 性能。 **非公平锁** 可以减少 `CPU` 唤醒线程的开销,整体的吞吐效率会高点,`CPU` 也不必取唤醒所有线程,会减少唤起线程的数量 **非公平锁** 性能虽然优于 **公平锁** ,但是会存在导致 **线程饥饿** 的情况。在最坏的情况下,可能存在某个线程 **一直获取不到锁** 。不过相比性能而言,饥饿问题可以暂时忽略,这可能就是 `ReentrantLock` 默认创建非公平锁的原因之一了。 +如果 `head.next` 不为空,那么接着判断 `head.next` 节点是否为当前线程,如果不是则返回 false。大家要记清楚,返回 false 代表 FIFO 队列中没有等待获取锁的节点,此时线程可以直接尝试获取锁,如果返回 true 代表有等待线程,当前线程如要入队排列,这就是体现 **公平锁** 的地方。**非公平锁** 和 **公平锁** 的区别: **非公平锁** 性能高于 **公平锁** 性能。**非公平锁** 可以减少 `CPU` 唤醒线程的开销,整体的吞吐效率会高点,`CPU` 也不必取唤醒所有线程,会减少唤起线程的数量 **非公平锁** 性能虽然优于 **公平锁**,但是会存在导致 **线程饥饿** 的情况。在最坏的情况下,可能存在某个线程 **一直获取不到锁** 。不过相比性能而言,饥饿问题可以暂时忽略,这可能就是 `ReentrantLock` 默认创建非公平锁的原因之一了。 ## Condition 实现原理 @@ -534,7 +534,7 @@ public class ReentrantLockDemo { ![image.png](../assets/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzIvMTcxZDJkNDNlNjc4NTFiMw.jfif) -这里 **线程一** 先获取锁,然后使用 `await ()` 方法挂起当前线程并 **释放锁** , **线程二** 获取锁后使用 `signal` 唤醒 **线程一** 。 +这里 **线程一** 先获取锁,然后使用 `await ()` 方法挂起当前线程并 **释放锁**,**线程二** 获取锁后使用 `signal` 唤醒 **线程一** 。 ### Condition 实现原理图解 @@ -590,11 +590,11 @@ private Node addConditionWaiter() { } ``` -这里会用当前线程创建一个 `Node` 节点,`waitStatus` 为 `CONDITION`。接着会释放该节点的锁,调用之前解析过的 `release ()` 方法,释放锁后此时会唤醒被挂起的 **线程二** , **线程二** 会继续尝试获取锁。 +这里会用当前线程创建一个 `Node` 节点,`waitStatus` 为 `CONDITION`。接着会释放该节点的锁,调用之前解析过的 `release ()` 方法,释放锁后此时会唤醒被挂起的 **线程二**,**线程二** 会继续尝试获取锁。 接着调用 `isOnSyncQueue ()` 方法是判断当前的线程节点是不是在同步队列中,因为上一步已经释放了锁,也就是说此时可能有线程已经获取锁同时可能已经调用了 `singal ()` 方法,如果已经唤醒,那么就不应该 `park` 了,而是退出 `while` 方法,从而继续争抢锁。 -此时 **线程一** 被挂起, **线程二** 获取锁成功。 +此时 **线程一** 被挂起,**线程二** 获取锁成功。 具体流程如下图: @@ -717,7 +717,7 @@ final boolean acquireQueued(final Node node, int arg) { } ``` -此时 **线程一** 的流程都已经分析完了,等 **线程二** 释放锁后, **线程一** 会继续重试获取锁,流程到此终结。 +此时 **线程一** 的流程都已经分析完了,等 **线程二** 释放锁后,**线程一** 会继续重试获取锁,流程到此终结。 ### Condition 总结 diff --git "a/docs/Article/Java/Java \344\270\255 9 \347\247\215\345\270\270\350\247\201\347\232\204 CMS GC \351\227\256\351\242\230\345\210\206\346\236\220\344\270\216\350\247\243\345\206\263.md" "b/docs/Article/Java/Java \344\270\255 9 \347\247\215\345\270\270\350\247\201\347\232\204 CMS GC \351\227\256\351\242\230\345\210\206\346\236\220\344\270\216\350\247\243\345\206\263.md" index ac3c52e33..8fafa66ad 100644 --- "a/docs/Article/Java/Java \344\270\255 9 \347\247\215\345\270\270\350\247\201\347\232\204 CMS GC \351\227\256\351\242\230\345\210\206\346\236\220\344\270\216\350\247\243\345\206\263.md" +++ "b/docs/Article/Java/Java \344\270\255 9 \347\247\215\345\270\270\350\247\201\347\232\204 CMS GC \351\227\256\351\242\230\345\210\206\346\236\220\344\270\216\350\247\243\345\206\263.md" @@ -376,7 +376,7 @@ Mutator 的类型根据对象存活时间比例图来看主要分为两种,在 #### 排查难度 -一个问题的 **解决难度跟它的常见程度成反比** ,大部分我们都可以通过各种搜索引擎找到类似的问题,然后用同样的手段尝试去解决。当一个问题在各种网站上都找不到相似的问题时,那么可能会有两种情况,一种这不是一个问题,另一种就是遇到一个隐藏比较深的问题,遇到这种问题可能就要深入到源码级别去调试了。以下 GC 问题场景,排查难度从上到下依次递增。 +一个问题的 **解决难度跟它的常见程度成反比**,大部分我们都可以通过各种搜索引擎找到类似的问题,然后用同样的手段尝试去解决。当一个问题在各种网站上都找不到相似的问题时,那么可能会有两种情况,一种这不是一个问题,另一种就是遇到一个隐藏比较深的问题,遇到这种问题可能就要深入到源码级别去调试了。以下 GC 问题场景,排查难度从上到下依次递增。 ## 4. 常见场景分析与解决 @@ -384,7 +384,7 @@ Mutator 的类型根据对象存活时间比例图来看主要分为两种,在 #### (1) 现象 -服务 **刚刚启动时 GC 次数较多** ,最大空间剩余很多但是依然发生 GC,这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause 一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,如下图所示: +服务 **刚刚启动时 GC 次数较多**,最大空间剩余很多但是依然发生 GC,这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause 一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,如下图所示: ![img](../../assets/v2-e460523f6afb99d552c6c4a9734df850_1440w.jpg) @@ -442,7 +442,7 @@ HeapWord* GenCollectedHeap::expand_heap_and_allocate(size_t size, bool is_tlab **定位** :观察 CMS GC 触发时间点 Old/MetaSpace 区的 committed 占比是不是一个固定的值,或者像上文提到的观察总的内存使用率也可以。 - **解决** :尽量 **将成对出现的空间大小配置参数设置成固定的** ,如 `-Xms` 和 `-Xmx`,`-XX:MaxNewSize` 和 `-XX:NewSize`,`-XX:MetaSpaceSize` 和 `-XX:MaxMetaSpaceSize` 等。 + **解决** :尽量 **将成对出现的空间大小配置参数设置成固定的**,如 `-Xms` 和 `-Xmx`,`-XX:MaxNewSize` 和 `-XX:NewSize`,`-XX:MetaSpaceSize` 和 `-XX:MaxMetaSpaceSize` 等。 #### (4) 小结 @@ -497,7 +497,7 @@ void GenCollectedHeap::collect(GCCause::Cause cause) { - **保留 System.gc** - 此处补充一个知识点, **CMS GC 共分为 Background 和 Foreground 两种模式** ,前者就是我们常规理解中的并发收集,可以不影响正常的业务线程运行,但 Foreground Collector 却有很大的差异,他会进行一次压缩式 GC。此压缩式 GC 使用的是跟 Serial Old GC 一样的 Lisp2 算法,其使用 Mark-Compact 来做 Full GC,一般称之为 MSC(Mark-Sweep-Compact),它收集的范围是 Java 堆的 Young 区和 Old 区以及 MetaSpace。由上面的算法章节中我们知道 compact 的代价是巨大的,那么使用 Foreground Collector 时将会带来非常长的 STW。如果在应用程序中 System.gc 被频繁调用,那就非常危险了。 + 此处补充一个知识点,**CMS GC 共分为 Background 和 Foreground 两种模式**,前者就是我们常规理解中的并发收集,可以不影响正常的业务线程运行,但 Foreground Collector 却有很大的差异,他会进行一次压缩式 GC。此压缩式 GC 使用的是跟 Serial Old GC 一样的 Lisp2 算法,其使用 Mark-Compact 来做 Full GC,一般称之为 MSC(Mark-Sweep-Compact),它收集的范围是 Java 堆的 Young 区和 Old 区以及 MetaSpace。由上面的算法章节中我们知道 compact 的代价是巨大的,那么使用 Foreground Collector 时将会带来非常长的 STW。如果在应用程序中 System.gc 被频繁调用,那就非常危险了。 - **去掉 System.gc** @@ -553,7 +553,7 @@ P.S. HotSpot 对 System.gc 有特别处理,最主要的地方体现在一次 S #### (1) 现象 -JVM 在启动后或者某个时间点开始, **MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决** 。 * +JVM 在启动后或者某个时间点开始,**MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决** 。 * #### (2) 原因 @@ -736,9 +736,9 @@ jcmd GC.class_stats|awk '{print$13}'|sed 's/\(.*\)\.\(.*\)/\1/g'|sort |un 过早晋升一般不会直接影响 GC,总会伴随着浮动垃圾、大对象担保失败等问题,但这些问题不是立刻发生的,我们可以观察以下几种现象来判断是否发生了过早晋升。 -**分配速率接近于晋升速率** ,对象晋升年龄较小。GC 日志中出现 “Desired survivor size 107347968 bytes, **new threshold 1(max 6)**” 等信息,说明此时经历过一次 GC 就会放到 Old 区。 +**分配速率接近于晋升速率**,对象晋升年龄较小。GC 日志中出现 “Desired survivor size 107347968 bytes,**new threshold 1(max 6)**” 等信息,说明此时经历过一次 GC 就会放到 Old 区。 -**Full GC 比较频繁** ,且经历过一次 GC 之后 Old 区的 **变化比例非常大** 。比如说 Old 区触发的回收阈值是 80%,经历过一次 GC 之后下降到了 10%,这就说明 Old 区的 70% 的对象存活时间其实很短,如下图所示,Old 区大小每次 GC 后从 2.1G 回收到 300M,也就是说回收掉了 1.8G 的垃圾,只有 **300M 的活跃对象** 。整个 Heap 目前是 4G,活跃对象只占了不到十分之一。 +**Full GC 比较频繁**,且经历过一次 GC 之后 Old 区的 **变化比例非常大** 。比如说 Old 区触发的回收阈值是 80%,经历过一次 GC 之后下降到了 10%,这就说明 Old 区的 70% 的对象存活时间其实很短,如下图所示,Old 区大小每次 GC 后从 2.1G 回收到 300M,也就是说回收掉了 1.8G 的垃圾,只有 **300M 的活跃对象** 。整个 Heap 目前是 4G,活跃对象只占了不到十分之一。 ![img](../../assets/v2-ade2c117e5590cee987e553c67a04fe1_1440w.jpg) @@ -814,11 +814,11 @@ uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { } ``` -可以看到 Hotspot 遍历所有对象时,从所有年龄为 0 的对象占用的空间开始累加,如果加上年龄等于 n 的所有对象的空间之后,使用 Survivor 区的条件值(TargetSurvivorRatio / 100,TargetSurvivorRatio 默认值为 50)进行判断,若大于这个值则结束循环,将 n 和 MaxTenuringThreshold 比较,若 n 小,则阈值为 n,若 n 大,则只能去设置最大阈值为 MaxTenuringThreshold。 **动态年龄触发后导致更多的对象进入了 Old 区,造成资源浪费** 。 +可以看到 Hotspot 遍历所有对象时,从所有年龄为 0 的对象占用的空间开始累加,如果加上年龄等于 n 的所有对象的空间之后,使用 Survivor 区的条件值(TargetSurvivorRatio / 100,TargetSurvivorRatio 默认值为 50)进行判断,若大于这个值则结束循环,将 n 和 MaxTenuringThreshold 比较,若 n 小,则阈值为 n,若 n 大,则只能去设置最大阈值为 MaxTenuringThreshold。**动态年龄触发后导致更多的对象进入了 Old 区,造成资源浪费** 。 #### (3) 策略 -知道问题原因后我们就有解决的方向,如果是 **Young/Eden 区过小** ,我们可以在总的 Heap 内存不变的情况下适当增大 Young 区,具体怎么增加?一般情况下 Old 的大小应当为活跃对象的 2~3 倍左右,考虑到浮动垃圾问题最好在 3 倍左右,剩下的都可以分给 Young 区。 +知道问题原因后我们就有解决的方向,如果是 **Young/Eden 区过小**,我们可以在总的 Heap 内存不变的情况下适当增大 Young 区,具体怎么增加?一般情况下 Old 的大小应当为活跃对象的 2~3 倍左右,考虑到浮动垃圾问题最好在 3 倍左右,剩下的都可以分给 Young 区。 拿笔者的一次典型过早晋升优化来看,原配置为 Young 1.2G + Old 2.8G,通过观察 CMS GC 的情况找到存活对象大概为 300~400M,于是调整 Old 1.5G 左右,剩下 2.5G 分给 Young 区。仅仅调了一个 Young 区大小参数(`-Xmn`),整个 JVM 一分钟 Young GC 从 26 次降低到了 11 次,单次时间也没有增加,总的 GC 时间从 1100ms 降低到了 500ms,CMS GC 次数也从 40 分钟左右一次降低到了 7 小时 30 分钟一次。 @@ -1254,7 +1254,7 @@ Final Remark 是最终的第二次标记,这种情况只有在 Background GC 知道了两个 STW 过程执行流程,我们分析解决就比较简单了,由于大部分问题都出在 Final Remark 过程,这里我们也拿这个场景来举例,主要步骤: -- **【方向】** 观察详细 GC 日志,找到出问题时 Final Remark 日志,分析下 Reference 处理和元数据处理 real 耗时是否正常,详细信息需要通过 -XX:+PrintReferenceGC 参数开启。 **基本在日志里面就能定位到大概是哪个方向出了问题,耗时超过 10% 的就需要关注** 。 +- **【方向】** 观察详细 GC 日志,找到出问题时 Final Remark 日志,分析下 Reference 处理和元数据处理 real 耗时是否正常,详细信息需要通过 -XX:+PrintReferenceGC 参数开启。**基本在日志里面就能定位到大概是哪个方向出了问题,耗时超过 10% 的就需要关注** 。 ```bash 2019-02-27T19:55:37.920+0800: 516952.915: [GC (CMS Final Remark) 516952.915: [ParNew516952.939: [SoftReference, 0 refs, 0.0003857 secs]516952.939: [WeakReference, 1362 refs, 0.0002415 secs]516952.940: [FinalReference, 146 refs, 0.0001233 secs]516952.940: [PhantomReference, 0 refs, 57 refs, 0.0002369 secs]516952.940: [JNI Weak Reference, 0.0000662 secs] @@ -1312,7 +1312,7 @@ if (should_unload_classes()) { #### (2) 原因 -CMS 发生收集器退化主要有以下几种情况。 **晋升失败(Promotion Failed)** +CMS 发生收集器退化主要有以下几种情况。**晋升失败(Promotion Failed)** 顾名思义,晋升失败就是指在进行 Young GC 时,Survivor 放不下,对象只能放入 Old,但此时 Old 也放不下。直觉上乍一看这种情况可能会经常发生,但其实因为有 concurrentMarkSweepThread 和担保机制的存在,发生的条件是很苛刻的,除非是短时间将 Old 区的剩余空间迅速填满,例如上文中说的动态年龄判断导致的过早晋升(见下文的增量收集担保失败)。另外还有一种情况就是内存碎片导致的 Promotion Failed,Young GC 以为 Old 有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。 @@ -1323,7 +1323,7 @@ CMS 发生收集器退化主要有以下几种情况。 **晋升失败(Promoti 碎片带来了两个问题: - **空间分配效率较低** :上文已经提到过,如果是连续的空间 JVM 可以通过使用 pointer bumping 的方式来分配,而对于这种有大量碎片的空闲链表则需要逐个访问 freelist 中的项来访问,查找可以存放新建对象的地址。 -- **空间利用效率变低** :Young 区晋升的对象大小大于了连续空间的大小,那么将会触发 Promotion Failed ,即使整个 Old 区的容量是足够的,但由于其不连续,也无法存放新对象,也就是本文所说的问题。 **增量收集担保失败** 分配内存失败后,会判断统计得到的 Young GC 晋升到 Old 的平均大小,以及当前 Young 区已使用的大小也就是最大可能晋升的对象大小,是否大于 Old 区的剩余空间。只要 CMS 的剩余空间比前两者的任意一者大,CMS 就认为晋升还是安全的,反之,则代表不安全,不进行 Young GC,直接触发 Full GC。 **显式 GC** 这种情况参见场景二。 **并发模式失败(Concurrent Mode Failure)** 最后一种情况,也是发生概率较高的一种,在 GC 日志中经常能看到 Concurrent Mode Failure 关键字。这种是由于并发 Background CMS GC 正在执行,同时又有 Young GC 晋升的对象要放入到了 Old 区中,而此时 Old 区空间不足造成的。 +- **空间利用效率变低** :Young 区晋升的对象大小大于了连续空间的大小,那么将会触发 Promotion Failed ,即使整个 Old 区的容量是足够的,但由于其不连续,也无法存放新对象,也就是本文所说的问题。**增量收集担保失败** 分配内存失败后,会判断统计得到的 Young GC 晋升到 Old 的平均大小,以及当前 Young 区已使用的大小也就是最大可能晋升的对象大小,是否大于 Old 区的剩余空间。只要 CMS 的剩余空间比前两者的任意一者大,CMS 就认为晋升还是安全的,反之,则代表不安全,不进行 Young GC,直接触发 Full GC。**显式 GC** 这种情况参见场景二。**并发模式失败(Concurrent Mode Failure)** 最后一种情况,也是发生概率较高的一种,在 GC 日志中经常能看到 Concurrent Mode Failure 关键字。这种是由于并发 Background CMS GC 正在执行,同时又有 Young GC 晋升的对象要放入到了 Old 区中,而此时 Old 区空间不足造成的。 为什么 CMS GC 正在执行还会导致收集器退化呢?主要是由于 CMS 无法处理浮动垃圾(Floating Garbage)引起的。CMS 的并发清理阶段,Mutator 还在运行,因此不断有新的垃圾产生,而这些垃圾不在这次清理标记的范畴里,无法在本次 GC 被清除掉,这些就是浮动垃圾,除此之外在 Remark 之前那些断开引用脱离了读写屏障控制的对象也算浮动垃圾。所以 Old 区回收的阈值不能太高,否则预留的内存空间很可能不够,从而导致 Concurrent Mode Failure 发生。 @@ -1345,7 +1345,7 @@ CMS 发生收集器退化主要有以下几种情况。 **晋升失败(Promoti #### (1) 现象 -内存使用率不断上升,甚至开始使用 SWAP 内存,同时可能出现 GC 时间飙升,线程被 Block 等现象, **通过 top 命令发现 Java 进程的 RES 甚至超过了** **-Xmx** **的大小** 。出现这些现象时,基本可以确定是出现了堆外内存泄漏。 +内存使用率不断上升,甚至开始使用 SWAP 内存,同时可能出现 GC 时间飙升,线程被 Block 等现象,**通过 top 命令发现 Java 进程的 RES 甚至超过了** **-Xmx** **的大小** 。出现这些现象时,基本可以确定是出现了堆外内存泄漏。 #### (2) 原因 @@ -1526,13 +1526,13 @@ JNI 产生的 GC 问题较难排查,需要谨慎使用。 ## 6. 写在最后 -最后,再说笔者个人的一些小建议,遇到一些 GC 问题,如果有精力,一定要探本穷源,找出最深层次的原因。另外,在这个信息泛滥的时代,有一些被 “奉为圭臬” 的经验可能都是错误的,尽量养成看源码的习惯,有一句话说到 “源码面前,了无秘密”,也就意味着遇到搞不懂的问题,我们可以从源码中一窥究竟,某些场景下确有奇效。但也不是只靠读源码来学习,如果硬啃源码但不理会其背后可能蕴含的理论基础,那很容易 “捡芝麻丢西瓜”,“只见树木,不见森林”,让 “了无秘密” 变成了一句空话,我们还是要结合一些实际的业务场景去针对性地学习。 **你的时间在哪里,你的成就就会在哪里** 。笔者也是在前两年才开始逐步地在 GC 方向上不断深入,查问题、看源码、做总结,每个 Case 形成一个小的闭环,目前初步摸到了 GC 问题处理的一些门道,同时将经验总结应用于生产环境实践,慢慢地形成一个良性循环。 +最后,再说笔者个人的一些小建议,遇到一些 GC 问题,如果有精力,一定要探本穷源,找出最深层次的原因。另外,在这个信息泛滥的时代,有一些被 “奉为圭臬” 的经验可能都是错误的,尽量养成看源码的习惯,有一句话说到 “源码面前,了无秘密”,也就意味着遇到搞不懂的问题,我们可以从源码中一窥究竟,某些场景下确有奇效。但也不是只靠读源码来学习,如果硬啃源码但不理会其背后可能蕴含的理论基础,那很容易 “捡芝麻丢西瓜”,“只见树木,不见森林”,让 “了无秘密” 变成了一句空话,我们还是要结合一些实际的业务场景去针对性地学习。**你的时间在哪里,你的成就就会在哪里** 。笔者也是在前两年才开始逐步地在 GC 方向上不断深入,查问题、看源码、做总结,每个 Case 形成一个小的闭环,目前初步摸到了 GC 问题处理的一些门道,同时将经验总结应用于生产环境实践,慢慢地形成一个良性循环。 本篇文章主要是介绍了 CMS GC 的一些常见场景分析,另外一些,如 CodeCache 问题导致 JIT 失效、SafePoint 就绪时间长、Card Table 扫描耗时等问题不太常见就没有花太多篇幅去讲解。Java GC 是在 “分代” 的思想下内卷了很多年才突破到了 “分区”,目前在美团也已经开始使用 G1 来替换使用了多年的 CMS,虽然在小的堆方面 G1 还略逊色于 CMS,但这是一个趋势,短时间无法升级到 ZGC,所以未来遇到的 G1 的问题可能会逐渐增多。目前已经收集到 Remember Set 粗化、Humongous 分配、Ergonomics 异常、Mixed GC 中 Evacuation Failure 等问题,除此之外也会给出 CMS 升级到 G1 的一些建议,接下来笔者将继续完成这部分文章整理,敬请期待。 -“防火” 永远要胜于 “救火”, **不放过任何一个异常的小指标** (一般来说,任何 **不平滑的曲线** 都是值得怀疑的) ,就有可能避免一次故障的发生。作为 Java 程序员基本都会遇到一些 GC 的问题,独立解决 GC 问题是我们必须迈过的一道坎。开篇中也提到过 GC 作为经典的技术,非常值得我们学习,一些 GC 的学习材料,如《The Garbage Collection Handbook》、《深入理解 Java 虚拟机》等也是常读常新,赶紧动起来,苦练 GC 基本功吧。 +“防火” 永远要胜于 “救火”,**不放过任何一个异常的小指标** (一般来说,任何 **不平滑的曲线** 都是值得怀疑的) ,就有可能避免一次故障的发生。作为 Java 程序员基本都会遇到一些 GC 的问题,独立解决 GC 问题是我们必须迈过的一道坎。开篇中也提到过 GC 作为经典的技术,非常值得我们学习,一些 GC 的学习材料,如《The Garbage Collection Handbook》、《深入理解 Java 虚拟机》等也是常读常新,赶紧动起来,苦练 GC 基本功吧。 -最后的最后,再多啰嗦一句,目前所有 GC 调优相关的文章,第一句讲的就是 “不要过早优化”,使得很多同学对 GC 优化望而却步。在这里笔者提出不一样的观点,熵增定律(在一个孤立系统里,如果没有外力做功,其总混乱度(即熵)会不断增大)在计算机系统同样适用, **如果不主动做功使熵减,系统终究会脱离你的掌控** ,在我们对业务系统和 GC 原理掌握得足够深的时候,可以放心大胆地做优化,因为我们基本可以预测到每一个操作的结果,放手一搏吧,少年! +最后的最后,再多啰嗦一句,目前所有 GC 调优相关的文章,第一句讲的就是 “不要过早优化”,使得很多同学对 GC 优化望而却步。在这里笔者提出不一样的观点,熵增定律(在一个孤立系统里,如果没有外力做功,其总混乱度(即熵)会不断增大)在计算机系统同样适用,**如果不主动做功使熵减,系统终究会脱离你的掌控**,在我们对业务系统和 GC 原理掌握得足够深的时候,可以放心大胆地做优化,因为我们基本可以预测到每一个操作的结果,放手一搏吧,少年! ## 7. 参考资料 diff --git "a/docs/Article/Java/Java \344\270\255\347\232\204 SPI.md" "b/docs/Article/Java/Java \344\270\255\347\232\204 SPI.md" index 6e2783f47..4f98a0a41 100644 --- "a/docs/Article/Java/Java \344\270\255\347\232\204 SPI.md" +++ "b/docs/Article/Java/Java \344\270\255\347\232\204 SPI.md" @@ -167,7 +167,7 @@ public static ServiceLoader load(Class service) { } ``` -看到 `Thread.currentThread ().getContextClassLoader ()`;我就明白是怎么回事了,这个就是 **线程上下文类加载器** ,因为 **线程上下文类加载器** 就是为了做类加载双亲委派模型的逆序而创建的。 +看到 `Thread.currentThread ().getContextClassLoader ()`;我就明白是怎么回事了,这个就是 **线程上下文类加载器**,因为 **线程上下文类加载器** 就是为了做类加载双亲委派模型的逆序而创建的。 !!! Note "《深入理解 Java 虚拟机(第三版)》" 使用这个线程上下文类加载器去加载所需的 SPI 服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了,双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。 diff --git "a/docs/Article/Java/\344\270\215\345\217\257\344\270\215\350\257\264\347\232\204 Java \342\200\234\351\224\201\342\200\235 \344\272\213.md" "b/docs/Article/Java/\344\270\215\345\217\257\344\270\215\350\257\264\347\232\204 Java \342\200\234\351\224\201\342\200\235 \344\272\213.md" index ae046f38d..0ede1d15a 100644 --- "a/docs/Article/Java/\344\270\215\345\217\257\344\270\215\350\257\264\347\232\204 Java \342\200\234\351\224\201\342\200\235 \344\272\213.md" +++ "b/docs/Article/Java/\344\270\215\345\217\257\344\270\215\350\257\264\347\232\204 Java \342\200\234\351\224\201\342\200\235 \344\272\213.md" @@ -152,7 +152,7 @@ synchronized 是悲观锁,在操作同步资源之前需要给同步资源先 我们以 Hotspot 虚拟机为例,Hotspot 的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。 -**Mark Word** :默认存储对象的 HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。 **Klass Point** :对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 +**Mark Word** :默认存储对象的 HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。**Klass Point** :对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 #### Monitor @@ -198,7 +198,7 @@ Monitor 是线程私有的数据结构,每一个线程都有一个可用 monit 10 **无锁** 无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。 -无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的 CAS 原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。 **偏向锁** 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。 +无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的 CAS 原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。**偏向锁** 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。 在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。 @@ -206,7 +206,7 @@ Monitor 是线程私有的数据结构,每一个线程都有一个可用 monit 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为 “01”)或轻量级锁(标志位为 “00”)的状态。 -偏向锁在 JDK 6 及以后的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。 **轻量级锁** 是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。 +偏向锁在 JDK 6 及以后的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。**轻量级锁** 是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为 “01” 状态,是否为偏向锁为 “0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,然后拷贝对象头中的 Mark Word 复制到锁记录中。 @@ -216,7 +216,7 @@ Monitor 是线程私有的数据结构,每一个线程都有一个可用 monit 如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。 -若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。 **重量级锁** +若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。**重量级锁** 升级为重量级锁时,锁标志的状态值变为 “10”,此时 Mark Word 中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。 diff --git "a/docs/Article/Java/\350\247\243\350\257\273\343\200\212\351\230\277\351\207\214\345\267\264\345\267\264Java\345\274\200\345\217\221\346\211\213\345\206\214\343\200\213\350\203\214\345\220\216\347\232\204\346\200\235\350\200\203.md" "b/docs/Article/Java/\350\247\243\350\257\273\343\200\212\351\230\277\351\207\214\345\267\264\345\267\264Java\345\274\200\345\217\221\346\211\213\345\206\214\343\200\213\350\203\214\345\220\216\347\232\204\346\200\235\350\200\203.md" index 8a134186b..828a40cc0 100644 --- "a/docs/Article/Java/\350\247\243\350\257\273\343\200\212\351\230\277\351\207\214\345\267\264\345\267\264Java\345\274\200\345\217\221\346\211\213\345\206\214\343\200\213\350\203\214\345\220\216\347\232\204\346\200\235\350\200\203.md" +++ "b/docs/Article/Java/\350\247\243\350\257\273\343\200\212\351\230\277\351\207\214\345\267\264\345\267\264Java\345\274\200\345\217\221\346\211\213\345\206\214\343\200\213\350\203\214\345\220\216\347\232\204\346\200\235\350\200\203.md" @@ -275,7 +275,7 @@ public void set(boolean m); 那这样做会带来什么问题呢。 -在一般情况下,其实是没有影响的。但是有一种特殊情况就会有问题,那就是发生序列化的时候。 **序列化带来的影响** 关于序列化和反序列化请参考 [Java 对象的序列化与反序列化](https://www.hollischuang.com/archives/1150)。我们这里拿比较常用的 JSON 序列化来举例,看看看常用的 fastJson、jackson 和 Gson 之间有何区别: +在一般情况下,其实是没有影响的。但是有一种特殊情况就会有问题,那就是发生序列化的时候。**序列化带来的影响** 关于序列化和反序列化请参考 [Java 对象的序列化与反序列化](https://www.hollischuang.com/archives/1150)。我们这里拿比较常用的 JSON 序列化来举例,看看看常用的 fastJson、jackson 和 Gson 之间有何区别: ```java public class BooleanMainTest { @@ -383,7 +383,7 @@ Model3[isSuccess=false] img -所以, **在定义 POJO 中的布尔类型的变量时,不要使用 isSuccess 这种形式,而要直接使用 success!** +所以,**在定义 POJO 中的布尔类型的变量时,不要使用 isSuccess 这种形式,而要直接使用 success!** ### Boolean 还是 boolean? @@ -466,7 +466,7 @@ default model : Model[success=null, failure=false] 后来,作者单独和《阿里巴巴 Java 开发手册》、《码出高效》的作者 —— 孤尽 单独 1V1 (qing) Battle (jiao) 了一下。 -最终达成共识,还是 **尽量使用包装类型** 。 **但是,作者还是想强调一个我的观点,尽量避免在你的代码中出现不确定的 null 值。** **null 何罪之有?** +最终达成共识,还是 **尽量使用包装类型** 。**但是,作者还是想强调一个我的观点,尽量避免在你的代码中出现不确定的 null 值。** **null 何罪之有?** 关于 null 值的使用,我在 [使用 Optional 避免 NullPointerException](https://www.hollischuang.com/archives/883)、[9 Things about Null in Java](https://www.hollischuang.com/archives/74) 等文中就介绍过。 @@ -508,9 +508,9 @@ default model : Model[success=null, failure=false] ### 背景知识 - Serializable 和 Externalizable -类通过实现 `java.io.Serializable` 接口以启用其序列化功能。 **未实现此接口的类将无法进行序列化或反序列化。** 可序列化类的所有子类型本身都是可序列化的。 +类通过实现 `java.io.Serializable` 接口以启用其序列化功能。**未实现此接口的类将无法进行序列化或反序列化。** 可序列化类的所有子类型本身都是可序列化的。 -如果读者看过 `Serializable` 的源码,就会发现,他只是一个空的接口,里面什么东西都没有。 **Serializable 接口没有方法或字段,仅用于标识可序列化的语义。** 但是,如果一个类没有实现这个接口,想要被序列化的话,就会抛出 `java.io.NotSerializableException` 异常。 +如果读者看过 `Serializable` 的源码,就会发现,他只是一个空的接口,里面什么东西都没有。**Serializable 接口没有方法或字段,仅用于标识可序列化的语义。** 但是,如果一个类没有实现这个接口,想要被序列化的话,就会抛出 `java.io.NotSerializableException` 异常。 它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢? @@ -656,7 +656,7 @@ java.io.InvalidClassException: com.hollis.User1; local class incompatible: strea 这是因为,在进行反序列化时,JVM 会把传来的字节流中的 `serialVersionUID` 与本地相应实体类的 `serialVersionUID` 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 `InvalidCastException`。 -这也是《阿里巴巴 Java 开发手册》中规定,在兼容性升级中,在修改类的时候,不要修改 `serialVersionUID` 的原因。 **除非是完全不兼容的两个版本** 。所以, **serialVersionUID 其实是验证版本一致性的。** 如果读者感兴趣,可以把各个版本的 JDK 代码都拿出来看一下,那些向下兼容的类的 `serialVersionUID` 是没有变化过的。比如 String 类的 `serialVersionUID` 一直都是 `-6849794470754667710L`。 +这也是《阿里巴巴 Java 开发手册》中规定,在兼容性升级中,在修改类的时候,不要修改 `serialVersionUID` 的原因。**除非是完全不兼容的两个版本** 。所以,**serialVersionUID 其实是验证版本一致性的。** 如果读者感兴趣,可以把各个版本的 JDK 代码都拿出来看一下,那些向下兼容的类的 `serialVersionUID` 是没有变化过的。比如 String 类的 `serialVersionUID` 一直都是 `-6849794470754667710L`。 但是,作者认为,这个规范其实还可以再严格一些,那就是规定: @@ -816,11 +816,11 @@ ObjectInputStream.readObject -> readObject0 -> readOrdinaryObject -> readClassDe 字符串拼接是我们在 Java 代码中比较经常要做的事情,就是把多个字符串拼接到一起。 -我们都知道, **String 是 Java 中一个不可变的类** ,所以他一旦被实例化就无法被修改。 +我们都知道,**String 是 Java 中一个不可变的类**,所以他一旦被实例化就无法被修改。 > 不可变类的实例一旦创建,其成员变量的值就不能被修改。这样设计有很多好处,比如可以缓存 hashcode、使用更加便利以及更加安全等。 -但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢? **字符串不变性与字符串拼接** 其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码: +但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?**字符串不变性与字符串拼接** 其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码: ```yml
@@ -849,7 +849,7 @@ String introduce = "每日更新 Java 相关技术文章";
 String hollis = wechat + "," + introduce;
 ```
 
-这里要特别说明一点,有人把 Java 中使用 `+` 拼接字符串的功能理解为 **运算符重载** 。其实并不是, **Java 是不支持运算符重载的** 。这其实只是 Java 提供的一个 **语法糖** 。后面再详细介绍。
+这里要特别说明一点,有人把 Java 中使用 `+` 拼接字符串的功能理解为 **运算符重载** 。其实并不是,**Java 是不支持运算符重载的** 。这其实只是 Java 提供的一个 **语法糖** 。后面再详细介绍。
 
 > 运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
 >
@@ -1588,7 +1588,7 @@ return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 
 Step 2 比较简单,就是做一下极限值的判断,然后把 Step 1 得到的数值 +1。
 
-Step 1 怎么理解呢? **其实是对一个二进制数依次向右移位,然后与原值取或。** 其目的对于一个数字的二进制,从第一个不为 0 的位开始,把后面的所有位都设置成 1。
+Step 1 怎么理解呢?**其实是对一个二进制数依次向右移位,然后与原值取或。** 其目的对于一个数字的二进制,从第一个不为 0 的位开始,把后面的所有位都设置成 1。
 
 随便拿一个二进制数,套一遍上面的公式就发现其目的了:
 
diff --git "a/docs/Article/Java/\351\235\242\350\257\225\346\234\200\345\270\270\350\242\253\351\227\256\347\232\204Java\345\220\216\347\253\257\351\242\230.md" "b/docs/Article/Java/\351\235\242\350\257\225\346\234\200\345\270\270\350\242\253\351\227\256\347\232\204Java\345\220\216\347\253\257\351\242\230.md"
index f250b1294..e8260dc86 100644
--- "a/docs/Article/Java/\351\235\242\350\257\225\346\234\200\345\270\270\350\242\253\351\227\256\347\232\204Java\345\220\216\347\253\257\351\242\230.md"
+++ "b/docs/Article/Java/\351\235\242\350\257\225\346\234\200\345\270\270\350\242\253\351\227\256\347\232\204Java\345\220\216\347\253\257\351\242\230.md"
@@ -10,7 +10,7 @@ java.lang.Object
 
 下面是对应方法的含义。
 
-**clone 方法** 保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常,深拷贝也需要实现 Cloneable,同时其成员变量为引用类型的也需要实现 Cloneable,然后重写 clone 方法。 **finalize 方法** 该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方法。 **equals 方法** 该方法使用频率非常高。一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。 **hashCode 方法**
+**clone 方法** 保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常,深拷贝也需要实现 Cloneable,同时其成员变量为引用类型的也需要实现 Cloneable,然后重写 clone 方法。**finalize 方法** 该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方法。**equals 方法** 该方法使用频率非常高。一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。**hashCode 方法**
 
 该方法用于哈希查找,重写了 equals 方法一般都要重写 hashCode 方法,这个方法在一些具有哈希功能的 Collection 中用到。
 
@@ -28,13 +28,13 @@ java.lang.Object
 1. 其他线程调用了 interrupt 中断该线程;
 1. 时间间隔到了。
 
-此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。 **notify 方法** 配合 synchronized 使用,该方法唤醒在该对象上 **等待队列** 中的某个线程(同步队列中的线程是给抢占 CPU 的线程,等待队列中的线程指的是等待唤醒的线程)。 **notifyAll 方法** 配合 synchronized 使用,该方法唤醒在该对象上等待队列中的所有线程。 **总结** 只要把上面几个方法熟悉就可以了,toString 和 getClass 方法可以不用去讨论它们。该题目考察的是对 Object 的熟悉程度,平时用的很多方法并没看其定义但是也在用,比如说:wait () 方法,equals () 方法等。
+此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。**notify 方法** 配合 synchronized 使用,该方法唤醒在该对象上 **等待队列** 中的某个线程(同步队列中的线程是给抢占 CPU 的线程,等待队列中的线程指的是等待唤醒的线程)。**notifyAll 方法** 配合 synchronized 使用,该方法唤醒在该对象上等待队列中的所有线程。**总结** 只要把上面几个方法熟悉就可以了,toString 和 getClass 方法可以不用去讨论它们。该题目考察的是对 Object 的熟悉程度,平时用的很多方法并没看其定义但是也在用,比如说:wait () 方法,equals () 方法等。
 
 ```bash
 Class Object is the root of the class hierarchy.Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.
 ```
 
-大致意思:Object 是所有类的根,是所有类的父类,所有对象包括数组都实现了 Object 的方法。 **面试扩散** 上面提到了 wait、notify、notifyAll 方法,或许面试官会问你为什么 sleep 方法不属于 Object 的方法呢?因为提到 wait 等方法,所以最好把 synchronized 都说清楚,把线程状态也都说清楚,尝试让面试官跟着你的节奏走。
+大致意思:Object 是所有类的根,是所有类的父类,所有对象包括数组都实现了 Object 的方法。**面试扩散** 上面提到了 wait、notify、notifyAll 方法,或许面试官会问你为什么 sleep 方法不属于 Object 的方法呢?因为提到 wait 等方法,所以最好把 synchronized 都说清楚,把线程状态也都说清楚,尝试让面试官跟着你的节奏走。
 
 ### 2. Java 创建对象有几种方式?
 
@@ -57,7 +57,7 @@ Object object=(Object)Class.forName("java.lang.Object").newInstance()
 
 - 4. 使用反序列化创建对象,调用 ObjectInputStream 类的 readObject () 方法。
 
-我们反序列化一个对象,JVM 会给我们创建一个单独的对象。JVM 创建对象并不会调用任何构造函数。一个对象实现了 Serializable 接口,就可以把对象写入到文件中,并通过读取文件来创建对象。 **总结** 创建对象的方式关键字:new、反射、clone 拷贝、反序列化。
+我们反序列化一个对象,JVM 会给我们创建一个单独的对象。JVM 创建对象并不会调用任何构造函数。一个对象实现了 Serializable 接口,就可以把对象写入到文件中,并通过读取文件来创建对象。**总结** 创建对象的方式关键字:new、反射、clone 拷贝、反序列化。
 
 ### 3. 获取一个类对象的方式有哪些?
 
@@ -100,7 +100,7 @@ Class clazz = Class.forName("com.tian.User");
 #### LinkedList
 
 - **优点** :LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作,LinkedList 比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。
-- **缺点** :因为 LinkedList 要移动指针,所以查询操作性能比较低。 **适用场景分析** - 当需要对数据进行对随机访问的时候,选用 ArrayList。
+- **缺点** :因为 LinkedList 要移动指针,所以查询操作性能比较低。**适用场景分析** - 当需要对数据进行对随机访问的时候,选用 ArrayList。
   - 当需要对数据进行多次增加删除修改时,采用 LinkedList。
 
 如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用 ArrayList。
@@ -343,13 +343,13 @@ public class MyTest {
 
 如果你在 Java 代码里创建一个线程,相应 JVM 虚拟机中就创建与之对应的程序计数器、Java 虚拟机栈、本地方法栈,同时方法区和堆是在虚拟机启动就已经有了。
 
-**程序计数器** 可以简单理解为:程序计数器是记录执行到你代码的的第几行了,每个线程各自对应自己的程序计数器。 **Java 虚拟机栈** 虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应 Java 代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个 Java 方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。一个线程的生命周期和与之对应的 Java 虚拟机栈的生命周期相同。
+**程序计数器** 可以简单理解为:程序计数器是记录执行到你代码的的第几行了,每个线程各自对应自己的程序计数器。**Java 虚拟机栈** 虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应 Java 代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个 Java 方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。一个线程的生命周期和与之对应的 Java 虚拟机栈的生命周期相同。
 
 > 一个线程进来就创建虚拟机栈,该线程调用的方法就是栈帧,进入方法,栈帧就入栈(虚拟机栈),出方法就是出虚拟机栈。可以通过下面两张图进行理解:
 
 ![在这里插入图片描述](../assets/37317500-61e3-11ea-baf8-f1bca404e984.jpg)
 
-![在这里插入图片描述](../assets/475e6320-61e3-11ea-b16a-f1bd5f6b62c7.jpg) **本地方法栈** 和 Java 虚拟机栈类似,Java 虚拟机栈针对的是 Java 方法,而本地方法栈针对的 native 修饰的方法。 **堆** JVM 几乎所有的对象的内存分配都在堆里。由于对象是有生命周期的,所以把堆又分成了新生代和老年代。
+![在这里插入图片描述](../assets/475e6320-61e3-11ea-b16a-f1bd5f6b62c7.jpg) **本地方法栈** 和 Java 虚拟机栈类似,Java 虚拟机栈针对的是 Java 方法,而本地方法栈针对的 native 修饰的方法。**堆** JVM 几乎所有的对象的内存分配都在堆里。由于对象是有生命周期的,所以把堆又分成了新生代和老年代。
 
 新生代和老年代大小比例 = 1:2(默认)。新生代又分为 Eden、S0、S1 区域,Ede:S0:S1=8:1:1。
 
@@ -542,7 +542,7 @@ G1=Garbage-First 收集器
 
 ### 7. Dubbo 容错策略
 
-**failover cluster 模式** provider 宕机重试以后,请求会分到其他的 provider 上,默认两次,可以手动设置重试次数,建议把写操作重试次数设置成 0。 **failback 模式** 失败自动恢复会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重试,适合执行消息通知等操作。 **failfast cluster 模式** 快速失败只会进行一次调用,失败后立即抛出异常。适用于幂等操作、写操作,类似于 failover cluster 模式中重试次数设置为 0 的情况。 **failsafe cluster 模式** 失败安全是指,当调用过程中出现异常时,仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。 **forking cluster 模式** 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 `forks="2"` 来设置最大并行数。 **broadcacst cluster 模式** 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
+**failover cluster 模式** provider 宕机重试以后,请求会分到其他的 provider 上,默认两次,可以手动设置重试次数,建议把写操作重试次数设置成 0。**failback 模式** 失败自动恢复会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重试,适合执行消息通知等操作。**failfast cluster 模式** 快速失败只会进行一次调用,失败后立即抛出异常。适用于幂等操作、写操作,类似于 failover cluster 模式中重试次数设置为 0 的情况。**failsafe cluster 模式** 失败安全是指,当调用过程中出现异常时,仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。**forking cluster 模式** 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 `forks="2"` 来设置最大并行数。**broadcacst cluster 模式** 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
 
 ### 8. Dubbo 动态代理策略有哪些?
 
@@ -551,7 +551,7 @@ G1=Garbage-First 收集器
 ### 9. 说说 Dubbo 与 Spring Cloud 的区别?
 
 这是很多面试官喜欢问的问题,本人认为其实他们没什么关联之处,但是硬是要问区别,那就说说吧。
-回答的时候主要围绕着四个关键点来说:通信方式、注册中心、监控、断路器,其余像 Spring 分布式配置、服务网关肯定得知道。 **通信方式** Dubbo 使用的是 RPC 通信;Spring Cloud 使用的是 HTTP RestFul 方式。 **注册中心** Dubbo 使用 ZooKeeper(官方推荐),还有 Redis、Multicast、Simple 注册中心,但不推荐。Spring Cloud 使用的是 Spring Cloud Netflix Eureka。 **监控** Dubbo 使用的是 Dubbo-monitor;Spring Cloud 使用的是 Spring Boot admin。 **断路器** Dubbo 在断路器这方面还不完善,Spring Cloud 使用的是 Spring Cloud Netflix Hystrix。分布式配置、网关服务、服务跟踪、消息总线、批量任务等。Dubbo 目前可以说还是空白,而 Spring Cloud 都有相应的组件来支撑。
+回答的时候主要围绕着四个关键点来说:通信方式、注册中心、监控、断路器,其余像 Spring 分布式配置、服务网关肯定得知道。**通信方式** Dubbo 使用的是 RPC 通信;Spring Cloud 使用的是 HTTP RestFul 方式。**注册中心** Dubbo 使用 ZooKeeper(官方推荐),还有 Redis、Multicast、Simple 注册中心,但不推荐。Spring Cloud 使用的是 Spring Cloud Netflix Eureka。**监控** Dubbo 使用的是 Dubbo-monitor;Spring Cloud 使用的是 Spring Boot admin。**断路器** Dubbo 在断路器这方面还不完善,Spring Cloud 使用的是 Spring Cloud Netflix Hystrix。分布式配置、网关服务、服务跟踪、消息总线、批量任务等。Dubbo 目前可以说还是空白,而 Spring Cloud 都有相应的组件来支撑。
 
 ### 10. 说说 TCP 与 UDP 的区别,以及各自的优缺点
 
@@ -575,7 +575,7 @@ G1=Garbage-First 收集器
 
 ### 13. 说一下 HTTP 的长连接与短连接的区别
 
-HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。 **短连接** 在 HTTP/1.0 中默认使用短链接,也就是说,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,但任务结束就中断连接。如果客户端访问的某个 HTML 或其他类型的 Web 资源,如 JavaScript 文件、图像文件、CSS 文件等。当浏览器每遇到这样一个 Web 资源,就会建立一个 HTTP 会话。 **长连接** 从 HTTP/1.1 起,默认使用长连接,用以保持连接特性。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭。如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如 Apache)中设定这个时间。
+HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。**短连接** 在 HTTP/1.0 中默认使用短链接,也就是说,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,但任务结束就中断连接。如果客户端访问的某个 HTML 或其他类型的 Web 资源,如 JavaScript 文件、图像文件、CSS 文件等。当浏览器每遇到这样一个 Web 资源,就会建立一个 HTTP 会话。**长连接** 从 HTTP/1.1 起,默认使用长连接,用以保持连接特性。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭。如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如 Apache)中设定这个时间。
 
 ## 四、MyBatis 篇
 
@@ -589,7 +589,7 @@ HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短
 
 1. MyBatis 一级缓存的生命周期和 SqlSession 一致。
 2. MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有所欠缺。
-3. MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为 Statement。 **二级缓存** 在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
+3. MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为 Statement。**二级缓存** 在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
 ![在这里插入图片描述](../assets/942a9d80-61e4-11ea-829b-7dbe678b494f.jpg)
 二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。
 当开启缓存后,数据的查询执行的流程为:
@@ -660,7 +660,7 @@ Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?cha
 
 动态 SQL 是 MyBatis 的主要特性之一,在 mapper 中定义的参数传到 xml 中之后,在查询之前 MyBatis 会对其进行动态解析。
 MyBatis 为我们提供了两种支持动态 SQL 的语法:`##{}` 以及 `${}`。
-`##{}` 是预编译处理,`${}` 是字符替换。在使用 ##{} 时,MyBatis 会将 SQL 中的 `##{}` 替换成 `?`,配合 PreparedStatement 的 set 方法赋值,这样可以有效的防止 SQL 注入,保证程序的运行安全。 **建议能不要用就不要用,“常在河边走哪能不湿鞋”** 。
+`##{}` 是预编译处理,`${}` 是字符替换。在使用 ##{} 时,MyBatis 会将 SQL 中的 `##{}` 替换成 `?`,配合 PreparedStatement 的 set 方法赋值,这样可以有效的防止 SQL 注入,保证程序的运行安全。**建议能不要用就不要用,“常在河边走哪能不湿鞋”** 。
 
 ### 4. MyBatis 中比如 UserMapper.java 是接口,为什么没有实现类还能调用?
 
@@ -696,11 +696,11 @@ JDK 动态代理:
 select name from t_user where id=1
 ```
 
-1. **取得链接** ,使用使用到 MySQL 中的连接器。
-2. **查询缓存** ,key 为 SQL 语句,value 为查询结果,如果查到就直接返回。不建议使用次缓存,在 MySQL 8.0 版本已经将查询缓存删除,也就是说 MySQL 8.0 版本后不存在此功能。
-3. **分析器** ,分为词法分析和语法分析。此阶段只是做一些 SQL 解析,语法校验。所以一般语法错误在此阶段。
-4. **优化器** ,是在表里有多个索引的时候,决定使用哪个索引;或者一个语句中存在多表关联的时候(join),决定各个表的连接顺序。
-5. **执行器** ,通过分析器让 SQL 知道你要干啥,通过优化器知道该怎么做,于是开始执行语句。执行语句的时候还要判断是否具备此权限,没有权限就直接返回提示没有权限的错误;有权限则打开表,根据表的引擎定义,去使用这个引擎提供的接口,获取这个表的第一行,判断 id 是都等于 1。如果是,直接返回;如果不是继续调用引擎接口去下一行,重复相同的判断,直到取到这个表的最后一行,最后返回。
+1. **取得链接**,使用使用到 MySQL 中的连接器。
+2. **查询缓存**,key 为 SQL 语句,value 为查询结果,如果查到就直接返回。不建议使用次缓存,在 MySQL 8.0 版本已经将查询缓存删除,也就是说 MySQL 8.0 版本后不存在此功能。
+3. **分析器**,分为词法分析和语法分析。此阶段只是做一些 SQL 解析,语法校验。所以一般语法错误在此阶段。
+4. **优化器**,是在表里有多个索引的时候,决定使用哪个索引;或者一个语句中存在多表关联的时候(join),决定各个表的连接顺序。
+5. **执行器**,通过分析器让 SQL 知道你要干啥,通过优化器知道该怎么做,于是开始执行语句。执行语句的时候还要判断是否具备此权限,没有权限就直接返回提示没有权限的错误;有权限则打开表,根据表的引擎定义,去使用这个引擎提供的接口,获取这个表的第一行,判断 id 是都等于 1。如果是,直接返回;如果不是继续调用引擎接口去下一行,重复相同的判断,直到取到这个表的最后一行,最后返回。
 MySQL 的典型的三层结构(连接器 + Server + 执行器):
 ![在这里插入图片描述](../assets/b60ffbc0-61e4-11ea-8fc3-cbeb82bc1da0.jpg)
 
@@ -720,7 +720,7 @@ int (11) 中的 11,不影响字段存储的范围,只影响展示效果。
 
 ### 5. 为什么 SELECT COUNT (\*) FROM table 在 InnoDB 比 MyISAM 慢?
 
-对于 SELECT COUNT (\*) FROM table 语句,在没有 WHERE 条件的情况下,InnoDB 比 MyISAM 可能会慢很多,尤其在大表的情况下。因为,InnoDB 是去实时统计结果,会 **全表扫描** ;而 MyISAM 内部维持了一个计数器,预存了结果,所以直接返回即可。 **面试扩散** 此题还有另外一种问法:`SELECT COUNT(*) FROM table` 在使用存储引擎 InnoDB 和 MyISAM,谁更快,为什么?
+对于 SELECT COUNT (\*) FROM table 语句,在没有 WHERE 条件的情况下,InnoDB 比 MyISAM 可能会慢很多,尤其在大表的情况下。因为,InnoDB 是去实时统计结果,会 **全表扫描** ;而 MyISAM 内部维持了一个计数器,预存了结果,所以直接返回即可。**面试扩散** 此题还有另外一种问法:`SELECT COUNT(*) FROM table` 在使用存储引擎 InnoDB 和 MyISAM,谁更快,为什么?
 
 ### 6. 说说数据库的三范式和反模式
 
@@ -816,7 +816,7 @@ MySQL 中有共享锁和排它锁,也就是读锁和写锁。
 ### 22. 说说悲观锁和乐观锁
 
 **悲观锁** 说的是数据库被外界(包括本系统当前的其他事物以及来自外部系统的事务处理)修改保持着保守态度,因此在整个数据修改过程中,将数据处于锁状态。悲观的实现往往是依靠数据库提供的锁机制,也只有数据库层面提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统汇总实现了加锁机制,也是没有办法保证系统不会修改数据。
-在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。 **乐观锁** 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
+在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。**乐观锁** 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
 而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
 
 ### 23. 怎样尽量避免死锁的出现?
@@ -894,7 +894,7 @@ DLX=Dead-Letter-Exchange。利用 DLX,当消息在一个队列中变成死信
 ![在这里插入图片描述](../assets/84a125e0-61e5-11ea-8032-6b1a3b46917c.jpg)
 总结为:生产者搞丢数据、RabbitMQ 搞丢数据、消费者搞丢数据。
 
-**生产者搞丢数据**  **事务功能机制** 使用 RabbitMQ 的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务 channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务 channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务 channel.txCommit。但是这种方案是存在问题的,即 RabbitMQ 事务机制(同步)一搞,基本上 **吞吐量会下来** ,因为 **太耗性能** 。 **confirm 机制** 在生产者那里设置开启 confirm 模式后,你每次写的消息都会分配一个唯一的 ID。如果写入 RabbitMQ 成功后会回传一个 ack 消息,告诉你这个消息已经到达 RabbitMQ 了;如果没收到你的消息或者失败了,则会回调你的一个 nack,告诉你这个消息 RabbitMQ 接受失败,然后你就可以继续重试发送,而且你可以结合这个机制在内存里维护一个 ID 的状态。如果超过一定时间没收到回调,那么就可以再次发送消息。所以一般在生产者这方避免数据丢失,都是使用 confirm 机制。 **事务机制 PK confirm 模式** 事务机制是同步的,提交事务后会阻塞在那里等待。confirm 是异步的,发送这个消息后就可以发送下一个消息了。消息被 RabbitMQ 接收之后会异步回调一个通知,告知你这个消息已接收到了。
+**生产者搞丢数据**  **事务功能机制** 使用 RabbitMQ 的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务 channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务 channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务 channel.txCommit。但是这种方案是存在问题的,即 RabbitMQ 事务机制(同步)一搞,基本上 **吞吐量会下来**,因为 **太耗性能** 。**confirm 机制** 在生产者那里设置开启 confirm 模式后,你每次写的消息都会分配一个唯一的 ID。如果写入 RabbitMQ 成功后会回传一个 ack 消息,告诉你这个消息已经到达 RabbitMQ 了;如果没收到你的消息或者失败了,则会回调你的一个 nack,告诉你这个消息 RabbitMQ 接受失败,然后你就可以继续重试发送,而且你可以结合这个机制在内存里维护一个 ID 的状态。如果超过一定时间没收到回调,那么就可以再次发送消息。所以一般在生产者这方避免数据丢失,都是使用 confirm 机制。**事务机制 PK confirm 模式** 事务机制是同步的,提交事务后会阻塞在那里等待。confirm 是异步的,发送这个消息后就可以发送下一个消息了。消息被 RabbitMQ 接收之后会异步回调一个通知,告知你这个消息已接收到了。
 
 **RabbitMQ 搞丢数据** RabbitMQ 接收到消息,默认是放在内存里,如果系统挂了或者重启,那对应的消息就会丢失。所以选择开启持久化,把消息写入磁盘中,这样就算系统挂了或者重启都不会丢失消息。
   持久化的两个步骤:
@@ -907,7 +907,7 @@ DLX=Dead-Letter-Exchange。利用 DLX,当消息在一个队列中变成死信
 **消费端搞丢消息数据** 消费端代码中可能有 bug,异常没有处理导致消费失败,或者系统重启、挂了等,那么 RabbitMQ 认为咱们已经消费了,所以对应消息数据就会丢失了。
    这时候我们就得使用 RabbitMQ 的 ack 机制。得把自动 ack 关闭,有个 api 直接调用,然后在自己代码里,确保消费者真的成功消费完成后,再进行一个手动 ack。
 
-**总结** ![在这里插入图片描述](../assets/a09d4d50-61e5-11ea-b15e-81680fd47bd3.jpg)
+**总结**![在这里插入图片描述](../assets/a09d4d50-61e5-11ea-b15e-81680fd47bd3.jpg)
 
 ### 7. RabbitMQ 怎么高可用呢?
 
@@ -918,7 +918,7 @@ DLX=Dead-Letter-Exchange。利用 DLX,当消息在一个队列中变成死信
 
 上面这种普通集群方式确实很麻烦,给人的感觉不是很好,没有做到真正的分布式,就是一个普通的集群。因为这导致要么消费者每次都链接一个实例然后拉取消息数据,要么固定连接那个 queue 所在实例消费数据。前者有数据拉取的开销,后者导致实例性能瓶颈。如果消息放的 queue 挂了,会导致接下来其他实例无法从该实例上拉取消息数据。如果开启了消息持久化,让 RabbitMQ 本地持久化,消息不一定会丢。得等到这个实例重启恢复后,才可以继续从这个 queue 上拉取消息数据。所以上面这种模式,就没有所谓的高可用。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务 queue 的读写操作。
 
-**镜像集群模式** 高可用模式,镜像集群模式才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是, **在镜像集群模式下,你所创建的 queue,无论元数据还是 queue 里的消息数据都会存在于多个实例上** 。也就是说,每个 RabbitMQ 都有 queue 的一个完整镜像,包含 queue 的全部数据。每次写消息到 queue 的时候,都会自动把消息数据同步到多个实例的 queue 上。
+**镜像集群模式** 高可用模式,镜像集群模式才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,**在镜像集群模式下,你所创建的 queue,无论元数据还是 queue 里的消息数据都会存在于多个实例上** 。也就是说,每个 RabbitMQ 都有 queue 的一个完整镜像,包含 queue 的全部数据。每次写消息到 queue 的时候,都会自动把消息数据同步到多个实例的 queue 上。
 
 ![在这里插入图片描述](../assets/b8673cc0-61e5-11ea-b16a-f1bd5f6b62c7.jpg)
 
@@ -999,7 +999,7 @@ DLX=Dead-Letter-Exchange。利用 DLX,当消息在一个队列中变成死信
 
 问这问题是因为前面回答的时候提到了 “Redis 是基于非阻塞的 IO 复用模型”。如果这个问题回答不上来,就相当于前面的回答是给自己挖坑,面试官对你的印象可能也会打点折扣。
 
-Redis 内部使用 **文件事件处理器** file event handler,这个文件事件处理器是 **单线程的** ,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
+Redis 内部使用 **文件事件处理器** file event handler,这个文件事件处理器是 **单线程的**,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
 
 文件事件处理器的结构包含 4 个部分:
 
@@ -1027,7 +1027,7 @@ Redis 内部使用 **文件事件处理器** file event handler,这个文件
       - 支持丰富的数据结构:支持 String 、List、Set、Sorted Set、Hash 五种基础的数据结构。
       - 持久化存储:Redis 提供 RDB 和 AOF 两种数据的持久化存储方案,解决内存数据库最担心的 “万一 Redis 挂掉,数据会消失掉” 的问题。
       - 高可用:内置 Redis Sentinel,提供高可用方案,实现主从故障自动转移。内置 Redis Cluster,提供集群方案,实现基于槽的分片方案,从而支持更大的 Redis 规模。
-      - 丰富的特性:Key 过期、计数、分布式锁、消息队列等。 **缺点** - 由于 Redis 是内存数据库,所以,单台机器存储的数据量,跟机器本身的内存大小有关。虽然 Redis 本身有 Key 过期策略,但还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
+      - 丰富的特性:Key 过期、计数、分布式锁、消息队列等。**缺点** - 由于 Redis 是内存数据库,所以,单台机器存储的数据量,跟机器本身的内存大小有关。虽然 Redis 本身有 Key 过期策略,但还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
       - 如果进行完整重同步,由于需要生成 RDB 文件,并进行传输。这会占用主机的 CPU,并会消耗现网的带宽。不过 Redis2.8 版本,已经有部分重同步的功能,但还是有可能有完整重同步的,比如:新上线的备机。
       - 修改配置文件,进行重启,将硬盘中的数据加载进内存,时间比较久。在这个过程中,Redis 不能提供服务。
 
@@ -1037,7 +1037,7 @@ Redis 内部使用 **文件事件处理器** file event handler,这个文件
 
 ### 6. Redis 持久化方式有哪些?以及有什么区别?
 
-Redis 提供两种持久化机制 RDB 和 AOF 机制: **RDB 持久化方式** 指用数据集快照的方式半持久化模式记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件。持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。 **优点:** - 只有一个文件 dump.rdb,方便持久化
+Redis 提供两种持久化机制 RDB 和 AOF 机制: **RDB 持久化方式** 指用数据集快照的方式半持久化模式记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件。持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。**优点:** - 只有一个文件 dump.rdb,方便持久化
     - 容灾性好,一个文件可以保存到安全的磁盘
     - 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以使 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能
     - 相对于数据集大时,比 AOF 的启动效率更 **缺点:** 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化期间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
@@ -1046,7 +1046,7 @@ Redis 提供两种持久化机制 RDB 和 AOF 机制: **RDB 持久化方式**
 
 **优点:** - 1. 数据安全。AOF 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 AOF 文件中一次。
     - 2. 通过 append 模式写文件。即使中途服务器宕机,也可以通过 redis-check-aof 工具解决数据一致性问题。
-    - 3. AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)。 **缺点:** 1. AOF 文件比 RDB 文件大,且恢复速度慢。
+    - 3. AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)。**缺点:** 1. AOF 文件比 RDB 文件大,且恢复速度慢。
     2. 数据集大的时候,比 RDB 启动效率低。
 
 ### 7. 持久化有两种,那应该怎么选择呢?
@@ -1063,10 +1063,10 @@ Redis 提供两种持久化机制 RDB 和 AOF 机制: **RDB 持久化方式**
 - 面试官可能会问可不可以不用 sleep 呢?list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。
 - 面试官可能还问能不能生产一次消费多次呢?使用 pub /sub 主题订阅者模式,可以实现 1:N 的消息队列。
 - 面试官可能还问 pub /sub 有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列,如 RabbitMQ 等。
-- 面试官可能还问 Redis 如何 **实现延时队列** ? 我估计现在你很想把面试官一棒打死,怎么问的这么详细。但是你会很克制,然后神态自若地回答道:使用 sortedset,拿时间戳作为 score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
+- 面试官可能还问 Redis 如何 **实现延时队列**? 我估计现在你很想把面试官一棒打死,怎么问的这么详细。但是你会很克制,然后神态自若地回答道:使用 sortedset,拿时间戳作为 score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
 
 **面试扩散** :
-很多面试官上来就直接这么问: Redis 如何 **实现延时队列** ?
+很多面试官上来就直接这么问: Redis 如何 **实现延时队列**?
 
 ### 9. 熟悉哪些 Redis 集群模式?
 
@@ -1102,7 +1102,7 @@ Codis 是一个代理中间件,当客户端向 Codis 发送指令时,Codis 
 
 ### 1.Spring Boot 提供了哪些核心功能?
 
-**独立运行 Spring 项目** Spring Boot 可以以 jar 包形式独立运行,运行一个 Spring Boot 项目只需要通过 java -jar xx.jar 来运行。 **内嵌 Servlet 容器** Spring Boot 可以选择内嵌 Tomcat、Jetty 或者 Undertow,这样我们无须以 war 包形式部署项目。 **提供 Starter 简化 Maven 配置** Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载,比如:spring-boot-starter-web。 **自动配置 Spring Bean** Spring Boot 检测到特定类的存在,就会针对这个应用做一定的配置,进行自动配置 Bean,这样会极大地减少我们要使用的配置。当然,Spring Boot 只考虑大多数的开发场景,并不是所有的场景,若在实际开发中我们需要配置 Bean,而 Spring Boot 没有提供支持,则可以自定义自动配置进行解决。 **准生产的应用监控** Spring Boot 提供基于 HTTP、JMX、SSH 对运行时的项目进行监控。 **无代码生成和 XML 配置** Spring Boot 没有引入任何形式的代码生成,它使用的是 Spring 4.0 的条件 @Condition 注解以实现根据条件进行配置。同时使用了 Maven /Gradle 的依赖传递解析机制来实现 Spring 应用里面的自动配置。
+**独立运行 Spring 项目** Spring Boot 可以以 jar 包形式独立运行,运行一个 Spring Boot 项目只需要通过 java -jar xx.jar 来运行。**内嵌 Servlet 容器** Spring Boot 可以选择内嵌 Tomcat、Jetty 或者 Undertow,这样我们无须以 war 包形式部署项目。**提供 Starter 简化 Maven 配置** Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载,比如:spring-boot-starter-web。**自动配置 Spring Bean** Spring Boot 检测到特定类的存在,就会针对这个应用做一定的配置,进行自动配置 Bean,这样会极大地减少我们要使用的配置。当然,Spring Boot 只考虑大多数的开发场景,并不是所有的场景,若在实际开发中我们需要配置 Bean,而 Spring Boot 没有提供支持,则可以自定义自动配置进行解决。**准生产的应用监控** Spring Boot 提供基于 HTTP、JMX、SSH 对运行时的项目进行监控。**无代码生成和 XML 配置** Spring Boot 没有引入任何形式的代码生成,它使用的是 Spring 4.0 的条件 @Condition 注解以实现根据条件进行配置。同时使用了 Maven /Gradle 的依赖传递解析机制来实现 Spring 应用里面的自动配置。
 
 ### 2. Spring Boot 核心注解是什么?
 
@@ -1183,7 +1183,7 @@ public @interface SpringBootApplication {
 
 ### 1. Spring 中 ApplicationContext 和 BeanFactory 的区别
 
-- **类图** ![在这里插入图片描述](../assets/720bf4e0-61e6-11ea-861e-fb2bdb9ba1ba.jpg)
+- **类图**![在这里插入图片描述](../assets/720bf4e0-61e6-11ea-861e-fb2bdb9ba1ba.jpg)
 
 - **包目录不同** - spring-beans.jar 中 org.springframework.beans.factory.BeanFactory
   - spring-context.jar 中 org.springframework.context.ApplicationContext
@@ -1267,7 +1267,7 @@ Bean 的声明周期为 bean 的创建、应用、销毁。
 
 ### 8. Spring 中用到了哪些设计模式?
 
-**简单工厂模式** :Spring 中的 BeanFactory 就是简单工厂模式的体现。根据传入一个唯一的标识来获得 Bean 对象,但是在传入参数后创建还是传入参数前创建,要根据具体情况来定。 **工厂模式** :Spring 中的 FactoryBean 就是典型的工厂方法模式,实现了 FactoryBean 接口的 bean 是一类叫做 factory 的 bean。其特点是,spring 在使用 getBean () 调用获得该 bean 时,会自动调用该 bean 的 getObject () 方法,所以返回的不是 factory 这个 bean,而是这个 bean.getOjbect () 方法的返回值。 **单例模式** :在 spring 中用到的单例模式有:`scope="singleton"`,注册式单例模式,bean 存放于 Map 中。bean name 当做 key,bean 当做 value。 **原型模式** :在 spring 中用到的原型模式有:`scope="prototype"`,每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。 **迭代器模式** :在 Spring 中有个 CompositeIterator 实现了 Iterator,Iterable 接口和 Iterator 接口,这两个都是迭代相关的接口。可以这么认为,实现了 Iterable 接口,则表示某个对象是可被迭代的。Iterator 接口相当于是一个迭代器,实现了 Iterator 接口,等于具体定义了这个可被迭代的对象时如何进行迭代的。 **代理模式** :Spring 中经典的 AOP,就是使用动态代理实现的,分 JDK 和 CGlib 动态代理。 **适配器模式** :Spring 中的 AOP 中 AdvisorAdapter 类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring 会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个 Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。 **观察者模式** :Spring 中的 Event 和 Listener。spring 事件:ApplicationEvent,该抽象类继承了 EventObject 类,JDK 建议所有的事件都应该继承自 EventObject。spring 事件监听器:ApplicationListener,该接口继承了 EventListener 接口,JDK 建议所有的事件监听器都应该继承 EventListener。 **模板模式** :Spring 中的 org.springframework.jdbc.core.JdbcTemplate 就是非常经典的模板模式的应用,里面的 execute 方法,把整个算法步骤都定义好了。 **责任链模式** :DispatcherServlet 中的 doDispatch () 方法中获取与请求匹配的处理器 HandlerExecutionChain,this.getHandler () 方法的处理使用到了责任链模式。
+**简单工厂模式** :Spring 中的 BeanFactory 就是简单工厂模式的体现。根据传入一个唯一的标识来获得 Bean 对象,但是在传入参数后创建还是传入参数前创建,要根据具体情况来定。**工厂模式** :Spring 中的 FactoryBean 就是典型的工厂方法模式,实现了 FactoryBean 接口的 bean 是一类叫做 factory 的 bean。其特点是,spring 在使用 getBean () 调用获得该 bean 时,会自动调用该 bean 的 getObject () 方法,所以返回的不是 factory 这个 bean,而是这个 bean.getOjbect () 方法的返回值。**单例模式** :在 spring 中用到的单例模式有:`scope="singleton"`,注册式单例模式,bean 存放于 Map 中。bean name 当做 key,bean 当做 value。**原型模式** :在 spring 中用到的原型模式有:`scope="prototype"`,每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。**迭代器模式** :在 Spring 中有个 CompositeIterator 实现了 Iterator,Iterable 接口和 Iterator 接口,这两个都是迭代相关的接口。可以这么认为,实现了 Iterable 接口,则表示某个对象是可被迭代的。Iterator 接口相当于是一个迭代器,实现了 Iterator 接口,等于具体定义了这个可被迭代的对象时如何进行迭代的。**代理模式** :Spring 中经典的 AOP,就是使用动态代理实现的,分 JDK 和 CGlib 动态代理。**适配器模式** :Spring 中的 AOP 中 AdvisorAdapter 类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring 会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个 Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。**观察者模式** :Spring 中的 Event 和 Listener。spring 事件:ApplicationEvent,该抽象类继承了 EventObject 类,JDK 建议所有的事件都应该继承自 EventObject。spring 事件监听器:ApplicationListener,该接口继承了 EventListener 接口,JDK 建议所有的事件监听器都应该继承 EventListener。**模板模式** :Spring 中的 org.springframework.jdbc.core.JdbcTemplate 就是非常经典的模板模式的应用,里面的 execute 方法,把整个算法步骤都定义好了。**责任链模式** :DispatcherServlet 中的 doDispatch () 方法中获取与请求匹配的处理器 HandlerExecutionChain,this.getHandler () 方法的处理使用到了责任链模式。
 
 ### 9. Spring 框架中的单例 Bean 是线程安全的么?
 
@@ -1296,8 +1296,8 @@ ZooKeeper 是一个开放源码的分布式协调服务,它是集群的管理
 
 ### 2. ZooKeeper 有哪些应用场景?
 
-**统一命名服务** 命名服务是指通过指定的名字来获取资源或服务的地址,利用 ZooKeeper 创建一个全局的路径,即时唯一的路径。这个路径就可以作为一个名字,指向集群中机器或者提供服务的地址,又或者一个远程的对象等。 **分布式服务** 相对来说,这个功能使用还是比较广泛的。ZooKeeper 实现的分布式锁的可靠性比 Redis 实现的高,当然相对性能来说,ZooKeeper 性能稍弱,但其实已经很牛了。 **配置管理** Spring Cloud Config ZooKeeper 就是基于 ZooKeeper 来实现的,提供配置中心的服务。 **注册与发现** 是否有新的机器加入或者是有机器退出(挂了)。所有机器约定在父目录下创建临时节点,然后监听父节点下的子节点变化。一旦有机器挂机,该机器与 ZooKeeper 的链接断开,其所创建的临时目录节点也被删除,所有其他机器都收到对应的通知:某个结点被删除了。Dubbo 就是典型应用案例。 **Master 选举** 基于 ZooKeeper 实现分布式协调,从而实现主从的选举。比如:Kafka、Elastic-job 等中间件都有使用到。 **队列管理** ZooKeeper 有两种类型的队列:
-  同步队列:当一个队列的成员都聚齐时,这个队列才可用,否则一直等待。在约定的目录下创建临时目录节点,再监听节点数目是否是我们要求的数据。队列按照先进先出 FIFO 方式进行入队和出队操作。和分布式锁服务中心的控制时序的场景基本原理相同,入列和出列都有编号。创建 PERSISTENT_SEQUENTIAL 节点,创建成功时 Watcher 通知等待的队列,队列删除序列号最小的节点以消费。此场景下,znode 用于消息存储,znode 存储的数据就是消息队列中的消息内容,SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息丢失的问题。 **分布式锁** 有了 ZooKeeper 的一致性文件系统,锁的问题就变得简单多了。锁服务可以分为保持独占和控制时序。
+**统一命名服务** 命名服务是指通过指定的名字来获取资源或服务的地址,利用 ZooKeeper 创建一个全局的路径,即时唯一的路径。这个路径就可以作为一个名字,指向集群中机器或者提供服务的地址,又或者一个远程的对象等。**分布式服务** 相对来说,这个功能使用还是比较广泛的。ZooKeeper 实现的分布式锁的可靠性比 Redis 实现的高,当然相对性能来说,ZooKeeper 性能稍弱,但其实已经很牛了。**配置管理** Spring Cloud Config ZooKeeper 就是基于 ZooKeeper 来实现的,提供配置中心的服务。**注册与发现** 是否有新的机器加入或者是有机器退出(挂了)。所有机器约定在父目录下创建临时节点,然后监听父节点下的子节点变化。一旦有机器挂机,该机器与 ZooKeeper 的链接断开,其所创建的临时目录节点也被删除,所有其他机器都收到对应的通知:某个结点被删除了。Dubbo 就是典型应用案例。**Master 选举** 基于 ZooKeeper 实现分布式协调,从而实现主从的选举。比如:Kafka、Elastic-job 等中间件都有使用到。**队列管理** ZooKeeper 有两种类型的队列:
+  同步队列:当一个队列的成员都聚齐时,这个队列才可用,否则一直等待。在约定的目录下创建临时目录节点,再监听节点数目是否是我们要求的数据。队列按照先进先出 FIFO 方式进行入队和出队操作。和分布式锁服务中心的控制时序的场景基本原理相同,入列和出列都有编号。创建 PERSISTENT_SEQUENTIAL 节点,创建成功时 Watcher 通知等待的队列,队列删除序列号最小的节点以消费。此场景下,znode 用于消息存储,znode 存储的数据就是消息队列中的消息内容,SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息丢失的问题。**分布式锁** 有了 ZooKeeper 的一致性文件系统,锁的问题就变得简单多了。锁服务可以分为保持独占和控制时序。
 
 - 保持独占。我们把 znode 看作是一把锁,通过 createZnode 的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁,用完删除掉自己创建的 /distribute_lock 节点就释放出锁。
 - 控制时序。/distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和 Master 一样,编号最小的获得锁,用完删除,依次方便。
@@ -1327,7 +1327,7 @@ ZooKeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听
 
 ### 5. ZooKeeper 对节点的 watch 监听通知是永久的吗?
 
-不是, **一次性** 的。无论是服务端还是客户端,一旦一个 Watcher 被触发, ZooKeeper 都会将其从相应的存储中移除。这样的设计有效地减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断地向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
+不是,**一次性** 的。无论是服务端还是客户端,一旦一个 Watcher 被触发, ZooKeeper 都会将其从相应的存储中移除。这样的设计有效地减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断地向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
 
 ### 6. ZooKeeper 集群中有哪些角色?
 
@@ -1336,7 +1336,7 @@ ZooKeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听
 
 ### 7. ZooKeeper 集群中 Server 有哪些工作状态?
 
-**LOOKING** 寻找 Leader 状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。 **FOLLOWING** 跟随者状态。表明当前服务器角色是 Follower。 **LEADING** 领导者状态。表明当前服务器角色是 Leader。 **OBSERVING** 观察者状态。表明当前服务器角色是 Observer。
+**LOOKING** 寻找 Leader 状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。**FOLLOWING** 跟随者状态。表明当前服务器角色是 Follower。**LEADING** 领导者状态。表明当前服务器角色是 Leader。**OBSERVING** 观察者状态。表明当前服务器角色是 Observer。
 
 ### 8. ZooKeeper 集群中是怎样选举 leader 的?
 
@@ -1425,9 +1425,9 @@ Leader 服务器会和每一个 Follower/Observer 服务器都建立 TCP 连接
 
 ### 6. 线程安全需要保证几个基本特征
 
-- **原子性** ,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
-- **可见性** ,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
-- **有序性** ,是保证线程内串行语义,避免指令重排等。
+- **原子性**,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
+- **可见性**,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
+- **有序性**,是保证线程内串行语义,避免指令重排等。
 
 ### 7. 说一下线程之间是如何通信的?
 
diff --git "a/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB Buffer Pool.md" "b/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB Buffer Pool.md"
index fb515096e..4626f485e 100644
--- "a/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB Buffer Pool.md"	
+++ "b/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB Buffer Pool.md"	
@@ -6,7 +6,7 @@
 
 本文主要分析 MySQL Buffer Pool 的相关技术以及实现原理,源码基于阿里云 RDS MySQL 5.6 分支,其中部分特性已经开源到 AliSQL。
 
-Buffer Pool 相关的源代码在 buf 目录下,主要包括 **LRU List**,**Flu List**,**Double write buffer**, **预读预写**,**Buffer Pool 预热**,**压缩页内存管理** 等模块,包括头文件和 IC 文件,一共两万行代码。
+Buffer Pool 相关的源代码在 buf 目录下,主要包括 **LRU List**,**Flu List**,**Double write buffer**,**预读预写**,**Buffer Pool 预热**,**压缩页内存管理** 等模块,包括头文件和 IC 文件,一共两万行代码。
 
 ## 基础知识
 
diff --git "a/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB \345\220\214\346\255\245\346\234\272\345\210\266.md" "b/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB \345\220\214\346\255\245\346\234\272\345\210\266.md"
index a9a8c90e5..55fb2a259 100644
--- "a/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB \345\220\214\346\255\245\346\234\272\345\210\266.md"	
+++ "b/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB \345\220\214\346\255\245\346\234\272\345\210\266.md"	
@@ -6,7 +6,7 @@
 
 ## 基础知识
 
-同步机制对于其他数据库模块来说相对独立,但是需要比较多的操作系统以及硬件知识,这里简单介绍一下几个有用的概念,便于读者理解后续概念。 **内存模型 :** 主要分为语言级别的内存模型和硬件级别的内存模型。语言级别的内存模型,C/C++属于weak memory model,简单的说就是编译器在进行编译优化的时候,可以对指令进行重排,只需要保证在单线程的环境下,优化前和优化后执行结果一致即可,执行中间过程不保证跟代码的语义顺序一致。所以在多线程的环境下,如果依赖代码中间过程的执行顺序,程序就会出现问题。硬件级别的内存模型,我们常用的cpu,也属于弱内存模型,即cpu在执行指令的时候,为了提升执行效率,也会对某些执行进行乱序执行(按照wiki提供的资料,在x86 64环境下,只会发生读写乱序,即读操作可能会被乱序到写操作之前),如果在编程的时候不做一些措施,同样容易造成错误。 ***内存屏障 :** \*为了解决弱内存模型造成的问题,需要一种能控制指令重排或者乱序执行程序的手段,这种技术就叫做内存屏障,程序员只需要在代码中插入特定的函数,就能控制弱内存模型带来的负面影响,当然,由于影响了乱序和重排这类的优化,对代码的执行效率有一定的影响。具体实现上,内存屏障技术分三种,一种是full memory barrier,即barrier之前的操作不能乱序或重排到barrier之后,同时barrier之后的操作不能乱序或重排到barrier之前,当然这种full barrier对性能影响最大,为了提高效率才有了另外两种:acquire barrier和release barrier,前者只保证barrier后面的操作不能移到之前,后者只保证barrier前面的操作不移到之后。 ***互斥锁 :** \*互斥锁有两层语义,除了大家都知道的排他性(即只允许一个线程同时访问)外,还有一层内存屏障(full memory barrier)的语义,即保证临界区的操作不会被乱序到临界区外。Pthread库里面常用的mutex,conditional variable等操作都自带内存屏障这层语义。此外,使用pthread库,每次调用都需要应用程序从用户态陷入到内核态中查看当前环境,在锁冲突不是很严重的情况下,效率相对比较低。 ***自旋锁 :** \*传统的互斥锁,只要一检测到锁被其他线程所占用了,就立刻放弃cpu时间片,把cpu留给其他线程,这就会产生一次上下文切换。当系统压力大的时候,频繁的上下文切换会导致sys值过高。自旋锁,在检测到锁不可用的时候,首先cpu忙等一小会儿,如果还是发现不可用,再放弃cpu,进行切换。互斥锁消耗cpu sys值,自旋锁消耗cpu usr值。 ***递归锁 :** \*如果在同一个线程中,对同一个互斥锁连续加锁两次,即第一次加锁后,没有释放,继续进行对这个锁进行加锁,那么如果这个互斥锁不是递归锁,将导致死锁。可以把递归锁理解为一种特殊的互斥锁。 ***死锁 :** \*构成死锁有四大条件,其中有一个就是加锁顺序不一致,如果能保证不同类型的锁按照某个特定的顺序加锁,就能大大降低死锁发生的概率,之所以不能完全消除,是因为同一种类型的锁依然可能发生死锁。另外,对同一个锁连续加锁两次,如果是非递归锁,也将导致死锁。
+同步机制对于其他数据库模块来说相对独立,但是需要比较多的操作系统以及硬件知识,这里简单介绍一下几个有用的概念,便于读者理解后续概念。**内存模型 :** 主要分为语言级别的内存模型和硬件级别的内存模型。语言级别的内存模型,C/C++属于weak memory model,简单的说就是编译器在进行编译优化的时候,可以对指令进行重排,只需要保证在单线程的环境下,优化前和优化后执行结果一致即可,执行中间过程不保证跟代码的语义顺序一致。所以在多线程的环境下,如果依赖代码中间过程的执行顺序,程序就会出现问题。硬件级别的内存模型,我们常用的cpu,也属于弱内存模型,即cpu在执行指令的时候,为了提升执行效率,也会对某些执行进行乱序执行(按照wiki提供的资料,在x86 64环境下,只会发生读写乱序,即读操作可能会被乱序到写操作之前),如果在编程的时候不做一些措施,同样容易造成错误。***内存屏障 :** \*为了解决弱内存模型造成的问题,需要一种能控制指令重排或者乱序执行程序的手段,这种技术就叫做内存屏障,程序员只需要在代码中插入特定的函数,就能控制弱内存模型带来的负面影响,当然,由于影响了乱序和重排这类的优化,对代码的执行效率有一定的影响。具体实现上,内存屏障技术分三种,一种是full memory barrier,即barrier之前的操作不能乱序或重排到barrier之后,同时barrier之后的操作不能乱序或重排到barrier之前,当然这种full barrier对性能影响最大,为了提高效率才有了另外两种:acquire barrier和release barrier,前者只保证barrier后面的操作不能移到之前,后者只保证barrier前面的操作不移到之后。***互斥锁 :** \*互斥锁有两层语义,除了大家都知道的排他性(即只允许一个线程同时访问)外,还有一层内存屏障(full memory barrier)的语义,即保证临界区的操作不会被乱序到临界区外。Pthread库里面常用的mutex,conditional variable等操作都自带内存屏障这层语义。此外,使用pthread库,每次调用都需要应用程序从用户态陷入到内核态中查看当前环境,在锁冲突不是很严重的情况下,效率相对比较低。***自旋锁 :** \*传统的互斥锁,只要一检测到锁被其他线程所占用了,就立刻放弃cpu时间片,把cpu留给其他线程,这就会产生一次上下文切换。当系统压力大的时候,频繁的上下文切换会导致sys值过高。自旋锁,在检测到锁不可用的时候,首先cpu忙等一小会儿,如果还是发现不可用,再放弃cpu,进行切换。互斥锁消耗cpu sys值,自旋锁消耗cpu usr值。***递归锁 :** \*如果在同一个线程中,对同一个互斥锁连续加锁两次,即第一次加锁后,没有释放,继续进行对这个锁进行加锁,那么如果这个互斥锁不是递归锁,将导致死锁。可以把递归锁理解为一种特殊的互斥锁。***死锁 :** \*构成死锁有四大条件,其中有一个就是加锁顺序不一致,如果能保证不同类型的锁按照某个特定的顺序加锁,就能大大降低死锁发生的概率,之所以不能完全消除,是因为同一种类型的锁依然可能发生死锁。另外,对同一个锁连续加锁两次,如果是非递归锁,也将导致死锁。
 
 ## 原子操作
 
diff --git "a/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB \345\264\251\346\272\203\346\201\242\345\244\215.md" "b/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB \345\264\251\346\272\203\346\201\242\345\244\215.md"
index f94304c35..c5f73c1b1 100644
--- "a/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB \345\264\251\346\272\203\346\201\242\345\244\215.md"	
+++ "b/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267InnoDB \345\264\251\346\272\203\346\201\242\345\244\215.md"	
@@ -6,7 +6,7 @@
 
 ## 基础知识
 
-**lsn:** 可以理解为数据库从创建以来产生的redo日志量,这个值越大,说明数据库的更新越多,也可以理解为更新的时刻。此外,每个数据页上也有一个lsn,表示最后被修改时的lsn,值越大表示越晚被修改。比如,数据页A的lsn为100,数据页B的lsn为200,checkpoint lsn为150,系统lsn为300,表示当前系统已经更新到300,小于150的数据页已经被刷到磁盘上,因此数据页A的最新数据一定在磁盘上,而数据页B则不一定,有可能还在内存中。 **redo日志:** 现代数据库都需要写redo日志,例如修改一条数据,首先写redo日志,然后再写数据。在写完redo日志后,就直接给客户端返回成功。这样虽然看过去多写了一次盘,但是由于把对磁盘的随机写入(写数据)转换成了顺序的写入(写redo日志),性能有很大幅度的提高。当数据库挂了之后,通过扫描redo日志,就能找出那些没有刷盘的数据页(在崩溃之前可能数据页仅仅在内存中修改了,但是还没来得及写盘),保证数据不丢。 **undo日志:** 数据库还提供类似撤销的功能,当你发现修改错一些数据时,可以使用rollback指令回滚之前的操作。这个功能需要undo日志来支持。此外,现代的关系型数据库为了提高并发(同一条记录,不同线程的读取不冲突,读写和写读不冲突,只有同时写才冲突),都实现了类似MVCC的机制,在InnoDB中,这个也依赖undo日志。为了实现统一的管理,与redo日志不同,undo日志在Buffer Pool中有对应的数据页,与普通的数据页一起管理,依据LRU规则也会被淘汰出内存,后续再从磁盘读取。与普通的数据页一样,对undo页的修改,也需要先写redo日志。 **检查点:** 英文名为checkpoint。数据库为了提高性能,数据页在内存修改后并不是每次都会刷到磁盘上。checkpoint之前的数据页保证一定落盘了,这样之前的日志就没有用了(由于InnoDB redolog日志循环使用,这时这部分日志就可以被覆盖),checkpoint之后的数据页有可能落盘,也有可能没有落盘,所以checkpoint之后的日志在崩溃恢复的时候还是需要被使用的。InnoDB会依据脏页的刷新情况,定期推进checkpoint,从而减少数据库崩溃恢复的时间。检查点的信息在第一个日志文件的头部。 **崩溃恢复:** 用户修改了数据,并且收到了成功的消息,然而对数据库来说,可能这个时候修改后的数据还没有落盘,如果这时候数据库挂了,重启后,数据库需要从日志中把这些修改后的数据给捞出来,重新写入磁盘,保证用户的数据不丢。这个从日志中捞数据的过程就是崩溃恢复的主要任务,也可以成为数据库前滚。当然,在崩溃恢复中还需要回滚没有提交的事务,提交没有提交成功的事务。由于回滚操作需要undo日志的支持,undo日志的完整性和可靠性需要redo日志来保证,所以崩溃恢复先做redo前滚,然后做undo回滚。
+**lsn:** 可以理解为数据库从创建以来产生的redo日志量,这个值越大,说明数据库的更新越多,也可以理解为更新的时刻。此外,每个数据页上也有一个lsn,表示最后被修改时的lsn,值越大表示越晚被修改。比如,数据页A的lsn为100,数据页B的lsn为200,checkpoint lsn为150,系统lsn为300,表示当前系统已经更新到300,小于150的数据页已经被刷到磁盘上,因此数据页A的最新数据一定在磁盘上,而数据页B则不一定,有可能还在内存中。**redo日志:** 现代数据库都需要写redo日志,例如修改一条数据,首先写redo日志,然后再写数据。在写完redo日志后,就直接给客户端返回成功。这样虽然看过去多写了一次盘,但是由于把对磁盘的随机写入(写数据)转换成了顺序的写入(写redo日志),性能有很大幅度的提高。当数据库挂了之后,通过扫描redo日志,就能找出那些没有刷盘的数据页(在崩溃之前可能数据页仅仅在内存中修改了,但是还没来得及写盘),保证数据不丢。**undo日志:** 数据库还提供类似撤销的功能,当你发现修改错一些数据时,可以使用rollback指令回滚之前的操作。这个功能需要undo日志来支持。此外,现代的关系型数据库为了提高并发(同一条记录,不同线程的读取不冲突,读写和写读不冲突,只有同时写才冲突),都实现了类似MVCC的机制,在InnoDB中,这个也依赖undo日志。为了实现统一的管理,与redo日志不同,undo日志在Buffer Pool中有对应的数据页,与普通的数据页一起管理,依据LRU规则也会被淘汰出内存,后续再从磁盘读取。与普通的数据页一样,对undo页的修改,也需要先写redo日志。**检查点:** 英文名为checkpoint。数据库为了提高性能,数据页在内存修改后并不是每次都会刷到磁盘上。checkpoint之前的数据页保证一定落盘了,这样之前的日志就没有用了(由于InnoDB redolog日志循环使用,这时这部分日志就可以被覆盖),checkpoint之后的数据页有可能落盘,也有可能没有落盘,所以checkpoint之后的日志在崩溃恢复的时候还是需要被使用的。InnoDB会依据脏页的刷新情况,定期推进checkpoint,从而减少数据库崩溃恢复的时间。检查点的信息在第一个日志文件的头部。**崩溃恢复:** 用户修改了数据,并且收到了成功的消息,然而对数据库来说,可能这个时候修改后的数据还没有落盘,如果这时候数据库挂了,重启后,数据库需要从日志中把这些修改后的数据给捞出来,重新写入磁盘,保证用户的数据不丢。这个从日志中捞数据的过程就是崩溃恢复的主要任务,也可以成为数据库前滚。当然,在崩溃恢复中还需要回滚没有提交的事务,提交没有提交成功的事务。由于回滚操作需要undo日志的支持,undo日志的完整性和可靠性需要redo日志来保证,所以崩溃恢复先做redo前滚,然后做undo回滚。
 
 我们从源码角度仔细剖析一下数据库崩溃恢复过程。整个过程都在引擎初始化阶段完成(`innobase_init`),其中最主要的函数是`innobase_start_or_create_for_mysql`,innodb通过这个函数完成创建和初始化,包括崩溃恢复。首先来介绍一下数据库的前滚。
 
diff --git "a/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267\344\270\264\346\227\266\350\241\250\351\202\243\344\272\233\344\272\213\345\204\277.md" "b/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267\344\270\264\346\227\266\350\241\250\351\202\243\344\272\233\344\272\213\345\204\277.md"
index c6629c705..13325ab99 100644
--- "a/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267\344\270\264\346\227\266\350\241\250\351\202\243\344\272\233\344\272\213\345\204\277.md"
+++ "b/docs/Article/MySQL/MySQL\302\267\345\274\225\346\223\216\347\211\271\346\200\247\302\267\344\270\264\346\227\266\350\241\250\351\202\243\344\272\233\344\272\213\345\204\277.md"
@@ -77,7 +77,7 @@ BinLog只有在事务提交的时候才会写入到文件中,在没提交前
 
 ## 相关参数
 
-**tmpdir:** 这个参数是临时目录的配置,在5.6以及之前的版本,临时表/文件默认都会放在这里。这个参数可以配置多个目录,这样就可以轮流在不同的目录上创建临时表/文件,如果不同的目录分别指向不同的磁盘,就可以达到分流的目的。 **innodb_tmpdir:** 这个参数只要是被DDL中的排序临时文件使用的。其占用的空间会很大,建议单独配置。这个参数可以动态设置,也是一个Session变量。 **slave_load_tmpdir:** 这个参数主要是给BinLog复制中Load Data时,配置备库存放临时文件位置时使用。因为数据库Crash后还需要依赖Load数据的文件,建议不要配置重启后会删除数据的目录。 **internal_tmp_disk_storage_engine:** 当隐式临时表被转换成磁盘临时表时,使用哪种引擎,默认只有MyISAM和InnoDB。5.7及以后的版本才支持。8.0.16版本后取消的这个参数。 **internal_tmp_mem_storage_engine:** 隐式临时表在内存时用的存储引擎,可以选择Memory或者Temptable引擎。建议选择新的Temptable引擎。 **default_tmp_storage_engine:** 默认的显式临时表的引擎,即用户通过SQL语句创建的临时表的引擎。 **tmp_table_size:** min(tmp_table_size,max_heap_table_size)是隐式临时表的内存大小,超过这个值会转换成磁盘临时表。 **max_heap_table_size:** 用户创建的Memory内存表的内存限制大小。 **big_tables:** 内存临时表转换成磁盘临时表需要有个转化操作,需要在不同引擎格式中转换,这个是需要消耗的。如果我们能提前知道执行某个SQL需要用到磁盘临时表,即内存肯定不够用,可以设置这个参数,这样优化器就跳过使用内存临时表,直接使用磁盘临时表,减少开销。 **temptable_max_ram:** 这个参数是8.0后才有的,主要是给Temptable引擎指定内存大小,超过这个后,要么就转换成磁盘临时表,要么就使用自带的overflow机制。 **temptable_use_mmap:** 是否使用Temptable的overflow机制。
+**tmpdir:** 这个参数是临时目录的配置,在5.6以及之前的版本,临时表/文件默认都会放在这里。这个参数可以配置多个目录,这样就可以轮流在不同的目录上创建临时表/文件,如果不同的目录分别指向不同的磁盘,就可以达到分流的目的。**innodb_tmpdir:** 这个参数只要是被DDL中的排序临时文件使用的。其占用的空间会很大,建议单独配置。这个参数可以动态设置,也是一个Session变量。**slave_load_tmpdir:** 这个参数主要是给BinLog复制中Load Data时,配置备库存放临时文件位置时使用。因为数据库Crash后还需要依赖Load数据的文件,建议不要配置重启后会删除数据的目录。**internal_tmp_disk_storage_engine:** 当隐式临时表被转换成磁盘临时表时,使用哪种引擎,默认只有MyISAM和InnoDB。5.7及以后的版本才支持。8.0.16版本后取消的这个参数。**internal_tmp_mem_storage_engine:** 隐式临时表在内存时用的存储引擎,可以选择Memory或者Temptable引擎。建议选择新的Temptable引擎。**default_tmp_storage_engine:** 默认的显式临时表的引擎,即用户通过SQL语句创建的临时表的引擎。**tmp_table_size:** min(tmp_table_size,max_heap_table_size)是隐式临时表的内存大小,超过这个值会转换成磁盘临时表。**max_heap_table_size:** 用户创建的Memory内存表的内存限制大小。**big_tables:** 内存临时表转换成磁盘临时表需要有个转化操作,需要在不同引擎格式中转换,这个是需要消耗的。如果我们能提前知道执行某个SQL需要用到磁盘临时表,即内存肯定不够用,可以设置这个参数,这样优化器就跳过使用内存临时表,直接使用磁盘临时表,减少开销。**temptable_max_ram:** 这个参数是8.0后才有的,主要是给Temptable引擎指定内存大小,超过这个后,要么就转换成磁盘临时表,要么就使用自带的overflow机制。**temptable_use_mmap:** 是否使用Temptable的overflow机制。
 
 ## 总结建议
 
diff --git "a/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266.md" "b/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266.md"
index abe56303a..97a807778 100644
--- "a/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266.md"
+++ "b/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266.md"
@@ -19,13 +19,13 @@ mysql复制是指从一个mysql服务器(MASTER)将数据 **通过日志的方
 
 它的工作原理很简单。首先 **确保master数据库上开启了二进制日志,这是复制的前提** 。
 
-- 在slave准备开始复制时,首先 **要执行change master to语句设置连接到master服务器的连接参数** ,在执行该语句的时候要提供一些信息,包括如何连接和要从哪复制binlog,这些信息在连接的时候会记录到slave的datadir下的master.info文件中,以后再连接master的时候将不用再提供这新信息而是直接读取该文件进行连接。
+- 在slave准备开始复制时,首先 **要执行change master to语句设置连接到master服务器的连接参数**,在执行该语句的时候要提供一些信息,包括如何连接和要从哪复制binlog,这些信息在连接的时候会记录到slave的datadir下的master.info文件中,以后再连接master的时候将不用再提供这新信息而是直接读取该文件进行连接。
 
 - 在slave上有两种线程,
 
   分别是IO线程和SQL线程
 
-  - IO线程用于连接master,监控和接受master的binlog。当启动IO线程成功连接master时, **master会同时启动一个dump线程** ,该线程将slave请求要复制的binlog给dump出来,之后IO线程负责监控并接收master上dump出来的二进制日志,当master上binlog有变化的时候,IO线程就将其复制过来并写入到自己的中继日志(relay log)文件中。
+  - IO线程用于连接master,监控和接受master的binlog。当启动IO线程成功连接master时,**master会同时启动一个dump线程**,该线程将slave请求要复制的binlog给dump出来,之后IO线程负责监控并接收master上dump出来的二进制日志,当master上binlog有变化的时候,IO线程就将其复制过来并写入到自己的中继日志(relay log)文件中。
   - slave上的另一个线程SQL线程用于监控、读取并重放relay log中的日志,将数据写入到自己的数据库中。如下图所示。
 
 站在slave的角度上看,过程如下:
@@ -42,9 +42,9 @@ mysql复制是指从一个mysql服务器(MASTER)将数据 **通过日志的方
 1. slave的IO线程复制这些变动的binlog到自己的relay log中。
 1. slave的SQL线程读取并重新应用relay log到自己的数据库上,让其和master数据库保持一致。
 
-从复制的机制上可以知道,在复制进行前,slave上必须具有master上部分完整内容作为复制基准数据。例如,master上有数据库A,二进制日志已经写到了pos1位置,那么在复制进行前,slave上必须要有数据库A,且如果要从pos1位置开始复制的话,还必须有和master上pos1之前完全一致的数据。如果不满足这样的一致性条件,那么在replay中继日志的时候将不知道如何进行应用而导致数据混乱。 **也就是说,复制是基于binlog的position进行的,复制之前必须保证position一致。** (注:这是传统的复制方式所要求的)
+从复制的机制上可以知道,在复制进行前,slave上必须具有master上部分完整内容作为复制基准数据。例如,master上有数据库A,二进制日志已经写到了pos1位置,那么在复制进行前,slave上必须要有数据库A,且如果要从pos1位置开始复制的话,还必须有和master上pos1之前完全一致的数据。如果不满足这样的一致性条件,那么在replay中继日志的时候将不知道如何进行应用而导致数据混乱。**也就是说,复制是基于binlog的position进行的,复制之前必须保证position一致。** (注:这是传统的复制方式所要求的)
 
-可以选择对哪些数据库甚至数据库中的哪些表进行复制。 **默认情况下,MySQL的复制是异步的。slave可以不用一直连着master,即使中间断开了也能从断开的position处继续进行复制。** MySQL 5.6对比MySQL 5.5在复制上进行了很大的改进,主要包括支持GTID(Global Transaction ID,全局事务ID)复制和多SQL线程并行重放。GTID的复制方式和传统的复制方式不一样,通过全局事务ID,它不要求复制前slave有基准数据,也不要求binlog的position一致。
+可以选择对哪些数据库甚至数据库中的哪些表进行复制。**默认情况下,MySQL的复制是异步的。slave可以不用一直连着master,即使中间断开了也能从断开的position处继续进行复制。** MySQL 5.6对比MySQL 5.5在复制上进行了很大的改进,主要包括支持GTID(Global Transaction ID,全局事务ID)复制和多SQL线程并行重放。GTID的复制方式和传统的复制方式不一样,通过全局事务ID,它不要求复制前slave有基准数据,也不要求binlog的position一致。
 
 MySQL 5.7.17则提出了组复制(MySQL Group Replication,MGR)的概念。像数据库这样的产品,必须要尽可能完美地设计一致性问题,特别是在集群、分布式环境下。Galera就是一个MySQL集群产品,它支持多主模型(多个master),但是当MySQL 5.7.17引入了MGR功能后,Galera的优势不再明显,甚至MGR可以取而代之。MGR为MySQL集群中多主复制的很多问题提供了很好的方案,可谓是一项革命性的功能。
 
@@ -58,11 +58,11 @@ MySQL 5.7.17则提出了组复制(MySQL Group Replication,MGR)的概念。像数
 
 主要有以下几点好处: **1.提供了读写分离的能力。** replication让所有的slave都和master保持数据一致,因此外界客户端可以从各个slave中读取数据,而写数据则从master上操作。也就是实现了读写分离。
 
-需要注意的是,为了保证数据一致性, **写操作必须在master上进行** 。
+需要注意的是,为了保证数据一致性,**写操作必须在master上进行** 。
 
-通常说到读写分离这个词,立刻就能意识到它会分散压力、提高性能。 **2.为MySQL服务器提供了良好的伸缩(scale-out)能力。** 由于各个slave服务器上只提供数据检索而没有写操作,因此"随意地"增加slave服务器数量来提升整个MySQL群的性能,而不会对当前业务产生任何影响。
+通常说到读写分离这个词,立刻就能意识到它会分散压力、提高性能。**2.为MySQL服务器提供了良好的伸缩(scale-out)能力。** 由于各个slave服务器上只提供数据检索而没有写操作,因此"随意地"增加slave服务器数量来提升整个MySQL群的性能,而不会对当前业务产生任何影响。
 
-之所以"随意地"要加上双引号,是因为每个slave都要和master建立连接,传输数据。如果slave数量巨多,master的压力就会增大,网络带宽的压力也会增大。 **3.数据库备份时,对业务影响降到最低。** 由于MySQL服务器群中所有数据都是一致的(至少几乎是一致的),所以在需要备份数据库的时候可以任意停止某一台slave的复制功能(甚至停止整个mysql服务),然后从这台主机上进行备份,这样几乎不会影响整个业务(除非只有一台slave,但既然只有一台slave,说明业务压力并不大,短期内将这个压力分配给master也不会有什么影响)。 **4.能提升数据的安全性。** 这是显然的,任意一台mysql服务器断开,都不会丢失数据。即使是master宕机,也只是丢失了那部分还没有传送的数据(异步复制时才会丢失这部分数据)。 **5.数据分析不再影响业务。**
+之所以"随意地"要加上双引号,是因为每个slave都要和master建立连接,传输数据。如果slave数量巨多,master的压力就会增大,网络带宽的压力也会增大。**3.数据库备份时,对业务影响降到最低。** 由于MySQL服务器群中所有数据都是一致的(至少几乎是一致的),所以在需要备份数据库的时候可以任意停止某一台slave的复制功能(甚至停止整个mysql服务),然后从这台主机上进行备份,这样几乎不会影响整个业务(除非只有一台slave,但既然只有一台slave,说明业务压力并不大,短期内将这个压力分配给master也不会有什么影响)。**4.能提升数据的安全性。** 这是显然的,任意一台mysql服务器断开,都不会丢失数据。即使是master宕机,也只是丢失了那部分还没有传送的数据(异步复制时才会丢失这部分数据)。**5.数据分析不再影响业务。**
 
 需要进行数据分析的时候,直接划分一台或多台slave出来专门用于数据分析。这样OLTP和OLAP可以共存,且几乎不会影响业务处理性能。
 
@@ -93,7 +93,7 @@ MySQL支持两种不同的复制方法:传统的复制方式和GTID复制。My
 
 ## 3.3 异步复制
 
-客户端发送DDL/DML语句给master, **master执行完毕立即返回成功信息给客户端,而不管slave是否已经开始复制** 。这样的复制方式导致的问题是,当master写完了binlog,而slave还没有开始复制或者复制还没完成时, **slave上和master上的数据暂时不一致,且此时master突然宕机,slave将会丢失一部分数据。如果此时把slave提升为新的master,那么整个数据库就永久丢失这部分数据。** ![img](../assets/733013-20180524205215240-203795747.png)
+客户端发送DDL/DML语句给master,**master执行完毕立即返回成功信息给客户端,而不管slave是否已经开始复制** 。这样的复制方式导致的问题是,当master写完了binlog,而slave还没有开始复制或者复制还没完成时,**slave上和master上的数据暂时不一致,且此时master突然宕机,slave将会丢失一部分数据。如果此时把slave提升为新的master,那么整个数据库就永久丢失这部分数据。**![img](../assets/733013-20180524205215240-203795747.png)
 
 ## 3.4 延迟复制
 
@@ -107,7 +107,7 @@ mysql支持一主一从和一主多从。但是每个slave必须只能是一个m
 
 以下是一主一从的结构图: ![img](../assets/733013-20180528163847611-1424365065.png)
 
-在开始传统的复制(非GTID复制)前,需要完成以下几个关键点, **这几个关键点指导后续复制的所有步骤** 。
+在开始传统的复制(非GTID复制)前,需要完成以下几个关键点,**这几个关键点指导后续复制的所有步骤** 。
 
 1. 为master和slave设定不同的`server-id`,这是主从复制结构中非常关键的标识号。到了MySQL 5.7,似乎不设置server id就无法开启binlog。设置server id需要重启MySQL实例。
 1. 开启master的binlog。刚安装并初始化的MySQL默认未开启binlog,建议手动设置binlog且为其设定文件名,否则默认以主机名为基名时修改主机名后会找不到日志文件。
@@ -245,7 +245,7 @@ mysql> select * from backuptest.num_isam limit 10;
 ```
 
 ### 4.2.1 获取master binlog的坐标 **如果master是全新的数据库实例,或者在此之前没有开启过binlog,那么它的坐标位置是position=4** 。之所以是4而非0,是因为binlog的前4个记录单元是每个binlog文件的头部信息。
-如果master已有数据,或者说master以前就开启了binlog并写过数据库,那么需要手动获取position。 **为了安全以及没有后续写操作,必须先锁表。** 
+如果master已有数据,或者说master以前就开启了binlog并写过数据库,那么需要手动获取position。**为了安全以及没有后续写操作,必须先锁表。** 
 
 ```python
 mysql> flush tables with read lock;
@@ -272,7 +272,7 @@ mysql> show master status;   # 为了排版,简化了输出结果
 
 2. 如果要复制的是某个或某几个库,直接拷贝相关目录即可。但注意,这种冷备份的方式只适合MyISAM表和开启了`innodb_file_per_table=ON`的InnoDB表。如果没有开启该变量,innodb表使用公共表空间,无法直接冷备份。
 1. 如果要冷备份innodb表,最安全的方法是先关闭master上的mysql,而不是通过表锁。
-   所以, **如果没有涉及到innodb表,那么在锁表之后,可以直接冷拷贝。最后释放锁。** 
+   所以,**如果没有涉及到innodb表,那么在锁表之后,可以直接冷拷贝。最后释放锁。** 
 
 ```python
    mysql> flush tables with read lock;
@@ -314,7 +314,7 @@ shell> mysqldump -uroot -p --all-databases --master-data=2 >dump.db
 
 注意,`--master-data`选项将再dump.db中加入`change master to`相关的语句,值为2时,`change master to`语句是注释掉的,值为1或者没有提供值时,这些语句是直接激活的。同时,`--master-data`会锁定所有表(如果同时使用了`--single-transaction`,则不是锁所有表,详细内容请参见[mysqldump](https://www.cnblogs.com/f-ck-need-u/p/9013458.html))。
 
-因此,可以直接从dump.db中获取到binlog的坐标。 **记住这个坐标。** 
+因此,可以直接从dump.db中获取到binlog的坐标。**记住这个坐标。** 
 
 ```plaintext
 \[\[email protected\] ~\]# grep -i -m 1 'change master to' dump.db
@@ -357,7 +357,7 @@ drwxr-x--- 2 root root    12288 May 29 04:12 sys
 -rw-r----- 1 root root     2560 May 29 04:12 xtrabackup_logfile
 ```
 
-其中xtrabackup\_binlog\_info中记录了binlog的坐标。 **记住这个坐标。** 
+其中xtrabackup\_binlog\_info中记录了binlog的坐标。**记住这个坐标。** 
 
 ```plaintext
 \[\[email protected\] ~\]# cat /backup/2018-05-29_04-12-15/xtrabackup_binlog_info
@@ -491,7 +491,7 @@ start slave sql_thread;
 
 ### 4.4.1 master.info
 
-master.info文件记录的是 **IO线程相关的信息** ,也就是连接master以及读取master binlog的信息。通过这个文件,下次连接master时就不需要再提供连接选项。
+master.info文件记录的是 **IO线程相关的信息**,也就是连接master以及读取master binlog的信息。通过这个文件,下次连接master时就不需要再提供连接选项。
 
 以下是master.info的内容,每一行的意义见[官方手册](https://dev.mysql.com/doc/refman/5.7/en/slave-logs-status.html)
 
@@ -769,10 +769,10 @@ IO线程每次从master复制日志要写入到relay log中,但是它是先放
 
 仍然有三种方法从slave上获取基准数据:冷备份、mysqldump和xtrabackup。方法见前文[将slave恢复到master指定的坐标](https://www.cnblogs.com/f-ck-need-u/p/9155003.html#blog4.2)。
 
-其实 **临时关闭一个slave对业务影响很小,所以我个人建议,新添加slave时采用冷备份slave的方式** ,不仅备份恢复的速度最快,配置成slave也最方便,这一点和前面配置"一主一从"不一样。但冷备份slave的时候需要注意几点:
+其实 **临时关闭一个slave对业务影响很小,所以我个人建议,新添加slave时采用冷备份slave的方式**,不仅备份恢复的速度最快,配置成slave也最方便,这一点和前面配置"一主一从"不一样。但冷备份slave的时候需要注意几点:
 
 1.  可以考虑将slave1完全shutdown再将整个datadir拷贝到新的slave2上。
-2. **建议新的slave2配置文件中的"relay-log"的值和slave1的值完全一致** ,否则应该手动从slave2的relay-log.info中获取IO线程连接master时的坐标,并在slave2上使用`change master to`语句设置连接参数。 方法很简单,所以不做演示了。
+2. **建议新的slave2配置文件中的"relay-log"的值和slave1的值完全一致**,否则应该手动从slave2的relay-log.info中获取IO线程连接master时的坐标,并在slave2上使用`change master to`语句设置连接参数。 方法很简单,所以不做演示了。
 
 5.2 配置一主多从(从中有从)
 ----------------
@@ -788,7 +788,7 @@ IO线程每次从master复制日志要写入到relay log中,但是它是先放
 > > 1.  将不同数据库复制到不同slave上。
 > > 2.  可以将master上的事务表(如InnoDB)复制为slave上的非事务表(如MyISAM),这样slave上回放的速度加快,查询数据的速度在一定程度上也会提升。
 
-回到这种主从结构,它有些不同,master只负责传送日志给slave1、slave2和slave3,slave 2\_1和slave 2\_2的日志由slave2负责传送,所以slave2上也必须要开启binlog选项。此外,还必须开启一个选项`--log-slave-updates`让slave2能够在重放relay log时也写自己的binlog,否则slave2的binlog仅接受人为的写操作。 **问:slave能否进行写操作?重放relay log的操作是否会记录到slave的binlog中?** 1.  在slave上没有开启`read-only`选项(只读变量)时,任何有写权限的用户都可以进行写操作,这些操作都会记录到binlog中。注意, **read-only选项对具有super权限的用户以及SQL线程执行的重放写操作无效** 。默认这个选项是关闭的。
+回到这种主从结构,它有些不同,master只负责传送日志给slave1、slave2和slave3,slave 2\_1和slave 2\_2的日志由slave2负责传送,所以slave2上也必须要开启binlog选项。此外,还必须开启一个选项`--log-slave-updates`让slave2能够在重放relay log时也写自己的binlog,否则slave2的binlog仅接受人为的写操作。**问:slave能否进行写操作?重放relay log的操作是否会记录到slave的binlog中?** 1.  在slave上没有开启`read-only`选项(只读变量)时,任何有写权限的用户都可以进行写操作,这些操作都会记录到binlog中。注意,**read-only选项对具有super权限的用户以及SQL线程执行的重放写操作无效** 。默认这个选项是关闭的。
 ```
 
 mysql> show variables like "read_only";
@@ -799,7 +799,7 @@ mysql> show variables like "read_only";
 +---------------+-------+
 
 ```plaintext
-1.  在slave上没有开启`log-slave-updates`和binlog选项时,重放relay log不会记录binlog。 **所以如果slave2要作为某些slave的master,那么在slave2上必须要开启log-slave-updates和binlog选项。为了安全和数据一致性,在slave2上还应该启用read-only选项。** 环境如下:
+1.  在slave上没有开启`log-slave-updates`和binlog选项时,重放relay log不会记录binlog。**所以如果slave2要作为某些slave的master,那么在slave2上必须要开启log-slave-updates和binlog选项。为了安全和数据一致性,在slave2上还应该启用read-only选项。** 环境如下:
 
 ![img](../assets/733013-20180608100823680-1104661841.png)
 
@@ -920,7 +920,7 @@ Replicate_Wild_Ignore_Table: 通配符方式指定不复制的表
 ```plaintext
 如果要指定列表,则多次使用这些变量进行设置。
 
-需要注意的是, **尽管显式指定了要复制和忽略的库或者表,但是master还是会将所有的binlog传给slave并写入到slave的relay log中,真正负责筛选的slave上的SQL线程** 。
+需要注意的是,**尽管显式指定了要复制和忽略的库或者表,但是master还是会将所有的binlog传给slave并写入到slave的relay log中,真正负责筛选的slave上的SQL线程** 。
 
 另外,如果slave上开启了binlog,SQL线程读取relay log后会将所有的事件都写入到自己的binlog中,只不过对于那些被忽略的事件只记录相关的事务号等信息,不记录事务的具体内容。所以,如果之前设置了被忽略的库或表,后来取消忽略后,它们在取消忽略以前的变化是不会再重放的,特别是基于gtid的复制会严格比较binlog中的gtid。
 
@@ -976,7 +976,7 @@ mysql> show slave hosts;
 6.4 多线程复制
 ---------
 
-在老版本中,只有一个SQL线程读取relay log并重放。重放的速度肯定比IO线程写relay log的速度慢非常多,导致SQL线程非常繁忙,且 **实现到从库上延迟较大** 。 **没错,多线程复制可以解决主从延迟问题,且使用得当的话效果非常的好(关于主从复制延迟,是生产环境下最常见的问题之一,且没有很好的办法来避免。后文稍微介绍了一点方法)** 。
+在老版本中,只有一个SQL线程读取relay log并重放。重放的速度肯定比IO线程写relay log的速度慢非常多,导致SQL线程非常繁忙,且 **实现到从库上延迟较大** 。**没错,多线程复制可以解决主从延迟问题,且使用得当的话效果非常的好(关于主从复制延迟,是生产环境下最常见的问题之一,且没有很好的办法来避免。后文稍微介绍了一点方法)** 。
 
 在MySQL 5.6中引入了多线程复制(multi-thread slave,简称MTS),这个 **多线程指的是多个SQL线程,IO线程还是只有一个** 。当IO线程将master binlog写入relay log中后,一个称为"多线程协调器(multithreaded slave coordinator)"会对多个SQL线程进行调度,让它们按照一定的规则去执行relay log中的事件。
 
@@ -1024,9 +1024,9 @@ mysql> show full processlist;
 
 是否还记得`show slave status`中的`Exec_master_log_pos`代表的意义?它表示SQL线程最近执行的事件对应的是master binlog中的哪个位置。问题由此而来。通过`show slave status`,我们看到已经执行事件对应的坐标,它前面可能还有事务没有执行。而在relay log中,事务B记录的位置是在事务A之后的(和master一样),于是事务A和事务B之间可能就存在一个孔洞(gap),这个孔洞是事务A剩余要执行的操作。
 
-正常情况下,多线程协调器记录了一切和多线程复制相关的内容,它能识别这种孔洞(通过打低水位标记low-watermark),也能正确填充孔洞。 **即使是在存在孔洞的情况下执行stop slave也不会有任何问题,因为在停止SQL线程之前,它会等待先把孔洞填充完** 。但危险因素太多,比如突然宕机、突然杀掉mysqld进程等等,这些都会导致孔洞持续下去,甚至可能因为操作不当而永久丢失这部分孔洞。
+正常情况下,多线程协调器记录了一切和多线程复制相关的内容,它能识别这种孔洞(通过打低水位标记low-watermark),也能正确填充孔洞。**即使是在存在孔洞的情况下执行stop slave也不会有任何问题,因为在停止SQL线程之前,它会等待先把孔洞填充完** 。但危险因素太多,比如突然宕机、突然杀掉mysqld进程等等,这些都会导致孔洞持续下去,甚至可能因为操作不当而永久丢失这部分孔洞。
 
-那么如何避免这种问题,出现这种问题如何解决? **1.如何避免gap。** 前面说了,多个SQL线程是通过协调器来调度的。默认情况下,可能会出现gap的情况,这是因为变量`slave_preserve_commit_order`的默认值为0。该变量指示协调器是否让每个SQL线程执行的事务按master binlog中的顺序提交。因此,将其设置为1,然后重启SQL线程即可保证SQL线程按序提交,也就不可能会有gap的出现。
+那么如何避免这种问题,出现这种问题如何解决?**1.如何避免gap。** 前面说了,多个SQL线程是通过协调器来调度的。默认情况下,可能会出现gap的情况,这是因为变量`slave_preserve_commit_order`的默认值为0。该变量指示协调器是否让每个SQL线程执行的事务按master binlog中的顺序提交。因此,将其设置为1,然后重启SQL线程即可保证SQL线程按序提交,也就不可能会有gap的出现。
 
 当事务B准备先于事务A提交的时候,它将一直等待。此时slave的状态将显示:
 ```
@@ -1140,7 +1140,7 @@ slave通过IO线程获取master的binlog,并通过SQL线程来应用获取到
 
 所以,第一个优化方式是不要在mysql中使用大事务,这是mysql主从优化的第一口诀。
 
-在回归正题,要解决slave的高延迟问题,先要知道`Second_Behind_Master`是如何计算延迟的:SQL线程比IO线程慢多少(其本质是NOW()减去`Exec_Master_Log_Pos`处设置的TIMESTAMP)。在主从网络状态良好的情况下,IO线程和master的binlog大多数时候都能保持一致(也即是IO线程没有多少延迟,除非事务非常大,导致二进制日志传输时间久, **但mysql优化的一个最基本口诀就是大事务切成小事务** ),所以在这种理想状态下,可以认为主从延迟说的是slave上的数据状态比master要延迟多少。它的计数单位是秒。
+在回归正题,要解决slave的高延迟问题,先要知道`Second_Behind_Master`是如何计算延迟的:SQL线程比IO线程慢多少(其本质是NOW()减去`Exec_Master_Log_Pos`处设置的TIMESTAMP)。在主从网络状态良好的情况下,IO线程和master的binlog大多数时候都能保持一致(也即是IO线程没有多少延迟,除非事务非常大,导致二进制日志传输时间久,**但mysql优化的一个最基本口诀就是大事务切成小事务** ),所以在这种理想状态下,可以认为主从延迟说的是slave上的数据状态比master要延迟多少。它的计数单位是秒。
 
 1. **从产生Binlog的master上考虑,可以在master上应用group commit的功能,并设置参数binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count,前者表示延迟多少秒才提交事务,后者表示要堆积多少个事务之后再提交。这样一来,事务的产生速度降低,slave的SQL线程相对就得到缓解** 。
 
diff --git "a/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266\345\215\212\345\220\214\346\255\245\345\244\215\345\210\266.md" "b/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266\345\215\212\345\220\214\346\255\245\345\244\215\345\210\266.md"
index b7662abc2..00fb3d890 100644
--- "a/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266\345\215\212\345\220\214\346\255\245\345\244\215\345\210\266.md"
+++ "b/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266\345\215\212\345\220\214\346\255\245\345\244\215\345\210\266.md"
@@ -203,7 +203,7 @@ mysql> show global variables like "%semi%";
 
 # 4.配置半同步复制
 
-需要注意的是,"半同步"是同步/异步类型的一种情况, **既可以实现半同步的传统复制,也可以实现半同步的GTID复制** 。其实半同步复制是基于异步复制的,它是在异步复制的基础上通过加载半同步插件的形式来实现半同步性的。
+需要注意的是,"半同步"是同步/异步类型的一种情况,**既可以实现半同步的传统复制,也可以实现半同步的GTID复制** 。其实半同步复制是基于异步复制的,它是在异步复制的基础上通过加载半同步插件的形式来实现半同步性的。
 
 此处以全新的环境进行配置,方便各位道友"依葫芦画瓢"。
 
@@ -563,4 +563,4 @@ mysql> show status like "%semi%";
 
 它先记录了当前slave2/slave3中已经同步到slave1的哪个位置。然后将Semi-sync复制切换为OFF状态,即降级为异步复制。
 
-在下次slave2或slave3启动IO线程时, **slave1将自动切换回半同步复制** ,并发送那些未被复制的binlog。
+在下次slave2或slave3启动IO线程时,**slave1将自动切换回半同步复制**,并发送那些未被复制的binlog。
diff --git "a/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266\345\237\272\344\272\216GTID\345\244\215\345\210\266.md" "b/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266\345\237\272\344\272\216GTID\345\244\215\345\210\266.md"
index e0a9dfdd3..121722a6f 100644
--- "a/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266\345\237\272\344\272\216GTID\345\244\215\345\210\266.md"
+++ "b/docs/Article/MySQL/MySQL\344\270\273\344\273\216\345\244\215\345\210\266\345\237\272\344\272\216GTID\345\244\215\345\210\266.md"
@@ -40,9 +40,9 @@ gtid在master和slave上是一直 **持久化保存** (即使删除了日志,
 
    注意,主从复制的情况下,sync-binlog基本上都会设置为1,这表示在每次提交事务时将缓存中的binlog刷盘。所以,在事务提交前,gtid以及事务相关操作的信息都在缓存中,提交后它们才写入到binlog file中,然后才会被dump线程dump出去。
 
-   换句话说, **只有提交了的事务,gtid和对应的事务操作才会记录到binlog文件中。记录的格式是先记录gtid,紧跟着再记录事务相关的操作。** 2.  当binlog传送到relay log中后,slave上的SQL线程首先读取该gtid,并设置变量 _gtid_next_ 的值为该gtid,表示下一个要操作的事务是该gtid。 _gtid_next_ **是基于会话的,不同会话的gtid_next不同。** 3.  随后slave检测该gtid在自己的binlog中是否存在。如果存在,则放弃此gtid事务;如果不存在,则将此gtid写入到 **自己的binlog中** ,然后立刻执行该事务,并在自己的binlog中记录该事务相关的操作。
+   换句话说,**只有提交了的事务,gtid和对应的事务操作才会记录到binlog文件中。记录的格式是先记录gtid,紧跟着再记录事务相关的操作。** 2.  当binlog传送到relay log中后,slave上的SQL线程首先读取该gtid,并设置变量 _gtid_next_ 的值为该gtid,表示下一个要操作的事务是该gtid。 _gtid_next_ **是基于会话的,不同会话的gtid_next不同。** 3.  随后slave检测该gtid在自己的binlog中是否存在。如果存在,则放弃此gtid事务;如果不存在,则将此gtid写入到 **自己的binlog中**,然后立刻执行该事务,并在自己的binlog中记录该事务相关的操作。
 
-   注意, **slave上replay的时候,gtid不是提交后才写到自己的binlog file的,而是判断gtid不存在后立即写入binlog file。** 通过这种在执行事务前先检查并写gtid到binlog的机制,不仅可以保证当前会话在此之前没有执行过该事务,还能保证没有其他会话读取了该gtid却没有提交。因为如果其他会话读取了该gtid会立即写入到binlog(不管是否已经开始执行事务),所以当前会话总能读取到binlog中的该gtid,于是当前会话就会放弃该事务。总之,一个gtid事务是决不允许多次执行、多个会话并行执行的。
+   注意,**slave上replay的时候,gtid不是提交后才写到自己的binlog file的,而是判断gtid不存在后立即写入binlog file。** 通过这种在执行事务前先检查并写gtid到binlog的机制,不仅可以保证当前会话在此之前没有执行过该事务,还能保证没有其他会话读取了该gtid却没有提交。因为如果其他会话读取了该gtid会立即写入到binlog(不管是否已经开始执行事务),所以当前会话总能读取到binlog中的该gtid,于是当前会话就会放弃该事务。总之,一个gtid事务是决不允许多次执行、多个会话并行执行的。
 
 1. slave在重放relay log中的事务时,不会自己生成gtid,所以所有的slave(无论是何种方式的一主一从或一主多从复制架构)通过重放relay log中事务获取的gtid都来源于master,并永久保存在slave上。
 
diff --git "a/docs/Article/MySQL/MySQL\344\272\213\345\212\241_\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253\350\257\246\350\247\243.md" "b/docs/Article/MySQL/MySQL\344\272\213\345\212\241_\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253\350\257\246\350\247\243.md"
index b557a38f8..1da5853b7 100644
--- "a/docs/Article/MySQL/MySQL\344\272\213\345\212\241_\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253\350\257\246\350\247\243.md"
+++ "b/docs/Article/MySQL/MySQL\344\272\213\345\212\241_\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253\350\257\246\350\247\243.md"
@@ -19,15 +19,15 @@
 >
 > ![这里写图片描述](../assets/20151216203147520)
 >
-> 此时如果连接A选择提交, **也就是commit操作。则连接B的数据也会发生变化。** >
-> 而如果连接A选择回滚, **也就是rollback操作。则连接A再次查询则发现数据还原。**
+> 此时如果连接A选择提交,**也就是commit操作。则连接B的数据也会发生变化。** >
+> 而如果连接A选择回滚,**也就是rollback操作。则连接A再次查询则发现数据还原。**
 
 ______________________________________________________________________
 
 ## 基本原理
 
-> 语法说完了,浮躁的人也不用继续看下去了。下面简单说一下事务的基本原理吧。 **提交,就会将结果持久化,不提交就不会。** >
-> 如果我们不开启事务,只执行一条sql,马上就会持久化数据,可以看出, **普通的执行就是立即提交。这是因为MySQL默认对sql语句的执行是自动提交的。** >
+> 语法说完了,浮躁的人也不用继续看下去了。下面简单说一下事务的基本原理吧。**提交,就会将结果持久化,不提交就不会。** >
+> 如果我们不开启事务,只执行一条sql,马上就会持久化数据,可以看出,**普通的执行就是立即提交。这是因为MySQL默认对sql语句的执行是自动提交的。** >
 > **也就是说,开启事务,实际上就是关闭了自动提交的功能,改成了commit手动提交!** >
 > 我们可以通过简单的对是否自动提交加以设置,完成开启事务的目的! 自动提交的特征是保存在服务的一个autocommit的变量内。可以进行修改:
 >
@@ -186,15 +186,15 @@ ______________________________________________________________________
 
 > **mysql> set tx_isolation = 0; (备注:0 - 3 对应数据库四种隔离级别)** Query OK, 0 rows affected (0.00 sec) **下面看看四种隔离级别的比较:** > Read Uncommitted(读取未提交内容)
 >
-> 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。 **读取未提交的数据,也被称之为脏读(Dirty Read)。** >
+> 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。**读取未提交的数据,也被称之为脏读(Dirty Read)。** >
 > Read Committed(读取提交内容)
 >
-> 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read), **因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。** >
+> 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),**因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。** >
 > Repeatable Read(可重读)
 >
-> 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。 **简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行** 。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
+> 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。**简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行** 。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
 >
-> Serializable(可串行化) 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之, **它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。**
+> Serializable(可串行化) 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,**它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。**
 
 隔离级别
 
diff --git "a/docs/Article/MySQL/MySQL\344\272\213\345\212\241\346\227\245\345\277\227(redolog\345\222\214undolog).md" "b/docs/Article/MySQL/MySQL\344\272\213\345\212\241\346\227\245\345\277\227(redolog\345\222\214undolog).md"
index 454a7713f..df7ccc225 100644
--- "a/docs/Article/MySQL/MySQL\344\272\213\345\212\241\346\227\245\345\277\227(redolog\345\222\214undolog).md"
+++ "b/docs/Article/MySQL/MySQL\344\272\213\345\212\241\346\227\245\345\277\227(redolog\345\222\214undolog).md"
@@ -10,9 +10,9 @@ undo log不是redo log的逆向过程,其实它们都算是用来恢复的日
 
 二进制日志相关内容,参考:[MariaDB/MySQL的二进制日志](https://www.cnblogs.com/f-ck-need-u/p/9001061.html#blog5)。
 
-redo log不是二进制日志。虽然二进制日志中也记录了innodb表的很多操作, **也能实现重做的功能,** 但是它们之间有很大区别。
+redo log不是二进制日志。虽然二进制日志中也记录了innodb表的很多操作,**也能实现重做的功能,** 但是它们之间有很大区别。
 
-1. 二进制日志是在 **存储引擎的上层** 产生的,不管是什么存储引擎,对数据库进行了修改都会产生二进制日志。而redo log是innodb层产生的,只记录该存储引擎中表的修改。 **并且二进制日志先于** redo log **被记录** 。具体的见后文group commit小结。
+1. 二进制日志是在 **存储引擎的上层** 产生的,不管是什么存储引擎,对数据库进行了修改都会产生二进制日志。而redo log是innodb层产生的,只记录该存储引擎中表的修改。**并且二进制日志先于** redo log **被记录** 。具体的见后文group commit小结。
 1. 二进制日志记录操作的方法是逻辑性的语句。即便它是基于行格式的记录方式,其本质也还是逻辑的SQL设置,如该行记录的每列的值是多少。而redo log是在物理格式上的日志,它记录的是数据库中每个页的修改。
 1. 二进制日志只在每次事务提交的时候一次性写入缓存中的日志"文件"(对于非事务表的操作,则是每次执行语句成功后就直接写入)。而redo log在数据准备修改前写入缓存中的redo log中,然后才对缓存中的数据执行修改操作;而且保证在发出事务提交指令时,先向缓存中的redo log写入日志,写入完成后才执行提交动作。
 1. 因为二进制日志只在提交的时候一次性写入,所以二进制日志中的记录方式和提交顺序有关,且一次提交对应一次记录。而redo log中是记录的物理页的修改,redo log文件中同一个事务可能多次记录,最后一个提交的事务记录会覆盖所有未提交的事务记录。例如事务T1,可能在redo log中记录了 T1-1,T1-2,T1-3,T1\* 共4个操作,其中 T1\* 表示最后提交时的日志记录,所以对应的数据页最终状态是 T1\* 对应的操作结果。而且redo log是并发写入的,不同事务之间的不同版本的记录会穿插写入到redo log文件中,例如可能redo log的记录方式如下: T1-1,T1-2,T2-1,T2-2,T2\*,T1-3,T1\* 。
@@ -106,7 +106,7 @@ Query OK, 0 rows affected (2.10 sec)
 
 ![img](../assets/733013-20180508105836098-1767966445.png)
 
-尽管设置为0和2可以大幅度提升插入性能,但是在故障的时候可能会丢失1秒钟数据,这1秒钟很可能有大量的数据,从上面的测试结果看,100W条记录也只消耗了20多秒,1秒钟大约有4W-5W条数据,尽管上述插入的数据简单,但却说明了数据丢失的大量性。 **更好的插入数据的做法是将值设置为** 1 **,然后修改存储过程,将每次循环都提交修改为只提交一次** ,**这样既能保证数据的一致性,也能提升性能,修改如下:
+尽管设置为0和2可以大幅度提升插入性能,但是在故障的时候可能会丢失1秒钟数据,这1秒钟很可能有大量的数据,从上面的测试结果看,100W条记录也只消耗了20多秒,1秒钟大约有4W-5W条数据,尽管上述插入的数据简单,但却说明了数据丢失的大量性。**更好的插入数据的做法是将值设置为** 1 **,然后修改存储过程,将每次循环都提交修改为只提交一次**,**这样既能保证数据的一致性,也能提升性能,修改如下:
 
 ```sql
 drop procedure if exists proc;
@@ -223,13 +223,13 @@ log buffer中未刷到磁盘的日志称为脏日志(dirty log)。
 
 上一节介绍了日志是何时刷到磁盘的,不仅仅是日志需要刷盘,脏数据页也一样需要刷盘。
 
-**在innodb中,数据刷盘的规则只有一个:checkpoint。** 但是触发checkpoint的情况却有几种。 **不管怎样,** checkpoint **触发后,会将buffer** 中脏数据页和脏日志页都刷到磁盘。**
+**在innodb中,数据刷盘的规则只有一个:checkpoint。** 但是触发checkpoint的情况却有几种。**不管怎样,** checkpoint **触发后,会将buffer** 中脏数据页和脏日志页都刷到磁盘。**
 
 innodb存储引擎中checkpoint分为两种:
 
 - sharp checkpoint:在重用redo log文件(例如切换日志文件)的时候,将所有已记录到redo log中对应的脏数据刷到磁盘。
 - fuzzy checkpoint:一次只刷一小部分的日志到磁盘,而非将所有脏日志刷盘。有以下几种情况会触发该检查点:
-  - master thread checkpoint:由master线程控制, **每秒或每10秒** 刷入一定比例的脏页到磁盘。
+  - master thread checkpoint:由master线程控制,**每秒或每10秒** 刷入一定比例的脏页到磁盘。
   - flush_lru_list checkpoint:从MySQL5.6开始可通过 innodb_page_cleaners 变量指定专门负责脏页刷盘的page cleaner线程的个数,该线程的目的是为了保证lru列表有可用的空闲页。
   - async/sync flush checkpoint:同步刷盘还是异步刷盘。例如还有非常多的脏页没刷到磁盘(非常多是多少,有比例控制),这时候会选择同步刷到磁盘,但这很少出现;如果脏页不是很多,可以选择异步刷到磁盘,如果脏页很少,可以暂时不刷脏页到磁盘
   - dirty page too much checkpoint:脏页太多时强制触发检查点,目的是为了保证缓存有足够的空闲空间。too much的比例由变量 innodb_max_dirty_pages_pct 控制,MySQL 5.6默认的值为75,即当脏页占缓冲池的百分之75后,就强制刷一部分脏页到磁盘。
@@ -361,11 +361,11 @@ undo log有两个作用:提供回滚和多个行版本控制(MVCC)。
 
 在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。
 
-undo log和redo log记录物理日志不一样,它是逻辑日志。 **可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。** 当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。 **undo log** 是采用段(segment) **的方式来记录的,每个undo** 操作在记录的时候占用一个undo log segment **。**
+undo log和redo log记录物理日志不一样,它是逻辑日志。**可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。** 当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。**undo log** 是采用段(segment) **的方式来记录的,每个undo** 操作在记录的时候占用一个undo log segment **。**
 
-## 另外, **undo log** 也会产生redo log **,因为undo log** 也要实现持久性保护。**2.2 undo log的存储方式
+## 另外,**undo log** 也会产生redo log **,因为undo log** 也要实现持久性保护。**2.2 undo log的存储方式
 
-innodb存储引擎对undo的管理采用段的方式。 **rollback segment** 称为回滚段,每个回滚段中有1024 **个undo log segment** 。**
+innodb存储引擎对undo的管理采用段的方式。**rollback segment** 称为回滚段,每个回滚段中有1024 **个undo log segment** 。**
 
 在以前老版本,只支持1个rollback segment,这样就只能记录1024个undo log segment。后来MySQL5.5可以支持128个rollback segment,即支持128\*1024个undo操作,还可以通过变量 innodb_undo_logs (5.6版本以前该变量是 innodb_rollback_segments )自定义多少个rollback segment,默认值为128。
 
diff --git "a/docs/Article/MySQL/MySQL\344\272\277\347\272\247\345\210\253\346\225\260\346\215\256\350\277\201\347\247\273\345\256\236\346\210\230\344\273\243\347\240\201\345\210\206\344\272\253.md" "b/docs/Article/MySQL/MySQL\344\272\277\347\272\247\345\210\253\346\225\260\346\215\256\350\277\201\347\247\273\345\256\236\346\210\230\344\273\243\347\240\201\345\210\206\344\272\253.md"
index d152be478..d0836042d 100644
--- "a/docs/Article/MySQL/MySQL\344\272\277\347\272\247\345\210\253\346\225\260\346\215\256\350\277\201\347\247\273\345\256\236\346\210\230\344\273\243\347\240\201\345\210\206\344\272\253.md"
+++ "b/docs/Article/MySQL/MySQL\344\272\277\347\272\247\345\210\253\346\225\260\346\215\256\350\277\201\347\247\273\345\256\236\346\210\230\344\273\243\347\240\201\345\210\206\344\272\253.md"
@@ -85,7 +85,7 @@ SpringApplication.run(BatchsrvApplication.class, args);
 }
 ```
 
-在启动类上添加注解 @EnableBatchProcessing。 **注意** :添加上了此注解,那么项目启动后 Spring Batch 就会自动化初始相关的数据库文件,并且直接启动相关的迁移任务。在实际的线上迁移中是不会直接上线后就自动执行任务的,通常会先通过手动执行来看下执行情况,再动态配置一些指标,所以我们的项目会禁用自动执行,并且手动执行 Spring batch 的数据库文件,数据库初始文件在 spring-batch-core 中,根据数据库进行选择即可。
+在启动类上添加注解 @EnableBatchProcessing。**注意** :添加上了此注解,那么项目启动后 Spring Batch 就会自动化初始相关的数据库文件,并且直接启动相关的迁移任务。在实际的线上迁移中是不会直接上线后就自动执行任务的,通常会先通过手动执行来看下执行情况,再动态配置一些指标,所以我们的项目会禁用自动执行,并且手动执行 Spring batch 的数据库文件,数据库初始文件在 spring-batch-core 中,根据数据库进行选择即可。
 禁用自动执行任务选项,`batch.job.enabled = false`。
 
 ```java
@@ -151,7 +151,7 @@ public class DataSourceConfig {
 ```
 
 此处我们使用多数据源的配置,并且使用 JdbcTemplate 进行数据的读取。
-主数据源中配置 Spring Batch 的数据库文件(如果不在意迁移的情况记录可以使用内存数据库,存储 Spring Batch 的迁移记录情况,这样能提高迁移性能,但是实际中不建议做,迁移性能的大幅提升会影响线上的业务正常运行,除非迁移有及时性这种需求再开启内存数据源)。 **如何定制 Spring Batch 的框架使用的数据源?** 从源码中找到:
+主数据源中配置 Spring Batch 的数据库文件(如果不在意迁移的情况记录可以使用内存数据库,存储 Spring Batch 的迁移记录情况,这样能提高迁移性能,但是实际中不建议做,迁移性能的大幅提升会影响线上的业务正常运行,除非迁移有及时性这种需求再开启内存数据源)。**如何定制 Spring Batch 的框架使用的数据源?** 从源码中找到:
 
 ```java
 public class BatchConfig extends DefaultBatchConfigurer
@@ -217,7 +217,7 @@ public void migratePayRecord() throws Exception {
 
 - 第二点可以看到我们读取数据使用的是 JdbcPagingItemReader 这个类,这个类的特点是可以通过分页的方式对数据记录进行读取,保证不会内存溢出,这里面 SortKey 是个很重要的点,一定要保证数据的唯一性,这个用于任务重新启动判断从哪再次开始。
 - PageSize 我们可以控制每一次读取多少条记录,这个用于调优用,根据实际的情况进行控制。SaveState 用于将迁移的记录情况进行记录。
-- 这个 Reader 的配置非常重要是保证亿级别数据不会发生内存溢出的关键配置。在这里的配置相当于我们对大数量级别的数据进行了一个分段处理。是分而治之的关键点。 **4. 写入的配置**
+- 这个 Reader 的配置非常重要是保证亿级别数据不会发生内存溢出的关键配置。在这里的配置相当于我们对大数量级别的数据进行了一个分段处理。是分而治之的关键点。**4. 写入的配置**
 
 ```java
 /**
@@ -272,7 +272,7 @@ public CompositeItemWriter compositePayRecordItemWriter(@Qualifier("d
 }
 ```
 
-写入的配置这里面使用了两个配置,一个是写入新的数据源,一个是删除原来的迁移数据连接的是原来的主数据源。写入器使用的是框架提供的 JdbcBatchItemWriter ,看名字就知道这个类的作用了,它支持批量的写入,性能非常高。Spring Batch 支持各种 Writer 的组合,通过 CompositeItemWriter 来实现。在这里只是为了演示组合 Writer 的用法,实际中最好分成两步来完成这种任务。 **5. Step 的配置** 
+写入的配置这里面使用了两个配置,一个是写入新的数据源,一个是删除原来的迁移数据连接的是原来的主数据源。写入器使用的是框架提供的 JdbcBatchItemWriter ,看名字就知道这个类的作用了,它支持批量的写入,性能非常高。Spring Batch 支持各种 Writer 的组合,通过 CompositeItemWriter 来实现。在这里只是为了演示组合 Writer 的用法,实际中最好分成两步来完成这种任务。**5. Step 的配置** 
 
 ```java
 @Bean
@@ -306,7 +306,7 @@ public class PayRecordExtProcessor implements ItemProcessor getJobNames();
 JobExecution abandon(long var1) throws NoSuchJobExecutionException, JobExecutionAlreadyRunningException;
 ```
 
-在 Spring Batch 的 Admin 管理系统中主要就是通过这个类来获取任务的执行情况。 **9. 总结** 通过上面的演示可以清楚的看到 Spring Batch 的执行逻辑,Spring Batch 定义了模板,我们在使用过程中只需要按照接口提供相应的数据来源和输出的接口即可。其他的事务处理,多线程池,批量处理,内存控制都由框架来完成。那么 Spring Batch 是怎么样保证在大数据量级的情况下,内存不溢出然后又保证性能的呢,下面用一张图进行展示 Spring Batch 是通过怎么的处理,保证在数据量巨大的情况下,高性能的进行数据的读写的。
+在 Spring Batch 的 Admin 管理系统中主要就是通过这个类来获取任务的执行情况。**9. 总结** 通过上面的演示可以清楚的看到 Spring Batch 的执行逻辑,Spring Batch 定义了模板,我们在使用过程中只需要按照接口提供相应的数据来源和输出的接口即可。其他的事务处理,多线程池,批量处理,内存控制都由框架来完成。那么 Spring Batch 是怎么样保证在大数据量级的情况下,内存不溢出然后又保证性能的呢,下面用一张图进行展示 Spring Batch 是通过怎么的处理,保证在数据量巨大的情况下,高性能的进行数据的读写的。
 ![在这里插入图片描述](../assets/d9d2ef90-2f8f-11ea-b7a2-bd62e8fb625b.png)
 
 #### 其他业务场景的扩展
 
-在实际中项目处理中,我们面对的系统情况可能并不是一个单表这样简单的业务情况,比如多表合一、一表分表为多表,在迁移过程中数据变更的情形。那么这些场景该如何处理呢? **1. 多表合一的场景** 在多表合一的场景下,实际上如何两张表有关联,那么附表上一定要建立索引。我们还是会让查询逻辑在主表上,千万不要使用 Join 联查,亿级别的数据如何联查,那么基础系统就要崩溃了。此时我们需要将需要拼装的业务逻辑放在 Processor 里面,比如在我们的案例场景下,需要将 User 的信息冗余到新的一个表中那么只需要将 UserService 注入到 PayRecordExtProcessor 中,然后再通过 `userService.getId (payRecord.getUserId ())` 这种方式去 load 用户信息,用户的 Id 肯定有索引,这样查询性能不会有太多的损耗。这也是现在大厂为啥要求在业务系统开发的过程中尽快不要使用 Join 联查的原因。
+在实际中项目处理中,我们面对的系统情况可能并不是一个单表这样简单的业务情况,比如多表合一、一表分表为多表,在迁移过程中数据变更的情形。那么这些场景该如何处理呢?**1. 多表合一的场景** 在多表合一的场景下,实际上如何两张表有关联,那么附表上一定要建立索引。我们还是会让查询逻辑在主表上,千万不要使用 Join 联查,亿级别的数据如何联查,那么基础系统就要崩溃了。此时我们需要将需要拼装的业务逻辑放在 Processor 里面,比如在我们的案例场景下,需要将 User 的信息冗余到新的一个表中那么只需要将 UserService 注入到 PayRecordExtProcessor 中,然后再通过 `userService.getId (payRecord.getUserId ())` 这种方式去 load 用户信息,用户的 Id 肯定有索引,这样查询性能不会有太多的损耗。这也是现在大厂为啥要求在业务系统开发的过程中尽快不要使用 Join 联查的原因。
 
 ```java
 @Component
@@ -480,7 +480,7 @@ return this.jobBuilderFactory.get("splitPayRecordJob")
 }
 ```
 
-在这里面我们定义了一个 ClassifierCompositeItemWriter 根据 Id % 表个数进行分片。具体的实现可以看代码。 **3. 迁移过程中数据有变更和增量的场景** 一般来说有单表性能的表数据,都有一个特点,随着时间的迁移会有大量的冷数据,上千万频繁变更的场景还是比较少的。如果想保证系统不间断运行,同时又能进行系统的迁移,迁移完成后再进行用户无感知的切换。需要考虑到迁移过程中有增量变更的情况。这种情况下我们一般引入 CDC (change data capture) 组件。比较常用的如阿里的 Canal、Debezium,这种中间件监听数据的变更。关于它们的使用可以去看官方的文档,非常的简单,添加好配置项就可以了。
+在这里面我们定义了一个 ClassifierCompositeItemWriter 根据 Id % 表个数进行分片。具体的实现可以看代码。**3. 迁移过程中数据有变更和增量的场景** 一般来说有单表性能的表数据,都有一个特点,随着时间的迁移会有大量的冷数据,上千万频繁变更的场景还是比较少的。如果想保证系统不间断运行,同时又能进行系统的迁移,迁移完成后再进行用户无感知的切换。需要考虑到迁移过程中有增量变更的情况。这种情况下我们一般引入 CDC (change data capture) 组件。比较常用的如阿里的 Canal、Debezium,这种中间件监听数据的变更。关于它们的使用可以去看官方的文档,非常的简单,添加好配置项就可以了。
 配置好后将变更的数据再导入的新的数据源。切换后完成再通过 Spring Batch 进行比对迁移的数据和变化数据就可以了。如果变化的量级特别大,最好选用后半夜,数据变更较小的情况下进行数据迁移。因为亿级别的数据实时同步这个是不可能的,找一个折中的方案即可。
 #### 如何控制部署的实例不同时运行
 为了保证服务的可靠性,通常我们部署的迁移服务实例是多个的,在这种情况下,我们需要保证数据不能迁移重复。在 Spring Batch 里面 Job 的概念相当于任务,还有一个概念叫 JobInstance。
diff --git "a/docs/Article/MySQL/MySQL\344\274\230\345\214\226\357\274\232\344\274\230\345\214\226 select count().md" "b/docs/Article/MySQL/MySQL\344\274\230\345\214\226\357\274\232\344\274\230\345\214\226 select count().md"
index d23e07cff..f01632d06 100644
--- "a/docs/Article/MySQL/MySQL\344\274\230\345\214\226\357\274\232\344\274\230\345\214\226 select count().md"	
+++ "b/docs/Article/MySQL/MySQL\344\274\230\345\214\226\357\274\232\344\274\230\345\214\226 select count().md"	
@@ -4,7 +4,7 @@
 
 某天,项目中业务人员反馈系统中某功能查询非常的慢,几乎等待了几十秒才有反应。于是联系了开发,开发人员调取日志,排查应用,定位 SQL,其实只是一个 `select count(*)`,这可急坏了开发人员,非常困惑,最简答的一个统计 SQL,为什么查询这么慢!不知道到如何优化与处理。
 
-**一个效果显著的简单操作** 在我知道这个问题,询问了开发人员哪张表后,做了一定分析后, **增加索引** ,无需开发做任何修改,性能得到大幅提升。为什么一个索引会有如此的性能提升,我先卖个关子,后面一一道来。
+**一个效果显著的简单操作** 在我知道这个问题,询问了开发人员哪张表后,做了一定分析后,**增加索引**,无需开发做任何修改,性能得到大幅提升。为什么一个索引会有如此的性能提升,我先卖个关子,后面一一道来。
 
 ### 问题复现,优化处理
 
diff --git "a/docs/Article/MySQL/MySQL\345\205\261\344\272\253\351\224\201\343\200\201\346\216\222\344\273\226\351\224\201\343\200\201\346\202\262\350\247\202\351\224\201\343\200\201\344\271\220\350\247\202\351\224\201.md" "b/docs/Article/MySQL/MySQL\345\205\261\344\272\253\351\224\201\343\200\201\346\216\222\344\273\226\351\224\201\343\200\201\346\202\262\350\247\202\351\224\201\343\200\201\344\271\220\350\247\202\351\224\201.md"
index aa1643555..efc74c7f4 100644
--- "a/docs/Article/MySQL/MySQL\345\205\261\344\272\253\351\224\201\343\200\201\346\216\222\344\273\226\351\224\201\343\200\201\346\202\262\350\247\202\351\224\201\343\200\201\344\271\220\350\247\202\351\224\201.md"
+++ "b/docs/Article/MySQL/MySQL\345\205\261\344\272\253\351\224\201\343\200\201\346\216\222\344\273\226\351\224\201\343\200\201\346\202\262\350\247\202\351\224\201\343\200\201\344\271\220\350\247\202\351\224\201.md"
@@ -26,9 +26,9 @@ MyISAM 操作数据都是使用的表锁,你更新一条记录就要锁整个
 
 在 Mysql 中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql 语句操作了主键索引,Mysql 就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
 
-InnoDB 行锁是通过给索引项加锁实现的,如果没有索引,InnoDB 会通过隐藏的聚簇索引来对记录加锁。也就是说:如果不通过索引条件检索数据,那么InnoDB将对表中所有数据加锁,实际效果跟表锁一样。因为没有了索引,找到某一条记录就得扫描全表,要扫描全表,就得锁定表。 **三、共享锁与排他锁**  1.首先说明:数据库的 **增删改** 操作默认都会加 **排他锁** ,而查询不会加任何锁。
+InnoDB 行锁是通过给索引项加锁实现的,如果没有索引,InnoDB 会通过隐藏的聚簇索引来对记录加锁。也就是说:如果不通过索引条件检索数据,那么InnoDB将对表中所有数据加锁,实际效果跟表锁一样。因为没有了索引,找到某一条记录就得扫描全表,要扫描全表,就得锁定表。**三、共享锁与排他锁**  1.首先说明:数据库的 **增删改** 操作默认都会加 **排他锁**,而查询不会加任何锁。
 
-mysql InnoDB引擎默认的修改数据语句, **update** , **delete** , **insert** 都会自动给涉及到的数据加上 **排他锁** , **select** 语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update语句,加共享锁可以使用select ... lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。
+mysql InnoDB引擎默认的修改数据语句,**update**,**delete**,**insert** 都会自动给涉及到的数据加上 **排他锁**,**select** 语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update语句,加共享锁可以使用select ... lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。
 
 **|--共享锁** :对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即 共享锁可多个共存),但无法修改。要想修改就必须等所有共享锁都释放完之后。语法为:
 
diff --git "a/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\344\272\213\345\212\241\345\222\214\351\224\201\347\232\204\351\235\242\347\272\261.md" "b/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\344\272\213\345\212\241\345\222\214\351\224\201\347\232\204\351\235\242\347\272\261.md"
index 91324d6e3..15458a555 100644
--- "a/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\344\272\213\345\212\241\345\222\214\351\224\201\347\232\204\351\235\242\347\272\261.md"
+++ "b/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\344\272\213\345\212\241\345\222\214\351\224\201\347\232\204\351\235\242\347\272\261.md"
@@ -1,10 +1,10 @@
 # MySQL 地基基础:事务和锁的面纱
 
-### 什么是事务,为什么需要事务
+## 什么是事务,为什么需要事务
 
 在 MySQL 中,事务是由一条或多条 SQL 组成的单位,在这个单位中所有的 SQL 共存亡,有点有福同享,有难同当的意思。要么全部成功,事务顺利完成;要么只要有一个 SQL 失败就会导致整个事务失败,所有已经做过的操作回退到原始数据状态。
 
-### 用日常细说事务的特性
+## 用日常细说事务的特性
 
 首先我们先说一下事务的四个特性:ACID。
 
@@ -44,7 +44,7 @@ update account set balance=balance+1000 where username='B';
 - **隔离性** :在这个事务发生的同时,发生了另一个事务(A 通过手机银行将钱全部转移到另外的账户,比如一共有 1500 元),第一个事务转 1000 元,第二个事务转 1500 元,我们仔细想想,如果都成功,那岂不是凭空获取了 1000 元,这是不合理的,每个事务在执行前都应查一下余额是否够本次转账的。这两个事务应该是隔离的,不能有冲突。
 - **持久性** :转账成功了(即事务完成),这代表钱已经发生了转移,这个时候发生 ATM 吞卡、ATM 断电、手机银行无法登陆等等一切故障,反正钱已经转走了,钱没有丢(即数据没有丢)
 
-### MySQL 并发控制技术
+## MySQL 并发控制技术
 
 并发控制技术可以说是数据库的底层基础技术,并发控制技术可以拆分来看,一是并发,一是控制。并发也就是说大量请求连接到数据库,控制就是数据库要控制好这些连接,保证资源的可用性、安全性,解决资源的挣用的问题。
 
@@ -58,7 +58,7 @@ update account set balance=balance+1000 where username='B';
 - Lock,并发连接到数据库,操作有读和读、读和写、写和写,锁来保证并发连接使得数据可以保持一致性。
 - MVCC(Multiversion Concurrency Control),多版本并发控制,是数据库的多版本,可以提高并发过程中的读和写操作,有效的避免写请求阻塞读请求。
 
-### 面试再也不怕被问到的 MVCC
+## 面试再也不怕被问到的 MVCC
 
 前面我们已经大致了解了 MVCC 是什么,以及他做什么事情,现在我们具体看看 MVCC 是如何工作的?
 
@@ -78,21 +78,9 @@ mysql> insert into tab1 values(1,'a','beijing',1);
 
 表中数据为:
 
-id
-
-name
-
-address
-
-status
-
-1
-
-a
-
-beijing
-
-1
+| id | name | address | status |
+|----|------|---------|--------|
+| 1  | a    | beijing | 1      |
 
 现在有一个请求,将数据 a 的地址改为 shanghai,这个数据更新的过程,我们细化一下,将历史数据置为失效,将新的数据插入:
 
@@ -103,29 +91,10 @@ mysql> insert into tab1 value(2,'a','shanghai',1);
 
 表中数据为:
 
-id
-
-name
-
-address
-
-status
-
-1
-
-a
-
-beijing
-
-0
-
-2
-
-a
-
-shanghai
-
-1
+| id | name | address  | status |
+|----|------|----------|--------|
+| 1  | a    | beijing  | 0      |
+| 2  | a    | shanghai | 1      |
 
 MVCC 的原理就类似是这样的,`address='beijing'` 就是历史数据,更新前保存了下来,`address='shanghai'` 就是当前数据,新插入数据,这样并发连接来了,既可以读取历史数据,也可以修改当前数据。比如,现在有三个事务:
 
@@ -137,49 +106,16 @@ T1 先获取了表中这一行数据,执行了 update,未提交;T2 获取
 
 以此类推,如果只对 `name='a'` 这一行数据有 N 个并发连接要做 M 个操作,这些历史数据都保存在表中,这个表的数据量无法预估,势必会造成压力与瓶颈。多版本数据到底如何保存,这就不是本节考虑的问题了,是数据库 undo 帮你做的工作。这里就不展开了。(后期可能会做 undo 相关的 chat,大家可以关注我)
 
-### 简单易懂的实例帮你理解 MySQL 事务隔离级别
+## 简单易懂的实例帮你理解 MySQL 事务隔离级别
 
 事务隔离级别,拆分来看,事务、隔离、级别,故是三个概念的集合,是保证事务之间相互隔离互不影响的,有多个级别。事务在执行过程中可能会出现脏读、不可重复读、幻读,那么 MySQL 的事务隔离级别到底有怎样的表现呢?
 
-事务隔离级别
-
-脏读
-
-不可重复读
-
-幻读
-
-读未提交(Read-Uncommited)
-
-可能
-
-可能
-
-可能
-
-读提交(Read-Commited)
-
-不可能
-
-可能
-
-可能
-
-可重复读交(Repeatable-Read)
-
-不可能
-
-不可能
-
-可能
-
-序列化(Serializable)
-
-不可能
-
-不可能
-
-不可能
+| 事务隔离级别                | 脏读   | 不可重复读 | 幻读   |
+|-----------------------------|--------|------------|--------|
+| 读未提交(Read-Uncommited)   | 可能   | 可能       | 可能   |
+| 读提交(Read-Commited)       | 不可能 | 可能       | 可能   |
+| 可重复读交(Repeatable-Read) | 不可能 | 不可能     | 可能   |
+| 序列化(Serializable)        | 不可能 | 不可能     | 不可能 |
 
 那么到底什么是脏读、不可重复读、幻读呢?
 
@@ -197,7 +133,7 @@ mysql> insert into t_account values('A',100);
 mysql> insert into t_account values('B',0);
 ```
 
-#### 读未提交
+### 读未提交
 
 设置事务隔离级别:
 
@@ -221,25 +157,20 @@ mysql>  SELECT @@tx_isolation;
 
 环境:用户 A 有 100 元钱,给用户 A 增加 100 元,然后用户 A 转账给用户 B。
 
-事务 1
-
-事务 2
-
-begin;
-
-begin;
-
-update t\_account set balance=balance+100 where name='A'; #给用户 A 增加 100 元
+|事务 1|事务 2|
+|--|--|
+|begin;|begin;|
+|update t\_account set balance=balance+100 where name='A'; #给用户 A 增加 100 元
 
 select balance from t\_account where name='A'; #转账前查询用户 A 余额为 200 元
 
-rollback; #决定不给用户 A 增加 100 元了,事务回滚
+rollback; #决定不给用户 A 增加 100 元了,事务回滚|
 
 update t\_account set balance=balance-200 where name='A'; #用户 A 继续给用户 B 转账,用户 A 减 200 元
 
-update t\_account set balance=balance+200 where name='B'; #用户 A 继续给用户 B 转账,用户加加 200 元
+update t\_account set balance=balance+200 where name='B'; #用户 A 继续给用户 B 转账,用户 B 加 200 元
 
-commit; #提交事务
+commit; #提交事务|
 
 现在我们查询一下用户 A 和用户 B 的余额:
 
@@ -256,11 +187,11 @@ mysql> select * from t_account;
 
 问题来了,这个结果不符合预期,用户 A 竟然是 -100 元,用户 B 增加了 200 元,这是因为事务 2 读取了事务 1 未提交的数据。
 
-#### 读提交
+### 读提交
 
 设置事务隔离级别:
 
-```plaintext
+```sql
 mysql> set global tx_isolation='read-committed';
 ```
 
@@ -298,7 +229,7 @@ select \* from t\_account where name='A'; #事务 2 查用户的余额,事务
 
 一个事务重新读取前面读取过的数据时,发现该数据已经被修改了,其实已被另一个已提交的事务操作了。
 
-#### 可重复读
+### 可重复读
 
 设置事务隔离级别:
 
@@ -350,7 +281,7 @@ select \* from t\_account where name='A'; #事务 2 查用户的余额,事务
 
 commit;
 
-select \* from t\_account where name='A'; ###事务 2 查用户的余额,为 200 元
+select \* from t\_account where name='A'; ##事务 2 查用户的余额,为 200 元
 
 为什么现在变成了 200 元了,因为事务 2 已经 commit,再次 select 是一个新的事务,读取数据当然又变为第一次获取数据(此时的数据是最新的数据)。
 
@@ -418,7 +349,7 @@ select \* from t\_account; #户 A 余额 100,用户 B 余额 200
 
 通过这两个例子你是不是了解了一个事务的 update 和 delete 操作了另外一个事务提交的数据,会使得这些数据在当前事务变得可见。就像幻影一下出现了!
 
-#### 序列化
+### 序列化
 
 设置事务隔离级别:
 
@@ -472,7 +403,7 @@ select \* from t\_account where name='A'; #用户 A 余额 200
 
 另外,结合前面说的 MVCC,Read-Committed 和 Repeatable-Read,支持 MVCC;Read-Uncommitted 由于可以读取未提交的数据,不支持 MVCC;Serializable 会对所有读取的数据加行锁,不支持 MVCC。
 
-### MySQL 锁机制(机智)
+## MySQL 锁机制(机智)
 
 锁是可以协调并发连接访问 MySQL 数据库资源的一种技术,可以保证数据的一致性。锁有两个阶段:加锁和解锁,InnoDB 引擎的锁主要有两类。
 
@@ -505,7 +436,7 @@ MySQL 在数据库内部自动管理,协调并发连接的资源争用。内
 
 我们举个例子来描述一下这个过程吧,比如有事务 1 和事务 2,事务 1 锁定了一行数据,加了一个 S 锁;事务 2 想要对整个表加锁,需要判断这个表是否被加了表锁,表中的每一行是否有行锁。仔细想想这个过程是很快呢?还是非常的慢?如果表很小无所谓了,如果表是海量级数据,那糟了,事务 2 势必耗费很多资源。
 
-如何解决事务 2 这种检索资源消耗的问题呢?事务意向锁帮你先获取意向,先一步问问情况,然后再获取我们想要的 S 和 X 锁,具体分为: **意向共享锁(IS)** 事务 1 说:我要加一个行锁,我有这个意向,你们其他人有没有意见,如果没有我就先拿这个 IS 锁了。 **意向排它锁(IX)**
+如何解决事务 2 这种检索资源消耗的问题呢?事务意向锁帮你先获取意向,先一步问问情况,然后再获取我们想要的 S 和 X 锁,具体分为: **意向共享锁(IS)** 事务 1 说:我要加一个行锁,我有这个意向,你们其他人有没有意见,如果没有我就先拿这个 IS 锁了。**意向排它锁(IX)**
 
 事务 2 说:我要加一个表锁,这个可是排他锁,我拿了你们就等我用完再说吧,我有这个意向,你们其他人有没有意见,如果没有我就先拿这个 IX 锁了。
 
@@ -564,13 +495,13 @@ IS
 
 兼容
 
-### 聊几个经典死锁案例
+## 聊几个经典死锁案例
 
 在实际应用中经常发生数据库死锁的情况,那么什么是死锁呢?说白了就是事务 1 锁事务 2,事务 2 锁事务 1,这两个事务都在等着对方释放锁资源,陷入了死循环。
 
 接下来我们介绍几个经典死锁案例,MySQL 默认级别使用的是 REPEATABLE-READ。
 
-#### 场景 1:insert 死锁
+### 场景 1:insert 死锁
 
 创建一个测试表:
 
@@ -612,11 +543,11 @@ ERROR 1213 (40001): ==Deadlock== found when trying to get lock; try restarting t
 
 session1 在插入(1,101) 的时候会加一个 X 锁;session2 插入(2,101),no 字段有着唯一性,故 session2 在插入时数据库会做 duplicate 冲突检测,由于事务冲突先加 S 锁;然后 session1 又插入了 (3,100),此时 session1 会加 insert intention X 锁(插入意向锁),之前 session1 已经有了 X 锁,故进入等待队列,结局就是 session1 和 session2 都在等待,陷入了僵局,MySQL 很机智,牺牲一方事务解决这个尴尬的局面,所以 session2 被干掉了,报错死锁。
 
-#### 场景 2:自增列死锁
+### 场景 2:自增列死锁
 
 自增列死锁问题和场景 1 的类似,比如将场景 1 的主键属性改为自增长属性,主键自增仍唯一,场景模拟类似,加锁的过程也类似,产生死锁的过程也类似,这里就不详细模拟了。
 
-#### 场景 3:rollback 死锁
+### 场景 3:rollback 死锁
 
 创建一个测试表:
 
@@ -665,7 +596,7 @@ ERROR 1213 (40001): ==Deadlock== found when trying to get lock; try restarting t
 
 session1 在插入 (1,100) 的时候会加一个 X 锁;session2 插入 (2,100),no 字段有着唯一性,故 session2 在插入时数据库会做 duplicate 冲突检测,由于事务冲突先加 S 锁;session3 插入 (3,100),no 字段有着唯一性,故 session3 在插入时数据库会做 duplicate 冲突检测,由于事务冲突先加 S 锁;session1 回滚,session2 申请 insert intention X 锁,等 session3;session3 申请 insert intention X 锁,等 session2,结局就是 session2 和 session3 都在等待,陷入了僵局,MySQL 很机智,牺牲一方事务解决这个尴尬的局面,所以 session3 被干掉了,报错死锁。
 
-#### 场景 4:commit 死锁
+### 场景 4:commit 死锁
 
 创建一个测试表:
 
@@ -715,7 +646,7 @@ ERROR 1213 (40001): ==Deadlock== found when trying to get lock; try restarting t
 
 这个产生死锁的过程和场景 3rollback 死锁类似,大家可以和之前的 rollback 死锁产生过程对应来看。
 
-### 小技巧——事务保存点帮你读档重闯关
+## 小技巧——事务保存点帮你读档重闯关
 
 玩游戏你是不是有过存档、读档的经历,过某一个比较难的关卡,先存档,过不了,就读档重新过。数据库中我们也可以如此,MySQL 事务保存点可以回滚到事务的某时间点,并且不用中止事务。下面举例说明一下。
 
@@ -779,7 +710,7 @@ Empty set (0.00 sec)
 
 结果:用户 A 成功转 100 元给用户 B,用户 C 果然不存才,设置了保存点,帮我们省了很多工作,中途不用取消全部操作。
 
-### 小技巧——一个死锁的具体分析方法
+## 小技巧——一个死锁的具体分析方法
 
 前面我们学习了事务、锁,以及介绍了几个经典死锁案例,当遇到死锁,我们怎样具体分析呢?
 
@@ -847,13 +778,13 @@ Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 
 通过这些日志,我们发现日志中的事务 1,持有 S 锁,S 锁的出现是因为需要检查数据唯一性,我们的 no 字段确实有唯一索引,这一点也正好验证了。日志中的事务 1,持有一个 X 锁,又等待一个 X 锁。所以场景 1 中的两个事务都在锁等,造成了死锁。
 
-### 小技巧——换种思路提高事务能力
+## 小技巧——换种思路提高事务能力
 
 在数据中如果是单一事务,那没的说,一个一个的事务来执行,毫无压力。现实是不允许这样的,肯定是有大量的并发连接,并发事务在所难免。如果高并发的环境中,事务处理效率肯定大幅下降,这个时候我们有没有方法提高并发事务能力呢?
 
 我们解决技术处理问题的限制,这次我们换一种思路来提高事务能力。比如:
 
-**合理的在线、离线数据库** 比如我们的系统数据量日益增加,还有一些业务需要查询大量的数据,我们可以改造系统为在线、离线数据库,在线表提供高效事务能力,离线表提供数据查询服务,互不影响。 **提高 delete 操作效率的思考**
+**合理的在线、离线数据库** 比如我们的系统数据量日益增加,还有一些业务需要查询大量的数据,我们可以改造系统为在线、离线数据库,在线表提供高效事务能力,离线表提供数据查询服务,互不影响。**提高 delete 操作效率的思考**
 
 如果你对表有大量数据的 delete 操作,比如定期的按日、月、年删除数据,可以设计表为日表、月表、年表亦或是相对应的分区表,这样清理数据会由大事务降低为小事务。
 ```
diff --git "a/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\346\225\260\346\215\256\345\255\227\345\205\270.md" "b/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\346\225\260\346\215\256\345\255\227\345\205\270.md"
index bb17e1f26..b6fb8fa60 100644
--- "a/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\346\225\260\346\215\256\345\255\227\345\205\270.md"
+++ "b/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\346\225\260\346\215\256\345\255\227\345\205\270.md"
@@ -741,7 +741,7 @@ Query OK, 1 row affected (0.02 sec)
 Rows matched: 1  Changed: 1  Warnings: 0
 ```
 
-**sysconfiginsertsetuser** 当对 sys.sys_config 表做 insert 操作时,该触发器会将 sys_config 表的 set_by 列设置为当前用户名。 **sysconfigupdatesetuser**
+**sysconfiginsertsetuser** 当对 sys.sys_config 表做 insert 操作时,该触发器会将 sys_config 表的 set_by 列设置为当前用户名。**sysconfigupdatesetuser**
 
 当对 sys.sys_config 表做 insert 操作时,该触发器会将 sys_config 表的 set_by 列设置为当前用户名。
 
diff --git "a/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\346\225\260\346\215\256\345\272\223\345\255\227\347\254\246\351\233\206.md" "b/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\346\225\260\346\215\256\345\272\223\345\255\227\347\254\246\351\233\206.md"
index 0ff272445..583632374 100644
--- "a/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\346\225\260\346\215\256\345\272\223\345\255\227\347\254\246\351\233\206.md"
+++ "b/docs/Article/MySQL/MySQL\345\234\260\345\237\272\345\237\272\347\241\200\357\274\232\346\225\260\346\215\256\345\272\223\345\255\227\347\254\246\351\233\206.md"
@@ -265,19 +265,19 @@ create table tab1(column1 varchar(5) character set utf8 collate utf8_bin);
 *   如果指定了字符集未指定校对规则,则使用指定字符集和默认校对规则
 *   如果未指定字符集和校对规则,则使用表字符集和校对规则
 
-**4. 如何处理带数据的字符集** 当表中已经存在数据,直接更改字符集,不会更改既有的数据字符集,我们需要先将数据导出,调整字符集再导入。 **第一步:导出表结构** 
+**4. 如何处理带数据的字符集** 当表中已经存在数据,直接更改字符集,不会更改既有的数据字符集,我们需要先将数据导出,调整字符集再导入。**第一步:导出表结构** 
 
 ```plaintext
 mysqldump -uroot -p --default-character-set=gbk -d db1> createtab.sql
 ```
 
-**第二步:修改表字符集** 编辑修改 createtab.sql 文件,将表结构定义中的字符集改为新的字符集。 **第三步:导出所有数据** 
+**第二步:修改表字符集** 编辑修改 createtab.sql 文件,将表结构定义中的字符集改为新的字符集。**第三步:导出所有数据** 
 
 ```plaintext
 mysqldump -uroot -p --quick --no-create-info --extended-insert --default-character-set=latin1 db1> data.sql
 ```
 
-**第四步:修改数据字符集** 编辑修改 data.sql,将 set names latin1 修改成 set names gbk。 **第五步:创建数据库** 
+**第四步:修改数据字符集** 编辑修改 data.sql,将 set names latin1 修改成 set names gbk。**第五步:创建数据库** 
 
 ```plaintext
 create database db1 default charset gbk;
diff --git "a/docs/Article/MySQL/MySQL\346\200\247\350\203\275\344\274\230\345\214\226\357\274\232\347\242\216\347\211\207\346\225\264\347\220\206.md" "b/docs/Article/MySQL/MySQL\346\200\247\350\203\275\344\274\230\345\214\226\357\274\232\347\242\216\347\211\207\346\225\264\347\220\206.md"
index bf004ea12..2c7572fee 100644
--- "a/docs/Article/MySQL/MySQL\346\200\247\350\203\275\344\274\230\345\214\226\357\274\232\347\242\216\347\211\207\346\225\264\347\220\206.md"
+++ "b/docs/Article/MySQL/MySQL\346\200\247\350\203\275\344\274\230\345\214\226\357\274\232\347\242\216\347\211\207\346\225\264\347\220\206.md"
@@ -6,11 +6,11 @@ MySQL 碎片就是 MySQL 数据文件中一些不连续的空白空间,这些
 
 ### 碎片是如何产生的
 
-**delete 操作** 在 MySQL 中删除数据,在存储中就会产生空白的空间,当有新数据插入时,MySQL 会试着在这些空白空间中保存新数据,但是呢总是用不满这些空白空间。所以日积月累,亦或是一下有大量的 delete 操作,一下就会有大量的空白空间,慢慢的会大到比表的数据使用的空间还大。 **update 操作** 在 MySQL 中更新数据,在可变长度的字段(比如 varchar)中更新数据,innodb 表存储数据的单位是页,update 操作会造成页分裂,分裂以后存储变的不连续,不规则,从而产生碎片。比如说原始字段长度 varchar(100),我们大量的更新数据长度位为 50,这样的话,有 50 的空间被空白了,新入库的数据不能完全利用剩余的 50,这就会产生碎片。
+**delete 操作** 在 MySQL 中删除数据,在存储中就会产生空白的空间,当有新数据插入时,MySQL 会试着在这些空白空间中保存新数据,但是呢总是用不满这些空白空间。所以日积月累,亦或是一下有大量的 delete 操作,一下就会有大量的空白空间,慢慢的会大到比表的数据使用的空间还大。**update 操作** 在 MySQL 中更新数据,在可变长度的字段(比如 varchar)中更新数据,innodb 表存储数据的单位是页,update 操作会造成页分裂,分裂以后存储变的不连续,不规则,从而产生碎片。比如说原始字段长度 varchar(100),我们大量的更新数据长度位为 50,这样的话,有 50 的空间被空白了,新入库的数据不能完全利用剩余的 50,这就会产生碎片。
 
 ### 碎片到底产生了什么影响
 
-MySQL 既然产生了碎片,你可能比较豪横说磁盘空间够大,浪费空间也没事,但是这些碎片也会产生性能问题,碎片会有什么影响呢? **空间浪费** 空间浪费不用多说,碎片占用了大量可用空间。 **读写性能下降**
+MySQL 既然产生了碎片,你可能比较豪横说磁盘空间够大,浪费空间也没事,但是这些碎片也会产生性能问题,碎片会有什么影响呢?**空间浪费** 空间浪费不用多说,碎片占用了大量可用空间。**读写性能下降**
 
 由于存在大量碎片,数据从连续规则的存储方式变为随机分散的存储方式,磁盘 IO 会变的繁忙,数据库读写性能就会下降。
 
@@ -134,14 +134,14 @@ where t.table_schema = 'employees';
 
 根据结果显示,data\_free 列数据就是我们要查询的表的碎片大小内容,是 4M。
 ### 如何清理碎片
-找到表碎片了,我们如何清理呢?有两种方法。 **1. 分析表** 命令:
+找到表碎片了,我们如何清理呢?有两种方法。**1. 分析表** 命令:
 
 ```plaintext
 optimize table table_name;
 ```
 
 这个方法主要针对 MyISAM 引擎表使用,因为 MyISAM 表的数据和索引是分离的,optimize 表可以整理数据文件,重新排列索引。
-注意:optimize 会锁表,时间长短依据表数据量的大小。 **2. 重建表引擎** 命令:
+注意:optimize 会锁表,时间长短依据表数据量的大小。**2. 重建表引擎** 命令:
 
 ```sql
 alter table table_name engine = innodb;
@@ -187,5 +187,5 @@ mysql> select count(*) from salaries;
 1 row in set (0.16 sec)
 ```
 
-速度还是提高了不少,清理碎片后提高了查询速度。 **总结一下** :清理表的碎片可以提高 MySQL 性能,在日常工作中我们可以定期执行表碎片整理,从而提高 MySQL 性能。
+速度还是提高了不少,清理碎片后提高了查询速度。**总结一下** :清理表的碎片可以提高 MySQL 性能,在日常工作中我们可以定期执行表碎片整理,从而提高 MySQL 性能。
 ```
diff --git "a/docs/Article/MySQL/MySQL\346\225\205\351\232\234\350\257\212\346\226\255\357\274\232\346\225\231\344\275\240\345\277\253\351\200\237\345\256\232\344\275\215\345\212\240\351\224\201\347\232\204SQL.md" "b/docs/Article/MySQL/MySQL\346\225\205\351\232\234\350\257\212\346\226\255\357\274\232\346\225\231\344\275\240\345\277\253\351\200\237\345\256\232\344\275\215\345\212\240\351\224\201\347\232\204SQL.md"
index d7cddf450..1d7ef1d7b 100644
--- "a/docs/Article/MySQL/MySQL\346\225\205\351\232\234\350\257\212\346\226\255\357\274\232\346\225\231\344\275\240\345\277\253\351\200\237\345\256\232\344\275\215\345\212\240\351\224\201\347\232\204SQL.md"
+++ "b/docs/Article/MySQL/MySQL\346\225\205\351\232\234\350\257\212\346\226\255\357\274\232\346\225\231\344\275\240\345\277\253\351\200\237\345\256\232\344\275\215\345\212\240\351\224\201\347\232\204SQL.md"
@@ -12,7 +12,7 @@
 
 在实际应用中你肯定遇到过锁问题,这个锁的威力很大,那出现了数据库锁,会造成什么影响呢?
 
-**锁等待** 一个连接申请了锁资源,其他连接要申请资源,无法获取,等待资源释放。 **死锁**
+**锁等待** 一个连接申请了锁资源,其他连接要申请资源,无法获取,等待资源释放。**死锁**
 
 你锁我,我也锁你,大家一起锁着吧。
 
@@ -109,7 +109,7 @@ mysql> select trx_id,trx_started,trx_requested_lock_id,trx_query,trx_mysql_threa
 2 rows in set (0.01 sec)
 ```
 
-结果有两个事务,MySQL 事务线程 id 为 38 和 41,很直观的看到 41 是我们的 delete 事务,被 38 锁定。 **定位线程** 
+结果有两个事务,MySQL 事务线程 id 为 38 和 41,很直观的看到 41 是我们的 delete 事务,被 38 锁定。**定位线程** 
 
 ```sql
 mysql> select * from performance_schema.threads where processlist_id=38;
@@ -121,7 +121,7 @@ mysql> select * from performance_schema.threads where processlist_id=38;
 1 row in set (0.00 sec)
 ```
 
-结果找到 MySQL 事务线程 38 对应的服务器线程 63。 **定位加锁 SQL** 
+结果找到 MySQL 事务线程 38 对应的服务器线程 63。**定位加锁 SQL** 
 
 ```sql
 mysql> select * from performance_schema.events_statements_current where thread_id=63;
@@ -133,5 +133,5 @@ mysql> select * from performance_schema.events_statements_current where thread_i
 1 row in set (0.00 sec)
 ```
 
-结果中我们找到了加锁的 update 的 SQL 语句。 **总结** 在 MySQL 数据库中出现了锁,不要着急,我们通过这个方法可以快速定位加锁的 SQL,你学会了吗?
+结果中我们找到了加锁的 update 的 SQL 语句。**总结** 在 MySQL 数据库中出现了锁,不要着急,我们通过这个方法可以快速定位加锁的 SQL,你学会了吗?
 ```
diff --git "a/docs/Article/MySQL/MySQL\346\227\245\345\277\227\350\257\246\350\247\243.md" "b/docs/Article/MySQL/MySQL\346\227\245\345\277\227\350\257\246\350\247\243.md"
index 3502d00f0..c439856fd 100644
--- "a/docs/Article/MySQL/MySQL\346\227\245\345\277\227\350\257\246\350\247\243.md"
+++ "b/docs/Article/MySQL/MySQL\346\227\245\345\277\227\350\257\246\350\247\243.md"
@@ -68,7 +68,7 @@ Version: '5.6.35'  socket: '/mydata/data/mysql.sock'  port: 3306  MySQL Communit
 
 # 3.一般查询日志
 
-查询日志分为一般查询日志和慢查询日志,它们是通过查询是否超出变量 long_query_time 指定时间的值来判定的。在超时时间内完成的查询是一般查询,可以将其记录到一般查询日志中, **但是建议关闭这种日志(默认是关闭的)** ,超出时间的查询是慢查询,可以将其记录到慢查询日志中。
+查询日志分为一般查询日志和慢查询日志,它们是通过查询是否超出变量 long_query_time 指定时间的值来判定的。在超时时间内完成的查询是一般查询,可以将其记录到一般查询日志中,**但是建议关闭这种日志(默认是关闭的)**,超出时间的查询是慢查询,可以将其记录到慢查询日志中。
 
 使用" --general_log={0|1} "来决定是否启用一般查询日志,使用" --general_log_file=file_name "来指定查询日志的路径。不给定路径时默认的文件名以 `hostname`.log 命名。
 
@@ -230,7 +230,7 @@ Count: 1  Time=10.00s (10s)  Lock=0.00s (0s)  Rows=1.0 (1), root[root]@localhost
 
 二进制日志包含了 **引起或可能引起数据库改变** (如delete语句但没有匹配行)的事件信息,但绝不会包括select和show这样的查询语句。语句以"事件"的形式保存,所以包含了时间、事件开始和结束位置等信息。
 
-二进制日志是 **以事件形式记录的,不是事务日志**  **(**  **但可能是基于事务来记录二进制日志)** ,不代表它只记录innodb日志,myisam表也一样有二进制日志。
+二进制日志是 **以事件形式记录的,不是事务日志**  **(**  **但可能是基于事务来记录二进制日志)**,不代表它只记录innodb日志,myisam表也一样有二进制日志。
 
 对于事务表的操作,二进制日志 **只在事务提交的时候一次性写入**  **(**  **基于事务的innodb**  **二进制日志),提交前的每个二进制日志记录都先cache,提交时写入** 。
 
@@ -246,13 +246,13 @@ MariaDB/MySQL默认没有启动二进制日志,要启用二进制日志使用
 `[mysqld]``# server_id=1234``log-bin=[on|filename]`
 ```
 
-mysqld还 **创建一个二进制日志索引文件** ,当二进制日志文件滚动的时候会向该文件中写入对应的信息。所以该文件包含所有使用的二进制日志文件的文件名。默认情况下该文件与二进制日志文件的文件名相同,扩展名为'.index'。要指定该文件的文件名使用 --log-bin-index\[=file_name\] 选项。当mysqld在运行时不应手动编辑该文件,免得mysqld变得混乱。
+mysqld还 **创建一个二进制日志索引文件**,当二进制日志文件滚动的时候会向该文件中写入对应的信息。所以该文件包含所有使用的二进制日志文件的文件名。默认情况下该文件与二进制日志文件的文件名相同,扩展名为'.index'。要指定该文件的文件名使用 --log-bin-index\[=file_name\] 选项。当mysqld在运行时不应手动编辑该文件,免得mysqld变得混乱。
 
 当重启mysql服务或刷新日志或者达到日志最大值时,将滚动二进制日志文件,滚动日志时只修改日志文件名的数字序列部分。
 
 二进制日志文件的最大值通过变量 max_binlog_size 设置(默认值为1G)。但由于二进制日志可能是基于事务来记录的(如innodb表类型),而事务是绝对不可能也不应该跨文件记录的,如果正好二进制日志文件达到了最大值但事务还没有提交则不会滚动日志,而是继续增大日志,所以 max_binlog_size 指定的值和实际的二进制日志大小不一定相等。
 
-因为二进制日志文件增长迅速,但官方说明因此而损耗的性能小于1%,且二进制目的是为了恢复定点数据库和主从复制,所以出于安全和功能考虑, **极不建议将二进制日志和**  **datadir**  **放在同一磁盘上** 。
+因为二进制日志文件增长迅速,但官方说明因此而损耗的性能小于1%,且二进制目的是为了恢复定点数据库和主从复制,所以出于安全和功能考虑,**极不建议将二进制日志和**  **datadir**  **放在同一磁盘上** 。
 
 ## 5.2 查看二进制日志
 
@@ -738,7 +738,7 @@ gPraWB4BAAAAOAAAADoBAAAAAF4AAAAAAAEAAgAE//AHAAAACXhpYW93b25pdQGZnDqBmCz35ow=
 - binlog\_format = { mixed | row | statement } #指定二进制日志基于什么模式记录
 - binlog\_rows\_query\_log\_events = { 1|0 } # MySQL5.6.2添加了该变量,当binlog format为row时,默认不会记录row对应的SQL语句,设置为1或其他true布尔值时会记录,但需要使用mysqlbinlog -v查看,这些语句是被注释的,恢复时不会被执行。
 - max\_binlog\_size = #指定二进制日志文件最大值,超出指定值将自动滚动。但由于事务不会跨文件,所以并不一定总是精确。
-- binlog\_cache\_size = 32768 # **基于事务类型的日志会先记录在缓冲区** ,当达到该缓冲大小时这些日志会写入磁盘
+- binlog\_cache\_size = 32768 # **基于事务类型的日志会先记录在缓冲区**,当达到该缓冲大小时这些日志会写入磁盘
 - max\_binlog\_cache\_size = #指定二进制日志缓存最大大小,硬限制。默认4G,够大了,建议不要改
 - binlog\_cache\_use:使用缓存写二进制日志的次数(这是一个实时变化的统计值)
 - binlog\_cache\_disk\_use:使用临时文件写二进制日志的次数,当日志超过了binlog\_cache\_size的时候会使用临时文件写日志,如果该变量值不为0,则考虑增大binlog\_cache\_size的值
@@ -747,7 +747,7 @@ gPraWB4BAAAAOAAAADoBAAAAAF4AAAAAAAEAAgAE//AHAAAACXhpYW93b25pdQGZnDqBmCz35ow=
 - binlog\_stmt\_cache\_disk\_use: 使用临时文件写二进制日志的次数,当日志超过了binlog\_cache\_size的时候会使用临时文件写日志,如果该变量值不为0,则考虑增大binlog\_cache\_size的值
 - sync\_binlog = { 0 | n } #这个参数直接影响mysql的性能和完整性
   - sync\_binlog=0:不同步,日志何时刷到磁盘由FileSystem决定,这个性能最好。
-  - sync\_binlog=n:每写n次事务(注意,对于非事务表来说,是n次事件,对于事务表来说,是n次事务,而一个事务里可能包含多个二进制事件),MySQL将执行一次磁盘同步指令fdatasync()将缓存日志刷新到磁盘日志文件中。Mysql中默认的设置是sync\_binlog=0,即不同步,这时性能最好,但风险最大。一旦系统奔溃,缓存中的日志都会丢失。 **在innodb的主从复制结构中,如果启用了二进制日志(几乎都会启用),要保证事务的一致性和持久性的时候,必须将sync_binlog的值设置为1,因为每次事务提交都会写入二进制日志,设置为1就保证了每次事务提交时二进制日志都会写入到磁盘中,从而立即被从服务器复制过去。** 5.6 二进制日志定点还原数据库
+  - sync\_binlog=n:每写n次事务(注意,对于非事务表来说,是n次事件,对于事务表来说,是n次事务,而一个事务里可能包含多个二进制事件),MySQL将执行一次磁盘同步指令fdatasync()将缓存日志刷新到磁盘日志文件中。Mysql中默认的设置是sync\_binlog=0,即不同步,这时性能最好,但风险最大。一旦系统奔溃,缓存中的日志都会丢失。**在innodb的主从复制结构中,如果启用了二进制日志(几乎都会启用),要保证事务的一致性和持久性的时候,必须将sync_binlog的值设置为1,因为每次事务提交都会写入二进制日志,设置为1就保证了每次事务提交时二进制日志都会写入到磁盘中,从而立即被从服务器复制过去。** 5.6 二进制日志定点还原数据库
 
 ----------------
 只需指定二进制日志的起始位置(可指定终止位置)并将其保存到sql文件中,由mysql命令来载入恢复即可。当然直接通过管道送给mysql命令也可。
diff --git "a/docs/Article/MySQL/MySQL\347\232\204MVCC\357\274\210\345\244\232\347\211\210\346\234\254\345\271\266\345\217\221\346\216\247\345\210\266\357\274\211.md" "b/docs/Article/MySQL/MySQL\347\232\204MVCC\357\274\210\345\244\232\347\211\210\346\234\254\345\271\266\345\217\221\346\216\247\345\210\266\357\274\211.md"
index e498076ca..db8e688c4 100644
--- "a/docs/Article/MySQL/MySQL\347\232\204MVCC\357\274\210\345\244\232\347\211\210\346\234\254\345\271\266\345\217\221\346\216\247\345\210\266\357\274\211.md"
+++ "b/docs/Article/MySQL/MySQL\347\232\204MVCC\357\274\210\345\244\232\347\211\210\346\234\254\345\271\266\345\217\221\346\216\247\345\210\266\357\274\211.md"
@@ -3,7 +3,7 @@
 **1 什么是MVCC** =============
 
 ```plaintext
-  MVCC全称是: **Multiversion concurrency control** ,多版本并发控制,提供并发访问数据库时,对事务内读取的到的内存做处理,用来避免写操作堵塞读操作的并发问题。
+  MVCC全称是: **Multiversion concurrency control**,多版本并发控制,提供并发访问数据库时,对事务内读取的到的内存做处理,用来避免写操作堵塞读操作的并发问题。
   举个例子,程序员A正在读数据库中某些内容,而程序员B正在给这些内容做修改(假设是在一个事务内修改,大概持续10s左右),A在这10s内 则可能看到一个不一致的数据,在B没有提交前,如何让A能够一直读到的数据都是一致的呢?
   有几种处理方法,第一种: 基于锁的并发控制,程序员B开始修改数据时,给这些数据加上锁,程序员A这时再读,就发现读取不了,处于等待情况,只能等B操作完才能读数据,这保证A不会读到一个不一致的数据,但是这个会影响程序的运行效率。还有一种就是:MVCC,每个用户连接数据库时,看到的都是某一特定时刻的数据库快照,在B的事务没有提交之前,A始终读到的是某一特定时刻的数据库快照,不会读到B事务中的数据修改情况,直到B事务提交,才会读取B的修改内容。
   一个支持MVCC的数据库,在更新某些数据时,并非使用新数据覆盖旧数据,而是标记旧数据是过时的,同时在其他地方新增一个数据版本。因此,同一份数据有多个版本存储,但只有一个是最新的。
@@ -11,9 +11,9 @@
   MVCC有两种实现方式,第一种实现方式是将数据记录的多个版本保存在数据库中,当这些不同版本数据不再需要时,垃圾收集器回收这些记录。这个方式被PostgreSQL和Firebird/Interbase采用,SQL Server使用的类似机制,所不同的是旧版本数据不是保存在数据库中,而保存在不同于主数据库的另外一个数据库tempdb中。第二种实现方式只在数据库保存最新版本的数据,但是会在使用undo时动态重构旧版本数据,这种方式被Oracle和MySQL/InnoDB使用。
 ```
 
-**2、InnoDB的MVCC实现机制**   MVCC可以认为是行级锁的一个变种,它可以在很多情况下避免加锁操作,因此开销更低。MVCC的实现大都都实现了非阻塞的读操作,写操作也只锁定必要的行。InnoDB的MVCC实现,是通过保存数据在某个时间点的快照来实现的。 **一个事务,不管其执行多长时间,其内部看到的数据是一致的** 。也就是事务在执行的过程中不会相互影响。下面我们简述一下MVCC在InnoDB中的实现。
+**2、InnoDB的MVCC实现机制**   MVCC可以认为是行级锁的一个变种,它可以在很多情况下避免加锁操作,因此开销更低。MVCC的实现大都都实现了非阻塞的读操作,写操作也只锁定必要的行。InnoDB的MVCC实现,是通过保存数据在某个时间点的快照来实现的。**一个事务,不管其执行多长时间,其内部看到的数据是一致的** 。也就是事务在执行的过程中不会相互影响。下面我们简述一下MVCC在InnoDB中的实现。
 
-  InnoDB的MVCC, **通过在每行记录后面保存两个隐藏的列来实现:一个保存了行的创建时间,一个保存行的过期时间(删除时间),当然,这里的时间并不是时间戳,而是系统版本号,每开始一个新的事务,系统版本号就会递增** 。在RR隔离级别下,MVCC的操作如下:
+  InnoDB的MVCC,**通过在每行记录后面保存两个隐藏的列来实现:一个保存了行的创建时间,一个保存行的过期时间(删除时间),当然,这里的时间并不是时间戳,而是系统版本号,每开始一个新的事务,系统版本号就会递增** 。在RR隔离级别下,MVCC的操作如下:
 
 1.  select操作。
     * **InnoDB只查找版本早于(包含等于)当前事务版本的数据行** 。可以确保事务读取的行,要么是事务开始前就已存在,或者事务自身插入或修改的记录。
@@ -22,7 +22,7 @@
 3.  delete操作。将删除的行保存当前版本号为删除标识。
 4.  update操作。变为insert和delete操作的组合,insert的行保存当前版本号为行版本号,delete则保存当前版本号到原来的行作为删除标识。
 
-  由于旧数据并不真正的删除,所以必须对这些数据进行清理,innodb会开启一个后台线程执行清理工作,具体的规则是 **将删除版本号小于当前系统版本的行删除** ,这个过程叫做purge。 **3、简单的小例子** 
+  由于旧数据并不真正的删除,所以必须对这些数据进行清理,innodb会开启一个后台线程执行清理工作,具体的规则是 **将删除版本号小于当前系统版本的行删除**,这个过程叫做purge。**3、简单的小例子** 
 
 ```sql
 create table yang( 
diff --git "a/docs/Article/MySQL/MySQL\347\232\204\345\215\212\345\220\214\346\255\245\346\230\257\344\273\200\344\271\210\357\274\237.md" "b/docs/Article/MySQL/MySQL\347\232\204\345\215\212\345\220\214\346\255\245\346\230\257\344\273\200\344\271\210\357\274\237.md"
index d0dd280ce..d96dcc837 100644
--- "a/docs/Article/MySQL/MySQL\347\232\204\345\215\212\345\220\214\346\255\245\346\230\257\344\273\200\344\271\210\357\274\237.md"
+++ "b/docs/Article/MySQL/MySQL\347\232\204\345\215\212\345\220\214\346\255\245\346\230\257\344\273\200\344\271\210\357\274\237.md"
@@ -28,13 +28,13 @@ MySQL的复制原理概述上来讲大体可以分为这三步
 1. 从库将主库上的日志复制到自己的中继日志(Relay Log)中。
 1. 备库读取中继日志中的事件,将其重放到备库数据之上。
 
-主要过程如下图: ![MySQL复制过程](../assets/20210321233214268.png) 下面来详细说一下复制的这三步: **第一步:是在主库上记录二进制日志,首先主库要开启binlog日志记录功能,并授权Slave从库可以访问的权限** 。这里需要注意的一点就是binlog的日志里的顺序是按照事务提交的顺序来记录的而非每条语句的执行顺序。 **第二步:从库将binLog复制到其本地的RelayLog中。首先从库会启动一个工作线程,称为I/O线程,I/O线程跟主库建立一个普通的客户端连接,然后主库上启动一个特殊的二进制转储(binlog dump)线程,此转储线程会读取binlog中的事件** 。当追赶上主库后,会进行休眠,直到主库通知有新的更新语句时才继续被唤醒。 这样通过从库上的I/O线程和主库上的binlog dump线程,就将binlog数据传输到从库上的relaylog中了。 **第三步:从库中启动一个SQL线程,从relaylog中读取事件并在备库中执行,从而实现备库数据的更新。** 这种复制架构实现了获取事件和重放事件的解耦,运行I/O线程能够独立于SQL线程之外工作。但是这种架构也限制复制的过程,最重要的一点是在主库上并发运行的查询在备库中只能串行化执行,因为只有一个SQL线程来重放中继日志中的事件。
+主要过程如下图: ![MySQL复制过程](../assets/20210321233214268.png) 下面来详细说一下复制的这三步: **第一步:是在主库上记录二进制日志,首先主库要开启binlog日志记录功能,并授权Slave从库可以访问的权限** 。这里需要注意的一点就是binlog的日志里的顺序是按照事务提交的顺序来记录的而非每条语句的执行顺序。**第二步:从库将binLog复制到其本地的RelayLog中。首先从库会启动一个工作线程,称为I/O线程,I/O线程跟主库建立一个普通的客户端连接,然后主库上启动一个特殊的二进制转储(binlog dump)线程,此转储线程会读取binlog中的事件** 。当追赶上主库后,会进行休眠,直到主库通知有新的更新语句时才继续被唤醒。 这样通过从库上的I/O线程和主库上的binlog dump线程,就将binlog数据传输到从库上的relaylog中了。**第三步:从库中启动一个SQL线程,从relaylog中读取事件并在备库中执行,从而实现备库数据的更新。** 这种复制架构实现了获取事件和重放事件的解耦,运行I/O线程能够独立于SQL线程之外工作。但是这种架构也限制复制的过程,最重要的一点是在主库上并发运行的查询在备库中只能串行化执行,因为只有一个SQL线程来重放中继日志中的事件。
 
 > 说到这个主从复制的串行化执行的问题,我就想到了一个之前在工作中遇到的一个问题,就是有这么一个业务场景,我们有一个操作是初始化一批数据,数据是从一个外部系统的接口中获取的,然后我是通过线程池里的多个线程并行从外部系统的接口中获取数据,每个线程获取到数据后,直接插入到数据库中。然后在数据全部入库完成后,然后去执行批量查询,将刚插入到数据库中的数据查询出来,放到ElasticSearch中。结果每次放入到ES中的数据总是不完整,后来研究了半天都不行,最终是让查询也走的主库才解决的问题。当时不知道是MySQL主从复制的串行化从而导致的这个问题。
 
 #### MySQL主从复制模式
 
-MySQL的主从复制其实是支持, **异步复制** 、 **半同步复制** 、 **GTID复制** 等多种复制模式的。
+MySQL的主从复制其实是支持,**异步复制** 、 **半同步复制** 、 **GTID复制** 等多种复制模式的。
 
 ##### 异步模式
 
@@ -50,7 +50,7 @@ MySQL从 **5.5** 版本开始通过以插件的形式开始支持半同步的主
 - **同步复制模式** :当主库执行完客户端提交的事务后,需要等到所有从库也都执行完这一事务后,才返回给客户端执行成功。因为要等到所有从库都执行完,执行过程中会被阻塞,等待返回结果,所以性能上会有很严重的影响。
 - **半同步复制模式** :半同步复制模式,可以说是介于异步和同步之间的一种复制模式,主库在执行完客户端提交的事务后,要等待至少一个从库接收到binlog并将数据写入到relay log中才返回给客户端成功结果。半同步复制模式,比异步模式提高了数据的可用性,但是也产生了一定的性能延迟,最少要一个TCP/IP连接的往返时间。
 
-半同步复制模式,可以很明确的知道,在一个事务提交成功之后,此事务至少会存在于两个地方一个是主库一个是从库中的某一个。 **主要原理是,在master的dump线程去通知从库时,增加了一个ACK机制,也就是会确认从库是否收到事务的标志码,master的dump线程不但要发送binlog到从库,还有负责接收slave的ACK。当出现异常时,Slave没有ACK事务,那么将自动降级为异步复制,直到异常修复后再自动变为半同步复制**
+半同步复制模式,可以很明确的知道,在一个事务提交成功之后,此事务至少会存在于两个地方一个是主库一个是从库中的某一个。**主要原理是,在master的dump线程去通知从库时,增加了一个ACK机制,也就是会确认从库是否收到事务的标志码,master的dump线程不但要发送binlog到从库,还有负责接收slave的ACK。当出现异常时,Slave没有ACK事务,那么将自动降级为异步复制,直到异常修复后再自动变为半同步复制**
 
 MySQL半同步复制的流程如下:
 
@@ -132,7 +132,7 @@ GTID:76147e28-8086-4f8c-9f98-1cf33d92978d:1-322 UUID:76147e28-8086-4f8c-9f98
 - 如果没有记录,Slave会从relay log中执行该GTID事务,并记录到binlog。
 - 在解析过程中,判断是否有主键,如果没有主键就使用二级索引,再没有二级索引就扫描全表。
 
-初始结构如下图 ![GTID](../assets/20210418151707365.png) 当Master出现宕机后,就会演变成下图。 ![GTID复制](../assets/2021041815183969.png) 通过上图我们可以看出来,当Master挂掉后,Slave-1执行完了Master的事务,Slave-2延时一些,所以没有执行完Master的事务,这个时候提升Slave-1为主,Slave-2连接了新主(Slave-1)后,将最新的GTID传给新主,然后Slave-1就从这个GTID的下一个GTID开始发送事务给Slave-2。 **这种自我寻找复制位置的模式减少事务丢失的可能性以及故障恢复的时间。**
+初始结构如下图 ![GTID](../assets/20210418151707365.png) 当Master出现宕机后,就会演变成下图。 ![GTID复制](../assets/2021041815183969.png) 通过上图我们可以看出来,当Master挂掉后,Slave-1执行完了Master的事务,Slave-2延时一些,所以没有执行完Master的事务,这个时候提升Slave-1为主,Slave-2连接了新主(Slave-1)后,将最新的GTID传给新主,然后Slave-1就从这个GTID的下一个GTID开始发送事务给Slave-2。**这种自我寻找复制位置的模式减少事务丢失的可能性以及故障恢复的时间。**
 
 #### GTID的优劣势
 
diff --git "a/docs/Article/MySQL/\345\275\273\345\272\225\347\220\206\350\247\243MySQL\347\232\204\347\264\242\345\274\225\346\234\272\345\210\266.md" "b/docs/Article/MySQL/\345\275\273\345\272\225\347\220\206\350\247\243MySQL\347\232\204\347\264\242\345\274\225\346\234\272\345\210\266.md"
index 1a1ef305a..98c94b1ae 100644
--- "a/docs/Article/MySQL/\345\275\273\345\272\225\347\220\206\350\247\243MySQL\347\232\204\347\264\242\345\274\225\346\234\272\345\210\266.md"
+++ "b/docs/Article/MySQL/\345\275\273\345\272\225\347\220\206\350\247\243MySQL\347\232\204\347\264\242\345\274\225\346\234\272\345\210\266.md"
@@ -12,7 +12,7 @@
 
 - **寻道时间** :磁头移动到指定磁道所需要的时间,主流磁盘一般在 5ms 以下,平均 3-15ms。
 - **旋转延迟** :指盘片旋转将请求数据所在的扇区移动到读写磁盘下方所需要的时间。磁盘转速,比如一个磁盘 7200 转,表示每分钟能转 7200 次,也就是说 1 秒钟能转 120 次,旋转延迟就是 1/120/2 = 4.17ms。
-- **传输时间** :从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,远远小于前面消耗的时间,几乎可以忽略不计。 **什么是预读?** 因为磁盘 IO 是非常昂贵的操作,所以计算机系统对此做了一些优化,当一次 IO 时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到,所以每次读取页的整数倍(通常一个节点就是一页)。
+- **传输时间** :从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,远远小于前面消耗的时间,几乎可以忽略不计。**什么是预读?** 因为磁盘 IO 是非常昂贵的操作,所以计算机系统对此做了一些优化,当一次 IO 时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到,所以每次读取页的整数倍(通常一个节点就是一页)。
 
 ## 什么是索引,索引的数据结构为何选择 B+Tree?
 
@@ -24,7 +24,7 @@
 
 InnoDB 的索引和数据都存放在同一文件中,而 MyIsAm 的索引和数据分别存放在不同的文件中。
 
-我们知道,MySQL 的 InnoDB 引擎下的索引数据结构为 B+tree 和 hash,为什么在这么多数据结构中会选择 B+tree 和 hash 呢? **首先我们先来了解一下以下四种数据结构** (不会详细分析,毕竟主题是 MySQL 索引)。
+我们知道,MySQL 的 InnoDB 引擎下的索引数据结构为 B+tree 和 hash,为什么在这么多数据结构中会选择 B+tree 和 hash 呢?**首先我们先来了解一下以下四种数据结构** (不会详细分析,毕竟主题是 MySQL 索引)。
 
 **1. 二叉树** 特征:要保证父节点大于左子结点,小于右子节点。极端情况下会产生如下所示的树:
 
@@ -93,7 +93,7 @@ B-tree 又做了优化,可以在磁盘容量允许的情况可控树的高度
 
 结论:我们只通过三次 IO 操作就获取到 65 的数据,大大的节省 IO 操作的时间,这在大数据量的情况下,节省的时间更加不可想象。
 
-**为什么要选择 B+ 树** 此时我们的心里的流程是这样的:如何减少获取数据的时间 —-> 减少 IO 操作 ——> 如何减少 IO 操作 —> 减少树的高度 —> 什么树能稳定的可控树的高度 —>(B 树和 B+ 树)—> 那为什么选择 B+ 树 —–> 因为 B+ 树节点不保存全部数据,因此在一页(一个节点)上能够存更加多的索引数据,让树的高度更低。 **还有一点很重要:** 对于组合索引,B+tree 索引是按照索引列名 (从左到右的顺序) 进行顺序排序的,因此可以将随机 IO 转换为顺序 IO 提升 IO 效率;并且可以支持 order by/group 等排序需求;适合范围查询。 **另外** MySQL 还支持 Hash 索引,但是 Hash 索引只能 **自适应** ,也就是说不能由我们手动指定,只能在优化器阶段,由优化器自主优化是使用 B+tree 还是 Hash 结构的索引,因此 Hash 在此就不赘述了。
+**为什么要选择 B+ 树** 此时我们的心里的流程是这样的:如何减少获取数据的时间 —-> 减少 IO 操作 ——> 如何减少 IO 操作 —> 减少树的高度 —> 什么树能稳定的可控树的高度 —>(B 树和 B+ 树)—> 那为什么选择 B+ 树 —–> 因为 B+ 树节点不保存全部数据,因此在一页(一个节点)上能够存更加多的索引数据,让树的高度更低。**还有一点很重要:** 对于组合索引,B+tree 索引是按照索引列名 (从左到右的顺序) 进行顺序排序的,因此可以将随机 IO 转换为顺序 IO 提升 IO 效率;并且可以支持 order by/group 等排序需求;适合范围查询。**另外** MySQL 还支持 Hash 索引,但是 Hash 索引只能 **自适应**,也就是说不能由我们手动指定,只能在优化器阶段,由优化器自主优化是使用 B+tree 还是 Hash 结构的索引,因此 Hash 在此就不赘述了。
 
 ## 如何创建高性能索引?
 
@@ -133,7 +133,7 @@ select count(Distinct columnName)/count(*) from Table
 ALTER TABLE person ADD KEY(name(7));
 ```
 
-前缀索引是针对大类型字段,比如 varchar、text、blob,如果使用这样的列做索引的话,会很消耗内存资源,而且大而慢。而且 MySQL 不允许索引这些列的完整长度。 **那么我们如何解决此类索引问题呢?**
+前缀索引是针对大类型字段,比如 varchar、text、blob,如果使用这样的列做索引的话,会很消耗内存资源,而且大而慢。而且 MySQL 不允许索引这些列的完整长度。**那么我们如何解决此类索引问题呢?**
 
 通常我们可以选择索引开始的部分字符,这样可以大大的节约索引空间,从而提高索引效率,但这样会降低索引的度。
 
@@ -156,9 +156,9 @@ ALTER TABLE person ADD KEY (name (N));
 
 前缀索引的缺点:无法使用前缀索引做 ORDER BY 和 GRUOP BY,也无法使用前缀索引做覆盖索引。
 
-- **4. 索引列不能参与计算** ![在这里插入图片描述](../assets/49a05260-ffc3-11e9-b0f3-059626abdf6a.jpg)
+- **4. 索引列不能参与计算**![在这里插入图片描述](../assets/49a05260-ffc3-11e9-b0f3-059626abdf6a.jpg)
 
-上述 SQL 无法使用到 (name,age,sex) 这个索引,因为 name 参与了计算,所以导致整个索引都无法使用。 **5. 尽量的扩展索引,不要新建索引** 索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。
+上述 SQL 无法使用到 (name,age,sex) 这个索引,因为 name 参与了计算,所以导致整个索引都无法使用。**5. 尽量的扩展索引,不要新建索引** 索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。
 
 比如:表中已经有 name 的索引,现在要加 (name,age) 的索引,那么只需要修改原来的索引即可 **6. 重复索引和冗余索引** 重复索引:相同列上按照相同顺序创建的相同类型的索引。
 
diff --git "a/docs/Article/MySQL/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\272\225\345\261\202\345\256\236\347\216\260.md" "b/docs/Article/MySQL/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\272\225\345\261\202\345\256\236\347\216\260.md"
index d160008ed..9ca8d1502 100644
--- "a/docs/Article/MySQL/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\272\225\345\261\202\345\256\236\347\216\260.md"
+++ "b/docs/Article/MySQL/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\272\225\345\261\202\345\256\236\347\216\260.md"
@@ -43,14 +43,14 @@ InnoDB和Myisam都是用B+Tree来存储数据的。
 硬盘在逻辑上被划分为磁道、柱面以及扇区。
 磁头靠近主轴接触的表面,即线速度最小的地方,是一个特殊的区域,它不存放任何数据,称为启停区或者着陆区,启停区外就是数据区。
 在最外圈,离主轴最远的地方是“0”磁道,硬盘数据的存放就是从最外圈开始的。
-在硬盘中还有一个叫“0”磁道检测器的构件,它是用来完成硬盘的初始定位。 **盘面** 硬盘的盘片一般用铝合金材料做基片,硬盘的每一个盘片都有上下两个盘面,一般每个盘面都会得到利用,都可以存储数据,成为有效盘面,也有极个别的硬盘盘面数为单数。
+在硬盘中还有一个叫“0”磁道检测器的构件,它是用来完成硬盘的初始定位。**盘面** 硬盘的盘片一般用铝合金材料做基片,硬盘的每一个盘片都有上下两个盘面,一般每个盘面都会得到利用,都可以存储数据,成为有效盘面,也有极个别的硬盘盘面数为单数。
 每一个这样的有效盘面都有一个盘面号,按顺序从上至下从0开始编号。
-在硬盘系统中,盘面号又叫磁头号,因为每一个有效盘面都有一个对应的读写磁头,硬盘的盘片组在2-14片不等,通常有2-3个盘片。 **磁道** 磁盘在格式化时被划分成许多同心圆,这些同心圆轨迹叫做磁道。
+在硬盘系统中,盘面号又叫磁头号,因为每一个有效盘面都有一个对应的读写磁头,硬盘的盘片组在2-14片不等,通常有2-3个盘片。**磁道** 磁盘在格式化时被划分成许多同心圆,这些同心圆轨迹叫做磁道。
 磁道从外向内从0开始顺序编号,硬盘的每一个盘面有300-1024个磁道,新式大容量硬盘每面的磁道数更多,信息以脉冲串的形式记录在这些轨迹中,这些同心圆不是连续记录数据,而是被划分成一段段的圆弧,这些圆弧的角速度一样,由于径向长度不一样,所以线速度也不一样,外圈的线速度较内圈的线速度大,即同样的转速度下,外圈在同样时间段里,划过的圆弧长度要比内圈划过的圆弧长度大。
 每段圆弧叫做一个扇区,扇区从1开始编号,每个扇区中的数据作为一个单元同时读出或写入。
-磁道是看不见的,只是盘面上以特殊形式磁化了的一些磁化区,在磁盘格式化时就已规划完毕。 **柱面** 所有盘面上的同一磁道构成一个圆柱,通常称作柱面。
+磁道是看不见的,只是盘面上以特殊形式磁化了的一些磁化区,在磁盘格式化时就已规划完毕。**柱面** 所有盘面上的同一磁道构成一个圆柱,通常称作柱面。
 每个圆柱上的磁头由上而下从0开始编号,数据的读/写按柱面进行,即磁头读/写数据时首先在同一柱面内从0磁头开始进行操作,依次向下在同一柱面的不同盘面即磁头上进行操作,只有在同一柱面所有的磁头全部读/写完毕后磁头才转移到下一柱面(同心圆再往里的柱面),因为选取磁头只需要通过电子切换即可,而选取柱面则必须机械切换,电子切换相当快,比在机械上的磁头向邻近磁道移动快得多。
-所以,数据的读/写按柱面进行,而不按盘面进行,也就是说,一个磁道写满数据后,就在同一柱面的下一个盘面来写,一个柱面写满后,才移到下一个扇区开始写数据,读数据也按照这种方式进行,这样就提高了硬盘的读/写效率。 **扇区** 操作系统以扇区形式将信息存储在硬盘上,每个扇区包括512个字节的数据和一些其他信息,一个扇区有两个主要部分:存储数据地点的标识符和存储数据的数据段。
+所以,数据的读/写按柱面进行,而不按盘面进行,也就是说,一个磁道写满数据后,就在同一柱面的下一个盘面来写,一个柱面写满后,才移到下一个扇区开始写数据,读数据也按照这种方式进行,这样就提高了硬盘的读/写效率。**扇区** 操作系统以扇区形式将信息存储在硬盘上,每个扇区包括512个字节的数据和一些其他信息,一个扇区有两个主要部分:存储数据地点的标识符和存储数据的数据段。
 标识符就是扇区头标,包括组成扇区三维地址的三个数字:盘面号,柱面号,扇区号(块号)。
 数据段可分为数据和保护数据的纠错码(ECC)。在初始准备期间,计算机用512个虚拟信息字节(实际数据的存放地)和与这些虚拟信息字节相应的ECC数字填入这个部分。
 ```
diff --git "a/docs/Article/MySQL/\350\256\244\350\257\206MySQL\345\222\214Redis\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264\346\200\247\351\227\256\351\242\230.md" "b/docs/Article/MySQL/\350\256\244\350\257\206MySQL\345\222\214Redis\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264\346\200\247\351\227\256\351\242\230.md"
index b50867eb7..a9eaaddc6 100644
--- "a/docs/Article/MySQL/\350\256\244\350\257\206MySQL\345\222\214Redis\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264\346\200\247\351\227\256\351\242\230.md"
+++ "b/docs/Article/MySQL/\350\256\244\350\257\206MySQL\345\222\214Redis\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264\346\200\247\351\227\256\351\242\230.md"
@@ -21,11 +21,11 @@
 
 只读缓存:新增数据时,直接写入数据库;更新(修改/删除)数据时,先删除缓存。 后续,访问这些增删改的数据时,会发生缓存缺失,进而查询数据库,更新缓存。
 
-- **新增数据时** ,写入数据库;访问数据时,缓存缺失,查数据库,更新缓存(始终是处于”数据一致“的状态,不会发生数据不一致性问题)
+- **新增数据时**,写入数据库;访问数据时,缓存缺失,查数据库,更新缓存(始终是处于”数据一致“的状态,不会发生数据不一致性问题)
 
 ![img](../assets/v2-743a97e88d1d0bf906b7e1e3bec7a99d_b.jpg)
 
-- **更新(修改/删除)数据时** ,会有个时序问题:更新数据库与删除缓存的顺序(这个过程会发生数据不一致性问题)
+- **更新(修改/删除)数据时**,会有个时序问题:更新数据库与删除缓存的顺序(这个过程会发生数据不一致性问题)
 
 ![img](../assets/v2-34d9206cb3158ab96e5c5462de05e817_b.jpg)
 
@@ -41,14 +41,14 @@
 
 接下来,我们针对有/无并发场景,进行分析并使用不同的策略。
 
-### **A. 无并发情况** 无并发请求下,在更新数据库和删除缓存值的过程中,因为操作被拆分成两步,那么就很有可能存在“步骤 1 成功,步骤 2 失败” 的情况发生(由于单线程中步骤 1 和步骤 2 是串行执行的,不太可能会发生 “步骤 2 成功,步骤 1 失败” 的情况)。 **(1) 先删除缓存,再更新数据库** ![img](../assets/v2-6a6d482d7f275d3dde89e134af9e7858_b.jpg) **(2) 先更新数据库,再删除缓存** ![img](../assets/v2-6f951d6291001bf7e9bbc6f2e856a1db_b.jpg)
+### **A. 无并发情况** 无并发请求下,在更新数据库和删除缓存值的过程中,因为操作被拆分成两步,那么就很有可能存在“步骤 1 成功,步骤 2 失败” 的情况发生(由于单线程中步骤 1 和步骤 2 是串行执行的,不太可能会发生 “步骤 2 成功,步骤 1 失败” 的情况)。**(1) 先删除缓存,再更新数据库**![img](../assets/v2-6a6d482d7f275d3dde89e134af9e7858_b.jpg) **(2) 先更新数据库,再删除缓存**![img](../assets/v2-6f951d6291001bf7e9bbc6f2e856a1db_b.jpg)
 
 ![img](../assets/v2-f4e7a92c44a67694ad91ca0eb1f189aa_b.jpg) **解决策略:**  **a.消息队列+异步重试** 无论使用哪一种执行时序,可以在执行步骤 1 时,将步骤 2 的请求写入消息队列,当步骤 2 失败时,就可以使用重试策略,对失败操作进行 “补偿”。
 
 ![img](../assets/v2-6d6dd4dc404567c1402eaba8a12f8f48_b.jpg) **具体步骤如下:** 1.  把要删除缓存值或者是要更新数据库值操作生成消息,暂存到消息队列中(例如使用 Kafka 消息队列);
 2\.  当删除缓存值或者是更新数据库值操作成功时,把这些消息从消息队列中去除(丢弃),以免重复操作;
 3\.  当删除缓存值或者是更新数据库值操作失败时,执行失败策略,重试服务从消息队列中重新读取(消费)这些消息,然后再次进行删除或更新;
-4\.  删除或者更新失败时,需要再次进行重试,重试超过的一定次数,向业务层发送报错信息。 **b.订阅 Binlog 变更日志**
+4\.  删除或者更新失败时,需要再次进行重试,重试超过的一定次数,向业务层发送报错信息。**b.订阅 Binlog 变更日志**
 
 - 创建更新缓存服务,接收数据变更的 MQ 消息,然后消费消息,更新/删除 Redis 中的缓存数据;
 - 使用 Binlog 实时更新/删除 Redis 缓存。利用 Canal,即将负责更新缓存的服务伪装成一个 MySQL 的从节点,从 MySQL 接收 Binlog,解析 Binlog 之后,得到实时的数据变更信息,然后根据变更信息去更新/删除 Redis 缓存;
@@ -77,13 +77,13 @@ sleep 时间:在业务程序运行的时候,统计下线程读数据和写
 
 ![img](../assets/v2-5ce3b6ddcae7a203a983fb74124e6f2e_b.jpg) **注意** :如果难以接受 sleep 这种写法,可以使用延时队列进行替代。
 
-先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。 **(2) 先更新数据库,再删除缓存** 如果线程 A 更新了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。其本质也是,本应后发生的“B 线程-读请求” 先于 “A 线程-删除缓存” 执行并返回了。
+先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。**(2) 先更新数据库,再删除缓存** 如果线程 A 更新了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。其本质也是,本应后发生的“B 线程-读请求” 先于 “A 线程-删除缓存” 执行并返回了。
 
 ![img](../assets/v2-17531f4ab67f9f3cd8450854e740bf46_b.jpg)
 
 或者,在”先更新数据库,再删除缓存”方案下,“读写分离 + 主从库延迟”也会导致不一致:
 
-![img](../assets/v2-862b9e280ded89432847506d1d70f65a_b.jpg) **解决方案:**  **a.延迟消息** 凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率 **b.订阅 binlog,异步删除** 通过数据库的 binlog 来异步淘汰 key,利用工具(canal)将 binlog 日志采集发送到 MQ 中,然后通过 ACK 机制确认处理删除缓存。 **c.删除消息写入数据库** 通过比对数据库中的数据,进行删除确认 先更新数据库再删除缓存,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。 **d.加锁** 更新数据时,加写锁;查询数据时,加读锁 保证两步操作的“原子性”,使得操作可以串行执行。“原子性”的本质是什么?不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。
+![img](../assets/v2-862b9e280ded89432847506d1d70f65a_b.jpg) **解决方案:**  **a.延迟消息** 凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率 **b.订阅 binlog,异步删除** 通过数据库的 binlog 来异步淘汰 key,利用工具(canal)将 binlog 日志采集发送到 MQ 中,然后通过 ACK 机制确认处理删除缓存。**c.删除消息写入数据库** 通过比对数据库中的数据,进行删除确认 先更新数据库再删除缓存,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。**d.加锁** 更新数据时,加写锁;查询数据时,加读锁 保证两步操作的“原子性”,使得操作可以串行执行。“原子性”的本质是什么?不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。
 
 ![img](../assets/v2-b14eed88789caca84a7cb25eaad54fb0_b.jpg) **建议:** 优先使用“先更新数据库再删除缓存”的执行时序,原因主要有两个:
 
@@ -99,7 +99,7 @@ sleep 时间:在业务程序运行的时候,统计下线程读数据和写
 
 一致性:同步直写 > 异步回写 因此,对于读写缓存,要保持数据强一致性的主要思路是:利用同步直写 同步直写也存在两个操作的时序问题:更新数据库和更新缓存
 
-### **A. 无并发情况** ![img](../assets/v2-a7d9ce347ea85a557ee9cd21d873a736_b.jpg)
+### **A. 无并发情况**![img](../assets/v2-a7d9ce347ea85a557ee9cd21d873a736_b.jpg)
 
 ### **B. 高并发情况** 有四种场景会造成数据不一致
 
@@ -113,7 +113,7 @@ sleep 时间:在业务程序运行的时候,统计下线程读数据和写
 
 ![img](../assets/v2-ea428da2331b4f04f65fb720ac80a829_b.jpg)
 
-### **2.3 强一致性策略** 上述策略只能保证数据的最终一致性。 要想做到强一致,最常见的方案是 2PC、3PC、Paxos、Raft 这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。 如果业务层要求必须读取数据的强一致性,可以采取以下策略: **(1)暂存并发读请求** 在更新数据库时,先在 Redis 缓存客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。 **(2)串行化** 读写请求入队列,工作线程从队列中取任务来依次执行
+### **2.3 强一致性策略** 上述策略只能保证数据的最终一致性。 要想做到强一致,最常见的方案是 2PC、3PC、Paxos、Raft 这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。 如果业务层要求必须读取数据的强一致性,可以采取以下策略: **(1)暂存并发读请求** 在更新数据库时,先在 Redis 缓存客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。**(2)串行化** 读写请求入队列,工作线程从队列中取任务来依次执行
 
 1. 修改服务 Service 连接池,id 取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上
 1. 修改数据库 DB 连接池,id 取模选取 DB 连接,能够保证同一个数据的读写在数据库层面是串行的 **(3)使用 Redis 分布式读写锁** 将淘汰缓存与更新库表放入同一把写锁中,与其它读请求互斥,防止其间产生旧数据。读写互斥、写写互斥、读读共享,可满足读多写少的场景数据一致,也保证了并发性。并根据逻辑平均运行时间、响应超时时间来确定过期时间。
@@ -145,7 +145,7 @@ public void read() {
 }
 ```
 
-### **2.4 小结** ![img](../assets/v2-34125bb8924b7c221739ceaae8f936e2_b.jpg)
+### **2.4 小结**![img](../assets/v2-34125bb8924b7c221739ceaae8f936e2_b.jpg)
 
 针对读写缓存时:同步直写,更新数据库+更新缓存
 
diff --git "a/docs/Article/MySQL/\351\230\277\351\207\214\344\272\221PolarDB\345\217\212\345\205\266\345\205\261\344\272\253\345\255\230\345\202\250PolarFS\346\212\200\346\234\257\345\256\236\347\216\260\345\210\206\346\236\220\357\274\210\344\270\212\357\274\211.md" "b/docs/Article/MySQL/\351\230\277\351\207\214\344\272\221PolarDB\345\217\212\345\205\266\345\205\261\344\272\253\345\255\230\345\202\250PolarFS\346\212\200\346\234\257\345\256\236\347\216\260\345\210\206\346\236\220\357\274\210\344\270\212\357\274\211.md"
index 4504c37ac..980e38dda 100644
--- "a/docs/Article/MySQL/\351\230\277\351\207\214\344\272\221PolarDB\345\217\212\345\205\266\345\205\261\344\272\253\345\255\230\345\202\250PolarFS\346\212\200\346\234\257\345\256\236\347\216\260\345\210\206\346\236\220\357\274\210\344\270\212\357\274\211.md"
+++ "b/docs/Article/MySQL/\351\230\277\351\207\214\344\272\221PolarDB\345\217\212\345\205\266\345\205\261\344\272\253\345\255\230\345\202\250PolarFS\346\212\200\346\234\257\345\256\236\347\216\260\345\210\206\346\236\220\357\274\210\344\270\212\357\274\211.md"
@@ -58,11 +58,11 @@ PolarFS的存储组织
 
 跟其他传统的块设备一样,卷上的读写IO以512B大小对齐,对卷上同个Chunk的修改操作是原子的。当然,卷还是块设备层面的概念,在提供给数据库实例使用前,需在卷上格式化一个PolarFS文件系统(PFS)实例,跟ext4、btrfs一样,PFS上也会在卷上存放文件系统元数据。这些元数据包括inode、directory entry和空闲块等对象。同时,PFS也是一个日志文件系统,为了实现文件系统的元数据一致性,元数据的更新会首先记录在卷上的Journal(日志)文件中,然后才更新指定的元数据。
 
-跟传统文件系统不一样的是PolarFS是个共享文件系统即一个卷会被挂载到多个计算节点上,也就是说可能存在有多个客户端(挂载点)对文件系统进行读写和更新操作,所以PolarFS在卷上额外维护了一个Paxos文件。每个客户端在更新Journal文件前,都需要使用Paxos文件执行Disk Paxos算法实现对Journal文件的互斥访问。更详细的PolarFS元数据更新实现,后续单独作为一个小节。 **Chunk** 前面提到,每个卷内部会被划分为多个Chunk(区),区是数据分布的最小粒度,每个区都位于单块SSD盘上,其目的是利于数据高可靠和高可用的管理,详见后续章节。每个Chunk大小设置为10GB,远大于其他类似的存储系统,例如GFS为64MB,Linux LVM的物理区(PE)为4MB。这样做的目的是减少卷到区映射的元数据量大小(例如,100TB的卷只包含10K个映射项)。一方面,全局元数据的存放和管理会更容易;另一方面,元数据可以全都缓存在内存中,避免关键IO路径上的额外元数据访问开销。
+跟传统文件系统不一样的是PolarFS是个共享文件系统即一个卷会被挂载到多个计算节点上,也就是说可能存在有多个客户端(挂载点)对文件系统进行读写和更新操作,所以PolarFS在卷上额外维护了一个Paxos文件。每个客户端在更新Journal文件前,都需要使用Paxos文件执行Disk Paxos算法实现对Journal文件的互斥访问。更详细的PolarFS元数据更新实现,后续单独作为一个小节。**Chunk** 前面提到,每个卷内部会被划分为多个Chunk(区),区是数据分布的最小粒度,每个区都位于单块SSD盘上,其目的是利于数据高可靠和高可用的管理,详见后续章节。每个Chunk大小设置为10GB,远大于其他类似的存储系统,例如GFS为64MB,Linux LVM的物理区(PE)为4MB。这样做的目的是减少卷到区映射的元数据量大小(例如,100TB的卷只包含10K个映射项)。一方面,全局元数据的存放和管理会更容易;另一方面,元数据可以全都缓存在内存中,避免关键IO路径上的额外元数据访问开销。
 
 当然,Chunk设置为10GB也有不足。当上层数据库应用出现区域级热点访问时,Chunk内热点无法进一步打散,但是由于每个存储节点提供的Chunk数量往往远大于节点数量(节点:Chunk在1:1000量级),PolarFS支持Chunk的在线迁移,其上服务着大量数据库实例,因此可以将热点Chunk分布到不同节点上以获得整体的负载均衡。
 
-在PolarFS上,卷上的每个Chunk都有3个副本,分布在不同的ChunkServer上,3个副本基于ParallelRaft分布式一致性协议来保证数据高可靠和高可用。 **Block**
+在PolarFS上,卷上的每个Chunk都有3个副本,分布在不同的ChunkServer上,3个副本基于ParallelRaft分布式一致性协议来保证数据高可靠和高可用。**Block**
 
 在ChunkServer内,Chunk会被进一步划分为163,840个Block(块),每个块大小为64KB。Chunk至Block的映射信息由ChunkServer自行管理和保存。每个Chunk除了用于存放数据库数据的Block外,还包含一些额外Block用来实现预写日志(Write Ahead Log,WAL)。
 
@@ -86,11 +86,11 @@ pfs_mount()用于将指定卷上文件系统挂载到对应的数据库计算节
 
 上图右侧的表描述了目录树中的某个文件的前3个块分别对应的是卷的第348,1500和201这几个块。假如数据库操作需要回刷一个脏页,该页在该表所属文件的偏移位置128KB处,也就是说要写该文件偏移128KB开始的16KB数据,通过文件映射表知道该写操作其实写的是卷的第201个块。这就是lipfs发送给PolarSwitch的请求包含的内容:volumeid,offset和len。其中offset就是201\*64KB,len就是16KB。
 
-**PolarSwitch** PolarSwitch是部署在计算节点的Daemon,即上图的Data Router&Cache模块,它负责接收libpfs发送而来的文件IO请求,PolarSwitch将其划分为对应的一到多个Chunk,并将请求发往Chunk所属的ChunkServer完成访问。具体来说PolarSwitch根据自己缓存的volumeid到Chunk的映射表,知道该文件请求属于那个Chunk。请求如果跨Chunk的话,会将其进一步拆分为多个块IO请求。PolarSwitch还缓存了该Chunk的三个副本分别属于那几个ChunkServer以及哪个ChunkServer是当前的Leader节点。PolarSwitch只将请求发送给Leader节点。 **ChunkServer** ChunkServer部署在存储节点上,即上图的Data Chunk Server,用于处理块IO(Block IO)请求和节点内的存储资源分布。一个存储节点可以有多个ChunkServer,每个ChunkServer绑定到一个CPU核,并管理一块独立的NVMe SSD盘,因此ChunkServer之间没有资源竞争。
+**PolarSwitch** PolarSwitch是部署在计算节点的Daemon,即上图的Data Router&Cache模块,它负责接收libpfs发送而来的文件IO请求,PolarSwitch将其划分为对应的一到多个Chunk,并将请求发往Chunk所属的ChunkServer完成访问。具体来说PolarSwitch根据自己缓存的volumeid到Chunk的映射表,知道该文件请求属于那个Chunk。请求如果跨Chunk的话,会将其进一步拆分为多个块IO请求。PolarSwitch还缓存了该Chunk的三个副本分别属于那几个ChunkServer以及哪个ChunkServer是当前的Leader节点。PolarSwitch只将请求发送给Leader节点。**ChunkServer** ChunkServer部署在存储节点上,即上图的Data Chunk Server,用于处理块IO(Block IO)请求和节点内的存储资源分布。一个存储节点可以有多个ChunkServer,每个ChunkServer绑定到一个CPU核,并管理一块独立的NVMe SSD盘,因此ChunkServer之间没有资源竞争。
 
 ChunkServer负责存储Chunk和提供Chunk上的IO随机访问。每个Chunk都包括一个WAL,对Chunk的修改会先写Log再执行修改操作,保证数据的原子性和持久性。ChunkServer使用了3D XPoint SSD和普通NVMe SSD混合型WAL buffer,Log会优先存放到更快的3DXPoint SSD中。
 
-前面提到Chunk有3副本,这三个副本基于ParallelRaft协议,作为该Chunk Leader的ChunkServer会将块IO请求发送给Follow节点其他ChunkServer)上,通过ParallelRaft一致性协议来保证已提交的Chunk数据不丢失。 **PolarCtrl**
+前面提到Chunk有3副本,这三个副本基于ParallelRaft协议,作为该Chunk Leader的ChunkServer会将块IO请求发送给Follow节点其他ChunkServer)上,通过ParallelRaft一致性协议来保证已提交的Chunk数据不丢失。**PolarCtrl**
 
 PolarCtrl是系统的控制平面,相应地Agent代理被部署到所有的计算和存储节点上,PolarCtrl与各个节点的交互通过Agent进行。PolarCtrl是PolarFS集群的控制核心,后端使用一个关系数据库云服务来管理PolarDB的元数据。其主要职责包括:
 
diff --git "a/docs/Article/MySQL/\351\230\277\351\207\214\344\272\221PolarDB\345\217\212\345\205\266\345\205\261\344\272\253\345\255\230\345\202\250PolarFS\346\212\200\346\234\257\345\256\236\347\216\260\345\210\206\346\236\220\357\274\210\344\270\213\357\274\211.md" "b/docs/Article/MySQL/\351\230\277\351\207\214\344\272\221PolarDB\345\217\212\345\205\266\345\205\261\344\272\253\345\255\230\345\202\250PolarFS\346\212\200\346\234\257\345\256\236\347\216\260\345\210\206\346\236\220\357\274\210\344\270\213\357\274\211.md"
index fe0c360d8..556eb48b1 100644
--- "a/docs/Article/MySQL/\351\230\277\351\207\214\344\272\221PolarDB\345\217\212\345\205\266\345\205\261\344\272\253\345\255\230\345\202\250PolarFS\346\212\200\346\234\257\345\256\236\347\216\260\345\210\206\346\236\220\357\274\210\344\270\213\357\274\211.md"
+++ "b/docs/Article/MySQL/\351\230\277\351\207\214\344\272\221PolarDB\345\217\212\345\205\266\345\205\261\344\272\253\345\255\230\345\202\250PolarFS\346\212\200\346\234\257\345\256\236\347\216\260\345\210\206\346\236\220\357\274\210\344\270\213\357\274\211.md"
@@ -147,7 +147,7 @@ PolarFS性能
 
 PolarDB整体性能
 
-**使用不同底层存储时性能表现** ![img](../assets/2018101218075831d79e75-8a89-4927-a9ff-c8883036b23c.png) **对外展示的性能表现**
+**使用不同底层存储时性能表现**![img](../assets/2018101218075831d79e75-8a89-4927-a9ff-c8883036b23c.png) **对外展示的性能表现**
 
 ![img](../assets/20181012180758768a9a74-9133-45e5-abb1-d8d65dccb592.png)
 
diff --git "a/docs/Article/Other/Docker\351\225\234\345\203\217\346\236\204\345\273\272\345\216\237\347\220\206\345\217\212\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/docs/Article/Other/Docker\351\225\234\345\203\217\346\236\204\345\273\272\345\216\237\347\220\206\345\217\212\346\272\220\347\240\201\345\210\206\346\236\220.md"
index 0111c5e95..91d84498e 100644
--- "a/docs/Article/Other/Docker\351\225\234\345\203\217\346\236\204\345\273\272\345\216\237\347\220\206\345\217\212\346\272\220\347\240\201\345\210\206\346\236\220.md"
+++ "b/docs/Article/Other/Docker\351\225\234\345\203\217\346\236\204\345\273\272\345\216\237\347\220\206\345\217\212\346\272\220\347\240\201\345\210\206\346\236\220.md"
@@ -85,7 +85,7 @@ API docs preview will be running at http://localhost:9000
 
 通过 API 我们也知道了该接口所需的请求体是一个 `tar` 归档文件(可选择压缩算法进行压缩),同时它的请求头中会携带用户在镜像仓库中的认证信息。
 
-这提醒我们, **如果在使用远程 Dockerd 构建时,请注意安全,尽量使用 tls 进行加密,以免数据泄漏。**
+这提醒我们,**如果在使用远程 Dockerd 构建时,请注意安全,尽量使用 tls 进行加密,以免数据泄漏。**
 
 ### CLI
 
@@ -226,7 +226,7 @@ Successfully built ce88644a7395
 invalid argument: can't use stdin for both build context and dockerfile
 ```
 
-就会报错了。所以, **不能同时使用 stdin 读取 Dockerfile 和 build context** 。
+就会报错了。所以,**不能同时使用 stdin 读取 Dockerfile 和 build context** 。
 
 - **build context 支持四种行为。** 
 
@@ -550,7 +550,7 @@ func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, std
 }
 ```
 
-从以上的介绍我们可以先做个小的总结。 **当 build context 从 stdin 读,并且是个 tar 归档时,实际会向 dockerd 发起两次 /build 请求** 而一般情况下只会发送一次请求。
+从以上的介绍我们可以先做个小的总结。**当 build context 从 stdin 读,并且是个 tar 归档时,实际会向 dockerd 发起两次 /build 请求** 而一般情况下只会发送一次请求。
 那这里会有什么差别呢?此处先不展开,我们留到下面讲 `dockerd` 后端的时候再来解释。
 
 #### 小结
@@ -636,7 +636,7 @@ return errf(err)
 }
 ```
 
-这里就需要注意了: 真正的构建过程要开始了。 **使用 backend 的 Build 函数来完成真正的构建过程** 
+这里就需要注意了: 真正的构建过程要开始了。**使用 backend 的 Build 函数来完成真正的构建过程** 
 
 ```go
 // api/server/backend/build/backend.go#L52
diff --git "a/docs/Article/Other/RocketMQ\351\235\242\350\257\225\351\242\230\351\233\206\351\224\246.md" "b/docs/Article/Other/RocketMQ\351\235\242\350\257\225\351\242\230\351\233\206\351\224\246.md"
index 88b184ab5..bbd2b7585 100644
--- "a/docs/Article/Other/RocketMQ\351\235\242\350\257\225\351\242\230\351\233\206\351\224\246.md"
+++ "b/docs/Article/Other/RocketMQ\351\235\242\350\257\225\351\242\230\351\233\206\351\224\246.md"
@@ -70,7 +70,7 @@ Producer 在发送消息的时候指定什么时刻发送,然后消息被发
 
 顺序消息在日常的功能场景中很常见,比如点外卖生成外卖订单、付款、送餐的消息需要保证严格的顺序。
 
-**全局顺序消息:** RocketMQ 的一个 Topic 下默认有八个读队列和八个写队列,如果要保证全局顺序消息的话需要在生产端只保留一个读写队列,然后消费端只有一个消费线程,这样会降低 RocketMQ 的高可用和高吞吐量。 **分区顺序消息:** 分区顺序消息同样需要生产端和消费端配合,生产端根据同一个订单 ID 把消息路由到同一个 MessageQueue,消费端控制从同一个 MessageQueue 取出的消息不被并发处理。 **生成端发送分区顺序消息:** 
+**全局顺序消息:** RocketMQ 的一个 Topic 下默认有八个读队列和八个写队列,如果要保证全局顺序消息的话需要在生产端只保留一个读写队列,然后消费端只有一个消费线程,这样会降低 RocketMQ 的高可用和高吞吐量。**分区顺序消息:** 分区顺序消息同样需要生产端和消费端配合,生产端根据同一个订单 ID 把消息路由到同一个 MessageQueue,消费端控制从同一个 MessageQueue 取出的消息不被并发处理。**生成端发送分区顺序消息:** 
 
 ```java
 SendResult sendResult = Producer.send(msg , new MessageQueueSelector() {
@@ -140,7 +140,7 @@ MappedByteBuffer#write() 方法会将数据直接写入到 PageCache 中,然
 ![在这里插入图片描述](../assets/79b95410-9ea6-11ea-853e-a34978cba4d6.png)
 MMAP 虽然可以提高磁盘读写的性能,但是仍然有诸多缺陷和限制,比如:
 *   MMAP 进行文件映射的时候对文件大小有限制,在 1.5GB~2GB 之间,所以 RocketMQ 设计 CommitLog 单个文件 1GB,ConsumeQueue 单个文件 5.7MB;
-*   当不再需要使用 MappedByteBuffer 的时候,需要手动释放占用的虚拟内存。 **PageCache 技术** 为了优化磁盘中数据文件的读写性能,PageCache 技术对数据文件进行了缓存。“对磁盘中数据文件的顺序读写性能接近于对内存的读写性能”,其主要原因就是 PageCache 对磁盘中数据文件的读写进行了优化。
+*   当不再需要使用 MappedByteBuffer 的时候,需要手动释放占用的虚拟内存。**PageCache 技术** 为了优化磁盘中数据文件的读写性能,PageCache 技术对数据文件进行了缓存。“对磁盘中数据文件的顺序读写性能接近于对内存的读写性能”,其主要原因就是 PageCache 对磁盘中数据文件的读写进行了优化。
 * **PageCache 对数据文件的读优化:** 由于读数据文件的时候会先从 PageCache 加载数据,如果 PageCache 没有数据的话,会从磁盘的数据文件中加载数据并且顺序把相邻的数据文件页面预加载到 PageCache 中,这样子如果之后读取的数据文件在 PageCache 中能找到的话就省去了去磁盘加载数据的操作相当于直接读内存。
 * **PageCache 对数据文件的写优化:** 往数据文件中写数据的时候同样先写到 PageCache 中,然后操作系统会定期刷盘把 PageCache 中的数据持久化到磁盘中。
 那么 RocketMQ 具体是怎么使用 PageCache + Mmap 技术的呢?又做了哪些优化呢?
@@ -150,10 +150,10 @@ MMAP 虽然可以提高磁盘读写的性能,但是仍然有诸多缺陷和限
 ![在这里插入图片描述](../assets/c7c074e0-9ea6-11ea-853e-a34978cba4d6.png)
 虽然 PageCache 技术优化了数据文件的读写性能,但是仍然有一些影响其性能的问题:
 操作系统的脏页回写(当空闲内存不足的时候操作系统会将脏页刷到磁盘释放内存空间)、内存回收、内存 SWAP(当内存不足的时候把一部分磁盘空间虚拟成内存使用) 等都会造成消息读写的延迟。
-为了解决消息读写的延迟问题,RocketMQ 还引入了其他优化方案,下文继续分析。 **预分配 MappedFile(内存预分配)** 为了解决 PageCache 技术带来的消息读写延迟问题,RocketMQ 进行了内存预分配处理。
+为了解决消息读写的延迟问题,RocketMQ 还引入了其他优化方案,下文继续分析。**预分配 MappedFile(内存预分配)** 为了解决 PageCache 技术带来的消息读写延迟问题,RocketMQ 进行了内存预分配处理。
 当往 CommitLog 写入消息的时候,会先判断 MappedFileQueue 队列中是否有对应的 MappedFile,如果没有的话会封装一个 AllocateRequest 请求,参数有:文件路径、下下个文件路径、文件大小等,并把请求放到一个 AllocateRequestQueue 队列里面;
 在 Broker 启动的时候会自动创建一个 AllocateMappedFileService 服务线程,该线程不停的运行并从 AllocateRequestQueue 队列消费请求,执行 MappedFile 映射文件的创建和下下个 MappedFile 的预分配逻辑,创建 MappedFile 映射文件的方式有两个:一个是在虚拟内存中通过 MapperByteBuffer 即 Mmap 创建一个 MappedFile 实例,另一个是在堆外内存中通过 DirectByteBuffer 创建一个 MappedFile 实例。创建完当前 MappedFile 实例后还会将下一个 MappedFile 实例创建好并且添加到 MappedFileQueue 队列里面,即预分配 MappedFile。
-![在这里插入图片描述](../assets/251a5070-9ea7-11ea-bf38-950ba54cfedc.png) **mlock 系统调用** 当内存不足的时候可能发生内存 SWAP,读写消息的进程所使用的内存可能会被交换到 SWAP 空间,为了保证 RocketMQ 的吞吐量和读写消息低延迟,肯定希望尽可能使用物理内存,所以 RocketMQ 采用了 mlock 系统调用将读写消息的进程所使用的部分或者全部内存锁在了物理内存中,防止被交换到 SWAP 空间。 **文件预热(内存预热)** mlock 系统调用未必会锁住读写消息进程所使用的物理内存,因为可能会有一些内存分页是写时复制的,所以 RocketMQ 在创建 MapperFile 的过程中,会将 Mmap 映射出的虚拟内存中随机写入一些值,防止内存被交换到 SWAP 空间。
+![在这里插入图片描述](../assets/251a5070-9ea7-11ea-bf38-950ba54cfedc.png) **mlock 系统调用** 当内存不足的时候可能发生内存 SWAP,读写消息的进程所使用的内存可能会被交换到 SWAP 空间,为了保证 RocketMQ 的吞吐量和读写消息低延迟,肯定希望尽可能使用物理内存,所以 RocketMQ 采用了 mlock 系统调用将读写消息的进程所使用的部分或者全部内存锁在了物理内存中,防止被交换到 SWAP 空间。**文件预热(内存预热)** mlock 系统调用未必会锁住读写消息进程所使用的物理内存,因为可能会有一些内存分页是写时复制的,所以 RocketMQ 在创建 MapperFile 的过程中,会将 Mmap 映射出的虚拟内存中随机写入一些值,防止内存被交换到 SWAP 空间。
 由于 Mmap 技术只是创建了虚拟内存地址至物理内存地址的映射表,并没有将磁盘中的数据文件加载到内存中来,如果内存中没有找到数据就会发生缺页中断,而去磁盘读取数据,所以 RocketMQ 采用了 madvise 系统调用将数据文件尽可能多的加载到内存中达到内存预热的效果。
 总结
 ==
diff --git "a/docs/Article/Other/SnowFlake\351\233\252\350\212\261\347\256\227\346\263\225\347\224\237\346\210\220\345\210\206\345\270\203\345\274\217ID.md" "b/docs/Article/Other/SnowFlake\351\233\252\350\212\261\347\256\227\346\263\225\347\224\237\346\210\220\345\210\206\345\270\203\345\274\217ID.md"
index a6815fd0e..a74856ff3 100644
--- "a/docs/Article/Other/SnowFlake\351\233\252\350\212\261\347\256\227\346\263\225\347\224\237\346\210\220\345\210\206\345\270\203\345\274\217ID.md"
+++ "b/docs/Article/Other/SnowFlake\351\233\252\350\212\261\347\256\227\346\263\225\347\224\237\346\210\220\345\210\206\345\270\203\345\274\217ID.md"
@@ -34,7 +34,7 @@ SnowFlake 的结构如下:
 
 #### 时钟回拨处理
 
-**运行时** 若偏差在指定时间(可配置)以内,则等待 2 倍的时间差后开始生成;若两者偏差大于某个设定的时间阈值(可配置),则立即抛出异常,避免阻塞。 **系统重启时**
+**运行时** 若偏差在指定时间(可配置)以内,则等待 2 倍的时间差后开始生成;若两者偏差大于某个设定的时间阈值(可配置),则立即抛出异常,避免阻塞。**系统重启时**
 
 jvmId 变化,基于 mac\\hostip\\jvmid 生成的机器 WorkerId 变化,即使在时钟回拨时也可以尽最大可能避免生成重复 id。
 
diff --git "a/docs/Article/Other/eBay\347\232\204Elasticsearch\346\200\247\350\203\275\350\260\203\344\274\230\345\256\236\350\267\265.md" "b/docs/Article/Other/eBay\347\232\204Elasticsearch\346\200\247\350\203\275\350\260\203\344\274\230\345\256\236\350\267\265.md"
index a92269ce0..0cb2ff407 100644
--- "a/docs/Article/Other/eBay\347\232\204Elasticsearch\346\200\247\350\203\275\350\260\203\344\274\230\345\256\236\350\267\265.md"
+++ "b/docs/Article/Other/eBay\347\232\204Elasticsearch\346\200\247\350\203\275\350\260\203\344\274\230\345\256\236\350\267\265.md"
@@ -83,7 +83,7 @@ Pronto 团队为每种类型的机器和每个支持的 Elasticsearch 版本运
 
 从上图可以看出,随着刷新时间间隔的增加,吞吐量增加,响应时间减少。我们可以使用下面的请求来检查我们有多少段以及刷新和合并花了多少时间。
 
-`Index/_stats?filter_path= indices. **.refresh,indices.** .segments,indices. **.merges`-** 减少副本数量。**对于每个索引请求,Elasticsearch 需要将文档写入主分片和所有副本分片。显然,副本过多会减慢索引速度,但另一方面,这将提高搜索性能。我们将在本文后面讨论这个问题。
+`Index/_stats?filter_path= indices. **.refresh,indices.**.segments,indices. **.merges`-** 减少副本数量。**对于每个索引请求,Elasticsearch 需要将文档写入主分片和所有副本分片。显然,副本过多会减慢索引速度,但另一方面,这将提高搜索性能。我们将在本文后面讨论这个问题。
 
 ![img](../assets/47d20b17cdc09959f3e1eedb03a296de.png) 性能和副本数之间的关系
 
@@ -232,4 +232,4 @@ Pronto 团队建立了基于 [Gatling](https://gatling.io/) 的在线性能分
 
 本文总结了在设计满足高期望的采集和搜索性能的 Elasticsearch 集群时应该考虑的索引 / 分片 / 副本设计以及一些其他配置,还说明了 Pronto 如何在策略上帮助客户进行初始规模调整、索引设计和调优以及性能测试。截至今天,Pronto 团队已经帮助包括订单管理系统(OMS)和搜索引擎优化(SEO)在内的众多客户实现了苛刻的性能目标,从而为 eBay 的关键业务作出了贡献。
 
-Elasticsearch 的性能取决于很多因素,包括文档结构、文档大小、索引设置 / 映射、请求率、数据集大小和查询命中次数等等。针对一种情况的建议不一定适用于另一种情况,因此,彻底进行性能测试、收集数据、根据负载调整配置以及优化集群以满足性能要求非常重要。 **查看英文原文:** [https://www.ebayinc.com/stories/blogs/tech/elasticsearch-performance-tuning-practice-at-ebay/](https://www.ebayinc.com/stories/blogs/tech/elasticsearch-performance-tuning-practice-at-ebay/)
+Elasticsearch 的性能取决于很多因素,包括文档结构、文档大小、索引设置 / 映射、请求率、数据集大小和查询命中次数等等。针对一种情况的建议不一定适用于另一种情况,因此,彻底进行性能测试、收集数据、根据负载调整配置以及优化集群以满足性能要求非常重要。**查看英文原文:** [https://www.ebayinc.com/stories/blogs/tech/elasticsearch-performance-tuning-practice-at-ebay/](https://www.ebayinc.com/stories/blogs/tech/elasticsearch-performance-tuning-practice-at-ebay/)
diff --git "a/docs/Article/Other/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\220\206\350\256\272\344\270\216\347\256\227\346\263\225.md" "b/docs/Article/Other/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\220\206\350\256\272\344\270\216\347\256\227\346\263\225.md"
index 4b78d0dfd..073f2fecb 100644
--- "a/docs/Article/Other/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\220\206\350\256\272\344\270\216\347\256\227\346\263\225.md"
+++ "b/docs/Article/Other/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\220\206\350\256\272\344\270\216\347\256\227\346\263\225.md"
@@ -71,13 +71,13 @@
 
 ## 2. 分布式带来的挑战
 
-**多个服务器的磁盘如何高效利用?** 为了有更大的空间存储文件,不得不将多个服务器组合起来,这就是分布式文件系统所解决的问题。NFS、DFS、GFS 等都是分布式文件系统领域最核心的模型。在此之上衍生出了许许多多工业级的分布式文件系统,比如 FastDFS、HDFS 等。 **数据的复制怎么保证一致?** 前面有提到,一个 MySQL 实例无法保证数据不丢失,所以需要一个甚至多个副本,这就不可避免的要做数据同步。多副本同步数据时,如何保证数据同步完成后,所有实例的数据能够保持一致,是一件非常难的事情。
+**多个服务器的磁盘如何高效利用?** 为了有更大的空间存储文件,不得不将多个服务器组合起来,这就是分布式文件系统所解决的问题。NFS、DFS、GFS 等都是分布式文件系统领域最核心的模型。在此之上衍生出了许许多多工业级的分布式文件系统,比如 FastDFS、HDFS 等。**数据的复制怎么保证一致?** 前面有提到,一个 MySQL 实例无法保证数据不丢失,所以需要一个甚至多个副本,这就不可避免的要做数据同步。多副本同步数据时,如何保证数据同步完成后,所有实例的数据能够保持一致,是一件非常难的事情。
 
-这个也是本文的主要内容,后面会做详细的分析。 **多个数据库如何执行事务?** 对于微服务来说,一个请求操作不同的数据库是很常见的。一些很重要的操作,需要保证严格的事务,单个数据库比较好操作。但是如果操作的是多个数据库,就需要一些特殊的手段来达成这个目的。
+这个也是本文的主要内容,后面会做详细的分析。**多个数据库如何执行事务?** 对于微服务来说,一个请求操作不同的数据库是很常见的。一些很重要的操作,需要保证严格的事务,单个数据库比较好操作。但是如果操作的是多个数据库,就需要一些特殊的手段来达成这个目的。
 
-分布式事务的解决方案有很多,比如二阶段提交、三阶段提交、XA、saga 柔性事务等等。分布式事务不是本篇文章主要讨论的问题。 **如何使缓存更加高效?** 单机环境下,做本地缓存就可以了。但是当机器越来越多的时候,如果每台机器都做本地缓存,那么就会出现数据冗余,降低了缓存的可用性。这个时候,就需要一个独立于应用之外的分布式缓存出现。所有的应用都共享一个分布式缓存,这样就可以避免同一份数据被多次缓存的问题。 **分布式环境下如何执行定时任务?** 有一些任务需要定时执行,单机环境下直接定时执行就好了。但是分布式环境下,多个机器可能会同时执行同一个定时任务,会造成重复执行,不仅可能造成数据错误,而且浪费了资源。所以需要有一种方式可以让一个分布式集群中只有一台机器可以执行这个任务。一般情况下,通过分布式锁来解决这个问题。
+分布式事务的解决方案有很多,比如二阶段提交、三阶段提交、XA、saga 柔性事务等等。分布式事务不是本篇文章主要讨论的问题。**如何使缓存更加高效?** 单机环境下,做本地缓存就可以了。但是当机器越来越多的时候,如果每台机器都做本地缓存,那么就会出现数据冗余,降低了缓存的可用性。这个时候,就需要一个独立于应用之外的分布式缓存出现。所有的应用都共享一个分布式缓存,这样就可以避免同一份数据被多次缓存的问题。**分布式环境下如何执行定时任务?** 有一些任务需要定时执行,单机环境下直接定时执行就好了。但是分布式环境下,多个机器可能会同时执行同一个定时任务,会造成重复执行,不仅可能造成数据错误,而且浪费了资源。所以需要有一种方式可以让一个分布式集群中只有一台机器可以执行这个任务。一般情况下,通过分布式锁来解决这个问题。
 
-全局只有一把锁,哪台机器获取到了这个锁,哪台机器就可以执行这个任务。 **小结**
+全局只有一把锁,哪台机器获取到了这个锁,哪台机器就可以执行这个任务。**小结**
 
 为了解决分布式带来的挑战,分布式存储系统、分布式缓存、分布式锁、分布式事务等被一个一个的提出来。每一个技术都解决了一种特定的业务。
 
@@ -194,7 +194,7 @@ CAP 定理中的一致性指的是:一个节点进行数据更新操作后,
 
 #### 3.4.3 定理证明
 
-由于网络总是不可靠的,而分布式集群的基础就是多机网络通信,所以 **分区是不可避免的** ,因为分区总可能出现,如果允许在分区出现的时候,各子集群可以正常对外提供服务,那么: **CP** 分区之间无法通信,数据互相不可达,对于写请求,是无法让所有的节点都更新成最新值的。
+由于网络总是不可靠的,而分布式集群的基础就是多机网络通信,所以 **分区是不可避免的**,因为分区总可能出现,如果允许在分区出现的时候,各子集群可以正常对外提供服务,那么: **CP** 分区之间无法通信,数据互相不可达,对于写请求,是无法让所有的节点都更新成最新值的。
 
 如果要保证一致性,那写请求就只能返回失败,这就违反了可用性。
 
@@ -202,9 +202,9 @@ CAP 定理中的一致性指的是:一个节点进行数据更新操作后,
 
 > CP 系统,出现分区,不能保证数据写入到所有节点,返回写入失败,违反可用性。**AP** 如果一定要保证可用性,那么写请求的数据就只能存在当前分区的节点上,而其他分区的节点无法更新为最新值,每个分区内同一数据的值可能都是不一样的,这样就违反了一致性。此时系统满足 AP。
 >
-> CP 系统,出现分区,为保证可用性,导致各个分区内同一个数据的值不一样,违反一致性。 **CA** 如果当分区出现时候,直接不对外提供服务,那么可以同时满足 CA。
+> CP 系统,出现分区,为保证可用性,导致各个分区内同一个数据的值不一样,违反一致性。**CA** 如果当分区出现时候,直接不对外提供服务,那么可以同时满足 CA。
 >
-> CA 系统,不允许出现分区,一致性和可用性都可以满足,违反分区容忍性。 **结论** 根据上述推算,一个分布式系统无法同时满足 CAP 三个条件,只能选择其中两个,这就是 CAP 定理。
+> CA 系统,不允许出现分区,一致性和可用性都可以满足,违反分区容忍性。**结论** 根据上述推算,一个分布式系统无法同时满足 CAP 三个条件,只能选择其中两个,这就是 CAP 定理。
 
 #### 3.4.5 缺陷
 
@@ -220,7 +220,7 @@ BASE 理论是 eBay 架构师结合 CAP 定理与实际分布式应用设计总
 
 其中,“BASE”是“Basically Available(基本可用)”、“Soft State(软状态)”和“Eventually Consistent(最终一致性)”三个短语的缩写。
 
-> CAP 是一个 **定理** ,而 BASE 是一个 **理论** 。BASE 理论本质上是 CAP 定理满足 P 的条件下,在 C 和 A 之间找到一个符合特定系统实际情况的平衡。
+> CAP 是一个 **定理**,而 BASE 是一个 **理论** 。BASE 理论本质上是 CAP 定理满足 P 的条件下,在 C 和 A 之间找到一个符合特定系统实际情况的平衡。
 
 #### 3.5.1 来源
 
@@ -247,7 +247,7 @@ BASE 理论是 eBay 架构师结合 CAP 定理与实际分布式应用设计总
 - 响应时间的损失。查询余票正常情况下 1s 即可拿到结果,但是出现故障的时候,可能需要 2s。
 - 系统功能的损失:抢票的时候,对用户进行限流,虽然无法完成既定功能,但是提升了用户体验。
 
-**Soft State(软状态)** 允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许数据异步同步过程中存在延迟。 **Eventually Consistent(最终一致性)** 允许数据同步存在延迟,但是需要保证在规定的时间内,数据最终会达到一致。时间期限需要综合实际情况进行设计。
+**Soft State(软状态)** 允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许数据异步同步过程中存在延迟。**Eventually Consistent(最终一致性)** 允许数据同步存在延迟,但是需要保证在规定的时间内,数据最终会达到一致。时间期限需要综合实际情况进行设计。
 
 ### 3.6 一致性
 
@@ -392,9 +392,9 @@ B: get x
 - 进程 B:只在读到 1
 - 进程 D:只能读到 2
 
-漏斗模型更能体现出线性一致性的要求,但是从实际情况来看,很难甚至无法明确出拐点到底出现在哪个时间点。所以,一般从生产情况出发,进 **程 B 和** 进 **程 D 读到 1 和 2 都是可以满足线性一致性的。** > 漏斗模型是一种理论价值很足的模型,但是很难用于生产。 **顺序一致性** > Sequential consistency 的是 Lamport 在 1979 年首次提出的。(参看他的论文 How to make a multiprocessor computer that correctly executes multiprocess programs)
+漏斗模型更能体现出线性一致性的要求,但是从实际情况来看,很难甚至无法明确出拐点到底出现在哪个时间点。所以,一般从生产情况出发,进 **程 B 和** 进 **程 D 读到 1 和 2 都是可以满足线性一致性的。** > 漏斗模型是一种理论价值很足的模型,但是很难用于生产。**顺序一致性** > Sequential consistency 的是 Lamport 在 1979 年首次提出的。(参看他的论文 How to make a multiprocessor computer that correctly executes multiprocess programs)
 
-线性一致性的实现成本是很高的,顺序一致性的要求比线性一致性低一些。 **要求:变更是原子的,不要求执行顺序按照真实发生的时间进行,但是单个进程内的操作顺序必须与编码时相同。**
+线性一致性的实现成本是很高的,顺序一致性的要求比线性一致性低一些。**要求:变更是原子的,不要求执行顺序按照真实发生的时间进行,但是单个进程内的操作顺序必须与编码时相同。**
 
 怎么理解呢?线性一致性要求所有的操作在全局是有序的,而顺序一致性弱化了这个要求,只需要在进程内有序即可,不需要全局有序。
 
@@ -426,7 +426,7 @@ B: get x
 
 一般 CPU 的设计,比如 x86 架构下,是允许指令重排的,这就导致顺序一致性可能会被破坏。CPU 一般不禁止指令重排,而把它抛给上层去控制,这也就是为什么会有 CAS、内存屏障、原子变量等技术,编码人员根据自己的需要,插入内存屏障阻止指令重排,最终达到顺序一致性。
 
-感兴趣的同学可以去了解一下 MESI 协议和缓存一致性技术,这里简单提一下,就不扩展了。 **最终一致性**
+感兴趣的同学可以去了解一下 MESI 协议和缓存一致性技术,这里简单提一下,就不扩展了。**最终一致性**
 
 最终一致性是弱一致性里最常见的一种,一般系统的设计都会考虑设计为最终一致性。最终一致性有分为以下几种:
 
@@ -754,7 +754,7 @@ Acceptor 可以在任何时候响应 prepare 请求;而对于 accept 请求,
 > 1. 如果 Proposer 收到来自半数以上的 Acceptor 对于它的 prepare 请求(编号为 n )的响应,那么它就会发送一个针对编号为 n,value 值为 v 的提案的 accept 请求给 Acceptors,在这里 v 是收到的响应中编号最大的提案的值,如果响应中不包含提案,那么它可以是任意值。
 > 2. 如果 Acceptor 收到一个针对编号 n 的提案的 accept 请求,只要它还未对编号大于 n 的 prepare 请求作出响应,它就可以通过这个提案。
 
-一个 Proposer 可能产生多个提案,只要它是遵循如上的算法即可。它可以在任意时刻丢弃某个提案。如果某个 Proposer 已经在试图生成编号更大的提案,那么丢弃未尝不是一个好的选择。因此如果一个 Acceptor 因为已经收到更大编号的 prepare 请求而忽略某个 prepare 或者 accept 请求时,那么它也应当通知它的 Proposer,然后该 Proposer 应该丢弃该提案。当然,这只是一个不影响正确性的性能优化。 **Learner 获取被选定的提案值**
+一个 Proposer 可能产生多个提案,只要它是遵循如上的算法即可。它可以在任意时刻丢弃某个提案。如果某个 Proposer 已经在试图生成编号更大的提案,那么丢弃未尝不是一个好的选择。因此如果一个 Acceptor 因为已经收到更大编号的 prepare 请求而忽略某个 prepare 或者 accept 请求时,那么它也应当通知它的 Proposer,然后该 Proposer 应该丢弃该提案。当然,这只是一个不影响正确性的性能优化。**Learner 获取被选定的提案值**
 
 为了获取被选定的值,一个 Learner 必须确定一个提案已经被半数以上的 Acceptor 通过。最明显的算法是,让每个 Acceptor,只要它通过了一个提案,就通知所有的 Learners,将它通过的提案告知它们。这可以让 Learners 尽快的找出被选定的值,但是它需要每个 Acceptor 都要与每个 Learner 通信—所需通信的次数等于二者个数的乘积。
 
@@ -942,13 +942,13 @@ Raft 使用一种心跳机制来触发领导人的选取。当节点启动时,
 
 **当前节点赢得选举** 当节点收到半数以上的投票时,该节点当选为本次任期的 Leader,当选为 Leader 后,需要按照 Raft 的规定向其他所有节点发送心跳,从而广播自己的 Leader 地位。
 
-为了使 Leader 尽快的选出来,Raft 规定:每一个 Follower 在一个任期内,只能给一个 Candidate 投票,这样就避免了多个 Candidate 同时获得了多数投票的情况。 **其他节点赢得选举** 当节点收到来气其他节点的 Leader 心跳后,这个时候节点会去比较心跳里的任期和自己当前的任期。如果心跳的任期不比自己当前的任期小,就认可该 Leader,自己回归 Follower 状态;如果心跳的任期小于自己,则忽略该请求,继续等待本轮选举结果。
+为了使 Leader 尽快的选出来,Raft 规定:每一个 Follower 在一个任期内,只能给一个 Candidate 投票,这样就避免了多个 Candidate 同时获得了多数投票的情况。**其他节点赢得选举** 当节点收到来气其他节点的 Leader 心跳后,这个时候节点会去比较心跳里的任期和自己当前的任期。如果心跳的任期不比自己当前的任期小,就认可该 Leader,自己回归 Follower 状态;如果心跳的任期小于自己,则忽略该请求,继续等待本轮选举结果。
 
-收到其他 Leader 发来的心跳有两种常见原因。一是另一个节点通过正常方式赢得了选举;另一种情况是上一个任期的 Leader 因为某种原因一段时间没有发送心跳,导致了本次任期的开始,在本次选举结束前,之前的 Leader 又恢复了服务,开始发送心跳,这个时候心跳里的任期号就会小于当前任期号。 **没有节点赢得选举** 一段时间后没有收到 Leader 发来的心跳,本节点也没有赢得本次选举,那么会重新发起一次选举。出现这种情况的原因是有多个 Candidate 竞争 Leader,Follower 将票投给不同的 Candidate 导致没有任何一个 Candidate 的票数超过一半,也就是分票了。
+收到其他 Leader 发来的心跳有两种常见原因。一是另一个节点通过正常方式赢得了选举;另一种情况是上一个任期的 Leader 因为某种原因一段时间没有发送心跳,导致了本次任期的开始,在本次选举结束前,之前的 Leader 又恢复了服务,开始发送心跳,这个时候心跳里的任期号就会小于当前任期号。**没有节点赢得选举** 一段时间后没有收到 Leader 发来的心跳,本节点也没有赢得本次选举,那么会重新发起一次选举。出现这种情况的原因是有多个 Candidate 竞争 Leader,Follower 将票投给不同的 Candidate 导致没有任何一个 Candidate 的票数超过一半,也就是分票了。
 
 这个问题很难从算法层面解决。Raft 为了解决这个问题,使用了“打散”的方式。分别对超时时间和再次发起新一轮选举的时间进行打散。也就是每个节点的选举超时时间是不同的,重新发起新的选举之前随机 sleep 的时间也是不同的。这样可以避免同一时间有大量的 Candidate 参与 Leader 选举。
 
-这种机制使得在大多数情况下只有一个节点会率先超时,它会在其它节点超时之前赢得选举并且向其它节点发送心跳信息,并且能够减小在新的选举中一开始选票就被瓜分的可能性。 **RequestVote RPC**
+这种机制使得在大多数情况下只有一个节点会率先超时,它会在其它节点超时之前赢得选举并且向其它节点发送心跳信息,并且能够减小在新的选举中一开始选票就被瓜分的可能性。**RequestVote RPC**
 
 ![在这里插入图片描述](../assets/4c6c39e0-465f-11ea-869e-dd0ddfb7a363.jpg)
 
@@ -1001,11 +1001,11 @@ RPC 的参数:
 
 如果当前节点,下标为 prevLogIndex 的日志条目的 term 和 prevLogTerm 不一样,拒绝该请求。
 
-这两种情况都是由于过期 Leader 发送 RPC 导致的。 **append 日志** 如果 AppendEntries RPC 符合要求,则当前节点需要将 RPC 中的所有日志条目追加到本节点的日志中。 **覆盖本节点错误日志** 如果 AppendEntries RPC 中的日志条目和本节点中的日志有冲突,则用 AppendEntries RPC 的日志条目将本节点的日志覆盖掉。从而保证 Follower 的日志和 Leader 的日志是完全一致的。 **提交日志** 只有被半数以上节点成功复制的日志条目才能被提交,而这个信息只有 Leader 知道,Follower 自身无法确认日志条目是否可以被提交。
+这两种情况都是由于过期 Leader 发送 RPC 导致的。**append 日志** 如果 AppendEntries RPC 符合要求,则当前节点需要将 RPC 中的所有日志条目追加到本节点的日志中。**覆盖本节点错误日志** 如果 AppendEntries RPC 中的日志条目和本节点中的日志有冲突,则用 AppendEntries RPC 的日志条目将本节点的日志覆盖掉。从而保证 Follower 的日志和 Leader 的日志是完全一致的。**提交日志** 只有被半数以上节点成功复制的日志条目才能被提交,而这个信息只有 Leader 知道,Follower 自身无法确认日志条目是否可以被提交。
 
 AppendEntries RPC 的参数 leaderCommit 将这个信息带给了 Follower。这个值是一个下标,意思是这个下标以及之前的所有日志条目都是安全的,可以被提交到状态机中执行。
 
-这里还有一个问题是:leaderCommit 可能大于当前节点日志的长度,因为 Follower 可能并没有复制到所有的日志。所以,Follower 的 `commitId = min(leaderCommit, index of last new entry)`。 **综合** ![在这里插入图片描述](../assets/628ee1f0-465f-11ea-815b-99042f14883a.jpg)
+这里还有一个问题是:leaderCommit 可能大于当前节点日志的长度,因为 Follower 可能并没有复制到所有的日志。所以,Follower 的 `commitId = min(leaderCommit, index of last new entry)`。**综合**![在这里插入图片描述](../assets/628ee1f0-465f-11ea-815b-99042f14883a.jpg)
 
 如上图:当最上边的 Leader 刚刚当选时,追随者日志可能有 a~f 这几种情况。图中,一个格子表示一个日志条目;格子中的数字是它的任期。一个追随者可能会丢失一些条目(a, b);可能多出来一些未提交的条目(c, d);或者两种情况都有(e, f)。
 
diff --git "a/docs/Article/Other/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225Raft.md" "b/docs/Article/Other/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225Raft.md"
index a28642f9f..bf686269c 100644
--- "a/docs/Article/Other/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225Raft.md"
+++ "b/docs/Article/Other/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225Raft.md"
@@ -31,7 +31,7 @@ Raft 将共识问题分解三个子问题:
 1. **follower 从节点** :
 
    - 节点默认是 follower;
-   - 如果 **刚刚开始** 或 **和 leader 通信超时** ,follower 会发起选举,变成 candidate,然后去竞选 leader;
+   - 如果 **刚刚开始** 或 **和 leader 通信超时**,follower 会发起选举,变成 candidate,然后去竞选 leader;
    - 如果收到其他 candidate 的竞选投票请求,按照 **先来先得** & **每个任期只能投票一次** 的投票原则投票;
 
 2. **candidate 候选者** :
@@ -65,7 +65,7 @@ Raft 将共识问题分解三个子问题:
 
    - **投票原则** :当多个 Candidate 竞选 Leader 时:
 
-     - 一个任期内,follower 只会 **投票一次票** ,且投票 **先来显得** ;
+     - 一个任期内,follower 只会 **投票一次票**,且投票 **先来显得** ;
      - Candidate 存储的日志至少要和 follower 一样新( **安全性准则** ),否则拒绝投票;
 
 5. **投票未超过半数,选举失败** :
@@ -80,7 +80,7 @@ Raft 将共识问题分解三个子问题:
 - 如果请求的任期 TermId 不小于 Candidate 当前任期 TermId,那么 Candidate 会承认该 Leader 的合法地位并转化为 Follower;
 - 否则,拒绝这次请求,并继续保持 Candidate;
 
-简单的多, **Leader Election 领导选举** 通过若干的投票原则,保证一次选举有且仅可能最多选出一个 leader,从而解决了脑裂问题。
+简单的多,**Leader Election 领导选举** 通过若干的投票原则,保证一次选举有且仅可能最多选出一个 leader,从而解决了脑裂问题。
 
 ## 2.2 Log Replication 日志复制
 
@@ -97,7 +97,7 @@ follower 收到日志复制请求的处理流程:
 3. 如此循环往复,直到找到一个共同的任期号&日志索引。此时 follower 从这个索引值开始复制,最终和 leader 节点日志保持一致;
 4. 日志复制过程中,Leader 会无限重试直到成功。如果超过半数的节点复制日志成功,就可以任务当前数据请求达成了共识,即日志可以 commite 提交了;
 
-综上, **Log Replication 日志复制** 有两个特点:
+综上,**Log Replication 日志复制** 有两个特点:
 
 1. 如果在不同日志中的两个条目有着相同索引和任期号,则所存储的命令是相同的,这点是由 leader 来保证的;
 2. 如果在不同日志中的两个条目有着相同索引和任期号,则它们之间所有条目完全一样,这点是由日志复制的规则来保证的;
@@ -108,7 +108,7 @@ follower 收到日志复制请求的处理流程:
 
 **日志不一致问题** :在正常情况下,leader 和 follower 的日志复制能够保证整个集群的一致性,但是遇到 leader 崩溃的时候,leader 和 follower 日志可能出现了不一致的状态,此时 follower 相比 leader 缺少部分日志。
 
-为了解决数据不一致性,Raft 算法规定 **follower 强制复制 leader 节日的日志** ,即 follower 不一致日志都会被 leader 的日志覆盖,最终 follower 和 leader 保持一致。简单的说,从前向后寻找 follower 和 leader 第一个公共 LogIndex 的位置,然后从这个位置开始,follower 强制复制 leader 的日志。但是这么多还有其他的安全性问题,所以需要引入 **Safety 安全性** 规则 。
+为了解决数据不一致性,Raft 算法规定 **follower 强制复制 leader 节日的日志**,即 follower 不一致日志都会被 leader 的日志覆盖,最终 follower 和 leader 保持一致。简单的说,从前向后寻找 follower 和 leader 第一个公共 LogIndex 的位置,然后从这个位置开始,follower 强制复制 leader 的日志。但是这么多还有其他的安全性问题,所以需要引入 **Safety 安全性** 规则 。
 
 ## 2.3 Safety 安全性
 
@@ -118,7 +118,7 @@ follower 收到日志复制请求的处理流程:
 
 选举安全性要求一个任期 Term 内只能有一个 leader,即不能出现脑裂现象,否者 raft 的日志复制原则很可能出现数据覆盖丢失的问题。Raft 算法通过规定若干投票原则来解决这个问题:
 
-- 一个任期内,follower 只会 **投票一次票** ,且先来先得;
+- 一个任期内,follower 只会 **投票一次票**,且先来先得;
 - Candidate 存储的日志至少要和 follower 一样新;
 - 只有获得超过半数投票才有机会成为 leader;
 
@@ -126,7 +126,7 @@ follower 收到日志复制请求的处理流程:
 
 Raft 算法规定,所有的数据请求都要交给 leader 节点处理,要求
 
-1. leader 只能日志追加日志, **不能覆盖日志** ;
+1. leader 只能日志追加日志,**不能覆盖日志** ;
 2. 只有 leader 的日志项才能被提交,follower 不能接收写请求和提交日志;
 3. 只有已经提交的日志项,才能被应用到状态机中;
 4. 选举时限制新 leader 日志包含所有已提交日志项;
diff --git "a/docs/Article/Other/\345\212\250\346\200\201\344\273\243\347\220\206\347\247\215\347\261\273\345\217\212\345\216\237\347\220\206\357\274\214\344\275\240\347\237\245\351\201\223\345\244\232\345\260\221\357\274\237.md" "b/docs/Article/Other/\345\212\250\346\200\201\344\273\243\347\220\206\347\247\215\347\261\273\345\217\212\345\216\237\347\220\206\357\274\214\344\275\240\347\237\245\351\201\223\345\244\232\345\260\221\357\274\237.md"
index 45bb546db..b3c0dcd1c 100644
--- "a/docs/Article/Other/\345\212\250\346\200\201\344\273\243\347\220\206\347\247\215\347\261\273\345\217\212\345\216\237\347\220\206\357\274\214\344\275\240\347\237\245\351\201\223\345\244\232\345\260\221\357\274\237.md"
+++ "b/docs/Article/Other/\345\212\250\346\200\201\344\273\243\347\220\206\347\247\215\347\261\273\345\217\212\345\216\237\347\220\206\357\274\214\344\275\240\347\237\245\351\201\223\345\244\232\345\260\221\357\274\237.md"
@@ -14,7 +14,7 @@
 
 为了下文叙述的方便,先来回顾一下静态代理。生活中身边不乏做微商的朋友,其实就是我们常说的微商代理,目的就是在朋友圈之类的为厂家宣传产品,厂家委托微商为其引流或者销售商品。将这个场景进行抽象,我们可以把微商代理看成“代理类”,厂家看成“委托类”或者“被代理类”等。
 
-那什么是静态代理呐? **若代理类在程序运行前就已经存在,那么这种代理方式就是静态代理。** 因此在程序运行前,我们都会在程序中定义好代理类。同时,静态代理中的代理类和委托类都会实现同一接口或者派生自相同的父类。接下来,我们将会用一段代码进行演示,Factory 代表厂家,即委托类,BusinessAgent 代表微商,即代理类。代理类和委托类都实现 Operator 接口:
+那什么是静态代理呐?**若代理类在程序运行前就已经存在,那么这种代理方式就是静态代理。** 因此在程序运行前,我们都会在程序中定义好代理类。同时,静态代理中的代理类和委托类都会实现同一接口或者派生自相同的父类。接下来,我们将会用一段代码进行演示,Factory 代表厂家,即委托类,BusinessAgent 代表微商,即代理类。代理类和委托类都实现 Operator 接口:
 
 ```java
 public interface Operator {
@@ -134,7 +134,7 @@ expand ....
 expand cost time is:0s
 ```
 
-这里我们将委托类对象 new Factory() 作为 AgencyHandler 构造方法入参创建了 agencyHandler 对象,然后通过 Proxy.newProxyInstance(…) 方法创建了一个代理对象,实际代理类就是这个时候动态生成的。我们调用该代理对象的方法就会调用到 agencyHandler 的 invoke 方法(类似于静态代理),而 invoke 方法实现中调用委托类对象 new Factory() 相应的 method(类似于静态代理)。因此, **动态代理内部可以看成是由两组静态代理构成** 。
+这里我们将委托类对象 new Factory() 作为 AgencyHandler 构造方法入参创建了 agencyHandler 对象,然后通过 Proxy.newProxyInstance(…) 方法创建了一个代理对象,实际代理类就是这个时候动态生成的。我们调用该代理对象的方法就会调用到 agencyHandler 的 invoke 方法(类似于静态代理),而 invoke 方法实现中调用委托类对象 new Factory() 相应的 method(类似于静态代理)。因此,**动态代理内部可以看成是由两组静态代理构成** 。
 
 ### 代理类源码分析
 
@@ -609,7 +609,7 @@ public class Main {
 - 实现接口方法,调用 InvocationHandler 的 invoke 方法
 - 在 InvocationHandler 中 invoke 方法使用反射调用被代理类的方法
 
-因此这里最关键的就是生成一个 **代理类** ,因此就是 JDK 动态代理中这一步的实现:
+因此这里最关键的就是生成一个 **代理类**,因此就是 JDK 动态代理中这一步的实现:
 
 ```java
 Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
@@ -716,7 +716,7 @@ public class Main {
 
 前面介绍 CGLib 时提到过,它底层是采用 ASM 作为字节码处理,生成的代理类就是使用 ASM 实现的。因此 ASM 库是一个基于 Java 字节码层面的代码分析和修改工具,可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。因此要想实际操作 ASM,对 class 文件格式的十分熟悉。
 
-ASM 中的 **每个 API 都和 class 文件格式中的特定部分相吻合** ,同时是采用 **访问者模式设计** 的。
+ASM 中的 **每个 API 都和 class 文件格式中的特定部分相吻合**,同时是采用 **访问者模式设计** 的。
 
 ASM 中比较重要的类有:
 
diff --git "a/docs/Article/Other/\345\223\215\345\272\224\345\274\217\346\236\266\346\236\204\344\270\216RxJava\345\234\250\346\234\211\350\265\236\351\233\266\345\224\256\347\232\204\345\256\236\350\267\265.md" "b/docs/Article/Other/\345\223\215\345\272\224\345\274\217\346\236\266\346\236\204\344\270\216RxJava\345\234\250\346\234\211\350\265\236\351\233\266\345\224\256\347\232\204\345\256\236\350\267\265.md"
index 92638e073..88b782018 100644
--- "a/docs/Article/Other/\345\223\215\345\272\224\345\274\217\346\236\266\346\236\204\344\270\216RxJava\345\234\250\346\234\211\350\265\236\351\233\266\345\224\256\347\232\204\345\256\236\350\267\265.md"
+++ "b/docs/Article/Other/\345\223\215\345\272\224\345\274\217\346\236\266\346\236\204\344\270\216RxJava\345\234\250\346\234\211\350\265\236\351\233\266\345\224\256\347\232\204\345\256\236\350\267\265.md"
@@ -4,7 +4,7 @@
 
 ## 实践响应式架构
 
-响应式架构是指业务组件和功能由 **事件驱动** ,每个组件异步驱动,可以并行和分布式部署及运行。
+响应式架构是指业务组件和功能由 **事件驱动**,每个组件异步驱动,可以并行和分布式部署及运行。
 
 响应式架构可以带来以下优势:
 
@@ -74,7 +74,7 @@ UpgradeItem.listItems(manager, shop)
 
 随着微服务架构兴起,我们将不同的业务域拆分成不同的系统。这样方便了系统的维护,提升了系统的扩展性,但是给上层业务系统也带来了很多麻烦。往往我们为了展示一个页面会涉及到2-3个或更多的应用,而多次的分布式调用不但使得系统的rt增加,也使得核心页面的出错风险更高。
 
-**降低rt** :在假设第三方接口已经达到性能顶点的情况下,并发是解决多次分布式调用降低rt的常用方法。 **自动降级** :传统编程方法中,自动降级处理,意味着我们代码中会出现一大堆try/catch,而使用rxjava,我们可以直接定义当流处理异常时,程序需要怎么做,这样的代码看起来非常简洁。
+**降低rt** :在假设第三方接口已经达到性能顶点的情况下,并发是解决多次分布式调用降低rt的常用方法。**自动降级** :传统编程方法中,自动降级处理,意味着我们代码中会出现一大堆try/catch,而使用rxjava,我们可以直接定义当流处理异常时,程序需要怎么做,这样的代码看起来非常简洁。
 
 商品搜索作为商品管理的核心入口,根据不同场景聚合商品、优惠、库存等信息。由于商品列表页展示的信息涉及到多服务数据的整合,一方面需要保证整个接口的rt,另一方面不希望由于一个商品数据或外部服务的异常影响到整个商品列表的加载。因此该场景非常适用于RxJava。
 
diff --git "a/docs/Article/Other/\345\246\202\344\275\225\344\274\230\351\233\205\345\234\260\350\256\260\345\275\225\346\223\215\344\275\234\346\227\245\345\277\227\357\274\237.md" "b/docs/Article/Other/\345\246\202\344\275\225\344\274\230\351\233\205\345\234\260\350\256\260\345\275\225\346\223\215\344\275\234\346\227\245\345\277\227\357\274\237.md"
index 489ef0d00..4f6b85ab9 100644
--- "a/docs/Article/Other/\345\246\202\344\275\225\344\274\230\351\233\205\345\234\260\350\256\260\345\275\225\346\223\215\344\275\234\346\227\245\345\277\227\357\274\237.md"
+++ "b/docs/Article/Other/\345\246\202\344\275\225\344\274\230\351\233\205\345\234\260\350\256\260\345\275\225\346\223\215\344\275\234\346\227\245\345\277\227\357\274\237.md"
@@ -4,7 +4,7 @@
 
 **1. 操作日志的使用场景** -----------------
 
-![img](../assets/v2-19e2c8c94f379833b38b41ca4d759bdb_1440w.jpg) **系统日志和操作日志的区别**  **系统日志** :系统日志主要是为开发排查问题提供依据,一般打印在日志文件中;系统日志的可读性要求没那么高,日志中会包含代码的信息,比如在某个类的某一行打印了一个日志。 **操作日志** :主要是对某个对象进行新增操作或者修改操作后记录下这个新增或者修改,操作日志要求可读性比较强,因为它主要是给用户看的,比如订单的物流信息,用户需要知道在什么时间发生了什么事情。再比如,客服对工单的处理记录信息。
+![img](../assets/v2-19e2c8c94f379833b38b41ca4d759bdb_1440w.jpg) **系统日志和操作日志的区别**  **系统日志** :系统日志主要是为开发排查问题提供依据,一般打印在日志文件中;系统日志的可读性要求没那么高,日志中会包含代码的信息,比如在某个类的某一行打印了一个日志。**操作日志** :主要是对某个对象进行新增操作或者修改操作后记录下这个新增或者修改,操作日志要求可读性比较强,因为它主要是给用户看的,比如订单的物流信息,用户需要知道在什么时间发生了什么事情。再比如,客服对工单的处理记录信息。
 
 操作日志的记录格式大概分为下面几种:
 
@@ -27,7 +27,7 @@ log.info("订单已经创建,订单编号:{}", orderNo)
 log.info("修改了订单的配送地址:从“{}”修改到“{}”, "金灿灿小区", "银盏盏小区")
 ```
 
-这种方式的操作记录需要解决三个问题。 **问题一:操作人如何记录** 借助 SLF4J 中的 MDC 工具类,把操作人放在日志中,然后在日志中统一打印出来。首先在用户的拦截器中把用户的标识 Put 到 MDC 中。
+这种方式的操作记录需要解决三个问题。**问题一:操作人如何记录** 借助 SLF4J 中的 MDC 工具类,把操作人放在日志中,然后在日志中统一打印出来。首先在用户的拦截器中把用户的标识 Put 到 MDC 中。
 
 ```java
 @Component
@@ -133,7 +133,7 @@ public void modifyAddress(updateDeliveryRequest request){
 }
 ```
 
-我们可以在注解的操作日志上记录固定文案,这样业务逻辑和业务代码可以做到解耦,让我们的业务代码变得纯净起来。可能有同学注意到,上面的方式虽然解耦了操作日志的代码,但是记录的文案并不符合我们的预期,文案是静态的,没有包含动态的文案,因为我们需要记录的操作日志是: 用户%s修改了订单的配送地址,从“%s”修改到“%s”。接下来,我们介绍一下如何优雅地使用 AOP 生成动态的操作日志。 **3. 优雅地支持 AOP 生成动态的操作日志** ---------------------------
+我们可以在注解的操作日志上记录固定文案,这样业务逻辑和业务代码可以做到解耦,让我们的业务代码变得纯净起来。可能有同学注意到,上面的方式虽然解耦了操作日志的代码,但是记录的文案并不符合我们的预期,文案是静态的,没有包含动态的文案,因为我们需要记录的操作日志是: 用户%s修改了订单的配送地址,从“%s”修改到“%s”。接下来,我们介绍一下如何优雅地使用 AOP 生成动态的操作日志。**3. 优雅地支持 AOP 生成动态的操作日志** ---------------------------
 
 ### **3.1 动态模板**
 
@@ -233,9 +233,9 @@ public void modifyAddress(updateDeliveryRequest request){
 }
 ```
 
-这样就不需要在 modifyAddress 方法中通过 LogRecordContext.putVariable() 设置老的快递员了,通过直接新加一个自定义函数 queryOldUser() 参数把派送订单传递进去,就能查到之前的配送人了,只需要让方法的解析在 modifyAddress() 方法执行之前运行。这样的话,我们让业务代码又变得纯净了起来,同时也让“强迫症”不再感到难受了。 **4. 代码实现解析** --------------
+这样就不需要在 modifyAddress 方法中通过 LogRecordContext.putVariable() 设置老的快递员了,通过直接新加一个自定义函数 queryOldUser() 参数把派送订单传递进去,就能查到之前的配送人了,只需要让方法的解析在 modifyAddress() 方法执行之前运行。这样的话,我们让业务代码又变得纯净了起来,同时也让“强迫症”不再感到难受了。**4. 代码实现解析** --------------
 
-### **4.1 代码结构** ![img](../assets/v2-79d10e58ddf22fd865bd7ffa2fd24dab_1440w.jpg)
+### **4.1 代码结构**![img](../assets/v2-79d10e58ddf22fd865bd7ffa2fd24dab_1440w.jpg)
 
 上面的操作日志主要是通过一个 AOP 拦截器实现的,整体主要分为 AOP 模块、日志解析模块、日志保存模块、Starter 模块;组件提供了4个扩展点,分别是:自定义函数、默认处理人、业务保存和查询;业务可以根据自己的业务特性定制符合自己业务的逻辑。
 
@@ -375,7 +375,7 @@ public class LogRecordExpressionEvaluator extends CachedExpressionEvaluator {
 getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class);
 ```
 
-`getExpression` 方法会从 expressionCache 中获取到 @LogRecordAnnotation 注解上的表达式的解析 Expression 的实例,然后调用 `getValue` 方法,`getValue` 传入一个 evalContext 就是类似上面例子中的 order 对象。其中 Context 的实现将会在下文介绍。 **日志上下文实现** 下面的例子把变量放到了 LogRecordContext 中,然后 SpEL 表达式就可以顺利的解析方法上不存在的参数了,通过上面的 SpEL 的例子可以看出,要把方法的参数和 LogRecordContext 中的变量都放到 SpEL 的 `getValue` 方法的 Object 中才可以顺利的解析表达式的值。下面看下如何实现:
+`getExpression` 方法会从 expressionCache 中获取到 @LogRecordAnnotation 注解上的表达式的解析 Expression 的实例,然后调用 `getValue` 方法,`getValue` 传入一个 evalContext 就是类似上面例子中的 order 对象。其中 Context 的实现将会在下文介绍。**日志上下文实现** 下面的例子把变量放到了 LogRecordContext 中,然后 SpEL 表达式就可以顺利的解析方法上不存在的参数了,通过上面的 SpEL 的例子可以看出,要把方法的参数和 LogRecordContext 中的变量都放到 SpEL 的 `getValue` 方法的 Object 中才可以顺利的解析表达式的值。下面看下如何实现:
 
 ```java
 @LogRecord(content = "修改了订单的配送员:从“{deveryUser{#oldDeliveryUserId}}”, 修改到“{deveryUser{#request.getUserId()}}”",
@@ -451,7 +451,7 @@ public void modifyAddress(updateDeliveryRequest request){
 ![img](../assets/v2-279274af416fa37384d75c909c8c8cfc_1440w.jpg)
 可以看到,当方法二执行了释放变量后,继续执行方法一的 logRecord 逻辑,此时解析的时候 ThreadLocal\>的 Map 已经被释放掉,所以方法一就获取不到对应的变量了。方法一和方法二共用一个变量 Map 还有个问题是:如果方法二设置了和方法一相同的变量两个方法的变量就会被相互覆盖。所以最终 LogRecordContext 的变量的生命周期需要是下面的形式:
 ![img](../assets/v2-4aacb45b5728ed404a4c52e2ee1cdab9_1440w.jpg)
-LogRecordContext 每执行一个方法都会压栈一个 Map,方法执行完之后会 Pop 掉这个 Map,从而避免变量共享和覆盖问题。 **默认操作人逻辑** 在 LogRecordInterceptor 中 IOperatorGetService 接口,这个接口可以获取到当前的用户。下面是接口的定义:
+LogRecordContext 每执行一个方法都会压栈一个 Map,方法执行完之后会 Pop 掉这个 Map,从而避免变量共享和覆盖问题。**默认操作人逻辑** 在 LogRecordInterceptor 中 IOperatorGetService 接口,这个接口可以获取到当前的用户。下面是接口的定义:
 
 ```plaintext
 public interface IOperatorGetService {
@@ -678,10 +678,10 @@ public class LogRecordProxyAutoConfiguration implements ImportAware {
 }
 ```
 
-这个类继承 ImportAware 是为了拿到 EnableLogRecord 上的租户属性,这个类使用变量 logRecordAdvisor 和 logRecordInterceptor 装配了 AOP,同时把自定义函数注入到了 logRecordAdvisor 中。 **对外扩展类** :分别是`IOperatorGetService`、`ILogRecordService`、`IParseFunction`。业务可以自己实现相应的接口,因为配置了 @ConditionalOnMissingBean,所以用户的实现类会覆盖组件内的默认实现。 **5. 总结** ----------
+这个类继承 ImportAware 是为了拿到 EnableLogRecord 上的租户属性,这个类使用变量 logRecordAdvisor 和 logRecordInterceptor 装配了 AOP,同时把自定义函数注入到了 logRecordAdvisor 中。**对外扩展类** :分别是`IOperatorGetService`、`ILogRecordService`、`IParseFunction`。业务可以自己实现相应的接口,因为配置了 @ConditionalOnMissingBean,所以用户的实现类会覆盖组件内的默认实现。**5. 总结** ----------
 
-这篇文章介绍了操作日志的常见写法,以及如何让操作日志的实现更加简单、易懂;通过组件的四个模块,介绍了组件的具体实现。对于上面的组件介绍,大家如果有疑问,也欢迎在文末留言,我们会进行答疑。 **6. 作者简介** ------------
+这篇文章介绍了操作日志的常见写法,以及如何让操作日志的实现更加简单、易懂;通过组件的四个模块,介绍了组件的具体实现。对于上面的组件介绍,大家如果有疑问,也欢迎在文末留言,我们会进行答疑。**6. 作者简介** ------------
 
-## 站通,2020年加入美团,基础研发平台/研发质量及效率部工程师。 **7. 参考资料**
+## 站通,2020年加入美团,基础研发平台/研发质量及效率部工程师。**7. 参考资料**
 
 - **Canal** - **spring-framework** - **Spring Expression Language (SpEL)** - **ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal三者之间区别** 
\ No newline at end of file
diff --git "a/docs/Article/Other/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\344\272\277\347\272\247\346\266\210\346\201\257\351\207\217\347\232\204IM\347\263\273\347\273\237.md" "b/docs/Article/Other/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\344\272\277\347\272\247\346\266\210\346\201\257\351\207\217\347\232\204IM\347\263\273\347\273\237.md"
index 392e8a85a..e59ab671f 100644
--- "a/docs/Article/Other/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\344\272\277\347\272\247\346\266\210\346\201\257\351\207\217\347\232\204IM\347\263\273\347\273\237.md"
+++ "b/docs/Article/Other/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\344\272\277\347\272\247\346\266\210\346\201\257\351\207\217\347\232\204IM\347\263\273\347\273\237.md"
@@ -122,9 +122,9 @@
 - 拉模式:由前端主动发起拉取消息的请求,为了保证消息的实时性,一般采用推模式,拉模式一般用于获取历史消息
 - 推拉结合模式:有新消息时服务器会先推一个有新消息的通知给前端,前端接收到通知后就向服务器拉取消息
 
-**推模式简化图如下:** ![img](../assets/0734f6991ecda0d1901da312a83a66b3.png)
+**推模式简化图如下:**![img](../assets/0734f6991ecda0d1901da312a83a66b3.png)
 
-如上图所示,正常情况下,用户发的消息经过服务器存储等操作后会推给接收方的所有端。但推是有可能会丢失的,最常见的情况就是用户可能会伪在线(是指如果推送服务基于长连接,而长连接可能已经断开,即用户已经掉线,但一般需要经过一个心跳周期后服务器才能感知到,这时服务器会错误地以为用户还在线;伪在线是本人自己想的一个概念,没想到合适的词来解释)。因此如果单纯使用推模式的话,是有可能会丢失消息的。 **推拉结合模式简化图如下:**
+如上图所示,正常情况下,用户发的消息经过服务器存储等操作后会推给接收方的所有端。但推是有可能会丢失的,最常见的情况就是用户可能会伪在线(是指如果推送服务基于长连接,而长连接可能已经断开,即用户已经掉线,但一般需要经过一个心跳周期后服务器才能感知到,这时服务器会错误地以为用户还在线;伪在线是本人自己想的一个概念,没想到合适的词来解释)。因此如果单纯使用推模式的话,是有可能会丢失消息的。**推拉结合模式简化图如下:**
 
 ![img](../assets/6066d5268519ccea930522283d0e690b.png)
 
@@ -248,11 +248,11 @@ Twitter 的自增 ID 设计估计大家都耳熟能详了,即大名鼎鼎的[S
 **读扩散** 对于读扩散来说,我们可以将会话未读数跟总未读数都存在后端,但后端需要保证两个未读数更新的原子性跟一致性,一般可以通过以下两种方法来实现:
 
 1. 使用 Redis 的 multi 事务功能,事务更新失败可以重试。但要注意如果你使用 Codis 集群的话并不支持事务功能。
-1. 使用 Lua 嵌入脚本的方式。使用这种方式需要保证会话未读数跟总未读数都在同一个 Redis 节点(Codis 的话可以使用 Hashtag)。这种方式会导致实现逻辑分散,加大维护成本。 **写扩散** 对于写扩散来说,服务端通常会弱化会话的概念,即服务端不存储历史会话列表。未读数的计算可由前端来负责,标记已读跟标记未读可以只记录一个事件到信箱里,各个端通过重放该事件的形式来处理会话未读数。使用这种方式可能会造成各个端的未读数不一致,至少微信就会有这个问题。
+1. 使用 Lua 嵌入脚本的方式。使用这种方式需要保证会话未读数跟总未读数都在同一个 Redis 节点(Codis 的话可以使用 Hashtag)。这种方式会导致实现逻辑分散,加大维护成本。**写扩散** 对于写扩散来说,服务端通常会弱化会话的概念,即服务端不存储历史会话列表。未读数的计算可由前端来负责,标记已读跟标记未读可以只记录一个事件到信箱里,各个端通过重放该事件的形式来处理会话未读数。使用这种方式可能会造成各个端的未读数不一致,至少微信就会有这个问题。
 
 如果写扩散也通过历史会话列表来存储未读数的话那用户时间线服务跟会话服务紧耦合,这个时候需要保证原子性跟一致性的话那就只能使用分布式事务了,会大大降低系统的性能。
 
-##### 如何存储历史消息 **读扩散** 对于读扩散,只需要按会话 ID 进行 Sharding 存储一份就可以了。 **写扩散**
+##### 如何存储历史消息 **读扩散** 对于读扩散,只需要按会话 ID 进行 Sharding 存储一份就可以了。**写扩散**
 
 对于写扩散,需要存储两份:一份是以用户为 Timeline 的消息列表,一份是以会话为 Timeline 的消息列表。以用户为 Timeline 的消息列表可以用用户 ID 来做 Sharding,以会话为 Timeline 的消息列表可以用会话 ID 来做 Sharding。
 
diff --git "a/docs/Article/Other/\345\274\202\346\255\245\347\275\221\347\273\234\346\250\241\345\236\213.md" "b/docs/Article/Other/\345\274\202\346\255\245\347\275\221\347\273\234\346\250\241\345\236\213.md"
index f707b57f7..30e10f553 100644
--- "a/docs/Article/Other/\345\274\202\346\255\245\347\275\221\347\273\234\346\250\241\345\236\213.md"
+++ "b/docs/Article/Other/\345\274\202\346\255\245\347\275\221\347\273\234\346\250\241\345\236\213.md"
@@ -6,11 +6,11 @@
 
 文中涉及接口调用的部分,都是指Linux系统的接口调用。 共分为5部分:
 
-**I/O模型** 从基础的系统调用方法出发,给大家从头回顾一下最基本的I/O模型,虽然简单,但是不可或缺的基础; **事件处理模型** 这部分在同步I/O、异步I/O的基础上分别介绍Reactor模型以及Proactor模型,着重两种模型的构成以及事件处理流程。Reactor模型是我们常见的;不同平台对异步I/O系统接口的支持力度不同,这部分还介绍了一种使用同步I/O来模拟Proactor模型的方法。 **并发模式** 就是多线程、多进程的编程的模式。介绍了两种较为高效的并发模型,半同步/半异步(包括其演变模式)、Follower/Leader模式。 **Swoole异步网络模型分析** 这部分是结合已介绍的事件处理模型、并发模式对Swoole的异步模型进行分析; 从分析的过程来看,看似复杂的网络模型,可以拆分为简单的模型单元,只不过我们需要权衡利弊,选取合适业务需求的模型单元进行组合。 我们团队基于Swoole 1.8.5版本,做了很多修改,部分模块做了重构,计划在17年6月底将修改后版本开源出去,敬请期待。 **改善性能的方法** 最后一部分是在引入话题,介绍的是几种常用的方法。性能优化是没有终点的,希望大家能贡献一些想法和具体方法。
+**I/O模型** 从基础的系统调用方法出发,给大家从头回顾一下最基本的I/O模型,虽然简单,但是不可或缺的基础; **事件处理模型** 这部分在同步I/O、异步I/O的基础上分别介绍Reactor模型以及Proactor模型,着重两种模型的构成以及事件处理流程。Reactor模型是我们常见的;不同平台对异步I/O系统接口的支持力度不同,这部分还介绍了一种使用同步I/O来模拟Proactor模型的方法。**并发模式** 就是多线程、多进程的编程的模式。介绍了两种较为高效的并发模型,半同步/半异步(包括其演变模式)、Follower/Leader模式。**Swoole异步网络模型分析** 这部分是结合已介绍的事件处理模型、并发模式对Swoole的异步模型进行分析; 从分析的过程来看,看似复杂的网络模型,可以拆分为简单的模型单元,只不过我们需要权衡利弊,选取合适业务需求的模型单元进行组合。 我们团队基于Swoole 1.8.5版本,做了很多修改,部分模块做了重构,计划在17年6月底将修改后版本开源出去,敬请期待。**改善性能的方法** 最后一部分是在引入话题,介绍的是几种常用的方法。性能优化是没有终点的,希望大家能贡献一些想法和具体方法。
 
 ## I/O模型
 
-POSIX 规范中定义了同步I/O 和异步I/O的术语, **同步I/O** : 需要进程去真正的去操作I/O; **异步I/O** :内核在I/O操作完成后再通知应用进程操作结果。
+POSIX 规范中定义了同步I/O 和异步I/O的术语,**同步I/O** : 需要进程去真正的去操作I/O; **异步I/O** :内核在I/O操作完成后再通知应用进程操作结果。
 
 在《UNIX网络编程》中介绍了5中I/O模型:阻塞I/O、非阻塞I/O、I/O复用、SIGIO 、异步I/O;本节对这5种I/O模型进行说明和对比。
 
@@ -229,7 +229,7 @@ a)供应用程序注册和删除关注的事件句柄;
 
 ### 半同步/半异步模式
 
-首先区分一个概念,并发模式中的“同步”、“异步”与 I/O模型中的“同步”、“异步”是两个不同的概念: **并发模式中** ,“同步”指程序按照代码顺序执行,“异步”指程序依赖事件驱动,如图12 所示并发模式的“同步”执行和“异步”执行的读操作; **I/O模型中** ,“同步”、“异步”用来区分I/O操作的方式,是主动通过I/O操作拿到结果,还是由内核异步的返回操作结果。
+首先区分一个概念,并发模式中的“同步”、“异步”与 I/O模型中的“同步”、“异步”是两个不同的概念: **并发模式中**,“同步”指程序按照代码顺序执行,“异步”指程序依赖事件驱动,如图12 所示并发模式的“同步”执行和“异步”执行的读操作; **I/O模型中**,“同步”、“异步”用来区分I/O操作的方式,是主动通过I/O操作拿到结果,还是由内核异步的返回操作结果。
 ![img](../assets/12.png)
 
 > ```
@@ -404,7 +404,7 @@ Swoole作为server时,支持3种运行模式,分别是多进程模式、多
 
 ### 有限状态机器
 
-有限状态机是一种高效的逻辑处理方式,在网络协议处理中应用非常广泛,最典型的是内核协议栈中TCP状态转移。有限状态机中每种类型对应执行逻辑单元的状态,对逻辑事务的处理非常有效。 有限状态机包括两种,一种是每个状态都是相互独立的,状态间不存在转移;另一种就是状态间存在转移。有限状态机比较容易理解,下面给出两种有限状态机的示例代码。 **不存在状态转移** `c typedef enum _tag_state_enum{       A_STATE,     B_STATE,     C_STATE,     D_STATE }state_enum; void STATE_MACHINE_HANDLER(state_enum cur_state) {     switch (cur_state){     case A_STATE:          process_A_STATE();          break;     case B_STATE:          process_B_STATE();          break;     case C_STATE:          process_C_STATE();          break;     default:          break;     }     return ; }` **存在状态转移** ```c
+有限状态机是一种高效的逻辑处理方式,在网络协议处理中应用非常广泛,最典型的是内核协议栈中TCP状态转移。有限状态机中每种类型对应执行逻辑单元的状态,对逻辑事务的处理非常有效。 有限状态机包括两种,一种是每个状态都是相互独立的,状态间不存在转移;另一种就是状态间存在转移。有限状态机比较容易理解,下面给出两种有限状态机的示例代码。**不存在状态转移** `c typedef enum _tag_state_enum{       A_STATE,     B_STATE,     C_STATE,     D_STATE }state_enum; void STATE_MACHINE_HANDLER(state_enum cur_state) {     switch (cur_state){     case A_STATE:          process_A_STATE();          break;     case B_STATE:          process_B_STATE();          break;     case C_STATE:          process_C_STATE();          break;     default:          break;     }     return ; }` **存在状态转移** ```c
 void TRANS_STATE_MACHINE_HANDLER(state_enum cur_state) {
 while (C_STATE != cur_state) {
 switch (cur_state) {
@@ -430,21 +430,21 @@ return ;
 
 ### 时间轮
 
-经常会面临一些业务定时超时的需求,用例子来说明吧。 **功能需求** :服务器需要维护来自大量客户端的TCP连接(假设单机服务器需要支持的最大TCP连接数在10W级别),如果某连接上60s内没有数据到达,就认为相应的客户端下线。
+经常会面临一些业务定时超时的需求,用例子来说明吧。**功能需求** :服务器需要维护来自大量客户端的TCP连接(假设单机服务器需要支持的最大TCP连接数在10W级别),如果某连接上60s内没有数据到达,就认为相应的客户端下线。
 
-先介绍一下两种容易想到的解决方案, **方案a**  **轮询扫描** 处理过程为:
+先介绍一下两种容易想到的解决方案,**方案a**  **轮询扫描** 处理过程为:
 
 1.  维护一个map 记录客户端最近一次的请求时间;
 2.  当client\_id对应连接有数据到达时,更新last\_update\_time;
 3.  启动一个定时器,轮询扫描map 中client\_id 对应的last\_update\_time,若超过 60s,则认为对应的客户端下线。
 
-轮询扫描,只启动一个定时器,但轮询效率低,特别是服务器维护的连接数很大时,部分连接超时事件得不到及时处理。 **方案b**  **多定时器触发** 处理过程为:
+轮询扫描,只启动一个定时器,但轮询效率低,特别是服务器维护的连接数很大时,部分连接超时事件得不到及时处理。**方案b**  **多定时器触发** 处理过程为:
 
 1.  维护一个map 记录客户端最近一次的请求时间;
 2.  当某client\_id 对应连接有数据到达时,更新last\_update\_time,同时为client\_id启用一个定时器,60s后触发;
 3.  当client\_id对应的定时器触发后,查看map中client\_id对应的last\_update\_time是否超过60s,若超时则认为对应客户端下线。
 
-多定时器触发,每次请求都要启动一个定时器,可以想象,消息请求非常频繁是,定时器的数量将会很庞大,消耗大量的系统资源。 **方案c 时间轮方案** 下面介绍一下利用时间轮的方式实现的一种高效、能批量的处理方案,先说一下需要的数据结构:
+多定时器触发,每次请求都要启动一个定时器,可以想象,消息请求非常频繁是,定时器的数量将会很庞大,消耗大量的系统资源。**方案c 时间轮方案** 下面介绍一下利用时间轮的方式实现的一种高效、能批量的处理方案,先说一下需要的数据结构:
 
 1.  创建0~60的数据,构成环形队列time\_wheel,current\_index维护环形队列的当前游标,如图19所示;
 2.  数组元素是slot 结构,slot是一个set,构成任务集;
@@ -463,7 +463,7 @@ return ;
 1.  启用一个定时器,运行间隔1s,更新current\_index,指向环形队列下一个元素,0->1->2->3...->58->59->60...0;
 2.  连接上数据到达时,从map中获取client\_id所在的slot,在slot的set中删除该client\_id;
 3.  将client\_id加入到current\_index - 1锁标记的slot中;
-4.  更新map中client\_id 为current\_id-1 。 **与a、b两种方案相比,方案c具有如下优势** :
+4.  更新map中client\_id 为current\_id-1 。**与a、b两种方案相比,方案c具有如下优势** :
 
 1.  只需要一个定时器,运行间隔1s,CPU消耗非常少;
 2.  current\_index 所标记的slot中的set不为空时,set中的所有client\_id对应的客户端均认为下线,即批量超时。
diff --git "a/docs/Article/Other/\345\275\223\346\210\221\344\273\254\345\234\250\350\256\250\350\256\272CQRS\346\227\266\357\274\214\346\210\221\344\273\254\345\234\250\350\256\250\350\256\272\344\272\233\347\245\236\351\251\254\357\274\237.md" "b/docs/Article/Other/\345\275\223\346\210\221\344\273\254\345\234\250\350\256\250\350\256\272CQRS\346\227\266\357\274\214\346\210\221\344\273\254\345\234\250\350\256\250\350\256\272\344\272\233\347\245\236\351\251\254\357\274\237.md"
index 43185c856..6d7464080 100644
--- "a/docs/Article/Other/\345\275\223\346\210\221\344\273\254\345\234\250\350\256\250\350\256\272CQRS\346\227\266\357\274\214\346\210\221\344\273\254\345\234\250\350\256\250\350\256\272\344\272\233\347\245\236\351\251\254\357\274\237.md"
+++ "b/docs/Article/Other/\345\275\223\346\210\221\344\273\254\345\234\250\350\256\250\350\256\272CQRS\346\227\266\357\274\214\346\210\221\344\273\254\345\234\250\350\256\250\350\256\272\344\272\233\347\245\236\351\251\254\357\274\237.md"
@@ -71,7 +71,7 @@ CRUD(Create、Read、Update、Delete)是 **面向数据** 的,它将对数
 
 ### Event Souring
 
-Event Souring,翻译过来叫事件溯源。什么意思呢?它把对象的创建、修改、删除等一系列的操作都当作事件(_注意:事件和命令还有区别,后面会讲到_),持久化的时候只存储事件,存储事件的介质叫做 **EventStore** ,当要获取一个对象的最新状态时,通过EventStore检索该对象的所有Event并重新加载来获取对象的最新状态。EventStore可以是数据库、磁盘文件、MongoDB等,由于Event的存储都是新增的,所以不存在并发冲突的问题。
+Event Souring,翻译过来叫事件溯源。什么意思呢?它把对象的创建、修改、删除等一系列的操作都当作事件(_注意:事件和命令还有区别,后面会讲到_),持久化的时候只存储事件,存储事件的介质叫做 **EventStore**,当要获取一个对象的最新状态时,通过EventStore检索该对象的所有Event并重新加载来获取对象的最新状态。EventStore可以是数据库、磁盘文件、MongoDB等,由于Event的存储都是新增的,所以不存在并发冲突的问题。
 
 ## Command和Event
 
diff --git "a/docs/Article/Other/\346\234\200\345\205\250\347\232\204116\351\201\223Redis\351\235\242\350\257\225\351\242\230\350\247\243\347\255\224.md" "b/docs/Article/Other/\346\234\200\345\205\250\347\232\204116\351\201\223Redis\351\235\242\350\257\225\351\242\230\350\247\243\347\255\224.md"
index 2d21795e9..6ea72fce0 100644
--- "a/docs/Article/Other/\346\234\200\345\205\250\347\232\204116\351\201\223Redis\351\235\242\350\257\225\351\242\230\350\247\243\347\255\224.md"
+++ "b/docs/Article/Other/\346\234\200\345\205\250\347\232\204116\351\201\223Redis\351\235\242\350\257\225\351\242\230\350\247\243\347\255\224.md"
@@ -110,7 +110,7 @@ Redis 支持主从同步、从从同步。如果是第一次进行主从同步
 
 Redis 是一个开源(BSD 许可),基于内存,支持多种数据结构的存储系统。可以作为数据库、缓存和消息中间件。它支持的数据结构有字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等,除此之外还支持 bitmaps、hyperloglogs 和地理空间( geospatial )索引半径查询等功能。根据它的特性,它适用的场景有:
 
-**1. 会话缓存** 会话(Session)是存储在服务端的,但是可以设置存储的时候不以文件的方式存储,而是存到 Redis 中,而且 Redis 支持数据持久化,不用担心数据因为服务器重启导致 Session 数据丢失的问题。这样做的好处不只是提高获取会话的速度,也对网站的整体性能有很大的提升。 **2. 数据缓存** Redis 支持多种数据结构,经常被用来做缓存中间件使用。缓存的数据不只是包括数据库中的数据,也可以缓存一些需要临时存储的数据,例如 token、会话数据等。 **3. 队列** Redis 是支持列表(lists)功能的,可以简单实现一个队列的功能,对数据进行入队、出队操作。实现的队列可以应用到电商的秒杀场景中。 **4. 排行榜、计数器** Redis 提供了有序集合,可以对数据进行排名,实现排行榜功能。 其次 Redis 中提供了 incr 对数字加 1 命令,也提供了 decr 对数字减 1 命令,所以可以实现一个简单的计数器功能。 **5. 发布、订阅功能**
+**1. 会话缓存** 会话(Session)是存储在服务端的,但是可以设置存储的时候不以文件的方式存储,而是存到 Redis 中,而且 Redis 支持数据持久化,不用担心数据因为服务器重启导致 Session 数据丢失的问题。这样做的好处不只是提高获取会话的速度,也对网站的整体性能有很大的提升。**2. 数据缓存** Redis 支持多种数据结构,经常被用来做缓存中间件使用。缓存的数据不只是包括数据库中的数据,也可以缓存一些需要临时存储的数据,例如 token、会话数据等。**3. 队列** Redis 是支持列表(lists)功能的,可以简单实现一个队列的功能,对数据进行入队、出队操作。实现的队列可以应用到电商的秒杀场景中。**4. 排行榜、计数器** Redis 提供了有序集合,可以对数据进行排名,实现排行榜功能。 其次 Redis 中提供了 incr 对数字加 1 命令,也提供了 decr 对数字减 1 命令,所以可以实现一个简单的计数器功能。**5. 发布、订阅功能**
 
 Redis 中提供了发布订阅相关的命令,可以用来做一些跟发布订阅相关的场景应用等。例如简单的消息队列功能等。
 
@@ -181,7 +181,7 @@ RESP 的特点为:实现简单、快速解析、可读性好。
 
 常用的设置有:
 
-**1. 端口设置** 只允许信任的客户端发送过来的请求,对其他所有请求都拒绝。如果存在暴露在外网的服务,那么需要使用防火墙阻止外部访问 Redis 端口。 **2. 身份验证** 使用 Redis 提供的身份验证功能,在 redis.conf 文件中进行配置生效,客户端可以发送(AUTH 密码)命令进行身份认证。 **3. 禁用特定的命令集**
+**1. 端口设置** 只允许信任的客户端发送过来的请求,对其他所有请求都拒绝。如果存在暴露在外网的服务,那么需要使用防火墙阻止外部访问 Redis 端口。**2. 身份验证** 使用 Redis 提供的身份验证功能,在 redis.conf 文件中进行配置生效,客户端可以发送(AUTH 密码)命令进行身份认证。**3. 禁用特定的命令集**
 
 可以考虑禁止一些容易产生安全问题的命令,预防被人恶意操作。
 
@@ -547,11 +547,11 @@ Redis 常见性能问题和解决方案如下:
 
 我们可以使用 keys 命令和 scan 命令,但是会发现使用 scan 更好。
 
-**1. 使用 keys 命令** 直接使用 keys 命令查询,但是如果是在生产环境下使用会出现一个问题,keys 命令是遍历查询的,查询的时间复杂度为 O(n),数据量越大查询时间越长。而且 Redis 是单线程,keys 指令会导致线程阻塞一段时间,会导致线上 Redis 停顿一段时间,直到 keys 执行完毕才能恢复。这在生产环境是不允许的。除此之外,需要注意的是,这个命令没有分页功能,会一次性查询出所有符合条件的 key 值,会发现查询结果非常大,输出的信息非常多。所以不推荐使用这个命令。 **2. 使用 scan 命令** scan 命令可以实现和 keys 一样的匹配功能,但是 scan 命令在执行的过程不会阻塞线程,并且查找的数据可能存在重复,需要客户端操作去重。因为 scan 是通过游标方式查询的,所以不会导致 Redis 出现假死的问题。Redis 查询过程中会把游标返回给客户端,单次返回空值且游标不为 0,则说明遍历还没结束,客户端继续遍历查询。scan 在检索的过程中,被删除的元素是不会被查询出来的,但是如果在迭代过程中有元素被修改,scan 不能保证查询出对应元素。相对来说,scan 指令查找花费的时间会比 keys 指令长。
+**1. 使用 keys 命令** 直接使用 keys 命令查询,但是如果是在生产环境下使用会出现一个问题,keys 命令是遍历查询的,查询的时间复杂度为 O(n),数据量越大查询时间越长。而且 Redis 是单线程,keys 指令会导致线程阻塞一段时间,会导致线上 Redis 停顿一段时间,直到 keys 执行完毕才能恢复。这在生产环境是不允许的。除此之外,需要注意的是,这个命令没有分页功能,会一次性查询出所有符合条件的 key 值,会发现查询结果非常大,输出的信息非常多。所以不推荐使用这个命令。**2. 使用 scan 命令** scan 命令可以实现和 keys 一样的匹配功能,但是 scan 命令在执行的过程不会阻塞线程,并且查找的数据可能存在重复,需要客户端操作去重。因为 scan 是通过游标方式查询的,所以不会导致 Redis 出现假死的问题。Redis 查询过程中会把游标返回给客户端,单次返回空值且游标不为 0,则说明遍历还没结束,客户端继续遍历查询。scan 在检索的过程中,被删除的元素是不会被查询出来的,但是如果在迭代过程中有元素被修改,scan 不能保证查询出对应元素。相对来说,scan 指令查找花费的时间会比 keys 指令长。
 
 #### 4. 什么是缓存穿透?怎么解决?
 
-大量的请求瞬时涌入系统,而这个数据在 Redis 中不存在,所有的请求都落到了数据库上把数据库打死。造成这种情况的原因有系统设计不合理、缓存数据更新不及时,或爬虫等恶意攻击。 解决办法有: **1. 使用布隆过滤器** 将查询的参数都存储到一个 bitmap 中,在查询缓存前,再找个新的 bitmap,在里面对参数进行验证。如果验证的 bitmap 中存在,则进行底层缓存的数据查询,如果 bitmap 中不存在查询参数,则进行拦截,不再进行缓存的数据查询。 **2. 缓存空对象**
+大量的请求瞬时涌入系统,而这个数据在 Redis 中不存在,所有的请求都落到了数据库上把数据库打死。造成这种情况的原因有系统设计不合理、缓存数据更新不及时,或爬虫等恶意攻击。 解决办法有: **1. 使用布隆过滤器** 将查询的参数都存储到一个 bitmap 中,在查询缓存前,再找个新的 bitmap,在里面对参数进行验证。如果验证的 bitmap 中存在,则进行底层缓存的数据查询,如果 bitmap 中不存在查询参数,则进行拦截,不再进行缓存的数据查询。**2. 缓存空对象**
 
 如果从数据库查询的结果为空,依然把这个结果进行缓存,那么当用 key 获取数据时,即使数据不存在,Redis 也可以直接返回结果,避免多次访问数据库。
 
@@ -621,7 +621,7 @@ Redis 可以使用主从同步、从从同步。第一次同步时,主节点
 
 #### 12. 怎么去发现 Redis 阻塞异常情况?
 
-可以从以下两方面准备: **1. 使用 Redis 自身监控系统** 使用 Redis 自身监控系统,可以对 CPU、内存、磁盘、命令耗时等阻塞问题进行监控,当监控系统发现各个监控指标存在异常的时候,发送报警。 **2. 使用应用服务监控**
+可以从以下两方面准备: **1. 使用 Redis 自身监控系统** 使用 Redis 自身监控系统,可以对 CPU、内存、磁盘、命令耗时等阻塞问题进行监控,当监控系统发现各个监控指标存在异常的时候,发送报警。**2. 使用应用服务监控**
 
 当 Redis 存在阻塞时,应用响应时间就会延长,应用可以感知发现问题,并发送报警给管理人员。
 
@@ -807,7 +807,7 @@ Redis 集群支持的主从复制,数据同步主要有两种方法:一种
 
 #### 6. Redis 集群会有写操作丢失吗?为什么?
 
-Redis 集群中有可能存在写操作丢失的问题,但是丢失概率一般可以忽略不计。主要是 Redis 并没有一个机制保证数据一定写不丢失。在以下问题中可能出现键值丢失的问题: **1. 超过内存的最大值,键值被清理** Redis 中可以设置缓存的最大内存值,当内存中的数据总量大于设置的最大内存值,会导致 Redis 对部分数据进行清理,导致键值丢失的问题。 **2. 大量的 key 过期,被清理** 这种情况比较正常,只是因为键值设置的时间过期了,被自动清理了。 **3. Redis 主库服务器故障重启** 由于 Redis 的数据是缓存在内存中的,如果 Redis 主库服务器出现故障重启,会出现数据被清空的问题。这时可能导致从库的数据同步被清空。如果有使用数据持久化,那么故障重启后数据是可以自动恢复的。 **4. 网络问题**
+Redis 集群中有可能存在写操作丢失的问题,但是丢失概率一般可以忽略不计。主要是 Redis 并没有一个机制保证数据一定写不丢失。在以下问题中可能出现键值丢失的问题: **1. 超过内存的最大值,键值被清理** Redis 中可以设置缓存的最大内存值,当内存中的数据总量大于设置的最大内存值,会导致 Redis 对部分数据进行清理,导致键值丢失的问题。**2. 大量的 key 过期,被清理** 这种情况比较正常,只是因为键值设置的时间过期了,被自动清理了。**3. Redis 主库服务器故障重启** 由于 Redis 的数据是缓存在内存中的,如果 Redis 主库服务器出现故障重启,会出现数据被清空的问题。这时可能导致从库的数据同步被清空。如果有使用数据持久化,那么故障重启后数据是可以自动恢复的。**4. 网络问题**
 
 可能出现网络故障,导致短时间内数据写入失败。
 
@@ -855,9 +855,9 @@ Redis 哨兵的好处在于可以保证系统的高可用,各个节点可以
 
 过期键的删除策略是将惰性删除策略和定期删除策略组合使用。
 
-**1. 定时删除策略** 该策略的作用是给 key 设置过期时间的同时,给 key 创建一个定时器,定时器在 key 的过期时间来临时,对这些 key 进行删除。 这样做的好处是保证内存空间得以释放。但是缺点是给 key 创建一个定时器会有一定的性能损失。如果 key 很多,删除这些 key 占用的内存空间也会占用 CPU 很多时间。 **2. 惰性删除策略** 每次从数据库取 key 的时候检查 key 是否过期,如果过期则删除,并返回 null,如果 key 没有过期,则直接返回数据。
+**1. 定时删除策略** 该策略的作用是给 key 设置过期时间的同时,给 key 创建一个定时器,定时器在 key 的过期时间来临时,对这些 key 进行删除。 这样做的好处是保证内存空间得以释放。但是缺点是给 key 创建一个定时器会有一定的性能损失。如果 key 很多,删除这些 key 占用的内存空间也会占用 CPU 很多时间。**2. 惰性删除策略** 每次从数据库取 key 的时候检查 key 是否过期,如果过期则删除,并返回 null,如果 key 没有过期,则直接返回数据。
 
-这样做的好处是占用 CPU 的时间比较少。但是缺点是如果 key 很长时间没有被获取,将不会被删除,容易造成内存泄露。 **3. 定期删除策略**
+这样做的好处是占用 CPU 的时间比较少。但是缺点是如果 key 很长时间没有被获取,将不会被删除,容易造成内存泄露。**3. 定期删除策略**
 
 该策略的作用是每隔一段时间执行一次删除过期 key 的操作,该删除频率可以在 redis.conf 配置文件中设置。
 
@@ -906,7 +906,7 @@ Redis 慢查询的参数配置有:
 
 **1. slowlog-log-slower-than** 该参数的作用为设置慢查询的阈值,当命令执行时间超过这个阈值就认为是慢查询。单位为微妙,默认为 10000。
 
-可以根据自己线上的并发量进行调整这个值。如果存在高流量的场景,那么建议设置这个值为 1 毫秒,因为每个命令执行的时间如果超过 1 毫秒,那么 Redis 的每秒操作数最多只能到 1000。 **2. slowlog-max-len**
+可以根据自己线上的并发量进行调整这个值。如果存在高流量的场景,那么建议设置这个值为 1 毫秒,因为每个命令执行的时间如果超过 1 毫秒,那么 Redis 的每秒操作数最多只能到 1000。**2. slowlog-max-len**
 
 该参数的作用为设置慢查询日志列表的最大长度,当慢查询日志列表处于最大长度时,最早插入的一个命令将会被从列表中移除。
 
@@ -940,7 +940,7 @@ Redis 慢查询的参数配置有:
 - 缓存复杂度变高:写入缓存的数据如果需要经过特殊的计算,那么这时候更新缓存操作比删除缓存要复杂得多。因为更新完数据库后,得到的数据需要执行一次加工,最后得到的值才能更新缓存。
 - 线程安全存在问题:如果存在线程 1 和线程 2 同时操作数据库和缓存,线程 1 先更新了数据库,线程 2 再更新数据库,这时候由于某种原因,线程 2 首先更新了缓存,线程 1 再更新。那么这样会导致产生脏数据的问题。因为数据库存储的是线程 2 更新后的数据,而缓存存储的是线程 1 更新的老数据。
 
-**2. 先删除缓存,再更新数据库** 这种策略可能导致数据库数据和缓存数据不一致的问题。如果存在线程 1 和线程 2,线程 1 写数据先删除缓存,有一个线程 2 正好需要查询该缓存,发现缓存不存在,去访问数据库,并得到旧值放入缓存重,线程 1 再更新数据库。那么这时就出现了数据不一致的问题。如果缓存没有过期时间,那么这个脏数据一直存在。如果要解决这个问题,那么可以在更新完数据库后,对缓存再淘汰一次。 **3. 先更新数据库,再删除缓存**
+**2. 先删除缓存,再更新数据库** 这种策略可能导致数据库数据和缓存数据不一致的问题。如果存在线程 1 和线程 2,线程 1 写数据先删除缓存,有一个线程 2 正好需要查询该缓存,发现缓存不存在,去访问数据库,并得到旧值放入缓存重,线程 1 再更新数据库。那么这时就出现了数据不一致的问题。如果缓存没有过期时间,那么这个脏数据一直存在。如果要解决这个问题,那么可以在更新完数据库后,对缓存再淘汰一次。**3. 先更新数据库,再删除缓存**
 
 这种策略可能导致数据库数据和缓存数据不一致的问题。如果在更新完数据库还没来得及删除缓存的时候,有请求过来从缓存中获取数据,那么可能会造成缓存和数据库数据不一致的问题。但是正常情况下,机器不出现故障或其他影响的情况下,不一致性的可能性相对较低。
 
@@ -982,7 +982,7 @@ Redis 内存不足可以这样处理:
 
 主要常见的淘汰算法有:
 
-**1. 最近最少使用算法(LRU)** LRU 是 Least Recently Used 的缩写,中文意思是最近最少使用,它是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 **2. 先进先出算法(FIFO)** FIFO 是 First Input First Output 的缩写,即先入先出队列,这是一种传统的按序执行方法,先进入的指令先完成并引退,跟着才执行第二条指令。 **3. 最不经常使用算法(LFU)**
+**1. 最近最少使用算法(LRU)** LRU 是 Least Recently Used 的缩写,中文意思是最近最少使用,它是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。**2. 先进先出算法(FIFO)** FIFO 是 First Input First Output 的缩写,即先入先出队列,这是一种传统的按序执行方法,先进入的指令先完成并引退,跟着才执行第二条指令。**3. 最不经常使用算法(LFU)**
 
 LFU 是 Least Frequently Used 的缩写,中文意思是最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。
 
diff --git "a/docs/Article/Other/\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\345\216\237\347\220\206\345\211\226\346\236\220\357\274\210Eureka\343\200\201Zookeeper\343\200\201Nacos\357\274\211.md" "b/docs/Article/Other/\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\345\216\237\347\220\206\345\211\226\346\236\220\357\274\210Eureka\343\200\201Zookeeper\343\200\201Nacos\357\274\211.md"
index 4103d2aad..ee7674700 100644
--- "a/docs/Article/Other/\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\345\216\237\347\220\206\345\211\226\346\236\220\357\274\210Eureka\343\200\201Zookeeper\343\200\201Nacos\357\274\211.md"
+++ "b/docs/Article/Other/\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\345\216\237\347\220\206\345\211\226\346\236\220\357\274\210Eureka\343\200\201Zookeeper\343\200\201Nacos\357\274\211.md"
@@ -20,15 +20,15 @@ Spring Cloud Eureka 是在 Netflix 的 Eureka 的基础上进行二次开发而
 
 网上很多人说 Eureka 闭源,其实没有,只是 Eurkea 2.x 分支不再维护,官方依然在积极地维护 Eureka 1.x,Spring Cloud 还是使用的 1.x 版本的 Eureka,所以不必过分担心,就算 Eureka 真的闭源了,Spring Cloud 还可以使用 ZooKeeper、Consul、Nacos 等等来实现服务治理。比如使用 ZooKeeper 替代 Eureka,也是改几行配置和换个 jar 的事情。
 
-**Eureka Server 与 Eureka Client 的关系:** ![服务注册中心Eureka](../assets/f7dc7750-3a10-11ea-96f3-5d8c8a393bcd.png)
+**Eureka Server 与 Eureka Client 的关系:**![服务注册中心Eureka](../assets/f7dc7750-3a10-11ea-96f3-5d8c8a393bcd.png)
 
 ### 服务端(Eureka Server)
 
-Eureka Server 其实就是服务注册中心,负责管理每个 Eureka Client 的服务信息(IP、端口等等)和状态。服务端主要提供以下功能。 **提供服务注册** 提供一个统一存储服务的地方,即服务列表,Eureka Client 应用启动时把自己的服务都注册到这里。 **提供注册表** 为 Eureka Client 提供服务列表,Eureka Client 首次获取服务列表后会缓存一份到自己的本地,定时更新本地缓存,下次调用时直接使用本地缓存的服务信息进行远程调用,可以提高效率。 **服务剔除(Eviction)** 如果 Eureka Client 超过 90 秒(默认)不向 Eureka Sever 上报心跳,Eureka Server 会剔除该 Eureka Client 实例,但是前提是不满足自我保护机制才剔除,避免杀错好人。 **自我保护机制** 如果出现网络不稳定的时候,Eureka Client 的都能正常提供服务,即使超过了 90 秒没有上报心跳,也不会马上剔除该 Eureka Client 实例,而是进入自我保护状态,不会做任何的删除服务操作,仍然可以提供注册服务,当网络稳定之时,则解除自我保护恢复正常。
+Eureka Server 其实就是服务注册中心,负责管理每个 Eureka Client 的服务信息(IP、端口等等)和状态。服务端主要提供以下功能。**提供服务注册** 提供一个统一存储服务的地方,即服务列表,Eureka Client 应用启动时把自己的服务都注册到这里。**提供注册表** 为 Eureka Client 提供服务列表,Eureka Client 首次获取服务列表后会缓存一份到自己的本地,定时更新本地缓存,下次调用时直接使用本地缓存的服务信息进行远程调用,可以提高效率。**服务剔除(Eviction)** 如果 Eureka Client 超过 90 秒(默认)不向 Eureka Sever 上报心跳,Eureka Server 会剔除该 Eureka Client 实例,但是前提是不满足自我保护机制才剔除,避免杀错好人。**自我保护机制** 如果出现网络不稳定的时候,Eureka Client 的都能正常提供服务,即使超过了 90 秒没有上报心跳,也不会马上剔除该 Eureka Client 实例,而是进入自我保护状态,不会做任何的删除服务操作,仍然可以提供注册服务,当网络稳定之时,则解除自我保护恢复正常。
 
 ### 客户端(Eureka Client)
 
-Eureka Client 可以是服务提供者客户端角色,也可以是服务消费者客户端角色,客户端主要提供以下功能。 **服务注册(Register)** 作为服务提供者角色,把自己的服务(IP、端口等等)注册到服务注册中心。 **自动刷新缓存(GetRegisty)** 作为服务消费者角色,从服务注册中心获取服务列表,并缓存在本地供下次使用,每 30 秒刷新一次缓存。 **服务续约(Renew)** Eureka Client 每 30 秒(默认可配置)向 Server 端上报心跳(http 请求)告诉自己很健康,如果 Server 端在 90 秒(默认可配置)内没有收到心跳,而且不是自我保护情况,则剔除之。 **远程调用(Remote Call)** 作为服务消费者角色,从服务注册中心获取服务列表后,就可以根据服务相关信息进行远程调用了,如果存在多个服务提供者实例时,默认使用负载均衡 Ribbon 的轮询策略调用服务。 **服务下线(Cancel)** 作为服务提供者角色,在应用关闭时会发请求到服务端,服务端接受请求并把该实例剔除。
+Eureka Client 可以是服务提供者客户端角色,也可以是服务消费者客户端角色,客户端主要提供以下功能。**服务注册(Register)** 作为服务提供者角色,把自己的服务(IP、端口等等)注册到服务注册中心。**自动刷新缓存(GetRegisty)** 作为服务消费者角色,从服务注册中心获取服务列表,并缓存在本地供下次使用,每 30 秒刷新一次缓存。**服务续约(Renew)** Eureka Client 每 30 秒(默认可配置)向 Server 端上报心跳(http 请求)告诉自己很健康,如果 Server 端在 90 秒(默认可配置)内没有收到心跳,而且不是自我保护情况,则剔除之。**远程调用(Remote Call)** 作为服务消费者角色,从服务注册中心获取服务列表后,就可以根据服务相关信息进行远程调用了,如果存在多个服务提供者实例时,默认使用负载均衡 Ribbon 的轮询策略调用服务。**服务下线(Cancel)** 作为服务提供者角色,在应用关闭时会发请求到服务端,服务端接受请求并把该实例剔除。
 
 ### 注册与发现的工作流程
 
@@ -45,7 +45,7 @@ Eureka Client 可以是服务提供者客户端角色,也可以是服务消费
 
 ![Eureka Server集群](../assets/aaca0b10-36c0-11ea-83e2-610758492683.png)
 
-Eureka Server 集群当中的每个节点都是 **通过 Replicate(即复制)来同步数据** ,没有主节点和从节点之分,所有节点都是平等而且数据都保持一致。因为结点之间是通过 **异步方式** 进行同步数据,不保证强一致性,保证可用性,所以是 AP。
+Eureka Server 集群当中的每个节点都是 **通过 Replicate(即复制)来同步数据**,没有主节点和从节点之分,所有节点都是平等而且数据都保持一致。因为结点之间是通过 **异步方式** 进行同步数据,不保证强一致性,保证可用性,所以是 AP。
 
 假如其中一个 Eureka Server 节点宕机了,不影响 Eureka Client 正常工作,Eureka Client 的请求由其他正常的 Eureka Server 节点接收,当出现宕机的那个 Eureka Server 节点正常启动后,复制其他节点的最新数据(服务列表)后,又可以正常提供服务了。
 
@@ -86,7 +86,7 @@ ZK 的文件结构类似于 Linux 系统的树状结构,注册服务时,即
 ![Nacos 架构图](../assets/45cfac70-3a89-11ea-9aa1-b99f5be963bb.png)
 
 主要功能点
------ **服务注册与发现** 类似 Eureka、ZooKeeper、Consul 等组件,既可以支持 HTTP、https 的服务注册和发现,也可以支持 RPC 的服务注册和发现,比如 Dubbo,也是出自于阿里,完全可以替代 Eureka、ZooKeeper、Consul。 **动态配置服务**
+----- **服务注册与发现** 类似 Eureka、ZooKeeper、Consul 等组件,既可以支持 HTTP、https 的服务注册和发现,也可以支持 RPC 的服务注册和发现,比如 Dubbo,也是出自于阿里,完全可以替代 Eureka、ZooKeeper、Consul。**动态配置服务**
 
 类似 Spring Cloud Config + Bus、Apollo 等组件。提供了后台管理界面来统一管理所有的服务和应用的配置,后台修改公共配置后不需重启应用程序即可生效。
 
diff --git "a/docs/Article/Other/\346\274\253\347\224\273\350\256\262\350\247\243 git rebase VS git merge.md" "b/docs/Article/Other/\346\274\253\347\224\273\350\256\262\350\247\243 git rebase VS git merge.md"
index 35182b7b3..6b25ccec1 100644
--- "a/docs/Article/Other/\346\274\253\347\224\273\350\256\262\350\247\243 git rebase VS git merge.md"	
+++ "b/docs/Article/Other/\346\274\253\347\224\273\350\256\262\350\247\243 git rebase VS git merge.md"	
@@ -329,11 +329,11 @@ git push origin master
 
 ![在这里插入图片描述](../assets/9067aaf0-e256-11eb-be77-f581c3259eef)
 
-## 由于没有多出 merge 的 commit 记录,所以不会存在分叉的 commit 记录,代码记录都是以线性的方式,做代码审查一目了然。 **总结:有的小伙伴可能会说,看着前面的演示步骤好复杂啊。确实是,rebase 其实相当于是 merge 的进阶使用方式,目的就是为了让代码 commit 呈线性记录。** git merge 对比 git rebase 该如何选择?
+## 由于没有多出 merge 的 commit 记录,所以不会存在分叉的 commit 记录,代码记录都是以线性的方式,做代码审查一目了然。**总结:有的小伙伴可能会说,看着前面的演示步骤好复杂啊。确实是,rebase 其实相当于是 merge 的进阶使用方式,目的就是为了让代码 commit 呈线性记录。** git merge 对比 git rebase 该如何选择?
 
 `git merge` 操作合并分支会让两个分支的每一次提交都按照提交时间(并不是 push 时间)排序,并且会将两个分支的最新一次 commit 点进行合并成一个新的 commit,最终的分支树呈现非整条线性直线的形式。
 
-## `git rebase` 操作实际上是将当前执行 rebase 分支的所有基于原分支提交点之后的 commit 打散成一个一个的 patch,并重新生成一个新的 commit hash 值,再次基于原分支目前最新的commit点上进行提交,并不根据两个分支上实际的每次提交的时间点排序,rebase 完成后,切到基分支进行合并另一个分支时也不会生成一个新的 commit 点,可以保持整个分支树的完美线性。 **从效果出发,如果代码版本迭代快,项目大,参与人多,建议最好用 rebase 方式合并。反之则直接用 merge 即可。** 加餐学习:git stash 解决线上代码冲突
+## `git rebase` 操作实际上是将当前执行 rebase 分支的所有基于原分支提交点之后的 commit 打散成一个一个的 patch,并重新生成一个新的 commit hash 值,再次基于原分支目前最新的commit点上进行提交,并不根据两个分支上实际的每次提交的时间点排序,rebase 完成后,切到基分支进行合并另一个分支时也不会生成一个新的 commit 点,可以保持整个分支树的完美线性。**从效果出发,如果代码版本迭代快,项目大,参与人多,建议最好用 rebase 方式合并。反之则直接用 merge 即可。** 加餐学习:git stash 解决线上代码冲突
 
 以上的演示合并冲突,是为了让我们的 commit 历史记录更加的便于审查。接下来我要说的`git stash`则是应急所需,平时工作中都是迫不得已的时候采用的,学会了以备不时之需。
 
diff --git "a/docs/Article/Other/\347\224\237\346\210\220\346\265\217\350\247\210\345\231\250\345\224\257\344\270\200\347\250\263\345\256\232ID\347\232\204\346\216\242\347\264\242.md" "b/docs/Article/Other/\347\224\237\346\210\220\346\265\217\350\247\210\345\231\250\345\224\257\344\270\200\347\250\263\345\256\232ID\347\232\204\346\216\242\347\264\242.md"
index 8fefe7e73..37eaf11ab 100644
--- "a/docs/Article/Other/\347\224\237\346\210\220\346\265\217\350\247\210\345\231\250\345\224\257\344\270\200\347\250\263\345\256\232ID\347\232\204\346\216\242\347\264\242.md"
+++ "b/docs/Article/Other/\347\224\237\346\210\220\346\265\217\350\247\210\345\231\250\345\224\257\344\270\200\347\250\263\345\256\232ID\347\232\204\346\216\242\347\264\242.md"
@@ -14,7 +14,7 @@
 
 ### **2.1. 一些参考维度** ### **2.1.1. MAC 地址** MAC 地址又称为物理地址、硬件地址,它是一个用来确认网络设备位置的位址,在 OSI 七层模型中,第二层数据链路层负责 MAC 地址,网卡的 MAC 地址通常是由网卡厂家烧入网卡里的,能在网络中唯一标识一个网卡
 
-如果我们能获得 MAC 地址,我们就能唯一标识一台电脑(一个网卡),这能在很大程度保证生成 ID 的唯一性和稳定性。但是想在浏览器端获取 MAC 地址基本是做不到的,如果是 Windows 用户, **一种可能的方法** 是让用户安装 ActiveX 控件。从服务端也很难操作,获取到的 MAC 地址可能是中间路由器的 MAC 地址,而不是浏览器设备的 MAC 地址。另外,MAC 地址也是可以伪造的。
+如果我们能获得 MAC 地址,我们就能唯一标识一台电脑(一个网卡),这能在很大程度保证生成 ID 的唯一性和稳定性。但是想在浏览器端获取 MAC 地址基本是做不到的,如果是 Windows 用户,**一种可能的方法** 是让用户安装 ActiveX 控件。从服务端也很难操作,获取到的 MAC 地址可能是中间路由器的 MAC 地址,而不是浏览器设备的 MAC 地址。另外,MAC 地址也是可以伪造的。
 
 ### **2.1.2. IP 地址** 当设备连接网络,设备将被分配一个 IP 地址,用作标识。IP 地址记录在 OSI 模型中的第三层网络层。服务端从 HTTP 请求中可以分析得到 IP 地址,浏览器端可以通过 `WebRTC` 网页实时通信技术 的 `RTCPeerConnection` 的 API, 获取到客户端的 IP 地址。IP 地址是可能重复的,可能有多个设备处在一个局域网下,共享一个公网 IP。 IP 地址也是多变的,计算机经常会在不同的网络和代理下使用。但是,IP 地址的唯一性比较强,两个设备的碰撞率是很低的,如果能利用 IP 地址去增强浏览器 ID 的唯一性是非常有价值的
 
@@ -28,11 +28,11 @@
 
 它最早的灵感就是来源于 EFF 提出浏览器指纹的概念,在此基础上,又增加了许多特征参数,包括一些新型的识别技术,比如:Canvas、AudioContext 等,在不断地迭代中,优化这些参数,并用最快的方式生成指纹。
 
-### **2.2.1. 开源版本** ![img](../assets/v2-7ba81f477d9a1aeca1c53ccfa18c11fc_1440w.jpg)
+### **2.2.1. 开源版本**![img](../assets/v2-7ba81f477d9a1aeca1c53ccfa18c11fc_1440w.jpg)
 
-开源版本通过浏览器捕捉到各种指标并通过哈希算法组合成一个指纹,我把这些指标分成四类,浏览器特性、存储、媒体查询、其它。具体如上图所示。目前 GitHub Star 数有 **14.1k** , 并被 **8000+** 网站使用。
+开源版本通过浏览器捕捉到各种指标并通过哈希算法组合成一个指纹,我把这些指标分成四类,浏览器特性、存储、媒体查询、其它。具体如上图所示。目前 GitHub Star 数有 **14.1k**, 并被 **8000+** 网站使用。
 
-### **2.2.2. 唯一性和稳定性** 到底什么样的指纹是唯一和稳定的? **唯一性** 指在不同的设备,或者不同的浏览器上,每个指纹都是不同的。实际情况是,总会有各种指标都完全相同的用户,那么就会生成一样的指纹,比如唯一性能达到 95%,那 100 个指纹中,有 5 个是重复的。 **稳定性** 指用户每次打开相同设备的相同浏览器,生成的指纹是一样的。如果用户没有修改设备或者浏览器的设置,且浏览器没有升级,生成的 ID 是不会变的。但是在实际情况中,如果选用了一些不稳定的指标参与计算或者用户使用了一些反指纹手段,那么可能每次生成的都会不一样
+### **2.2.2. 唯一性和稳定性** 到底什么样的指纹是唯一和稳定的?**唯一性** 指在不同的设备,或者不同的浏览器上,每个指纹都是不同的。实际情况是,总会有各种指标都完全相同的用户,那么就会生成一样的指纹,比如唯一性能达到 95%,那 100 个指纹中,有 5 个是重复的。**稳定性** 指用户每次打开相同设备的相同浏览器,生成的指纹是一样的。如果用户没有修改设备或者浏览器的设置,且浏览器没有升级,生成的 ID 是不会变的。但是在实际情况中,如果选用了一些不稳定的指标参与计算或者用户使用了一些反指纹手段,那么可能每次生成的都会不一样
 
 为了提高唯一和稳定,我们的指纹不仅应该结合许多指标,还应该尽量筛选出区分度大和稳定性高的指标,需要找到平衡唯一和稳定的指标组合。在控制变量的情况下,随着指标的增多,唯一性增强,但同时稳定性会减弱。
 
@@ -42,11 +42,11 @@
 
 ### **2.2.3. 一些有趣的指标**  **Canvas 指纹** `Canvas`(画布)是 HTML5 中的一种动态绘图标签,可以用它来绘制图片。在不同操作系统、不同浏览器上,Canvas 绘制的图像将以不同的方式呈现,具有很强的唯一性。原理是:在图片格式上,浏览器使用不同的图形处理引擎、图像导出选项、压缩级别,在系统层面,操作系统有不同的字体,它们使用不同的算法和设置来进行抗锯齿和子像素渲染。另外,Canvas 具有良好的兼容性,几乎被所有主流浏览器支持
 
-在具体代码上,通过 Canvas 绘图 API 绘制文字或图形后,通过 `canvas.toDataURL()` 方法获得 base64 编码,根据需要可再 hash 成指纹。 **判断是否包含某字体** 首先,前端不存在兼容性比较好的原生方法判断是否包含某字体,那么该怎么判断呢?
+在具体代码上,通过 Canvas 绘图 API 绘制文字或图形后,通过 `canvas.toDataURL()` 方法获得 base64 编码,根据需要可再 hash 成指纹。**判断是否包含某字体** 首先,前端不存在兼容性比较好的原生方法判断是否包含某字体,那么该怎么判断呢?
 
-不同字体显示相同的文案时,宽度是不同的。我们可以利用这一点,设置三种默认字体,'monospace', 'sans-serif', 'serif', 新建一个 span 标签,设置 font-family 为当前字体和默认字体, 设置另外一个 span 为默认字体,如果存在当前字体,两个 span 的宽度或者高度是不一样的,如果完全一致,则代表不存在该字体,第一个 span 回退回默认字体。 **Math 类几个函数精度不同** 不同的操作系统和架构,不同的浏览器的几个 Math 数学函数可能产生不同结果。比如 Math.sin(-1e300),在不同的系统和浏览器上算出来的值可能是不同的。有趣的是,在代码注释中可以看到 **相关链接** ,是来自于 tor 浏览器,这是一个宣称保护隐私的浏览器,具备一定的反指纹策略。所以,有时候有效信息可以从对手那里获取。 **音频指纹**  **查看链接** ,原理和 Canvas 类似,都是利用硬件和软件的差异,一个生成音频,一个生成图片。
+不同字体显示相同的文案时,宽度是不同的。我们可以利用这一点,设置三种默认字体,'monospace', 'sans-serif', 'serif', 新建一个 span 标签,设置 font-family 为当前字体和默认字体, 设置另外一个 span 为默认字体,如果存在当前字体,两个 span 的宽度或者高度是不一样的,如果完全一致,则代表不存在该字体,第一个 span 回退回默认字体。**Math 类几个函数精度不同** 不同的操作系统和架构,不同的浏览器的几个 Math 数学函数可能产生不同结果。比如 Math.sin(-1e300),在不同的系统和浏览器上算出来的值可能是不同的。有趣的是,在代码注释中可以看到 **相关链接**,是来自于 tor 浏览器,这是一个宣称保护隐私的浏览器,具备一定的反指纹策略。所以,有时候有效信息可以从对手那里获取。**音频指纹**  **查看链接**,原理和 Canvas 类似,都是利用硬件和软件的差异,一个生成音频,一个生成图片。
 
-### **2.2.4. Pro 付费版本** ![img](../assets/v2-b6cfc81c0d888c8d0fc00306df717999_1440w.jpg)
+### **2.2.4. Pro 付费版本**![img](../assets/v2-b6cfc81c0d888c8d0fc00306df717999_1440w.jpg)
 
 开源版本和 **专业版** 主要区别在于,开源版本仅仅运行于浏览器端,浏览器通过 JS 获取到一系列指标后计算哈希值。当两个用户有同样的设备和浏览器时,他们可能生成同样的指纹,从而无法区分两个用户(从提取的指标来看,它们的确是相同的,只是有些业务场景需要更加细分的手段去区分用户)。专业版建立在开源版的基础上,并增加了很多服务器端的手段去辅助识别用户,比如:IP 地址、Cookie、本地存储、存储历史记录进行模糊匹配和处理浏览器升级、服务器端分析和机器学习,并最终由服务端生成指纹。
 
@@ -56,13 +56,13 @@
 
 可以看出,专业版增加了一些服务端检测技术后,在生成 ID 的唯一性和稳定性都有很大的提高,另外,ID 在服务端生成也更安全可靠。笔者曾试图调试研究专业版压缩混淆的代码,但能力和时间有限,只能看得出小部分。后来,尝试通过实验和对比的方法,了解专业版是怎么提高唯一性和稳定性的。
 
-### **分析和发现** ![img](../assets/v2-504158c0cbe7ee8bcd4fb3bc6f1d62b3_1440w.jpg)
+### **分析和发现**![img](../assets/v2-504158c0cbe7ee8bcd4fb3bc6f1d62b3_1440w.jpg)
 
 ![img](../assets/v2-2f950787bcc29fe62176b84407fa42fb_1440w.jpg)
 
 尝试 1:IP 是否会影响指纹
 
-结果:通过 VPN 或者连接不同的网络,指纹的生成是稳定的, **直接切换 IP 并不影响指纹** 。可以理解,因为 IP 是非常不稳定的,用户很容易切换 IP,导致指纹变化。个人看法,IP 只是作为辅助手段进行校验,比如曾经有使用某个 IP 的历史记录,就可以佐证是之前生成的某个指纹,即使某些指标发生异动。
+结果:通过 VPN 或者连接不同的网络,指纹的生成是稳定的,**直接切换 IP 并不影响指纹** 。可以理解,因为 IP 是非常不稳定的,用户很容易切换 IP,导致指纹变化。个人看法,IP 只是作为辅助手段进行校验,比如曾经有使用某个 IP 的历史记录,就可以佐证是之前生成的某个指纹,即使某些指标发生异动。
 
 尝试 2:伪造某些指标是否会影响指纹
 
@@ -76,7 +76,7 @@
 
 尝试 3:伪造某些指标并清除 Cookie 、本地存储和缓存后是否影响专业版指纹
 
-结果:会影响,基本上可以实现随机指纹。所以,基本可以确认, **当 Cookie 等内容存在时,能够匹配历史记录里的值就直接从里面取值,这样能在很大程度上保持指纹的稳定。在实验中,Cookie、本地存储和缓存都可以用于保持稳定。**
+结果:会影响,基本上可以实现随机指纹。所以,基本可以确认,**当 Cookie 等内容存在时,能够匹配历史记录里的值就直接从里面取值,这样能在很大程度上保持指纹的稳定。在实验中,Cookie、本地存储和缓存都可以用于保持稳定。**
 
 其他发现:
 
@@ -94,7 +94,7 @@
 
 ### **3. 采用的做法**
 
-- 首先将 FPJS v3.0.6 在项目中打点测试,在几百万~几千万的独立用户数量级上,用独立用户数 / 独立指纹数 约等于 1.3,这个在移动端 App 的数据大约是 1.1 ~ 1.2,一个用户可能有多个账号,可能用多个账号登录同一个浏览器,也可能同一个账号登录多个浏览器,是多对多的关系。在几百万到几千万的变化上,这个比例相对比较稳定,至少随着用户数的增加,稳定性不会降低;另外,这个比例跟移动端比没有偏差太多,还算合格,后面可以结合其他手段提高唯一性;其实在实现像设备管理这样的功能时,只需要保证同一用户在不同浏览器上生成不同的指纹即可。 **综上所述,可以采用 FPJS 作为前端生成指纹的选择,并结合 Cookie 等策略优化唯一和稳定。**
+- 首先将 FPJS v3.0.6 在项目中打点测试,在几百万~几千万的独立用户数量级上,用独立用户数 / 独立指纹数 约等于 1.3,这个在移动端 App 的数据大约是 1.1 ~ 1.2,一个用户可能有多个账号,可能用多个账号登录同一个浏览器,也可能同一个账号登录多个浏览器,是多对多的关系。在几百万到几千万的变化上,这个比例相对比较稳定,至少随着用户数的增加,稳定性不会降低;另外,这个比例跟移动端比没有偏差太多,还算合格,后面可以结合其他手段提高唯一性;其实在实现像设备管理这样的功能时,只需要保证同一用户在不同浏览器上生成不同的指纹即可。**综上所述,可以采用 FPJS 作为前端生成指纹的选择,并结合 Cookie 等策略优化唯一和稳定。**
 
 ![img](../assets/v2-61d5a8ea0c9d19ca6e190c20e8019ee3_1440w.jpg)
 
@@ -119,10 +119,10 @@
 
 - tor 浏览器
 
-tor 浏览器号称是最安全的浏览器,它在反指纹上做了很多工作,它的主要策略是 **让所有 tor 浏览器的用户都拥有完全相同的指纹** ,无论你是什么设备或者操作系统,尽量降低指纹的唯一性。另外,tor 天然就是隐身模式,可以更换身份以及可以隐藏真实 IP。当然,在使用体验上,tor 相对会慢一点,毕竟经历了这么多安全策略。
+tor 浏览器号称是最安全的浏览器,它在反指纹上做了很多工作,它的主要策略是 **让所有 tor 浏览器的用户都拥有完全相同的指纹**,无论你是什么设备或者操作系统,尽量降低指纹的唯一性。另外,tor 天然就是隐身模式,可以更换身份以及可以隐藏真实 IP。当然,在使用体验上,tor 相对会慢一点,毕竟经历了这么多安全策略。
 
 - 攻与防
 
-浏览器指纹技术根植于 Web 诞生以来就存在的机制里,在可预见的未来,要完全摆脱它是很困难的。随着浏览器指纹攻防两端技术的不断提升,这种竞争可能愈演愈烈。试想, **随着技术的不断提升,希望保护隐私的用户为了不被追踪就需要更高的防御手段,这势必会影响用户体验,造成两败俱伤的局面。** 其实,就像上文说的,浏览器指纹只是一种技术,它的好与坏取决于你所身处的位置以及怎么使用它, **笔者觉得,可以建立一种机制,允许它的合理存在,并合理地使用它,用于一些合法合规,并且对用户友好的场景。** ### **5. 总结**
+浏览器指纹技术根植于 Web 诞生以来就存在的机制里,在可预见的未来,要完全摆脱它是很困难的。随着浏览器指纹攻防两端技术的不断提升,这种竞争可能愈演愈烈。试想,**随着技术的不断提升,希望保护隐私的用户为了不被追踪就需要更高的防御手段,这势必会影响用户体验,造成两败俱伤的局面。** 其实,就像上文说的,浏览器指纹只是一种技术,它的好与坏取决于你所身处的位置以及怎么使用它,**笔者觉得,可以建立一种机制,允许它的合理存在,并合理地使用它,用于一些合法合规,并且对用户友好的场景。** ### **5. 总结**
 
 本文从几个参考维度开始分析,介绍了浏览器指纹的生成原理。而后,展开分析了 FingerprintJS 的开源版和 Pro 版,最后在项目中进行了首次应用,在后续迭代的过程中,会不断根据实际情况进行优化。本文只是初步探索与尝试,如有任何错误和不足,欢迎批评指正。
diff --git "a/docs/Article/Other/\347\274\223\345\255\230\345\246\202\344\275\225\344\277\235\350\257\201\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\347\232\204\345\217\214\345\206\231\344\270\200\350\207\264\346\200\247\357\274\237.md" "b/docs/Article/Other/\347\274\223\345\255\230\345\246\202\344\275\225\344\277\235\350\257\201\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\347\232\204\345\217\214\345\206\231\344\270\200\350\207\264\346\200\247\357\274\237.md"
index a4f6c6406..12863afa0 100644
--- "a/docs/Article/Other/\347\274\223\345\255\230\345\246\202\344\275\225\344\277\235\350\257\201\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\347\232\204\345\217\214\345\206\231\344\270\200\350\207\264\346\200\247\357\274\237.md"
+++ "b/docs/Article/Other/\347\274\223\345\255\230\345\246\202\344\275\225\344\277\235\350\257\201\347\274\223\345\255\230\344\270\216\346\225\260\346\215\256\345\272\223\347\232\204\345\217\214\345\206\231\344\270\200\350\207\264\346\200\247\357\274\237.md"
@@ -22,7 +22,7 @@
 
 举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低,用到缓存才去算缓存。
 
-其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。 **分析:先更新数据库,再删除缓存** ![img](../assets/885859-20200513161105788-1423496257.png)
+其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。**分析:先更新数据库,再删除缓存**![img](../assets/885859-20200513161105788-1423496257.png)
 
 上图流程:建立中缓存突然失效了
 
@@ -32,11 +32,11 @@
 3)请求A这时才设置缓存为100
 ```
 
-这种情况发生的不一致,是因为缓存突然失效了。而且还要保证请求B更新操作 比 请求A的查询操作还要快;才会导致不一致。 **这种情况概率会很少。一般要求不高的项目可以采用此方式(推荐)** 。
+这种情况发生的不一致,是因为缓存突然失效了。而且还要保证请求B更新操作 比 请求A的查询操作还要快;才会导致不一致。**这种情况概率会很少。一般要求不高的项目可以采用此方式(推荐)** 。
 
 ## 缓存删除失败,导致不一致
 
-这种 **先更新数据库,再删除缓存的策略中** ,因为要删除缓存,但如果缓存删除失败,就会导致数据库与缓存不一致。这个问题怎么办?我们正常想到的是利用我们MQ中间件去实现。
+这种 **先更新数据库,再删除缓存的策略中**,因为要删除缓存,但如果缓存删除失败,就会导致数据库与缓存不一致。这个问题怎么办?我们正常想到的是利用我们MQ中间件去实现。
 
 ![你知道如何更新缓存吗?如何保证缓存和数据库双写一致性?](../assets/caf14923aece4c83b2d626d173530630.jpg)
 
@@ -66,7 +66,7 @@
 
 为什么上亿流量高并发场景下,缓存会出现这个问题?
 
-只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。 **解决方案如下:**   更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。
+只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。**解决方案如下:**   更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。
 
 一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
 
@@ -98,8 +98,8 @@
 
 **2、读请求并发量过高**   这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。
 
-但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。 **3、多服务实例部署的请求路由**   可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。
+但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。**3、多服务实例部署的请求路由**   可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。
 
-比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。 **4、热点商品的路由问题,导致请求的倾斜**
+比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。**4、热点商品的路由问题,导致请求的倾斜**
 
 万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。
diff --git "a/docs/Article/Other/\347\275\221\346\230\223\344\270\245\351\200\211\346\200\216\344\271\210\345\201\232\345\205\250\351\223\276\350\267\257\347\233\221\346\216\247\347\232\204\357\274\237.md" "b/docs/Article/Other/\347\275\221\346\230\223\344\270\245\351\200\211\346\200\216\344\271\210\345\201\232\345\205\250\351\223\276\350\267\257\347\233\221\346\216\247\347\232\204\357\274\237.md"
index fc5037f21..7e777ebfb 100644
--- "a/docs/Article/Other/\347\275\221\346\230\223\344\270\245\351\200\211\346\200\216\344\271\210\345\201\232\345\205\250\351\223\276\350\267\257\347\233\221\346\216\247\347\232\204\357\274\237.md"
+++ "b/docs/Article/Other/\347\275\221\346\230\223\344\270\245\351\200\211\346\200\216\344\271\210\345\201\232\345\205\250\351\223\276\350\267\257\347\233\221\346\216\247\347\232\204\357\274\237.md"
@@ -186,9 +186,9 @@ load = 首次渲染时间 + DOM 解析耗时 + 同步 JS 执行 + 资源加载
 
 > 在 Chrome 浏览器中按 F12 打开开发者工具面板,当 Network 页签上的 Disable cache 未勾选时,transferSize 为 0。
 >
-> 勾选 Disable cache 后,transferSize 即恢复正常。 **2. 资源加载的 Time 为 0?** Time 数据是通过 PerformanceResourceTiming.duration 获取的。在瀑布图中查看静态资源加载情况时,部分情况下 Time 为 0,是由于该请求命中了缓存,并且是通过 max-age 控制的长缓存。
+> 勾选 Disable cache 后,transferSize 即恢复正常。**2. 资源加载的 Time 为 0?** Time 数据是通过 PerformanceResourceTiming.duration 获取的。在瀑布图中查看静态资源加载情况时,部分情况下 Time 为 0,是由于该请求命中了缓存,并且是通过 max-age 控制的长缓存。
 
-> 在 Chrome 浏览器中按 F12 打开开发者工具面板,取消勾选 Network 页签上的 Disable cache,刷新页面后即可看到经过网络过程所耗的时间。 **3. 页面性能指标时间为 0?**
+> 在 Chrome 浏览器中按 F12 打开开发者工具面板,取消勾选 Network 页签上的 Disable cache,刷新页面后即可看到经过网络过程所耗的时间。**3. 页面性能指标时间为 0?**
 
 查看 API 返回的数据时,如果发现很多返回的时间数据为 0,是因为受同源策略的影响,跨域资源获取的时间点会为 0,主要包括以下属性:redirectStart、redirectEnd、domainLookupStart、domainLookupEnd、connectStart、connectEnd、secureConnectionStart、requestStart、responseStart。
 
diff --git "a/docs/Article/Other/\347\276\216\345\233\242\347\202\271\350\257\204Kubernetes\351\233\206\347\276\244\347\256\241\347\220\206\345\256\236\350\267\265.md" "b/docs/Article/Other/\347\276\216\345\233\242\347\202\271\350\257\204Kubernetes\351\233\206\347\276\244\347\256\241\347\220\206\345\256\236\350\267\265.md"
index b08e14240..37610b90c 100644
--- "a/docs/Article/Other/\347\276\216\345\233\242\347\202\271\350\257\204Kubernetes\351\233\206\347\276\244\347\256\241\347\220\206\345\256\236\350\267\265.md"
+++ "b/docs/Article/Other/\347\276\216\345\233\242\347\202\271\350\257\204Kubernetes\351\233\206\347\276\244\347\256\241\347\220\206\345\256\236\350\267\265.md"
@@ -113,9 +113,9 @@ kube-scheduler的局部最优解
 
 前面提到,稳定性和风险可控性对大规模集群管理来说非常重要。从架构上来看,Kubelet是离真实业务最近的集群管理组件,我们知道社区版本的Kubelet对本机资源管理有着很大的自主性,试想一下,如果某个业务正在运行,但是Kubelet由于出发了驱逐策略而把这个业务的容器干掉了会发生什么?这在我们的集群中是不应该发生的,所以需要收敛和封锁Kubelet的自决策能力,它对本机上业务容器的操作都应该从上层平台发起。
 
-**容器重启策略** Kernel升级是日常的运维操作,在通过重启宿主机来升级Kernel版本的时候,我们发现宿主机重启后,上面的容器无法自愈或者自愈后版本不对,这会引发业务的不满,也造成了我们不小的运维压力。后来我们为Kubelet增加了一个重启策略(Reuse),同时保留了原生重启策略(Rebuild),保证容器系统盘和数据盘的信息都能保留,宿主机重启后容器也能自愈。 **IP状态保持** 根据美团点评的网络环境,我们自研了CNI插件,并通过基于Pod唯一标识来申请和复用IP。做到了应用IP在Pod迁移和容器重启之后也能复用,为业务上线和运维带来了不少的收益。 **限制驱逐策略** 我们知道Kubelet拥有节点自动修复的能力,例如在发现异常容器或不合规容器后,会对它们进行驱逐删除操作,这对于我们来说风险太大,我们允许容器在一些次要因素方面可以不合规。例如当Kubelet发现当前宿主机上容器个数比设置的最大容器个数大时,会挑选驱逐和删除某些容器,虽然正常情况下不会轻易发生这种问题,但是我们也需要对此进行控制,降低此类风险。
+**容器重启策略** Kernel升级是日常的运维操作,在通过重启宿主机来升级Kernel版本的时候,我们发现宿主机重启后,上面的容器无法自愈或者自愈后版本不对,这会引发业务的不满,也造成了我们不小的运维压力。后来我们为Kubelet增加了一个重启策略(Reuse),同时保留了原生重启策略(Rebuild),保证容器系统盘和数据盘的信息都能保留,宿主机重启后容器也能自愈。**IP状态保持** 根据美团点评的网络环境,我们自研了CNI插件,并通过基于Pod唯一标识来申请和复用IP。做到了应用IP在Pod迁移和容器重启之后也能复用,为业务上线和运维带来了不少的收益。**限制驱逐策略** 我们知道Kubelet拥有节点自动修复的能力,例如在发现异常容器或不合规容器后,会对它们进行驱逐删除操作,这对于我们来说风险太大,我们允许容器在一些次要因素方面可以不合规。例如当Kubelet发现当前宿主机上容器个数比设置的最大容器个数大时,会挑选驱逐和删除某些容器,虽然正常情况下不会轻易发生这种问题,但是我们也需要对此进行控制,降低此类风险。
 
-#### 可扩展性 **资源调配** 在Kubelet的扩展性方面我们增强了资源的可操作性,例如为容器绑定Numa从而提升应用的稳定性;根据应用等级为容器设置CPUShare,从而调整调度权重;为容器绑定CPUSet等等。 **增强容器** 我们打通并增强了业务对容器的配置能力,支持业务给自己的容器扩展ulimit、io limit、pid limit、swap等参数的同时也增强容器之间的隔离能力。 **应用原地升级** 大家都知道,Kubernetes默认只要Pod的关键信息有改动,例如镜像信息,就会出发Pod的重建和替换,这在生产环境中代价是很大的,一方面IP和HostName会发生改变,另一方面频繁的重建也给集群管理带来了更多的压力,甚至还可能导致无法调度成功。为了解决该问题,我们打通了自上而下的应用原地升级功能,即可以动态高效地修改应用的信息,并能在原地(宿主机)进行升级。 **镜像分发**
+#### 可扩展性 **资源调配** 在Kubelet的扩展性方面我们增强了资源的可操作性,例如为容器绑定Numa从而提升应用的稳定性;根据应用等级为容器设置CPUShare,从而调整调度权重;为容器绑定CPUSet等等。**增强容器** 我们打通并增强了业务对容器的配置能力,支持业务给自己的容器扩展ulimit、io limit、pid limit、swap等参数的同时也增强容器之间的隔离能力。**应用原地升级** 大家都知道,Kubernetes默认只要Pod的关键信息有改动,例如镜像信息,就会出发Pod的重建和替换,这在生产环境中代价是很大的,一方面IP和HostName会发生改变,另一方面频繁的重建也给集群管理带来了更多的压力,甚至还可能导致无法调度成功。为了解决该问题,我们打通了自上而下的应用原地升级功能,即可以动态高效地修改应用的信息,并能在原地(宿主机)进行升级。**镜像分发**
 
 镜像分发是影响容器扩容时长的一个重要环节,我们采取了一系列手段来优化,保证镜像分发效率高且稳定:
 
diff --git "a/docs/Article/Other/\347\276\216\345\233\242\347\231\276\344\272\277\350\247\204\346\250\241API\347\275\221\345\205\263\346\234\215\345\212\241Shepherd\347\232\204\350\256\276\350\256\241\344\270\216\345\256\236\347\216\260.md" "b/docs/Article/Other/\347\276\216\345\233\242\347\231\276\344\272\277\350\247\204\346\250\241API\347\275\221\345\205\263\346\234\215\345\212\241Shepherd\347\232\204\350\256\276\350\256\241\344\270\216\345\256\236\347\216\260.md"
index a5d748fea..b87f5ddc6 100644
--- "a/docs/Article/Other/\347\276\216\345\233\242\347\231\276\344\272\277\350\247\204\346\250\241API\347\275\221\345\205\263\346\234\215\345\212\241Shepherd\347\232\204\350\256\276\350\256\241\344\270\216\345\256\236\347\216\260.md"
+++ "b/docs/Article/Other/\347\276\216\345\233\242\347\231\276\344\272\277\350\247\204\346\250\241API\347\275\221\345\205\263\346\234\215\345\212\241Shepherd\347\232\204\350\256\276\350\256\241\344\270\216\345\256\236\347\216\260.md"
@@ -76,7 +76,7 @@ API配置的详细说明:
 
 一种是不包含路径变量的直接映射的MAP结构。其中,Key就是完整的域名和路径信息,Value是具体的API配置。
 
-另外一种是包含路径变量的前缀树数据结构。通过前缀匹配的方式,先进行叶子节点精确查找,并将查找节点入栈处理,如果匹配不上,则将栈顶节点出栈,再将同级的变量节点入栈,如果仍然找不到,则继续回溯,直到找到(或没找到)路径节点并退出。 **功能组件** 当请求流量命中API请求路径进入服务端,具体处理逻辑由DSL中配置的一系列功能组件完成。网关提供了丰富的功能组件集成,包括链路追踪、实时监控、访问日志、参数校验、鉴权、限流、熔断降级、灰度分流等,如下图所示:
+另外一种是包含路径变量的前缀树数据结构。通过前缀匹配的方式,先进行叶子节点精确查找,并将查找节点入栈处理,如果匹配不上,则将栈顶节点出栈,再将同级的变量节点入栈,如果仍然找不到,则继续回溯,直到找到(或没找到)路径节点并退出。**功能组件** 当请求流量命中API请求路径进入服务端,具体处理逻辑由DSL中配置的一系列功能组件完成。网关提供了丰富的功能组件集成,包括链路追踪、实时监控、访问日志、参数校验、鉴权、限流、熔断降级、灰度分流等,如下图所示:
 
 ![img](../assets/v2-13e15f905a1566cdb6bba23c030b98f0_1440w.jpg) **协议转换&服务调用** API调用的最后一步,就是协议转换以及服务调用了。网关需要完成的工作包括:获取HTTP请求参数、Context本地参数,拼装后端服务参数,完成HTTP协议到后端服务的协议转换,调用后端服务获取响应结果并转换为HTTP响应结果。
 
@@ -123,11 +123,11 @@ Shepherd提供了一些常规的稳定性保障手段,来保证自身和后端
 
 ### **2.2.4 请求安全** 请求安全是API网关非常重要的能力,Shepherd集成了丰富的安全相关的系统组件,包括有基础的请求签名、SSO单点登录、基于SSO鉴权的UAC/UPM访问控制、用户鉴权Passport、商家鉴权EPassport、商家权益鉴权、反爬等等。业务研发人员只需要简单配置,即可使用
 
-### **2.2.5 可灰度** API网关作为请求入口,往往肩负着请求流量灰度验证的重任。 **灰度场景** Shepherd在灰度能力上,支持灰度API自身逻辑,也支持灰度下游服务,也可以同时灰度API自身逻辑和下游服务。如下图所示
+### **2.2.5 可灰度** API网关作为请求入口,往往肩负着请求流量灰度验证的重任。**灰度场景** Shepherd在灰度能力上,支持灰度API自身逻辑,也支持灰度下游服务,也可以同时灰度API自身逻辑和下游服务。如下图所示
 
 ![img](../assets/v2-fd99a04d59e3a86e719963fd01b22ae3_1440w.jpg)
 
-灰度API自身逻辑时,通过将流量分流到不同的API版本实现灰度能力;灰度下游服务时,通过给流量打标,分流到指定的下游灰度单元中。 **灰度策略** Shepherd支持丰富的灰度策略,可以按照比例数灰度,也可以按照特定条件灰度。
+灰度API自身逻辑时,通过将流量分流到不同的API版本实现灰度能力;灰度下游服务时,通过给流量打标,分流到指定的下游灰度单元中。**灰度策略** Shepherd支持丰富的灰度策略,可以按照比例数灰度,也可以按照特定条件灰度。
 
 ### **2.2.6 监控告警**  **立体化监控** Shepherd提供360度的立体化监控,从业务指标、机器指标、JVM指标提供7x24小时的专业守护,如下表
 
@@ -141,9 +141,9 @@ Shepherd提供了一些常规的稳定性保障手段,来保证自身和后端
 
 ### **2.2.8 可迁移** 对于一些已经在对外提供API的Web服务,业务研发人员为了减少运维成本和后续的研发提效,考虑将其迁移到Shepherd API网关
 
-对于一些非核心API,可以考虑使用Oceanus的灰度发布功能直接迁移。但是对于一些核心API,上面的灰度发布功能是机器级别的,粒度较大,不够灵活,不能很好的支持灰度验证过程。 **解决方案** Shepherd为业务研发人员提供了一个灰度SDK,接入SDK的Web服务,可在识别灰度流量后转发到Shepherd API网关进行验证。
+对于一些非核心API,可以考虑使用Oceanus的灰度发布功能直接迁移。但是对于一些核心API,上面的灰度发布功能是机器级别的,粒度较大,不够灵活,不能很好的支持灰度验证过程。**解决方案** Shepherd为业务研发人员提供了一个灰度SDK,接入SDK的Web服务,可在识别灰度流量后转发到Shepherd API网关进行验证。
 
-灰度哪些API、灰度百分比可以在Shepherd管理端动态调节,实时生效,业务研发人员还可以通过SPI的方式自定义灰度策略。灰度验证通过后,再把API迁移到Shepherd API网关,保障迁移过程的稳定性。 **灰度过程**  **灰度前** :在Shepherd管理平台创建API分组,域名配置为目前使用的域名。在Oceanus上,原域名规则不变。
+灰度哪些API、灰度百分比可以在Shepherd管理端动态调节,实时生效,业务研发人员还可以通过SPI的方式自定义灰度策略。灰度验证通过后,再把API迁移到Shepherd API网关,保障迁移过程的稳定性。**灰度过程**  **灰度前** :在Shepherd管理平台创建API分组,域名配置为目前使用的域名。在Oceanus上,原域名规则不变。
 
 ![img](../assets/v2-6bee0f888d5296c3460937a7839339d5_1440w.jpg) **灰度中** :在Shepherd管理平台开启灰度功能,灰度SDK将灰度流量转发到网关服务,进行验证。
 
@@ -161,11 +161,11 @@ Shepherd提供了一些常规的稳定性保障手段,来保证自身和后端
 1. 填写参数映射规则。
 1. 最后手工录入管理平台,发布API。
 
-整个过程非常繁琐,且容易出错。如果需要录入的API多达几十上百个,全部由业务研发人员手工录入的效率是非常低下的。 **解决方案** 那么能不能将服务参数DSL的生成过程给自动化呢? 答案是可以的,业务RD只需在网关录入API文档信息,然后录入服务的Appkey、服务名、方法名信息,Shepherd管理端会从最新发布的服务框架控制台获取到服务参数的JSON Schema信息,JSON Schema定义了服务参数的类型和结构信息,管理端可根据这些信息,自动生成服务参数的JSON Mock数据。结合API文档的信息,自动替换参数名相同的Value值。 这套DSL自动生成方案,使用过程中对业务透明、标准化,业务方只需升级最新版本服务框架即可使用,极大提升研发效率,目前受到业务研发人员的广泛好评。
+整个过程非常繁琐,且容易出错。如果需要录入的API多达几十上百个,全部由业务研发人员手工录入的效率是非常低下的。**解决方案** 那么能不能将服务参数DSL的生成过程给自动化呢? 答案是可以的,业务RD只需在网关录入API文档信息,然后录入服务的Appkey、服务名、方法名信息,Shepherd管理端会从最新发布的服务框架控制台获取到服务参数的JSON Schema信息,JSON Schema定义了服务参数的类型和结构信息,管理端可根据这些信息,自动生成服务参数的JSON Mock数据。结合API文档的信息,自动替换参数名相同的Value值。 这套DSL自动生成方案,使用过程中对业务透明、标准化,业务方只需升级最新版本服务框架即可使用,极大提升研发效率,目前受到业务研发人员的广泛好评。
 
 ![img](../assets/v2-e0556e3e90108b0b69e664f43da9236f_1440w.jpg)
 
-### **2.3.2 API操作提效**  **快速创建API** API网关的核心能力是建立在API配置的基础上的,但提供强大功能的同时带来了较高的复杂性,不少业务研发人员吐槽API配置太繁琐,学习成本高。快速创建API的功能应运而生,业务研发人员只需要提供少量的信息就可以创建API。快速创建API的功能当前分为4种类型(后端RPC服务API、后端HTTP服务API、SSO CallBack API、Nest API),未来会根据业务应用场景的不同,提供更多的快速创建API类型。 **批量操作** 业务研发人员在API网关上,需要管理非常多的业务分组,每个业务分组,最多可以有200个API配置,多个API可能有很多相同的配置,如组件配置,错误码配置和跨域配置的。每个API对于相同的配置都要配置一遍,操作重复度很高。因此Shepherd支持批量操作多个API:勾选多个API后,通过【批量操作】功能可一次性完成多个API配置更新,降低业务重复配置的操作成本。 **API导入导出** Shepherd提供在不同研发环境相互导入导出API的能力,业务研发人员在线下测试完成后,只需要使用API导入导出功能,即可将配置导出到线上生产环境,避免重复配置
+### **2.3.2 API操作提效**  **快速创建API** API网关的核心能力是建立在API配置的基础上的,但提供强大功能的同时带来了较高的复杂性,不少业务研发人员吐槽API配置太繁琐,学习成本高。快速创建API的功能应运而生,业务研发人员只需要提供少量的信息就可以创建API。快速创建API的功能当前分为4种类型(后端RPC服务API、后端HTTP服务API、SSO CallBack API、Nest API),未来会根据业务应用场景的不同,提供更多的快速创建API类型。**批量操作** 业务研发人员在API网关上,需要管理非常多的业务分组,每个业务分组,最多可以有200个API配置,多个API可能有很多相同的配置,如组件配置,错误码配置和跨域配置的。每个API对于相同的配置都要配置一遍,操作重复度很高。因此Shepherd支持批量操作多个API:勾选多个API后,通过【批量操作】功能可一次性完成多个API配置更新,降低业务重复配置的操作成本。**API导入导出** Shepherd提供在不同研发环境相互导入导出API的能力,业务研发人员在线下测试完成后,只需要使用API导入导出功能,即可将配置导出到线上生产环境,避免重复配置
 
 ### **2.4 可扩展性设计** 一个设计良好的基础组件,除了能提供强大的基础能力,还需要有良好的扩展能力。 Shepherd的可扩展性主要体现在:支持自定义组件、服务编排的能力
 
diff --git "a/docs/Article/Other/\350\277\233\351\230\266\357\274\232Dockerfile\351\253\230\351\230\266\344\275\277\347\224\250\346\214\207\345\215\227\345\217\212\351\225\234\345\203\217\344\274\230\345\214\226.md" "b/docs/Article/Other/\350\277\233\351\230\266\357\274\232Dockerfile\351\253\230\351\230\266\344\275\277\347\224\250\346\214\207\345\215\227\345\217\212\351\225\234\345\203\217\344\274\230\345\214\226.md"
index ce32c35cb..26b7ad4c5 100644
--- "a/docs/Article/Other/\350\277\233\351\230\266\357\274\232Dockerfile\351\253\230\351\230\266\344\275\277\347\224\250\346\214\207\345\215\227\345\217\212\351\225\234\345\203\217\344\274\230\345\214\226.md"
+++ "b/docs/Article/Other/\350\277\233\351\230\266\357\274\232Dockerfile\351\253\230\351\230\266\344\275\277\347\224\250\346\214\207\345\215\227\345\217\212\351\225\234\345\203\217\344\274\230\345\214\226.md"
@@ -22,7 +22,7 @@ Docker 构建系统中,默认情况下为了加快构建的速度,会将构
 
 ### builder
 
-这里我们需要引入一个概念 **builder** .
+这里我们需要引入一个概念 **builder**.
 
 builder 就是上面提到的特定模块,也就是说构建内容 context 是由 Docker CLI 发送给 dockerd;并最终由 builder 完成构建。
 
@@ -216,9 +216,9 @@ IMAGE               CREATED             CREATED BY
 ...
 ```
 
-很明显,刚才增加的 ENV 可以直接通过 docker history/docker image history 看到。 **不建议真的这样做** 。
+很明显,刚才增加的 ENV 可以直接通过 docker history/docker image history 看到。**不建议真的这样做** 。
 
-由此,得出了我们的第一个结论, **Docker 镜像的构建历史是不安全的,通过 ENV 设置的信息可在 history 中看到** 。
+由此,得出了我们的第一个结论,**Docker 镜像的构建历史是不安全的,通过 ENV 设置的信息可在 history 中看到** 。
 
 这也引出了我们的第一个问题: **Docker 镜像的构建记录是可查看的,如何管理构建过程中需要的密码/密钥等敏感信息?** ### 高阶特性:密码管理
 
@@ -357,7 +357,7 @@ rpc error: code = Unknown desc = executor failed running \[/bin/sh -c git clone
 
 在上面的内容中,我们学习到了通过 `docker image history` 可以查看镜像的构建历史,但构建历史是透明的,凡是可以拿到该镜像的人均可查看到其构建历史;所以它是不安全的。
 
-尤其是当我们通过 ENV 或者 RUN 指令等,将密码/配置信息等传递进去,或者是将自己的私钥之类的文件拷贝到镜像中, **这些操作都是不安全的,不应该这样使用** ,在启用 BuildKit 之后,我们可以通过使用新的实验性语法做到更安全的操作。
+尤其是当我们通过 ENV 或者 RUN 指令等,将密码/配置信息等传递进去,或者是将自己的私钥之类的文件拷贝到镜像中,**这些操作都是不安全的,不应该这样使用**,在启用 BuildKit 之后,我们可以通过使用新的实验性语法做到更安全的操作。
 
 实验性语法是在 Dockerfile 的头部增加了一个表示当前语法规则的 `# syntax = docker/dockerfile:experimental` (事实上,我们将它称之为 frontend)它其实是一个真实存在的 Docker 镜像,在构建过程中,会将它拉取下来使用,这里的详细内容我们可以之后对 frontend 详解的时候再进行讨论。
 
@@ -494,7 +494,7 @@ REPOSITORY           TAG                 IMAGE ID            CREATED
 remote/spring-boot   1                   644867602b8a        About a minute ago   103MB
 ```
 
-镜像构建成功了。 **注意** 这里给 `docker buildx build` 命令传递了 `--load` 参数,表示我们要将构建好的镜像加载到我们现在在用的 dockerd 当中。
+镜像构建成功了。**注意** 这里给 `docker buildx build` 命令传递了 `--load` 参数,表示我们要将构建好的镜像加载到我们现在在用的 dockerd 当中。
 
 此时再查看 builder 的状态:
 
@@ -620,7 +620,7 @@ a2c1e139697b        About a minute ago
            2 weeks ago          /bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a…   0B
 ```
 
-可以看到之前的每层大小都已经变成了 0,这是因为把所有的层都合并到了最终的镜像上去了。 **特别注意:** `--squash` 虽然在 1.13.0 版本中就已经加入了 Docker 中,但他至今仍然是实验形式;所以你需要按照我在本篇文章开始部分的介绍那样,打开实验性功能的支持。
+可以看到之前的每层大小都已经变成了 0,这是因为把所有的层都合并到了最终的镜像上去了。**特别注意:** `--squash` 虽然在 1.13.0 版本中就已经加入了 Docker 中,但他至今仍然是实验形式;所以你需要按照我在本篇文章开始部分的介绍那样,打开实验性功能的支持。
 
 但直接传递 `--squash` 的方式,相对来说足够的简单,也更安全。
 
diff --git "a/docs/Article/Other/\351\223\201\346\200\273\345\234\250\347\224\250\347\232\204\351\253\230\346\200\247\350\203\275\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230\350\256\241\347\256\227\346\241\206\346\236\266Geode.md" "b/docs/Article/Other/\351\223\201\346\200\273\345\234\250\347\224\250\347\232\204\351\253\230\346\200\247\350\203\275\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230\350\256\241\347\256\227\346\241\206\346\236\266Geode.md"
index 99c8c3186..7157d398a 100644
--- "a/docs/Article/Other/\351\223\201\346\200\273\345\234\250\347\224\250\347\232\204\351\253\230\346\200\247\350\203\275\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230\350\256\241\347\256\227\346\241\206\346\236\266Geode.md"
+++ "b/docs/Article/Other/\351\223\201\346\200\273\345\234\250\347\224\250\347\232\204\351\253\230\346\200\247\350\203\275\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230\350\256\241\347\256\227\346\241\206\346\236\266Geode.md"
@@ -256,7 +256,7 @@ Monitor and Manage Apache Geode
 
 **注意:命令行可以 tab 键自动补全,及其方便操作。** #### connect
 
-要管理 Goode 集群我们需要连接到主 locator 上, 有两种方式。 **1. 直接使用 JMX 进行连接** 当我们知道哪个是主的时候,就直接使用 JMX 进行连接即可。
+要管理 Goode 集群我们需要连接到主 locator 上, 有两种方式。**1. 直接使用 JMX 进行连接** 当我们知道哪个是主的时候,就直接使用 JMX 进行连接即可。
 
 ```plaintext
 gfsh>connect --jmx-manager=192.168.33.15
@@ -964,7 +964,7 @@ Geode 只会读取到一致性的结果,如果处在事务提交中的状态
 
 #### 持久性
 
-关系数据库通过使用磁盘存储进行恢复和事务日志记录来提供持久性。Geode 针对性能进行了优化, **不支持事务的磁盘持久性** 。从测试中发现,确实不支持 persisternt 类型的 region。
+关系数据库通过使用磁盘存储进行恢复和事务日志记录来提供持久性。Geode 针对性能进行了优化,**不支持事务的磁盘持久性** 。从测试中发现,确实不支持 persisternt 类型的 region。
 
 ```java
 Caused by: java.lang.UnsupportedOperationException: Operations on persist-backup regions are not allowed because this thread has an active transaction
@@ -1350,7 +1350,7 @@ server_33_23 | geode-study.jar | /opt/geode_work/server_33_23/geode-study.v5.jar
 server_33_29 | geode-study.jar | /opt/geode_work/server_33_29/geode-study.v5.jar
 ```
 
-我们在做 deploy 的时候,Geode 会自动将实现了 function 接口的类型进行函数注册。 **3. 执行函数** 方式一:
+我们在做 deploy 的时候,Geode 会自动将实现了 function 接口的类型进行函数注册。**3. 执行函数** 方式一:
 
 ```javascript
 Cluster-254 gfsh>execute function --id=func-a --region=test99 --filter=KEY_4,KEY_7
@@ -1398,7 +1398,7 @@ http://192.168.33.15:7070/pulse
 
 在 locator 变为 leader 之后会自动启用 pulse,用户名密码为 admin/admin。
 
-pulse 中可以在不同维护查看数据。 **1. 总览** 内存,成员数,服务数,region 数量,集群读写等等。
+pulse 中可以在不同维护查看数据。**1. 总览** 内存,成员数,服务数,region 数量,集群读写等等。
 
 ![在这里插入图片描述](../assets/133ac250-1689-11ea-94bc-f516225b4bcb.png) **2. ip 维度** 这里多了一个机器的链接详情:
 
@@ -1485,7 +1485,7 @@ metricList.add(build);
 
 #### 防止和恢复磁盘完全错误
 
-监视 Geode 成员的磁盘使用情况非常重要。 如果成员缺少足够的磁盘空间用于磁盘存储,则该成员会尝试关闭磁盘存储及其关联的缓存,并记录错误消息。由于成员磁盘空间不足而导致的关闭可能导致数据丢失,数据文件损坏,日志文件损坏以及可能对您的应用程序产生负面影响 的其他错误情况。为成员提供足够的磁盘空间后,可以重新启动该成员。 **换言之,一定要做磁盘容量监控!** #### java.lang.OutOfMemoryError
+监视 Geode 成员的磁盘使用情况非常重要。 如果成员缺少足够的磁盘空间用于磁盘存储,则该成员会尝试关闭磁盘存储及其关联的缓存,并记录错误消息。由于成员磁盘空间不足而导致的关闭可能导致数据丢失,数据文件损坏,日志文件损坏以及可能对您的应用程序产生负面影响 的其他错误情况。为成员提供足够的磁盘空间后,可以重新启动该成员。**换言之,一定要做磁盘容量监控!** #### java.lang.OutOfMemoryError
 
 如果应用程序经常内存不足,您可能需要对其进行分析以确定原因,可以尝试 -Xmx 重置最大堆大小来增加直接内存。
 
@@ -1521,7 +1521,7 @@ list is: [[ent(27134):60330/45855, ent(27130):60333/36743]]
 
 描述:成员(27130)60333/36743 由于可疑验证失败而面临被迫退出集群的危险。在达到 ack-wait-threshold 之后,将在警告级别发出此警报。
 
-操作员应检查过程以确定其是否健康。 在名为 ent 的机器上,慢响应器的进程 ID 是 27130。慢响应者的端口是 60333/36743。 查找字符串,Starting distribution manager ent:60333/36743,并检查拥有包含此字符串的日志文件的进程。 **如上只是摘抄了几个官方常见异常** ,[具体请查看这里](https://geode.apache.org/docs/guide/19/managing/troubleshooting/chapter_overview.html)。
+操作员应检查过程以确定其是否健康。 在名为 ent 的机器上,慢响应器的进程 ID 是 27130。慢响应者的端口是 60333/36743。 查找字符串,Starting distribution manager ent:60333/36743,并检查拥有包含此字符串的日志文件的进程。**如上只是摘抄了几个官方常见异常**,[具体请查看这里](https://geode.apache.org/docs/guide/19/managing/troubleshooting/chapter_overview.html)。
 
 实际在近一年多的使用中,并未发现不可思议的异常与灾难。
 
@@ -1554,7 +1554,7 @@ gfsh>start server
 ```
 
 **HTTP 分布式 session** 之前接触的分布式 session 方案是 Redis-cluster + Tomcat 来做的, 其实道理是一样的, Geode 替换了 Redis 就成功 geode-session-Tomcat 了。
-Geode 使用了不小的篇幅来描述该扩展功能,[详见](https://geode.apache.org/docs/guide/110/tools_modules/http_session_mgmt/quick_start.html)。 **Redis 适配器** 
+Geode 使用了不小的篇幅来描述该扩展功能,[详见](https://geode.apache.org/docs/guide/110/tools_modules/http_session_mgmt/quick_start.html)。**Redis 适配器** 
 
 ```plaintext
 gfsh> start server --name=server1 --redis-bind-address=localhost \
@@ -1565,7 +1565,7 @@ gfsh> start server --name=server1 --redis-bind-address=localhost \
 
 > Geode 相比较 Redis 具有多线程、高并发、扩展性强、结果报告, 并且 Geode wan 复制模式能够水平扩展,跨数据中心还能维护数据一致性。
 
-虽然这么厉害,但是经过简单测试,还是不要冒险改用 Redis,老老实实用 Redis。 **Lucene 支持**
+虽然这么厉害,但是经过简单测试,还是不要冒险改用 Redis,老老实实用 Redis。**Lucene 支持**
 
 Geode 底层引入了 Lucene 的包并支持创建 index、查询 index,换句话说 Geode 引入了 Lucene 的一些特性在它的内部,依然持有和 Redis 一样的观点,改用 ES 还是老老实实去用 ES 吧。
 
diff --git "a/docs/Article/Other/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\234\250\344\272\222\350\201\224\347\275\221\344\270\232\345\212\241\345\274\200\345\217\221\344\270\255\347\232\204\345\256\236\350\267\265.md" "b/docs/Article/Other/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\234\250\344\272\222\350\201\224\347\275\221\344\270\232\345\212\241\345\274\200\345\217\221\344\270\255\347\232\204\345\256\236\350\267\265.md"
index ae825f4d1..d8504adf1 100644
--- "a/docs/Article/Other/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\234\250\344\272\222\350\201\224\347\275\221\344\270\232\345\212\241\345\274\200\345\217\221\344\270\255\347\232\204\345\256\236\350\267\265.md"
+++ "b/docs/Article/Other/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\234\250\344\272\222\350\201\224\347\275\221\344\270\232\345\212\241\345\274\200\345\217\221\344\270\255\347\232\204\345\256\236\350\267\265.md"
@@ -79,7 +79,7 @@ for (Award award : awardPool.getAwards()) {
 }
 ```
 
-- 按照我们通常思路实现,可以发现:在业务领域里非常重要的抽奖,我的业务逻辑都是写在Service中的,Award充其量只是个数据载体,没有任何行为。 **简单的业务系统采用这种贫血模型和过程化设计是没有问题的,** 但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。
+- 按照我们通常思路实现,可以发现:在业务领域里非常重要的抽奖,我的业务逻辑都是写在Service中的,Award充其量只是个数据载体,没有任何行为。**简单的业务系统采用这种贫血模型和过程化设计是没有问题的,** 但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。
 
 更好的是采用领域模型的开发方式,将数据和行为封装在一起,并与现实世界中的业务对象相映射。各类具备明确的职责划分,将领域逻辑分散到领域对象中。继续举我们上述抽奖的例子,使用概率选择对应的奖品就应当放到AwardPool类中。
 
@@ -87,7 +87,7 @@ for (Award award : awardPool.getAwards()) {
 
 解决 **复杂和大规模软件** 的武器可以被粗略地归为三类:抽象、分治和知识。
 
-**分治** 把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。即更容易设计各部分的协作方式。评判什么是分治得好,即高内聚低耦合。 **抽象** 使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐他们需要注意什么。 **知识** 顾名思义,DDD可以认为是知识的一种。
+**分治** 把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。即更容易设计各部分的协作方式。评判什么是分治得好,即高内聚低耦合。**抽象** 使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐他们需要注意什么。**知识** 顾名思义,DDD可以认为是知识的一种。
 
 DDD提供了这样的知识手段,让我们知道如何抽象出限界上下文以及如何去分治。
 
@@ -99,7 +99,7 @@ DDD提供了这样的知识手段,让我们知道如何抽象出限界上下
 
 在系统复杂之后,我们都需要用分治来拆解问题。一般有两种方式,技术维度和业务维度。技术维度是类似MVC这样,业务维度则是指按业务领域来划分系统。
 
-微服务架构更强调从业务维度去做分治来应对系统复杂度,而DDD也是同样的着重业务视角。 如果 **两者在追求的目标(业务维度)达到了上下文的统一** ,那么在具体做法上有什么联系和不同呢?
+微服务架构更强调从业务维度去做分治来应对系统复杂度,而DDD也是同样的着重业务视角。 如果 **两者在追求的目标(业务维度)达到了上下文的统一**,那么在具体做法上有什么联系和不同呢?
 
 我们将架构设计活动精简为以下三个层面:
 
@@ -150,7 +150,7 @@ DDD与微服务关系
 
 划分限界上下文,不管是Eric Evans还是Vaughn Vernon,在他们的大作里都没有怎么提及。
 
-显然我们不应该按技术架构或者开发任务来创建限界上下文,应该按照语义的边界来考虑。 **我们的实践是,考虑产品所讲的通用语言,从中提取一些术语称之为概念对象,寻找对象之间的联系;或者从需求里提取一些动词,观察动词和对象之间的关系;我们将紧耦合的各自圈在一起,观察他们内在的联系,从而形成对应的界限上下文。形成之后,我们可以尝试用语言来描述下界限上下文的职责,看它是否清晰、准确、简洁和完整。简言之,限界上下文应该从需求出发,按领域划分。**
+显然我们不应该按技术架构或者开发任务来创建限界上下文,应该按照语义的边界来考虑。**我们的实践是,考虑产品所讲的通用语言,从中提取一些术语称之为概念对象,寻找对象之间的联系;或者从需求里提取一些动词,观察动词和对象之间的关系;我们将紧耦合的各自圈在一起,观察他们内在的联系,从而形成对应的界限上下文。形成之后,我们可以尝试用语言来描述下界限上下文的职责,看它是否清晰、准确、简洁和完整。简言之,限界上下文应该从需求出发,按领域划分。**
 
 前文提到,我们的用户划分为运营和用户。其中,运营对抽奖活动的配置十分复杂但相对低频。用户对这些抽奖活动配置的使用是高频次且无感知的。根据这样的业务特点,我们首先将抽奖平台划分为C端抽奖和M端抽奖管理平台两个子域,让两者完全解耦。
 
@@ -281,7 +281,7 @@ C端抽奖领域
 
 在抽奖上下文中,我们通过抽奖(DrawLottery)这个聚合根来控制抽奖行为,可以看到,一个抽奖包括了抽奖ID(LotteryId)以及多个奖池(AwardPool),而一个奖池针对一个特定的用户群体(UserGroup)设置了多个奖品(Award)。
 
-另外,在抽奖领域中,我们还会使用抽奖结果(SendResult)作为输出信息,使用用户领奖记录(UserLotteryLog)作为领奖凭据和存根。 **谨慎使用值对象**
+另外,在抽奖领域中,我们还会使用抽奖结果(SendResult)作为输出信息,使用用户领奖记录(UserLotteryLog)作为领奖凭据和存根。**谨慎使用值对象**
 
 在实践中,我们发现虽然一些领域对象符合值对象的概念,但是随着业务的变动,很多原有的定义会发生变更,值对象可能需要在业务意义具有唯一标识,而对这类值对象的重构往往需要较高成本。因此在特定的情况下,我们也要根据实际情况来权衡领域对象的选型。
 
diff --git "a/docs/Article/Other/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\350\217\261\345\275\242\345\257\271\347\247\260\346\236\266\346\236\204.md" "b/docs/Article/Other/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\350\217\261\345\275\242\345\257\271\347\247\260\346\236\266\346\236\204.md"
index 2e6bbafcd..d7bdd7f91 100644
--- "a/docs/Article/Other/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\350\217\261\345\275\242\345\257\271\347\247\260\346\236\266\346\236\204.md"
+++ "b/docs/Article/Other/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\350\217\261\345\275\242\345\257\271\347\247\260\346\236\266\346\236\204.md"
@@ -14,9 +14,9 @@
 
 - 领域层次
 
-**系统层次的架构** 以限界上下文为核心,从纵向维度根据业务能力纵向切分形成限界上下文,然后从横向维度提炼出由限界上下文组成的价值层(Value-Added Layer)。 **领域层次的架构** 针对限界上下文内部以领域为核心进行关注点分解,形成由内部领域模型与外部网关组成的内外分层架构。
+**系统层次的架构** 以限界上下文为核心,从纵向维度根据业务能力纵向切分形成限界上下文,然后从横向维度提炼出由限界上下文组成的价值层(Value-Added Layer)。**领域层次的架构** 针对限界上下文内部以领域为核心进行关注点分解,形成由内部领域模型与外部网关组成的内外分层架构。
 
-    本文提出的 **菱形对称架构(Rhombic Symmetric Architecture)** 主要针对领域层次的架构,借鉴了六边形架构、分层架构、整洁架构的知识,并结合了领域驱动设计的元模型,使其能够更好地运用到限界上下文的架构设计中。因此,本文会依次介绍六边形架构、整洁架构与分层架构,由此再引出我定义的菱形对称架构。 **说明:** 由于菱形又可以表示为 **diamond** ,故而该架构模式也可以称之为“钻石架构”,简称 diamond。我在 GitHub 上建立了名为[diamond](https://github.com/agiledon/diamond)的代码库,提供了本文案例的 Demo 代码,也清晰地展现了限界上下文的代码结构。
+    本文提出的 **菱形对称架构(Rhombic Symmetric Architecture)** 主要针对领域层次的架构,借鉴了六边形架构、分层架构、整洁架构的知识,并结合了领域驱动设计的元模型,使其能够更好地运用到限界上下文的架构设计中。因此,本文会依次介绍六边形架构、整洁架构与分层架构,由此再引出我定义的菱形对称架构。**说明:** 由于菱形又可以表示为 **diamond**,故而该架构模式也可以称之为“钻石架构”,简称 diamond。我在 GitHub 上建立了名为[diamond](https://github.com/agiledon/diamond)的代码库,提供了本文案例的 Demo 代码,也清晰地展现了限界上下文的代码结构。
 
 ## 架构模式的演进
 
@@ -166,11 +166,11 @@ Eric Evans 在将分层架构引入到领域驱动设计时,结合自己的洞
 
 分层架构仅仅是对限界上下文的逻辑划分,没有任何一门语言提供了`layer`关键字,这就使得层无法作为语法中的一等公民对逻辑层加以约束和限制。在编码实现时,逻辑层或许会以模块的形式表现,但是也可能将整个限界上下文作为一个模块,每个层不过是命名空间的差异,定义为模块内的一个包。不管是物理分离的模块,还是逻辑分离的包,只要能保证限界上下文在六边形边界的保护下能够维持内部结构的清晰,就能降低架构腐蚀的风险。
 然而,当我们为出口端口划定层次时,发现 **六边形架构与领域驱动设计的分层架构存在设计概念上的冲突** 。
-出口端口用于抽象领域模型对外部环境的访问,位于领域六边形的边线之上。根据分层架构的定义,领域六边形的内部属于领域层,介于领域六边形与应用六边形的中间区域属于基础设施层,那么, **位于六边形边线之上的出口端口就应该既不属于领域层,又不属于基础设施层** 。它的职责与属于应用层的入口端口也不同,因为应用层的应用服务是对外部请求的封装,相当于是一个业务用例的外观。
-根据六边形架构的协作原则,领域模型若要访问外部设备,需要调用出口端口。依据整洁架构遵循的“稳定依赖原则”,领域层不能依赖于外层。因此, **出口端口只能放在领域层** 。事实上,领域驱动设计也是如此要求的,它在领域模型中定义了资源库(Repository),用于管理聚合的生命周期,同时,它也将作为抽象的访问外部数据库的出口端口。
+出口端口用于抽象领域模型对外部环境的访问,位于领域六边形的边线之上。根据分层架构的定义,领域六边形的内部属于领域层,介于领域六边形与应用六边形的中间区域属于基础设施层,那么,**位于六边形边线之上的出口端口就应该既不属于领域层,又不属于基础设施层** 。它的职责与属于应用层的入口端口也不同,因为应用层的应用服务是对外部请求的封装,相当于是一个业务用例的外观。
+根据六边形架构的协作原则,领域模型若要访问外部设备,需要调用出口端口。依据整洁架构遵循的“稳定依赖原则”,领域层不能依赖于外层。因此,**出口端口只能放在领域层** 。事实上,领域驱动设计也是如此要求的,它在领域模型中定义了资源库(Repository),用于管理聚合的生命周期,同时,它也将作为抽象的访问外部数据库的出口端口。
 将资源库放在领域层确有论据佐证,毕竟,在抹掉数据库技术的实现细节后,资源库的接口方法就是对聚合领域模型对象的管理,包括查询、修改、增加与删除行为,这些行为也可视为领域逻辑的一部分。
 然而,限界上下文可能不仅限于访问数据库,还可能访问同样属于外部设备的文件、网络与消息队列。为了隔离领域模型与外部设备,同样需要为它们定义抽象的出口端口,这些出口端口该放在哪里呢?如果依然放在领域层,就很难自圆其说。例如,出口端口`EventPublisher`支持将事件消息发布到消息队列,要将这样的接口放在领域层,就显得不伦不类了。倘若不放在位于内部核心的领域层,就只能放在领域层外部,这又违背了整洁架构思想。
-如果我们将六边形架构看作是一个 **对称的架构** ,以领域为轴心,入口适配器和入口端口就应该与出口适配器和出口端口是对称的;同时,适配器又需和端口相对应,如此方可保证架构的松耦合。
+如果我们将六边形架构看作是一个 **对称的架构**,以领域为轴心,入口适配器和入口端口就应该与出口适配器和出口端口是对称的;同时,适配器又需和端口相对应,如此方可保证架构的松耦合。
 剖析端口与适配器的本质,实质上都是对外部系统或外部资源的处理,只是处理的方向各有不同。Martin Fowler 将“封装访问外部系统或资源行为的对象”定义为网关(Gateway),在限界上下文的内部架构中,它代表了领域层与外部环境之间交互的出入口,即:
 
 ```bash
@@ -248,7 +248,7 @@ gateway = port + adapter
 currentcontext - ohs(northbound) - remote - controllers - resources - providers - subscribers - local - appservices - pl(messages) - domain - acl(southbound) - ports - repositories - clients - publishers - adapters - repositories - client - publishers - pl(messages)
 ```
 
-该代码模型使用了上下文映射的模式名,ohs 为开放主机服务模式的缩写,acl 是防腐层模式的缩写,pl 代表了发布语言。 **注意** ,北向网关和南向网关都定义了 pl 包,其中,北向网关定义的消息契约模型为当前限界上下文服务,南向网关定义的消息契约则为上游限界上下文服务。如果下游上下文重用了上游上下文的消息契约模型,则南向网关可以不用定义。
+该代码模型使用了上下文映射的模式名,ohs 为开放主机服务模式的缩写,acl 是防腐层模式的缩写,pl 代表了发布语言。**注意**,北向网关和南向网关都定义了 pl 包,其中,北向网关定义的消息契约模型为当前限界上下文服务,南向网关定义的消息契约则为上游限界上下文服务。如果下游上下文重用了上游上下文的消息契约模型,则南向网关可以不用定义。
 当然,我们也可以使用北向(northbound)与南向(sourthbound)取代 ohs 与 acl 作为包名,使用消息(messages)契约取代 pl 的包名。这取决于不同团队对这些设计要素的认识。无论如何,作为整个系统的架构师,一旦确定在限界上下文层次运用菱形对称架构,就意味着他向所有团队成员传递了统一的设计元语,且潜在地给出了架构的设计原则与指导思想,即:维持领域模型的清晰边界,隔离业务复杂度与技术复杂度,并将限界上下文之间的协作通信隔离在领域模型之外。
 
 ## 菱形对称架构的价值
@@ -308,9 +308,9 @@ currentcontext - ohs(northbound) - remote - controllers - resources - providers
 采用这种模式时,限界上下文之间的耦合主要来自对事件的定义。作为事件发布者的限界上下文可以不用知道有哪些事件的订阅者,反之亦然,彼此之间的解耦往往通过引入事件总线(可以是本地的事件总线,也可以是单独进程的事件总线)来保证。
 在限界上下文内部,同样需要隔离领域模型与事件通信机制,这一工作由菱形对称架构网关层中的设计元素来完成。事件的发布者位于防腐层,发布者端口(`acl.ports.EventPublisher`)提供抽象定义,发布者适配器(`acl.adapters.EventPublisherAdapter`)负责将事件发布给事件总线;事件的订阅者(`ohs.remote.EventSubscriber`)属于开放主机服务层的远程服务,在订阅到事件之后,交由本地服务(`ohs.local.ApplicationService`)来处理事件。
 我们还需要判断是谁引起了事件的发布?
-如果是事件发布者所在限界上下文边界外的调用者引起,就需要由当前限界上下文的远程服务接受客户端调用,再将该调用委派给本地服务,由本地服务调用领域层执行了领域逻辑之后,组装好待发布的事件,由事件发布者完成事件的发布。由于事件的组装与发布逻辑的调用皆由本地服务承担,且考虑到本地服务实际上就是应用服务,故而将这样的事件称之为 **应用事件(Application Event)** ,整个调用时序如下所示:
+如果是事件发布者所在限界上下文边界外的调用者引起,就需要由当前限界上下文的远程服务接受客户端调用,再将该调用委派给本地服务,由本地服务调用领域层执行了领域逻辑之后,组装好待发布的事件,由事件发布者完成事件的发布。由于事件的组装与发布逻辑的调用皆由本地服务承担,且考虑到本地服务实际上就是应用服务,故而将这样的事件称之为 **应用事件(Application Event)**,整个调用时序如下所示:
  ![应用事件的发布与订阅](../assets/6c3ed5c0-61df-11ea-829b-7dbe678b494f.jpg)
-当领域层执行某一个领域行为时,也可能触发事件,最终引起事件的发布。这时,就将由领域模型的领域服务发起对事件发布者的调用。领域服务传递给事件发布者的事件,称之为 **领域事件(Domain Event)** ,调用时序如下所示:
+当领域层执行某一个领域行为时,也可能触发事件,最终引起事件的发布。这时,就将由领域模型的领域服务发起对事件发布者的调用。领域服务传递给事件发布者的事件,称之为 **领域事件(Domain Event)**,调用时序如下所示:
 ![领域事件的发布与订阅](../assets/8443d260-61df-11ea-b16a-f1bd5f6b62c7.jpg)
 
 ## 案例:菱形对称架构的运用
@@ -598,7 +598,7 @@ public class NotificationService {
 
 ### 案例小结
 
-显然,若每个限界上下文都采用菱形对称架构,则代码结构是非常清晰的,各个层各个模块各个类都有着各自的职责,泾渭分明,共同协作。同时,网关层对领域层的保护是不遗余力的,没有让任何领域模型对象泄露在限界上下文边界之外,唯一带来的成本就是可能存在重复定义的消息契约对象,以及对应的转换逻辑实现。 **说明:** 本案例的样例代码可以从 GitHub 上的[diamond](https://github.com/agiledon/diamond)库获得。
+显然,若每个限界上下文都采用菱形对称架构,则代码结构是非常清晰的,各个层各个模块各个类都有着各自的职责,泾渭分明,共同协作。同时,网关层对领域层的保护是不遗余力的,没有让任何领域模型对象泄露在限界上下文边界之外,唯一带来的成本就是可能存在重复定义的消息契约对象,以及对应的转换逻辑实现。**说明:** 本案例的样例代码可以从 GitHub 上的[diamond](https://github.com/agiledon/diamond)库获得。
 
 ## 参考文献
 
diff --git "a/docs/Article/Other/\351\253\230\346\225\210\346\236\204\345\273\272Docker\351\225\234\345\203\217\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/docs/Article/Other/\351\253\230\346\225\210\346\236\204\345\273\272Docker\351\225\234\345\203\217\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md"
index 69df962ed..64dcf4f3d 100644
--- "a/docs/Article/Other/\351\253\230\346\225\210\346\236\204\345\273\272Docker\351\225\234\345\203\217\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md"
+++ "b/docs/Article/Other/\351\253\230\346\225\210\346\236\204\345\273\272Docker\351\225\234\345\203\217\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md"
@@ -728,7 +728,7 @@ RUN mvn -e -B package
 CMD [ "java", "-jar", "/app/target/gs-spring-boot-0.1.0.jar" ]
 ```
 
-这样, **即使业务代码发生改变,也不需要重新解决依赖,可有效的利用了缓存,加快构建的速度** 。
+这样,**即使业务代码发生改变,也不需要重新解决依赖,可有效的利用了缓存,加快构建的速度** 。
 
 当然,现在我们构建的镜像中,还是包含着项目的源代码,这其实并非我们所需要的。那么我们可以使用 **多阶段构建** 来解决这个问题。`Dockerfile` 可以修改为:
 
diff --git "a/docs/Article/Spring/SpringBoot2.x\347\273\223\345\220\210k8s\345\256\236\347\216\260\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204.md" "b/docs/Article/Spring/SpringBoot2.x\347\273\223\345\220\210k8s\345\256\236\347\216\260\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204.md"
index 17fab2565..d7b60a94a 100644
--- "a/docs/Article/Spring/SpringBoot2.x\347\273\223\345\220\210k8s\345\256\236\347\216\260\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204.md"
+++ "b/docs/Article/Spring/SpringBoot2.x\347\273\223\345\220\210k8s\345\256\236\347\216\260\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204.md"
@@ -48,7 +48,7 @@ ConfigMap,看到这个名字可以理解:它是用于保存配置信息的
 kubectl create configmap test-config --from-literal=baseDir=/usr
 ```
 
-上面的命令创建了一个名为 test-config,拥有一条 key 为 baseDir,value 为 "/usr" 的键值对数据。 **2. 根据 yml 描述文件创建** 
+上面的命令创建了一个名为 test-config,拥有一条 key 为 baseDir,value 为 "/usr" 的键值对数据。**2. 根据 yml 描述文件创建** 
 
 ```plaintext
 apiVersion: v1
@@ -699,7 +699,7 @@ public void setMessage(String message) {
 
 ```plaintext
 这就是配置 ConfigMap 中的属性的类。剩下的可以自己定义一个接口类,来实现服务生产者。
-最后,我们需要在 K8s 下部署的话,需要准备几个脚本。 **1. 创建 ConfigMap** ```
+最后,我们需要在 K8s 下部署的话,需要准备几个脚本。**1. 创建 ConfigMap** ```
 kind: ConfigMap
 
 apiVersion: v1
@@ -743,7 +743,7 @@ data:
       message: Say Hello to the Prod
 ```
 
-设置了不同环境的配置,注意,这里的 namespace 需要与服务部署的 namespace 一致,这里默认的是 default,而且在创建服务之前,先得创建这个。 **2. 创建服务部署脚本** 
+设置了不同环境的配置,注意,这里的 namespace 需要与服务部署的 namespace 一致,这里默认的是 default,而且在创建服务之前,先得创建这个。**2. 创建服务部署脚本** 
 
 ```plaintext
 apiVersion: apps/v1
@@ -842,7 +842,7 @@ kubectl scale --replicas=3 deployment cas-server-deployment
 ```plaintext
 这里,我建议使用 Deployment 类型的来创建 pod,因为 Deployment 类型更好的支持弹性伸缩与滚动更新。
 
-同时,我们通过 `--spring.profiles.active=dev` 来指定当前 pod 的运行环境。 **3. 创建一个 Service**
+同时,我们通过 `--spring.profiles.active=dev` 来指定当前 pod 的运行环境。**3. 创建一个 Service**
 
 最后,如果服务想被发现,需要创建一个 Service:
 ```
diff --git "a/docs/Article/Spring/SpringMVC\345\216\237\347\220\206.md" "b/docs/Article/Spring/SpringMVC\345\216\237\347\220\206.md"
index 09eafa605..7a2df430c 100644
--- "a/docs/Article/Spring/SpringMVC\345\216\237\347\220\206.md"
+++ "b/docs/Article/Spring/SpringMVC\345\216\237\347\220\206.md"
@@ -1574,7 +1574,7 @@ protected ModelAndView processHandlerException(HttpServletRequest request, HttpS
 }
 ```
 
-处,遍历 HandlerExceptionResolver 数组,调用 HandlerExceptionResolver#resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 方法,解析异常,生成 ModelAndView 对象。 **处,情况一,生成了 ModelAndView 对象,进行返回。当然,这里的后续代码还有 10 多行,比较简单,胖友自己瞅瞅就 OK 啦。** **
+处,遍历 HandlerExceptionResolver 数组,调用 HandlerExceptionResolver#resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 方法,解析异常,生成 ModelAndView 对象。**处,情况一,生成了 ModelAndView 对象,进行返回。当然,这里的后续代码还有 10 多行,比较简单,胖友自己瞅瞅就 OK 啦。** **
 处,情况二,未生成 ModelAndView 对象,则抛出异常。
 <3.1> 处,调用 `#render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response)` 方法,渲染页面。
 <3.2> 处,当是 <2> 处的情况二时,则调用 `WebUtils#clearErrorRequestAttributes(HttpServletRequest request)` 方法,清理请求中的错误消息属性。为什么会有这一步呢?答案在 `#processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)` 方法中,会调用 `WebUtils#exposeErrorRequestAttributes(HttpServletRequest request, Throwable ex, String servletName)` 方法,设置请求中的错误消息属性。
diff --git "a/docs/Article/Spring/\344\273\216SpringCloud\345\274\200\345\247\213\357\274\214\350\201\212\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204.md" "b/docs/Article/Spring/\344\273\216SpringCloud\345\274\200\345\247\213\357\274\214\350\201\212\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204.md"
index a48c0190c..774aaa933 100644
--- "a/docs/Article/Spring/\344\273\216SpringCloud\345\274\200\345\247\213\357\274\214\350\201\212\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204.md"
+++ "b/docs/Article/Spring/\344\273\216SpringCloud\345\274\200\345\247\213\357\274\214\350\201\212\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204.md"
@@ -133,7 +133,7 @@
 
 **设计阶段:** 架构组将产品功能拆分为若干微服务,为每个微服务设计 API 接口(例如 REST API),需要给出 API 文档,包括 API 的名称、版本、请求参数、响应结果、错误代码等信息。
 
-在开发阶段,开发工程师去实现 API 接口,也包括完成 API 的单元测试工作,在此期间,前端工程师会并行开发 Web UI 部分,可根据 API 文档造出一些假数据(我们称为“mock 数据”),这样一来,前端工程师就不必等待后端 API 全部开发完毕,才能开始自己的工作了,实现了前后端并行开发。 **测试阶段:**
+在开发阶段,开发工程师去实现 API 接口,也包括完成 API 的单元测试工作,在此期间,前端工程师会并行开发 Web UI 部分,可根据 API 文档造出一些假数据(我们称为“mock 数据”),这样一来,前端工程师就不必等待后端 API 全部开发完毕,才能开始自己的工作了,实现了前后端并行开发。**测试阶段:**
 
 这一阶段过程全自动化过程,开发人员提交代码到代码服务器,代码服务器触发持续集成构建、测试,如果测试通过则会自动通过Ansible脚本推送到模拟环境;在实践中对于线上环境则是先要走审核流程,通过之后才能推送到生产环境。提高工作效率,并且控制了部分可能因为测试不充分而导致的线上不稳定。
 
@@ -237,7 +237,7 @@ Bus
 
 Task
 
-**工程结构规范** ![enter image description here](../assets/1145efc0-7a5e-11e7-a25d-25787154610f.png)
+**工程结构规范**![enter image description here](../assets/1145efc0-7a5e-11e7-a25d-25787154610f.png)
 
 上图是我们实践中每个服务应该具有的项目组成结构。
 
@@ -249,7 +249,7 @@ Task
 
 1. 微服务名+web:
 
-   供上层web应用请求的入口,该服务中一般会调用底层微服务完成请求。 **API 网关实践**
+   供上层web应用请求的入口,该服务中一般会调用底层微服务完成请求。**API 网关实践**
 
 ![enter image description here](../assets/30da4300-7a67-11e7-be31-0ba46ae5a9d4.png)
 
diff --git "a/docs/Basic/12 \346\255\245\351\200\232\345\205\263\346\261\202\350\201\214\351\235\242\350\257\225/\347\254\25408\350\256\262.md" "b/docs/Basic/12 \346\255\245\351\200\232\345\205\263\346\261\202\350\201\214\351\235\242\350\257\225/\347\254\25408\350\256\262.md"
index 2bacd84ea..22d5ed9a1 100644
--- "a/docs/Basic/12 \346\255\245\351\200\232\345\205\263\346\261\202\350\201\214\351\235\242\350\257\225/\347\254\25408\350\256\262.md"	
+++ "b/docs/Basic/12 \346\255\245\351\200\232\345\205\263\346\261\202\350\201\214\351\235\242\350\257\225/\347\254\25408\350\256\262.md"	
@@ -4,7 +4,7 @@
 
 ## 认清自己的实力
 
-我经常会被身边的小伙伴问到:我应该选择什么工作方向啊?目前的工作好累啊,想换个工作方向但是不知道如何选择?某某行业的薪资好高啊,要不我去试试吧? ![img](assets/Cgq2xl357HuAOGynAADrI_pANNA147.png) 如果这样问,有可能被目前社会的很多利益所引导,已经忘了 **出发点是什么** 或者没有思考过 **未来的职业规划** 。平时也会听到身边有人说“希望可以拥有一家自己的咖啡厅”,如果这只是一句玩笑话,说说就过去了;但如果你是认真的,那有没有考虑过:在做这件事情之前都准备了什么呢?所以, **在**  **选择工作时**  **先要想想自己的擅长点是什么,同时为这份工作做了哪些准备等** 。 这里我给你推荐一款职业人格评估工具,即 MBTI,来测一测自己是偏外向还是内向、是一个有规划的人还是一个探索性的人、是喜欢做挑战性的工作还是喜欢辅助团队做一些执行层面的工作等,当然在测试的时候要依托于自己的内心哦。 ![img](assets/Cgq2xl357FGAEP2nAADFGH1SM3o459.png)
+我经常会被身边的小伙伴问到:我应该选择什么工作方向啊?目前的工作好累啊,想换个工作方向但是不知道如何选择?某某行业的薪资好高啊,要不我去试试吧? ![img](assets/Cgq2xl357HuAOGynAADrI_pANNA147.png) 如果这样问,有可能被目前社会的很多利益所引导,已经忘了 **出发点是什么** 或者没有思考过 **未来的职业规划** 。平时也会听到身边有人说“希望可以拥有一家自己的咖啡厅”,如果这只是一句玩笑话,说说就过去了;但如果你是认真的,那有没有考虑过:在做这件事情之前都准备了什么呢?所以,**在**  **选择工作时**  **先要想想自己的擅长点是什么,同时为这份工作做了哪些准备等** 。 这里我给你推荐一款职业人格评估工具,即 MBTI,来测一测自己是偏外向还是内向、是一个有规划的人还是一个探索性的人、是喜欢做挑战性的工作还是喜欢辅助团队做一些执行层面的工作等,当然在测试的时候要依托于自己的内心哦。 ![img](assets/Cgq2xl357FGAEP2nAADFGH1SM3o459.png)
 
 ## 明确求职方向
 
diff --git "a/docs/Basic/12 \346\255\245\351\200\232\345\205\263\346\261\202\350\201\214\351\235\242\350\257\225/\347\254\25411\350\256\262.md" "b/docs/Basic/12 \346\255\245\351\200\232\345\205\263\346\261\202\350\201\214\351\235\242\350\257\225/\347\254\25411\350\256\262.md"
index 56a289742..b9d96a846 100644
--- "a/docs/Basic/12 \346\255\245\351\200\232\345\205\263\346\261\202\350\201\214\351\235\242\350\257\225/\347\254\25411\350\256\262.md"	
+++ "b/docs/Basic/12 \346\255\245\351\200\232\345\205\263\346\261\202\350\201\214\351\235\242\350\257\225/\347\254\25411\350\256\262.md"	
@@ -4,7 +4,7 @@
 
 ## 知道自己想要什么
 
-在开始谈薪资之前,需要明确自己到底想要什么,希望在这次的工作变动中有什么收获,比如想在团队氛围很和谐的公司里工作、希望积累更多的项目经验、还是仅仅为了涨薪等。 当你明确自己想要的是什么,同时清晰的表达出来以后,HR 会根据你的需求去匹配这个职位是否可以给到你所期望的东西,或者你也可以直接询问 HR 来判断是否能得到你想要的。 ![img](assets/Cgq2xl4Jk7uAU2QlAAB8BU7HoSQ711.png) 很多小伙伴会说, **如果我确实不知道自己这次换工作想要得到什么,该怎么办** ? 你可以参考在之前的工作过程中,是不是有让自己感觉不舒服或者有挫败感的时候,同时想想是什么原因造成的,然后把它们整理出来写在纸上,标出来哪些是你希望可以得到改善和需要得到成长的。 ![img](assets/Cgq2xl4Jk-SAEbE0AABxDK6ssrM497.png) 通过这样的方式,再去想想在面试的过程中或者在和面试官沟通的过程中,这家企业是否可以给到你想要的东西。
+在开始谈薪资之前,需要明确自己到底想要什么,希望在这次的工作变动中有什么收获,比如想在团队氛围很和谐的公司里工作、希望积累更多的项目经验、还是仅仅为了涨薪等。 当你明确自己想要的是什么,同时清晰的表达出来以后,HR 会根据你的需求去匹配这个职位是否可以给到你所期望的东西,或者你也可以直接询问 HR 来判断是否能得到你想要的。 ![img](assets/Cgq2xl4Jk7uAU2QlAAB8BU7HoSQ711.png) 很多小伙伴会说,**如果我确实不知道自己这次换工作想要得到什么,该怎么办**? 你可以参考在之前的工作过程中,是不是有让自己感觉不舒服或者有挫败感的时候,同时想想是什么原因造成的,然后把它们整理出来写在纸上,标出来哪些是你希望可以得到改善和需要得到成长的。 ![img](assets/Cgq2xl4Jk-SAEbE0AABxDK6ssrM497.png) 通过这样的方式,再去想想在面试的过程中或者在和面试官沟通的过程中,这家企业是否可以给到你想要的东西。
 
 ## 明确自己的优势
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25400\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25400\350\256\262.md"
index 9dd585913..7fd9bfb84 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25400\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25400\350\256\262.md"
@@ -6,7 +6,7 @@
 
 这么多年一直在开发软件,我深感软件这个行业变化太快了。语言上,十年前流行 Java,这两年流行 Go;框架上,前两年流行 TensorFlow,最近又流行 PyTorch。我逐渐发现,学习应用层的各种语言、框架,好比在练拳法招式,可以短期给予你回报,而深入学习“底层知识”,就是在练扎马步、核心肌肉力量,是在提升你自己的“根骨”和“资质”。
 
-## 正所谓“练拳不练功,到老一场空”。 **如果越早去弄清楚计算机的底层原理,在你的知识体系中“储蓄”起这些知识,也就意味着你有越长的时间来收获学习知识的“利息”。虽然一开始可能不起眼,但是随着时间带来的复利效应,你的长线投资项目,就能让你在成长的过程中越走越快。** 计算机底层知识的“第一课”
+## 正所谓“练拳不练功,到老一场空”。**如果越早去弄清楚计算机的底层原理,在你的知识体系中“储蓄”起这些知识,也就意味着你有越长的时间来收获学习知识的“利息”。虽然一开始可能不起眼,但是随着时间带来的复利效应,你的长线投资项目,就能让你在成长的过程中越走越快。** 计算机底层知识的“第一课”
 
 如果找出各大学计算机系的培养计划,你会发现,它们都有差不多十来门核心课程。其中,“计算机组成原理”是入门和底层层面的第一课。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25401\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25401\350\256\262.md"
index 17994f5e9..02b813f3e 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25401\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25401\350\256\262.md"
@@ -8,7 +8,7 @@
 
 早年,要自己组装一台计算机,要先有三大件,CPU、内存和主板。
 
-在这三大件中,我们首先要说的是 **CPU** ,它是计算机最重要的核心配件,全名你肯定知道,叫中央处理器(Central Processing Unit)。为什么说 CPU 是“最重要”的呢?因为计算机的所有“计算”都是由 CPU 来进行的。自然,CPU 也是整台计算机中造价最昂贵的部分之一。
+在这三大件中,我们首先要说的是 **CPU**,它是计算机最重要的核心配件,全名你肯定知道,叫中央处理器(Central Processing Unit)。为什么说 CPU 是“最重要”的呢?因为计算机的所有“计算”都是由 CPU 来进行的。自然,CPU 也是整台计算机中造价最昂贵的部分之一。
 
 ![img](assets/a9af6307db5b3dde094c964e8940d83c.jpg)
 
@@ -22,7 +22,7 @@ CPU 是一个超级精细的印刷电路版,[图片来源](https://www.flickr.
 
 存放在内存里的程序和数据,需要被 CPU 读取,CPU 计算完之后,还要把数据写回到内存。然而 CPU 不能直接插到内存上,反之亦然。于是,就带来了最后一个大件—— **主板** (Motherboard)。
 
-主板是一个有着各种各样,有时候多达数十乃至上百个插槽的配件。我们的 CPU 要插在主板上,内存也要插在主板上。主板的 **芯片组** (Chipset)和 **总线** (Bus)解决了 CPU 和内存之间如何通信的问题。芯片组控制了数据传输的流转,也就是数据从哪里到哪里的问题。总线则是实际数据传输的高速公路。因此, **总线速度** (Bus Speed)决定了数据能传输得多快。
+主板是一个有着各种各样,有时候多达数十乃至上百个插槽的配件。我们的 CPU 要插在主板上,内存也要插在主板上。主板的 **芯片组** (Chipset)和 **总线** (Bus)解决了 CPU 和内存之间如何通信的问题。芯片组控制了数据传输的流转,也就是数据从哪里到哪里的问题。总线则是实际数据传输的高速公路。因此,**总线速度** (Bus Speed)决定了数据能传输得多快。
 
 ![img](assets/16bed40e3f1b1484e842cac3d6e596b0.jpg)
 
@@ -48,7 +48,7 @@ CPU 是一个超级精细的印刷电路版,[图片来源](https://www.flickr.
 
 刚才我们讲了一台计算机的硬件组成,这说的是我们平时用的个人电脑或者服务器。那我们平时最常用的智能手机的组成,也是这样吗?
 
-我们手机里只有 SD 卡(Secure Digital Memory Card)这样类似硬盘功能的存储卡插槽,并没有内存插槽、CPU 插槽这些东西。没错,因为手机尺寸的原因,手机制造商们选择把 CPU、内存、网络通信,乃至摄像头芯片,都封装到一个芯片,然后再嵌入到手机主板上。这种方式叫 **SoC** ,也就是 System on a Chip(系统芯片)。
+我们手机里只有 SD 卡(Secure Digital Memory Card)这样类似硬盘功能的存储卡插槽,并没有内存插槽、CPU 插槽这些东西。没错,因为手机尺寸的原因,手机制造商们选择把 CPU、内存、网络通信,乃至摄像头芯片,都封装到一个芯片,然后再嵌入到手机主板上。这种方式叫 **SoC**,也就是 System on a Chip(系统芯片)。
 
 这样看起来,个人电脑和智能手机的硬件组成方式不太一样。可是,我们写智能手机上的 App,和写个人电脑的客户端应用似乎没有什么差别,都是通过“高级语言”这样的编程语言撰写、编译之后,一样是把代码和数据加载到内存里来执行。这是为什么呢?因为,无论是个人电脑、服务器、智能手机,还是 Raspberry Pi 这样的微型卡片机,都遵循着同一个“计算机”的抽象概念。这是怎么样一个“计算机”呢?这其实就是,计算机祖师爷之一冯·诺依曼(John von Neumann)提出的 **冯·诺依曼体系结构** (Von Neumann architecture),也叫 **存储程序计算机** 。
 
@@ -68,7 +68,7 @@ CPU 是一个超级精细的印刷电路版,[图片来源](https://www.flickr.
 
 可以看到,无论是“不可编程”还是“不可存储”,都会让使用计算机的效率大大下降。而这个对于效率的追求,也就是“存储程序计算机”的由来。
 
-于是我们的冯祖师爷,基于当时在秘密开发的 EDVAC 写了一篇报告[_First Draft of a Report on the EDVAC_](https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC),描述了他心目中的一台计算机应该长什么样。这篇报告在历史上有个很特殊的简称,叫 **First Draft** ,翻译成中文,其实就是《第一份草案》。这样,现代计算机的发展就从祖师爷写的一份草案开始了。
+于是我们的冯祖师爷,基于当时在秘密开发的 EDVAC 写了一篇报告[_First Draft of a Report on the EDVAC_](https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC),描述了他心目中的一台计算机应该长什么样。这篇报告在历史上有个很特殊的简称,叫 **First Draft**,翻译成中文,其实就是《第一份草案》。这样,现代计算机的发展就从祖师爷写的一份草案开始了。
 
 **First Draft** 里面说了一台计算机应该有哪些部分组成,我们一起来看看。
 
@@ -76,9 +76,9 @@ CPU 是一个超级精细的印刷电路版,[图片来源](https://www.flickr.
 
 然后是一个包含指令寄存器(Instruction Reigster)和程序计数器(Program Counter)的 **控制器单元** (Control Unit/CU),用来控制程序的流程,通常就是不同条件下的分支和跳转。在现在的计算机里,上面的算术逻辑单元和这里的控制器单元,共同组成了我们说的 CPU。
 
-接着是用来存储数据(Data)和指令(Instruction)的 **内存** 。以及更大容量的 **外部存储** ,在过去,可能是磁带、磁鼓这样的设备,现在通常就是硬盘。
+接着是用来存储数据(Data)和指令(Instruction)的 **内存** 。以及更大容量的 **外部存储**,在过去,可能是磁带、磁鼓这样的设备,现在通常就是硬盘。
 
-最后就是各种 **输入和输出设备** ,以及对应的输入和输出机制。我们现在无论是使用什么样的计算机,其实都是和输入输出设备在打交道。个人电脑的鼠标键盘是输入设备,显示器是输出设备。我们用的智能手机,触摸屏既是输入设备,又是输出设备。而跑在各种云上的服务器,则是通过网络来进行输入和输出。这个时候,网卡既是输入设备又是输出设备。
+最后就是各种 **输入和输出设备**,以及对应的输入和输出机制。我们现在无论是使用什么样的计算机,其实都是和输入输出设备在打交道。个人电脑的鼠标键盘是输入设备,显示器是输出设备。我们用的智能手机,触摸屏既是输入设备,又是输出设备。而跑在各种云上的服务器,则是通过网络来进行输入和输出。这个时候,网卡既是输入设备又是输出设备。
 
 任何一台计算机的任何一个部件都可以归到运算器、控制器、存储器、输入设备和输出设备中,而所有的现代计算机也都是基于这个基础架构来设计开发的。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25402\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25402\350\256\262.md"
index da1f0d816..f48e4138f 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25402\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25402\350\256\262.md"
@@ -6,7 +6,7 @@
 
 建议保存后查看大图
 
-从这张图可以看出来, **整个计算机组成原理,就是围绕着计算机是如何组织运作展开的** 。
+从这张图可以看出来,**整个计算机组成原理,就是围绕着计算机是如何组织运作展开的** 。
 
 ## 计算机组成原理知识地图
 
@@ -34,7 +34,7 @@ CPU 时钟可以用来构造寄存器和内存的锁存器和触发器,因此
 
 既然 CPU 作为控制器要和输入输出设备通信,那么我们就要知道异常和中断发生的机制。在 CPU 设计部分的最后,我会讲一讲指令的并行执行,看看如何直接在 CPU 层面,通过 SIMD 来支持并行计算。
 
-最后,我们需要看一看,计算机五大组成部分之一, **存储器的原理** 。通过存储器的层次结构作为基础的框架引导,你需要掌握从上到下的 CPU 高速缓存、内存、SSD 硬盘和机械硬盘的工作原理,它们之间的性能差异,以及实际应用中利用这些设备会遇到的挑战。存储器其实很多时候又扮演了输入输出设备的角色,所以你需要进一步了解,CPU 和这些存储器之间是如何进行通信的,以及我们最重视的性能问题是怎么一回事;理解什么是 IO_WAIT,如何通过 DMA 来提升程序性能。
+最后,我们需要看一看,计算机五大组成部分之一,**存储器的原理** 。通过存储器的层次结构作为基础的框架引导,你需要掌握从上到下的 CPU 高速缓存、内存、SSD 硬盘和机械硬盘的工作原理,它们之间的性能差异,以及实际应用中利用这些设备会遇到的挑战。存储器其实很多时候又扮演了输入输出设备的角色,所以你需要进一步了解,CPU 和这些存储器之间是如何进行通信的,以及我们最重视的性能问题是怎么一回事;理解什么是 IO_WAIT,如何通过 DMA 来提升程序性能。
 
 对于存储器,我们不仅需要它们能够正常工作,还要确保里面的数据不能丢失。于是你要掌握我们是如何通过 RAID、Erasure Code、ECC 以及分布式 HDFS,这些不同的技术,来确保数据的完整性和访问性能。
 
@@ -48,16 +48,16 @@ CPU 时钟可以用来构造寄存器和内存的锁存器和触发器,因此
 
 因此,为了帮你更快更好地学计算机组成,我为你总结了三个学习方法,帮你更好地掌握这些知识点,并且能够学为所用,让你在工作中能够用得上。
 
-首先, **学会提问自己来串联知识点** 。学完一个知识点之后,你可以从下面两个方面,问一下自己。
+首先,**学会提问自己来串联知识点** 。学完一个知识点之后,你可以从下面两个方面,问一下自己。
 
 - 我写的程序,是怎样从输入的代码,变成运行的程序,并得到最终结果的?
 - 整个过程中,计算器层面到底经历了哪些步骤,有哪些地方是可以优化的?
 
 无论是程序的编译、链接、装载和执行,以及计算时需要用到的逻辑电路、ALU,乃至 CPU 自发为你做的流水线、指令级并行和分支预测,还有对应访问到的硬盘、内存,以及加载到高速缓存中的数据,这些都对应着我们学习中的一个个知识点。建议你自己脑子里过一遍,最好时口头表述一遍或者写下来,这样对你彻底掌握这些知识点都会非常有帮助。
 
-其次, **写一些示例程序来验证知识点。** 计算机科学是一门实践的学科。计算机组成中的大量原理和设计,都对应着“性能”这个词。因此,通过把对应的知识点,变成一个个性能对比的示例代码程序记录下来,是把这些知识点融汇贯通的好方法。因为,相比于强记硬背知识点,一个有着明确性能对比的示例程序,会在你脑海里留下更深刻的印象。当你想要回顾这些知识点的时候,一个程序也更容易提示你把它从脑海深处里面找出来。
+其次,**写一些示例程序来验证知识点。** 计算机科学是一门实践的学科。计算机组成中的大量原理和设计,都对应着“性能”这个词。因此,通过把对应的知识点,变成一个个性能对比的示例代码程序记录下来,是把这些知识点融汇贯通的好方法。因为,相比于强记硬背知识点,一个有着明确性能对比的示例程序,会在你脑海里留下更深刻的印象。当你想要回顾这些知识点的时候,一个程序也更容易提示你把它从脑海深处里面找出来。
 
-最后, **通过和计算机硬件发展的历史做对照** 。计算机的发展并不是一蹴而就的。从第一台电子计算机 ENIAC(Electronic Numerical Integrator And Computer,电子数值积分计算机)的发明到现在,已经有 70 多年了。现代计算机用的各个技术,都是跟随实际应用中遇到的挑战,一个个发明、打磨,最后保留下来的。这当中不仅仅有学术层面的碰撞,更有大量商业层面的交锋。通过了解充满戏剧性和故事性的计算机硬件发展史,让你更容易理解计算机组成中各种原理的由来。
+最后,**通过和计算机硬件发展的历史做对照** 。计算机的发展并不是一蹴而就的。从第一台电子计算机 ENIAC(Electronic Numerical Integrator And Computer,电子数值积分计算机)的发明到现在,已经有 70 多年了。现代计算机用的各个技术,都是跟随实际应用中遇到的挑战,一个个发明、打磨,最后保留下来的。这当中不仅仅有学术层面的碰撞,更有大量商业层面的交锋。通过了解充满戏剧性和故事性的计算机硬件发展史,让你更容易理解计算机组成中各种原理的由来。
 
 比如说,奔腾 4 和 SPARC 的失败,以及 ARM 的成功,能让我们记住 CPU 指令集的繁与简、权衡性能和功耗的重要性,而现今高速发展的机器学习和边缘计算,又给计算机硬件设计带来了新的挑战。
 
@@ -97,6 +97,6 @@ CPU 时钟可以用来构造寄存器和内存的锁存器和触发器,因此
 
 学习不是死记硬背,学习材料也不是越多越好。到了这里,希望你不要因为我给出了太多可以学习的材料,结果成了“松鼠症”患者,光囤积材料,却没有花足够多的时间去学习这些知识。
 
-我工作之后一直在持续学习,在这个过程中,我发现最有效的办法, **不是短时间冲刺,而是有节奏地坚持,希望你能够和专栏的发布节奏同步推进,做好思考题,并且多在留言区和其他朋友一起交流** ,就更容易能够“积小步而至千里”,在程序员这个职业上有更长足的发展。
+我工作之后一直在持续学习,在这个过程中,我发现最有效的办法,**不是短时间冲刺,而是有节奏地坚持,希望你能够和专栏的发布节奏同步推进,做好思考题,并且多在留言区和其他朋友一起交流**,就更容易能够“积小步而至千里”,在程序员这个职业上有更长足的发展。
 
 好了,对于学习资料的介绍就到这里了。希望在接下来的几个月里,你能和我一起走完这趟“计算机组成”之旅,从中收获到知识和成长。
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25403\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25403\350\256\262.md"
index 196bedeb2..46e6c93b2 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25403\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25403\350\256\262.md"
@@ -52,11 +52,11 @@ SPEC 提供的 CPU 基准测试程序,就好像 CPU 届的“高考”,通
 
 为什么会不准呢?这里面有好几个原因。首先,我们统计时间是用类似于“掐秒表”一样,记录程序运行结束的时间减去程序开始运行的时间。这个时间也叫 Wall Clock Time 或者 Elapsed Time,就是在运行程序期间,挂在墙上的钟走掉的时间。
 
-但是,计算机可能同时运行着好多个程序,CPU 实际上不停地在各个程序之间进行切换。在这些走掉的时间里面,很可能 CPU 切换去运行别的程序了。而且,有些程序在运行的时候,可能要从网络、硬盘去读取数据,要等网络和硬盘把数据读出来,给到内存和 CPU。所以说, **要想准确统计某个程序运行时间,进而去比较两个程序的实际性能,我们得把这些时间给刨除掉** 。
+但是,计算机可能同时运行着好多个程序,CPU 实际上不停地在各个程序之间进行切换。在这些走掉的时间里面,很可能 CPU 切换去运行别的程序了。而且,有些程序在运行的时候,可能要从网络、硬盘去读取数据,要等网络和硬盘把数据读出来,给到内存和 CPU。所以说,**要想准确统计某个程序运行时间,进而去比较两个程序的实际性能,我们得把这些时间给刨除掉** 。
 
 那这件事怎么实现呢?Linux 下有一个叫 time 的命令,可以帮我们统计出来,同样的 Wall Clock Time 下,程序实际在 CPU 上到底花了多少时间。
 
-我们简单运行一下 time 命令。它会返回三个值,第一个是 **real time** ,也就是我们说的 Wall Clock Time,也就是运行程序整个过程中流逝掉的时间;第二个是 **user time** ,也就是 CPU 在运行你的程序,在用户态运行指令的时间;第三个是 **sys time** ,是 CPU 在运行你的程序,在操作系统内核里运行指令的时间。而 **程序实际花费的 CPU 执行时间(CPU Time),就是 user time 加上 sys time** 。
+我们简单运行一下 time 命令。它会返回三个值,第一个是 **real time**,也就是我们说的 Wall Clock Time,也就是运行程序整个过程中流逝掉的时间;第二个是 **user time**,也就是 CPU 在运行你的程序,在用户态运行指令的时间;第三个是 **sys time**,是 CPU 在运行你的程序,在操作系统内核里运行指令的时间。而 **程序实际花费的 CPU 执行时间(CPU Time),就是 user time 加上 sys time** 。
 
 ```plaintext
  time seq 1000000 | wc -l
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25404\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25404\350\256\262.md"
index 2765fd7ca..ab26cffe7 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25404\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25404\350\256\262.md"
@@ -56,7 +56,7 @@ CPU 的主频变化,在奔腾 4 时代进入了瓶颈期,[图片来源](http
 
 那你可能要问了,接下来该怎么办呢?相比于给飞机提速,工程师们又想到了新的办法,可以一次同时开 2 架、4 架乃至 8 架飞机,这就好像我们现在用的 2 核、4 核,乃至 8 核的 CPU。
 
-虽然从上海到北京的时间没有变,但是一次飞 8 架飞机能够运的东西自然就变多了,也就是所谓的“吞吐率”变大了。所以,不管你有没有需要,现在 CPU 的性能就是提升了 2 倍乃至 8 倍、16 倍。这也是一个最常见的提升性能的方式, **通过并行提高性能** 。
+虽然从上海到北京的时间没有变,但是一次飞 8 架飞机能够运的东西自然就变多了,也就是所谓的“吞吐率”变大了。所以,不管你有没有需要,现在 CPU 的性能就是提升了 2 倍乃至 8 倍、16 倍。这也是一个最常见的提升性能的方式,**通过并行提高性能** 。
 
 这个思想在很多地方都可以使用。举个例子,我们做机器学习程序的时候,需要计算向量的点积,比如向量 W=\[W0,W1,W2,…,W15\]W=\[W0,W1,W2,…,W15\] 和向量 X=\[X0,X1,X2,…,X15\]X=\[X0,X1,X2,…,X15\],W⋅X=W0∗X0+W1∗X1+W·X=W0∗X0+W1∗X1+ W2∗X2+…+W15∗X15W2∗X2+…+W15∗X15。这些式子由 16 个乘法和 1 个连加组成。如果你自己一个人用笔来算的话,需要一步一步算 16 次乘法和 15 次加法。如果这个时候我们把这个人物分配给 4 个人,同时去算 W0~W3W0~W3, W4~W7W4~W7, W8~W11W8~W11, W12~W15W12~W15 这样四个部分的结果,再由一个人进行汇总,需要的时间就会缩短。
 
@@ -70,7 +70,7 @@ CPU 的主频变化,在奔腾 4 时代进入了瓶颈期,[图片来源](http
 
 第三,在“汇总”这个阶段,是没有办法并行进行的,还是得顺序执行,一步一步来。
 
-这就引出了我们在进行性能优化中,常常用到的一个经验定律, **阿姆达尔定律** (Amdahl’s Law)。这个定律说的就是,对于一个程序进行优化之后,处理器并行运算之后效率提升的情况。具体可以用这样一个公式来表示:
+这就引出了我们在进行性能优化中,常常用到的一个经验定律,**阿姆达尔定律** (Amdahl’s Law)。这个定律说的就是,对于一个程序进行优化之后,处理器并行运算之后效率提升的情况。具体可以用这样一个公式来表示:
 
 优化后的执行时间 = 受优化影响的执行时间 / 加速倍数 + 不受影响的执行时间
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25405\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25405\350\256\262.md"
index bd2e5913f..1f8a8676b 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25405\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25405\350\256\262.md"
@@ -22,7 +22,7 @@
 
 如果我们从 **软件** 工程师的角度来讲,CPU 就是一个执行各种 **计算机指令** (Instruction Code)的逻辑机器。这里的计算机指令,就好比一门 CPU 能够听得懂的语言,我们也可以把它叫作 **机器语言** (Machine Language)。
 
-不同的 CPU 能够听懂的语言不太一样。比如,我们的个人电脑用的是 Intel 的 CPU,苹果手机用的是 ARM 的 CPU。这两者能听懂的语言就不太一样。类似这样两种 CPU 各自支持的语言,就是两组不同的 **计算机指令集** ,英文叫 Instruction Set。这里面的“Set”,其实就是数学上的集合,代表不同的单词、语法。
+不同的 CPU 能够听懂的语言不太一样。比如,我们的个人电脑用的是 Intel 的 CPU,苹果手机用的是 ARM 的 CPU。这两者能听懂的语言就不太一样。类似这样两种 CPU 各自支持的语言,就是两组不同的 **计算机指令集**,英文叫 Instruction Set。这里面的“Set”,其实就是数学上的集合,代表不同的单词、语法。
 
 所以,如果我们在自己电脑上写一个程序,然后把这个程序复制一下,装到自己的手机上,肯定是没办法正常运行的,因为这两者语言不通。而一台电脑上的程序,简单复制一下到另外一台电脑上,通常就能正常运行,因为这两台 CPU 有着相同的指令集,也就是说,它们的语言相通的。
 
@@ -121,7 +121,7 @@ MIPS 是一组由 MIPS 技术公司在 80 年代中期设计出来的 CPU 指令
 
 MIPS 的指令是一个 32 位的整数,高 6 位叫 **操作码** (Opcode),也就是代表这条指令具体是一条什么样的指令,剩下的 26 位有三种格式,分别是 R、I 和 J。
 
-**R 指令** 是一般用来做算术和逻辑操作,里面有读取和写入数据的寄存器的地址。如果是逻辑位移操作,后面还有位移操作的位移量,而最后的功能码,则是在前面的操作码不够的时候,扩展操作码表示对应的具体指令的。 **I 指令** ,则通常是用在数据传输、条件分支,以及在运算的时候使用的并非变量还是常数的时候。这个时候,没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或者一个常数。 **J 指令** 就是一个跳转指令,高 6 位之外的 26 位都是一个跳转后的地址。
+**R 指令** 是一般用来做算术和逻辑操作,里面有读取和写入数据的寄存器的地址。如果是逻辑位移操作,后面还有位移操作的位移量,而最后的功能码,则是在前面的操作码不够的时候,扩展操作码表示对应的具体指令的。**I 指令**,则通常是用在数据传输、条件分支,以及在运算的时候使用的并非变量还是常数的时候。这个时候,没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或者一个常数。**J 指令** 就是一个跳转指令,高 6 位之外的 26 位都是一个跳转后的地址。
 
 ```plaintext
 add $t0,$s2,$s1
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25407\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25407\350\256\262.md"
index 95ce49ae2..7d15097cc 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25407\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25407\350\256\262.md"
@@ -93,7 +93,7 @@ Infinite Mirror Effect,如果函数 A 调用 B,B 再调用 A,那么代码
 
 最终,计算机科学家们想到了一个比单独记录跳转回来的地址更完善的办法。我们在内存里面开辟一段空间,用栈这个 **后进先出** (LIFO,Last In First Out)的数据结构。栈就像一个乒乓球桶,每次程序调用函数之前,我们都把调用返回后的地址写在一个乒乓球上,然后塞进这个球桶。这个操作其实就是我们常说的 **压栈** 。如果函数执行完了,我们就从球桶里取出最上面的那个乒乓球,很显然,这就是 **出栈** 。
 
-拿到出栈的乒乓球,找到上面的地址,把程序跳转过去,就返回到了函数调用后的下一条指令了。如果函数 A 在执行完成之前又调用了函数 B,那么在取出乒乓球之前,我们需要往球桶里塞一个乒乓球。而我们从球桶最上面拿乒乓球的时候,拿的也一定是最近一次的,也就是最下面一层的函数调用完成后的地址。乒乓球桶的底部,就是 **栈底** ,最上面的乒乓球所在的位置,就是 **栈顶** 。
+拿到出栈的乒乓球,找到上面的地址,把程序跳转过去,就返回到了函数调用后的下一条指令了。如果函数 A 在执行完成之前又调用了函数 B,那么在取出乒乓球之前,我们需要往球桶里塞一个乒乓球。而我们从球桶最上面拿乒乓球的时候,拿的也一定是最近一次的,也就是最下面一层的函数调用完成后的地址。乒乓球桶的底部,就是 **栈底**,最上面的乒乓球所在的位置,就是 **栈顶** 。
 
 ![img](assets/d0c75219d3a528c920c2a593daaf77be.jpeg)
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25408\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25408\350\256\262.md"
index 56cf13fb3..77bccdbea 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25408\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25408\350\256\262.md"
@@ -146,7 +146,7 @@ Disassembly of section .fini:
 ...
 ```
 
-你会发现,可执行代码 dump 出来内容,和之前的目标代码长得差不多,但是长了很多。因为在 Linux 下,可执行文件和目标文件所使用的都是一种叫 **ELF** (Execuatable and Linkable File Format)的文件格式,中文名字叫 **可执行与可链接文件格式** ,这里面不仅存放了编译成的汇编指令,还保留了很多别的数据。
+你会发现,可执行代码 dump 出来内容,和之前的目标代码长得差不多,但是长了很多。因为在 Linux 下,可执行文件和目标文件所使用的都是一种叫 **ELF** (Execuatable and Linkable File Format)的文件格式,中文名字叫 **可执行与可链接文件格式**,这里面不仅存放了编译成的汇编指令,还保留了很多别的数据。
 
 比如我们过去所有 objdump 出来的代码里,你都可以看到对应的函数名称,像 add、main 等等,乃至你自己定义的全局可以访问的变量名称,都存放在这个 ELF 格式文件里。这些名字和它们对应的地址,在 ELF 文件里面,存储在一个叫作 **符号表** (Symbols Table)的位置里。符号表相当于一个地址簿,把名字和地址关联了起来。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25410\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25410\350\256\262.md"
index a567d6b88..59be585d4 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25410\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25410\350\256\262.md"
@@ -131,7 +131,7 @@ call   400550 <[email protected]>
 复制代码
 ```
 
-在动态链接对应的共享库,我们在共享库的 data section 里面,保存了一张 **全局偏移表** (GOT,Global Offset Table)。 **虽然共享库的代码部分的物理内存是共享的,但是数据部分是各个动态链接它的应用程序里面各加载一份的。** 所有需要引用当前共享库外部的地址的指令,都会查询 GOT,来找到当前运行程序的虚拟内存里的对应位置。而 GOT 表里的数据,则是在我们加载一个个共享库的时候写进去的。
+在动态链接对应的共享库,我们在共享库的 data section 里面,保存了一张 **全局偏移表** (GOT,Global Offset Table)。**虽然共享库的代码部分的物理内存是共享的,但是数据部分是各个动态链接它的应用程序里面各加载一份的。** 所有需要引用当前共享库外部的地址的指令,都会查询 GOT,来找到当前运行程序的虚拟内存里的对应位置。而 GOT 表里的数据,则是在我们加载一个个共享库的时候写进去的。
 
 不同的进程,调用同样的 lib.so,各自 GOT 里面指向最终加载的动态链接库里面的虚拟内存地址是不同的。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25411\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25411\350\256\262.md"
index 9bbb6a08c..733ec8779 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25411\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25411\350\256\262.md"
@@ -52,7 +52,7 @@ ASCII 码就好比一个字典,用 8 位二进制中的 128 个不同的数,
 
 我们可以看到,最大的 32 位整数,就是 2147483647。如果用整数表示法,只需要 32 位就能表示了。但是如果用字符串来表示,一共有 10 个字符,每个字符用 8 位的话,需要整整 80 位。比起整数表示法,要多占很多空间。
 
-这也是为什么,很多时候我们在存储数据的时候,要采用二进制序列化这样的方式,而不是简单地把数据通过 CSV 或者 JSON,这样的文本格式存储来进行序列化。 **不管是整数也好,浮点数也好,采用二进制序列化会比存储文本省下不少空间。** ASCII 码只表示了 128 个字符,一开始倒也堪用,毕竟计算机是在美国发明的。然而随着越来越多的不同国家的人都用上了计算机,想要表示譬如中文这样的文字,128 个字符显然是不太够用的。于是,计算机工程师们开始各显神通,给自己国家的语言创建了对应的 **字符集** (Charset)和 **字符编码** (Character Encoding)。
+这也是为什么,很多时候我们在存储数据的时候,要采用二进制序列化这样的方式,而不是简单地把数据通过 CSV 或者 JSON,这样的文本格式存储来进行序列化。**不管是整数也好,浮点数也好,采用二进制序列化会比存储文本省下不少空间。** ASCII 码只表示了 128 个字符,一开始倒也堪用,毕竟计算机是在美国发明的。然而随着越来越多的不同国家的人都用上了计算机,想要表示譬如中文这样的文字,128 个字符显然是不太够用的。于是,计算机工程师们开始各显神通,给自己国家的语言创建了对应的 **字符集** (Charset)和 **字符编码** (Character Encoding)。
 
 字符集,表示的可以是字符的一个集合。比如“中文”就是一个字符集,不过这样描述一个字符集并不准确。想要更精确一点,我们可以说,“第一版《新华字典》里面出现的所有汉字”,这是一个字符集。这样,我们才能明确知道,一个字符在不在这个集合里面。比如,我们日常说的 Unicode,其实就是一个字符集,包含了 150 种语言的 14 万个不同的字符。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25412\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25412\350\256\262.md"
index f9c3ebdf9..9456dddea 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25412\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25412\350\256\262.md"
@@ -46,19 +46,19 @@
 
 你可能要说了,我们可以提高电压或者用更粗的电线,使得电阻更小,这样就可以让整个线路铺得更长一些。但是这个再长,也没办法从北京铺设到上海吧。要想从北京把电报发到上海,我们还得想些别的办法。
 
-对于电报来说,电线太长了,使得线路接通也没有办法让蜂鸣器响起来。那么,我们就不要一次铺太长的线路,而把一小段距离当成一个线路,也和驿站建立一个小电报站。我们在小电报站里面安排一个电报员,他听到上一个小电报站发来的信息,然后原样输入,发到下一个电报站去。这样,我们的信号就可以一段段传输下去,而不会因为距离太长,导致电阻太大,没有办法成功传输信号。为了能够实现这样 **接力传输信号** ,在电路里面,工程师们造了一个叫作 **继电器** (Relay)的设备。
+对于电报来说,电线太长了,使得线路接通也没有办法让蜂鸣器响起来。那么,我们就不要一次铺太长的线路,而把一小段距离当成一个线路,也和驿站建立一个小电报站。我们在小电报站里面安排一个电报员,他听到上一个小电报站发来的信息,然后原样输入,发到下一个电报站去。这样,我们的信号就可以一段段传输下去,而不会因为距离太长,导致电阻太大,没有办法成功传输信号。为了能够实现这样 **接力传输信号**,在电路里面,工程师们造了一个叫作 **继电器** (Relay)的设备。
 
 ![img](assets/1186a10341202ea36df27cba95f1cbea.jpg)
 
 中继,其实就是不断地通过新的电源重新放大已经开始衰减的原有信号
 
-事实上,这个过程中,我们需要在每一阶段 **原样传输信号** ,所以你可以想想,我们是不是可以设计一个设备来代替这个电报员?相比使用人工听蜂鸣器的声音,来重复输入信号,利用电磁效应和磁铁,来实现这个事情会更容易。
+事实上,这个过程中,我们需要在每一阶段 **原样传输信号**,所以你可以想想,我们是不是可以设计一个设备来代替这个电报员?相比使用人工听蜂鸣器的声音,来重复输入信号,利用电磁效应和磁铁,来实现这个事情会更容易。
 
 我们把原先用来输出声音的蜂鸣器,换成一段环形的螺旋线圈,让电路封闭通上电。因为电磁效应,这段螺旋线圈会产生一个带有磁性的电磁场。我们原本需要输入的按钮开关,就可以用一块磁力稍弱的磁铁把它设在“关”的状态。这样,按下上一个电报站的开关,螺旋线圈通电产生了磁场之后,磁力就会把开关“吸”下来,接通到下一个电报站的电路。
 
 如果我们在中间所有小电报站都用这个“ **螺旋线圈 + 磁性开关** ”的方式,来替代蜂鸣器和普通开关,而只在电报的始发和终点用普通的开关和蜂鸣器,我们就有了一个拆成一段一段的电报线路,接力传输电报信号。这样,我们就不需要中间安排人力来听打电报内容,也不需要解决因为线缆太长导致的电阻太大或者电压不足的问题了。我们只要在终点站安排电报员,听写最终的电报内容就可以了。这样是不是比之前更省事了?
 
-事实上,继电器还有一个名字就叫作 **电驿** ,这个“驿”就是驿站的驿,可以说非常形象了。这个接力的策略不仅可以用在电报中,在通信类的科技产品中其实都可以用到。
+事实上,继电器还有一个名字就叫作 **电驿**,这个“驿”就是驿站的驿,可以说非常形象了。这个接力的策略不仅可以用在电报中,在通信类的科技产品中其实都可以用到。
 
 比如说,你在家里用 WiFi,如果你的屋子比较大,可能某些房间的信号就不好。你可以选用支持“中继”的 WiFi 路由器,在信号衰减的地方,增加一个 WiFi 设备,接收原来的 WiFi 信号,再重新从当前节点传输出去。这种中继对应的英文名词和继电器是一样的,也叫 Relay。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25413\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25413\350\256\262.md"
index 1b6dd428c..2fa9d61f2 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25413\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25413\350\256\262.md"
@@ -32,11 +32,11 @@
 
 一方面,我们需要知道,加法计算之后的个位是什么,在输入的两位是 00 和 11 的情况下,对应的输出都应该是 0;在输入的两位是 10 和 01 的情况下,输出都是 1。结果你会发现,这个输入和输出的对应关系,其实就是我在上一讲留给你的思考题里面的“异或门(XOR)”。
 
-讲与、或、非门的时候,我们很容易就能和程序里面的“AND(通常是 & 符号)”“ OR(通常是 | 符号)”和“ NOT(通常是 ! 符号)”对应起来。可能你没有想过,为什么我们会需要“异或(XOR)”,这样一个在逻辑运算里面没有出现的形式,作为一个基本电路。 **其实,异或门就是一个最简单的整数加法,所需要使用的基本门电路** 。
+讲与、或、非门的时候,我们很容易就能和程序里面的“AND(通常是 & 符号)”“ OR(通常是 | 符号)”和“ NOT(通常是 ! 符号)”对应起来。可能你没有想过,为什么我们会需要“异或(XOR)”,这样一个在逻辑运算里面没有出现的形式,作为一个基本电路。**其实,异或门就是一个最简单的整数加法,所需要使用的基本门电路** 。
 
 算完个位的输出还不算完,输入的两位都是 11 的时候,我们还需要向更左侧的一位进行进位。那这个就对应一个与门,也就是有且只有在加数和被加数都是 1 的时候,我们的进位才会是 1。
 
-所以,通过一个异或门计算出个位,通过一个与门计算出是否进位,我们就通过电路算出了一个一位数的加法。于是, **我们把两个门电路打包,给它取一个名字,就叫作半加器** (Half Adder)。
+所以,通过一个异或门计算出个位,通过一个与门计算出是否进位,我们就通过电路算出了一个一位数的加法。于是,**我们把两个门电路打包,给它取一个名字,就叫作半加器** (Half Adder)。
 
 ![img](assets/5860fd8c4ace079b40e66b9568d2b81e.jpg)
 
@@ -48,7 +48,7 @@
 
 二位用一个半加器不能计算完成的原因也很简单。因为二位除了一个加数和被加数之外,还需要加上来自个位的进位信号,一共需要三个数进行相加,才能得到结果。但是我们目前用到的,无论是最简单的门电路,还是用两个门电路组合而成的半加器,输入都只能是两个 bit,也就是两个开关。那我们该怎么办呢?
 
-实际上,解决方案也并不复杂。 **我们用两个半加器和一个或门,就能组合成一个全加器** 。第一个半加器,我们用和个位的加法一样的方式,得到是否进位 X 和对应的二个数加和后的结果 Y,这样两个输出。然后,我们把这个加和后的结果 Y,和个位数相加后输出的进位信息 U,再连接到一个半加器上,就会再拿到一个是否进位的信号 V 和对应的加和后的结果 W。
+实际上,解决方案也并不复杂。**我们用两个半加器和一个或门,就能组合成一个全加器** 。第一个半加器,我们用和个位的加法一样的方式,得到是否进位 X 和对应的二个数加和后的结果 Y,这样两个输出。然后,我们把这个加和后的结果 Y,和个位数相加后输出的进位信息 U,再连接到一个半加器上,就会再拿到一个是否进位的信号 V 和对应的加和后的结果 W。
 
 ![img](assets/3f11f278ba8f24209a56fb3ee1ca9e2a.jpg)
 
@@ -76,7 +76,7 @@
 
 我们用两个门电路,搭出一个半加器,就好像我们拿两块乐高,叠在一起,变成一个长方形的乐高,这样我们就有了一个新的积木组件,柱子。我们再用两个柱子和一个长条的积木组合一下,就变成一个积木桥。然后几个积木桥串接在一起,又成了积木楼梯。
 
-当我们想要搭建一个摩天大楼,我们需要很多很多楼梯。但是这个时候,我们已经不再关注最基础的一节楼梯是怎么用一块块积木搭建起来的。这其实就是计算机中,无论软件还是硬件中一个很重要的设计思想, **分层** 。
+当我们想要搭建一个摩天大楼,我们需要很多很多楼梯。但是这个时候,我们已经不再关注最基础的一节楼梯是怎么用一块块积木搭建起来的。这其实就是计算机中,无论软件还是硬件中一个很重要的设计思想,**分层** 。
 
 ![img](assets/8a7740f698236fda4e5f900d88fdf194.jpg)
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25415\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25415\350\256\262.md"
index 4a37b0860..ba6117a2d 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25415\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25415\350\256\262.md"
@@ -57,7 +57,7 @@
 
 单精度的 32 个比特可以分成三部分。
 
-第一部分是一个 **符号位** ,用来表示是正数还是负数。我们一般用 **s** 来表示。在浮点数里,我们不像正数分符号数还是无符号数,所有的浮点数都是有符号的。
+第一部分是一个 **符号位**,用来表示是正数还是负数。我们一般用 **s** 来表示。在浮点数里,我们不像正数分符号数还是无符号数,所有的浮点数都是有符号的。
 
 接下来是一个 8 个比特组成的 **指数位** 。我们一般用 **e** 来表示。8 个比特能够表示的整数空间,就是 0~255。我们在这里用 1~254 映射到 -126~127 这 254 个有正有负的数上。因为我们的浮点数,不仅仅想要表示很大的数,还希望能够表示很小的数,所以指数位也会有负数。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25417\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25417\350\256\262.md"
index 23d1af95a..83b37db7d 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25417\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25417\350\256\262.md"
@@ -34,9 +34,9 @@
 
 不同步骤在不同组件之内完成
 
-除了 Instruction Cycle 这个指令周期,在 CPU 里面我们还会提到另外两个常见的 Cycle。一个叫 **Machine Cycle** , **机器周期** 或者 **CPU 周期** 。CPU 内部的操作速度很快,但是访问内存的速度却要慢很多。每一条指令都需要从内存里面加载而来,所以我们一般把从内存里面读取一条指令的最短时间,称为 CPU 周期。
+除了 Instruction Cycle 这个指令周期,在 CPU 里面我们还会提到另外两个常见的 Cycle。一个叫 **Machine Cycle**,**机器周期** 或者 **CPU 周期** 。CPU 内部的操作速度很快,但是访问内存的速度却要慢很多。每一条指令都需要从内存里面加载而来,所以我们一般把从内存里面读取一条指令的最短时间,称为 CPU 周期。
 
-还有一个是我们之前提过的 **Clock Cycle** ,也就是 **时钟周期** 以及我们机器的主频。一个 CPU 周期,通常会由几个时钟周期累积起来。一个 CPU 周期的时间,就是这几个 Clock Cycle 的总和。
+还有一个是我们之前提过的 **Clock Cycle**,也就是 **时钟周期** 以及我们机器的主频。一个 CPU 周期,通常会由几个时钟周期累积起来。一个 CPU 周期的时间,就是这几个 Clock Cycle 的总和。
 
 对于一个指令周期来说,我们取出一条指令,然后执行它,至少需要两个 CPU 周期。取出指令至少需要一个 CPU 周期,执行至少也需要一个 CPU 周期,复杂的指令则需要更多的 CPU 周期。
 
@@ -52,9 +52,9 @@
 
 名字是什么其实并不重要,一般来说,我们可以认为,数据通路就是我们的处理器单元。它通常由两类原件组成。
 
-第一类叫 **操作元件** ,也叫组合逻辑元件(Combinational Element),其实就是我们的 ALU。在前面讲 ALU 的过程中可以看到,它们的功能就是在特定的输入下,根据下面的组合电路的逻辑,生成特定的输出。
+第一类叫 **操作元件**,也叫组合逻辑元件(Combinational Element),其实就是我们的 ALU。在前面讲 ALU 的过程中可以看到,它们的功能就是在特定的输入下,根据下面的组合电路的逻辑,生成特定的输出。
 
-第二类叫 **存储元件** ,也有叫状态元件(State Element)的。比如我们在计算过程中需要用到的寄存器,无论是通用寄存器还是状态寄存器,其实都是存储元件。
+第二类叫 **存储元件**,也有叫状态元件(State Element)的。比如我们在计算过程中需要用到的寄存器,无论是通用寄存器还是状态寄存器,其实都是存储元件。
 
 我们通过数据总线的方式,把它们连接起来,就可以完成数据的存储、处理和传输了,这就是所谓的 **建立数据通路** 了。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25418\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25418\350\256\262.md"
index 59c4fe95d..def522728 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25418\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25418\350\256\262.md"
@@ -54,12 +54,12 @@
 
 或非门的真值表
 
-1. 在这个电路一开始,输入开关都是关闭的,所以或非门(NOR)A 的输入是 0 和 0。对应到我列的这个真值表,输出就是 1。而或非门 B 的输入是 0 和 A 的输出 1,对应输出就是 0。B 的输出 0 反馈到 A,和之前的输入没有变化,A 的输出仍然是 1。而整个电路的 **输出 Q** ,也就是 0。
+1. 在这个电路一开始,输入开关都是关闭的,所以或非门(NOR)A 的输入是 0 和 0。对应到我列的这个真值表,输出就是 1。而或非门 B 的输入是 0 和 A 的输出 1,对应输出就是 0。B 的输出 0 反馈到 A,和之前的输入没有变化,A 的输出仍然是 1。而整个电路的 **输出 Q**,也就是 0。
 1. 当我们把 A 前面的开关 R 合上的时候,A 的输入变成了 1 和 0,输出就变成了 0,对应 B 的输入变成 0 和 0,输出就变成了 1。B 的输出 1 反馈给到了 A,A 的输入变成了 1 和 1,输出仍然是 0。所以把 A 的开关合上之后,电路仍然是稳定的,不会像晶振那样振荡,但是整个电路的 **输出 Q** 变成了 1。
-1. 这个时候,如果我们再把 A 前面的开关 R 打开,A 的输入变成和 1 和 0,输出还是 0,对应的 B 的输入没有变化,输出也还是 1。B 的输出 1 反馈给到了 A,A 的输入变成了 1 和 0,输出仍然是 0。这个时候,电路仍然稳定。 **开关 R 和 S 的状态和上面的第一步是一样的,但是最终的输出 Q 仍然是 1,** 和第 1 步里 Q 状态是相反的。我们的输入和刚才第二步的开关状态不一样,但是输出结果仍然保留在了第 2 步时的输出没有发生变化。
+1. 这个时候,如果我们再把 A 前面的开关 R 打开,A 的输入变成和 1 和 0,输出还是 0,对应的 B 的输入没有变化,输出也还是 1。B 的输出 1 反馈给到了 A,A 的输入变成了 1 和 0,输出仍然是 0。这个时候,电路仍然稳定。**开关 R 和 S 的状态和上面的第一步是一样的,但是最终的输出 Q 仍然是 1,** 和第 1 步里 Q 状态是相反的。我们的输入和刚才第二步的开关状态不一样,但是输出结果仍然保留在了第 2 步时的输出没有发生变化。
 1. 这个时候,只有我们再去关闭下面的开关 S,才可以看到,这个时候,B 有一个输入必然是 1,所以 B 的输出必然是 0,也就是电路的最终 **输出 Q** 必然是 0。
 
-这样一个电路,我们称之为触发器(Flip-Flop)。接通开关 R,输出变为 1,即使断开开关,输出还是 1 不变。接通开关 S,输出变为 0,即使断开开关,输出也还是 0。也就是, **当两个开关都断开的时候,最终的输出结果,取决于之前动作的输出结果,这个也就是我们说的记忆功能** 。
+这样一个电路,我们称之为触发器(Flip-Flop)。接通开关 R,输出变为 1,即使断开开关,输出还是 1 不变。接通开关 S,输出变为 0,即使断开开关,输出也还是 0。也就是,**当两个开关都断开的时候,最终的输出结果,取决于之前动作的输出结果,这个也就是我们说的记忆功能** 。
 
 这里的这个电路是最简单的 RS 触发器,也就是所谓的复位置位触发器(Reset-Set Flip Flop) 。对应的输出结果的真值表,你可以看下面这个表格。可以看到,当两个开关都是 0 的时候,对应的输出不是 1 或者 0,而是和 Q 的上一个状态一致。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25420\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25420\350\256\262.md"
index f120a357f..f5d4d3722 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25420\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25420\350\256\262.md"
@@ -52,7 +52,7 @@
 
 如果我们把一个指令拆分成“取指令 - 指令译码 - 执行指令”这样三个部分,那这就是一个三级的流水线。如果我们进一步把“执行指令”拆分成“ALU 计算(指令执行)- 内存访问 - 数据写回”,那么它就会变成一个五级的流水线。
 
-五级的流水线,就表示我们在同一个时钟周期里面,同时运行五条指令的不同阶段。这个时候,虽然执行一条指令的时钟周期变成了 5,但是我们可以把 CPU 的主频提得更高了。 **我们不需要确保最复杂的那条指令在时钟周期里面执行完成,而只要保障一个最复杂的流水线级的操作,在一个时钟周期内完成就好了。**
+五级的流水线,就表示我们在同一个时钟周期里面,同时运行五条指令的不同阶段。这个时候,虽然执行一条指令的时钟周期变成了 5,但是我们可以把 CPU 的主频提得更高了。**我们不需要确保最复杂的那条指令在时钟周期里面执行完成,而只要保障一个最复杂的流水线级的操作,在一个时钟周期内完成就好了。**
 
 如果某一个操作步骤的时间太长,我们就可以考虑把这个步骤,拆分成更多的步骤,让所有步骤需要执行的时间尽量都差不多长。这样,也就可以解决我们在单指令周期处理器中遇到的,性能瓶颈来自于最复杂的指令的问题。像我们现代的 ARM 或者 Intel 的 CPU,流水线级数都已经到了 14 级。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25421\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25421\350\256\262.md"
index 80c1f67cd..7c14d7429 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25421\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25421\350\256\262.md"
@@ -54,7 +54,7 @@ Pentium 4 之前的 Pentium III CPU,流水线的深度是 11 级,也就是
 
 主频的提升和晶体管数量的增加都使得我们 CPU 的功耗变大了。这个问题导致了 Pentium 4 在整个生命周期里,都成为了耗电和散热的大户。而 Pentium 4 是在 2000~2004 年作为 Intel 的主打 CPU 出现在市场上的。这个时间段,正是笔记本电脑市场快速发展的时间。在笔记本电脑上,功耗和散热比起台式机是一个更严重的问题了。即使性能更好,别人的笔记本可以用上 2 小时,你的只能用 30 分钟,那谁也不爱买啊!
 
-更何况,Pentium 4 的性能还更差一些。 **这个就要我们说到第二点了,就是上面说的流水线技术带来的性能提升,是一个理想情况。在实际的程序执行中,并不一定能够做得到** 。
+更何况,Pentium 4 的性能还更差一些。**这个就要我们说到第二点了,就是上面说的流水线技术带来的性能提升,是一个理想情况。在实际的程序执行中,并不一定能够做得到** 。
 
 还回到我们刚才举的三条指令的例子。如果这三条指令,是下面这样的三条代码,会发生什么情况呢?
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25422\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25422\350\256\262.md"
index 160c42199..37ac941b1 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25422\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25422\350\256\262.md"
@@ -81,7 +81,7 @@ int main() {
 
 所以,我们需要保证,在内存地址为 16 的指令读取 rbp-0x4 里面的值之前,内存地址 12 的指令写入到 rbp-0x4 的操作必须完成。这就是先写后读所面临的数据依赖。如果这个顺序保证不了,我们的程序就会出错。
 
-这个先写后读的依赖关系,我们一般被称之为 **数据依赖** ,也就是 Data Dependency。
+这个先写后读的依赖关系,我们一般被称之为 **数据依赖**,也就是 Data Dependency。
 
 ### 先读后写(Write After Read)
 
@@ -116,7 +116,7 @@ int main() {
 
 如果我们在内存地址 18 的 eax 的写入先完成了,在内存地址为 15 的代码里面取出 eax 才发生,我们的程序计算就会出错。这里,我们同样要保障对于 eax 的先读后写的操作顺序。
 
-这个先读后写的依赖,一般被叫作 **反依赖** ,也就是 Anti-Dependency。
+这个先读后写的依赖,一般被叫作 **反依赖**,也就是 Anti-Dependency。
 
 ### 写后再写(Write After Write)
 
@@ -139,7 +139,7 @@ int main() {
 
 在这个情况下,你会看到,内存地址 4 所在的指令和内存地址 b 所在的指令,都是将对应的数据写入到 rbp-0x4 的内存地址里面。如果内存地址 b 的指令在内存地址 4 的指令之后写入。那么这些指令完成之后,rbp-0x4 里的数据就是错误的。这就会导致后续需要使用这个内存地址里的数据指令,没有办法拿到正确的值。所以,我们也需要保障内存地址 4 的指令的写入,在内存地址 b 的指令的写入之前完成。
 
-这个写后再写的依赖,一般被叫作 **输出依赖** ,也就是 Output Dependency。
+这个写后再写的依赖,一般被叫作 **输出依赖**,也就是 Output Dependency。
 
 ### 再等等:通过流水线停顿解决数据冒险
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25423\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25423\350\256\262.md"
index 9acb6d5d6..5fa28fefc 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25423\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25423\350\256\262.md"
@@ -8,7 +8,7 @@
 
 那针对流水线冒险的问题,我们有没有更高级或者更高效的解决方案呢?既不用简单花钱加硬件电路这样“堆资源”,也不是纯粹等待之前的任务完成这样“等排期”。
 
-答案当然是有的。这一讲,我们就来看看计算机组成原理中,一个更加精巧的解决方案, **操作数前推** 。
+答案当然是有的。这一讲,我们就来看看计算机组成原理中,一个更加精巧的解决方案,**操作数前推** 。
 
 ## NOP 操作和指令对齐
 
@@ -64,7 +64,7 @@ add $s2, $s1,$t0
 
 这样的解决方案,我们就叫作 **操作数前推** (Operand Forwarding),或者操作数旁路(Operand Bypassing)。其实我觉得,更合适的名字应该叫 **操作数转发** 。这里的 Forward,其实就是我们写 Email 时的“转发”(Forward)的意思。不过现有的经典教材的中文翻译一般都叫“前推”,我们也就不去纠正这个说法了,你明白这个意思就好。
 
-转发,其实是这个技术的 **逻辑含义** ,也就是在第 1 条指令的执行结果,直接“转发”给了第 2 条指令的 ALU 作为输入。另外一个名字,旁路(Bypassing),则是这个技术的 **硬件含义** 。为了能够实现这里的“转发”,我们在 CPU 的硬件里面,需要再单独拉一根信号传输的线路出来,使得 ALU 的计算结果,能够重新回到 ALU 的输入里来。这样的一条线路,就是我们的“旁路”。它越过(Bypass)了写入寄存器,再从寄存器读出的过程,也为我们节省了 2 个时钟周期。
+转发,其实是这个技术的 **逻辑含义**,也就是在第 1 条指令的执行结果,直接“转发”给了第 2 条指令的 ALU 作为输入。另外一个名字,旁路(Bypassing),则是这个技术的 **硬件含义** 。为了能够实现这里的“转发”,我们在 CPU 的硬件里面,需要再单独拉一根信号传输的线路出来,使得 ALU 的计算结果,能够重新回到 ALU 的输入里来。这样的一条线路,就是我们的“旁路”。它越过(Bypass)了写入寄存器,再从寄存器读出的过程,也为我们节省了 2 个时钟周期。
 
 操作数前推的解决方案不但可以单独使用,还可以和流水线冒泡一起使用。有的时候,虽然我们可以把操作数转发到下一条指令,但是下一条指令仍然需要停顿一个时钟周期。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25425\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25425\350\256\262.md"
index c286236fc..48a049147 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25425\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25425\350\256\262.md"
@@ -68,7 +68,7 @@
 
 ![img](assets/ea82f279b48c10ad95027c91ed62ab5d.jpeg)
 
-这个状态机里,我们一共有 4 个状态,所以我们需要 2 个比特来记录对应的状态。这样这整个策略,就可以叫作 **2 比特饱和计数** ,或者叫 **双模态预测器** (Bimodal Predictor)。
+这个状态机里,我们一共有 4 个状态,所以我们需要 2 个比特来记录对应的状态。这样这整个策略,就可以叫作 **2 比特饱和计数**,或者叫 **双模态预测器** (Bimodal Predictor)。
 
 好了,现在你可以用这个策略,再去对照一下上面的天气情况。如果天气的初始状态我们放在“多半放晴”的状态下,我们预测的结果的正确率会是 22 次,也就是 73.3% 的正确率。可以看到,并不是更复杂的算法,效果一定就更好。实际的预测效果,和实际执行的指令高度相关。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25426\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25426\350\256\262.md"
index 66eb65120..b5425a0f2 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25426\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25426\350\256\262.md"
@@ -58,7 +58,7 @@ CPU 需要在指令执行之前,去判断指令之间是否有依赖关系。
 
 围绕着这个设计的,是 Intel 一个著名的“史诗级”失败,也就是著名的 IA-64 架构的安腾(Itanium)处理器。只不过,这一次,责任不全在 Intel,还要拉上可以称之为硅谷起源的另一家公司,也就是惠普。
 
-之所以称为“史诗”级失败,这个说法来源于惠普最早给这个架构取的名字, **显式并发指令运算** (Explicitly Parallel Instruction Computer),这个名字的缩写 **EPIC** ,正好是“史诗”的意思。
+之所以称为“史诗”级失败,这个说法来源于惠普最早给这个架构取的名字,**显式并发指令运算** (Explicitly Parallel Instruction Computer),这个名字的缩写 **EPIC**,正好是“史诗”的意思。
 
 好巧不巧,安腾处理器和和我之前给你介绍过的 Pentium 4 一样,在市场上是一个失败的产品。在经历了 12 年之久的设计研发之后,安腾一代只卖出了几千套。而安腾二代,在从 2002 年开始反复挣扎了 16 年之后,最终在 2018 年被 Intel 宣告放弃,退出了市场。自此,世上再也没有这个“史诗”服务器了。
 
@@ -74,7 +74,7 @@ CPU 需要在指令执行之前,去判断指令之间是否有依赖关系。
 
 CPU 在运行的时候,不再是取一条指令,而是取出一个指令包。然后,译码解析整个指令包,解析出 3 条指令直接并行运行。可以看到,使用 **超长指令字** 架构的 CPU,同样是采用流水线架构的。也就是说,一组(Group)指令,仍然要经历多个时钟周期。同样的,下一组指令并不是等上一组指令执行完成之后再执行,而是在上一组指令的指令译码阶段,就开始取指令了。
 
-值得注意的一点是,流水线停顿这件事情在 **超长指令字** 里面,很多时候也是由编译器来做的。除了停下整个处理器流水线, **超长指令字** 的 CPU 不能在某个时钟周期停顿一下,等待前面依赖的操作执行完成。编译器需要在适当的位置插入 NOP 操作,直接在编译出来的机器码里面,就把流水线停顿这个事情在软件层面就安排妥当。
+值得注意的一点是,流水线停顿这件事情在 **超长指令字** 里面,很多时候也是由编译器来做的。除了停下整个处理器流水线,**超长指令字** 的 CPU 不能在某个时钟周期停顿一下,等待前面依赖的操作执行完成。编译器需要在适当的位置插入 NOP 操作,直接在编译出来的机器码里面,就把流水线停顿这个事情在软件层面就安排妥当。
 
 虽然安腾的设想很美好,Intel 也曾经希望能够让安腾架构成为替代 x86 的新一代架构,但是最终安腾还是在前前后后折腾将近 30 年后失败了。2018 年,Intel 宣告安腾 9500 会在 2021 年停止供货。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25427\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25427\350\256\262.md"
index 0b6fb0368..2358dbefa 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25427\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25427\350\256\262.md"
@@ -48,7 +48,7 @@
 
 ## SIMD:如何加速矩阵乘法?
 
-在上面的 CPU 信息的图里面,你会看到,中间有一组信息叫作 Instructions,里面写了有 MMX、SSE 等等。这些信息就是这个 CPU 所支持的指令集。这里的 MMX 和 SSE 的指令集,也就引出了我要给你讲的最后一个提升 CPU 性能的技术方案, **SIMD** ,中文叫作 **单指令多数据流** (Single Instruction Multiple Data)。
+在上面的 CPU 信息的图里面,你会看到,中间有一组信息叫作 Instructions,里面写了有 MMX、SSE 等等。这些信息就是这个 CPU 所支持的指令集。这里的 MMX 和 SSE 的指令集,也就引出了我要给你讲的最后一个提升 CPU 性能的技术方案,**SIMD**,中文叫作 **单指令多数据流** (Single Instruction Multiple Data)。
 
 我们先来体会一下 SIMD 的性能到底怎么样。下面是两段示例程序,一段呢,是通过循环的方式,给一个 list 里面的每一个数加 1。另一段呢,是实现相同的功能,但是直接调用 NumPy 这个库的 add 方法。在统计两段程序的性能的时候,我直接调用了 Python 里面的 timeit 的库。
 
@@ -69,7 +69,7 @@ $ python
 
 有些同学可能会猜测,是不是因为 Python 是一门解释性的语言,所以这个性能差异会那么大。第一段程序的循环的每一次操作都需要 Python 解释器来执行,而第二段的函数调用是一次调用编译好的原生代码,所以才会那么快。如果你这么想,不妨试试直接用 C 语言实现一下 1000 个元素的数组里面的每个数加 1。你会发现,即使是 C 语言编译出来的代码,还是远远低于 NumPy。原因就是,NumPy 直接用到了 SIMD 指令,能够并行进行向量的操作。
 
-而前面使用循环来一步一步计算的算法呢,一般被称为 **SISD** ,也就是 **单指令单数据** (Single Instruction Single Data)的处理方式。如果你手头的是一个多核 CPU 呢,那么它同时处理多个指令的方式可以叫作 **MIMD** ,也就是 **多指令多数据** (Multiple Instruction Multiple Dataa)。
+而前面使用循环来一步一步计算的算法呢,一般被称为 **SISD**,也就是 **单指令单数据** (Single Instruction Single Data)的处理方式。如果你手头的是一个多核 CPU 呢,那么它同时处理多个指令的方式可以叫作 **MIMD**,也就是 **多指令多数据** (Multiple Instruction Multiple Dataa)。
 
 为什么 SIMD 指令能快那么多呢?这是因为,SIMD 在获取数据和执行指令的时候,都做到了并行。一方面,在从内存里面读取数据的时候,SIMD 是一次性读取多个数据。
 
@@ -81,7 +81,7 @@ $ python
 
 所以,对于那些在计算层面存在大量“数据并行”(Data Parallelism)的计算中,使用 SIMD 是一个很划算的办法。在这个大量的“数据并行”,其实通常就是实践当中的向量运算或者矩阵运算。在实际的程序开发过程中,过去通常是在进行图片、视频、音频的处理。最近几年则通常是在进行各种机器学习算法的计算。
 
-而基于 SIMD 的向量计算指令,也正是在 Intel 发布 Pentium 处理器的时候,被引入的指令集。当时的指令集叫作 **MMX** ,也就是 Matrix Math eXtensions 的缩写,中文名字就是 **矩阵数学扩展** 。而 Pentium 处理器,也是 CPU 第一次有能力进行多媒体处理。这也正是拜 SIMD 和 MMX 所赐。
+而基于 SIMD 的向量计算指令,也正是在 Intel 发布 Pentium 处理器的时候,被引入的指令集。当时的指令集叫作 **MMX**,也就是 Matrix Math eXtensions 的缩写,中文名字就是 **矩阵数学扩展** 。而 Pentium 处理器,也是 CPU 第一次有能力进行多媒体处理。这也正是拜 SIMD 和 MMX 所赐。
 
 从 Pentium 时代开始,我们能在电脑上听 MP3、看 VCD 了,而不用专门去买一块“声霸卡”或者“显霸卡”了。没错,在那之前,在电脑上看 VCD,是需要专门买能够解码 VCD 的硬件插到电脑上去的。而到了今天,通过 GPU 快速发展起来的深度学习技术,也一样受益于 SIMD 这样的指令级并行方案,在后面讲解 GPU 的时候,我们还会遇到它。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25428\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25428\350\256\262.md"
index 70dda20c6..32bd30cdd 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25428\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25428\350\256\262.md"
@@ -18,7 +18,7 @@
 
 同样,来自软件层面的,比如我们的程序进行系统调用,发起一个读文件的请求。这样应用程序向系统调用发起请求的情况,一样是通过“异常”来实现的。
 
-**关于异常,最有意思的一点就是,它其实是一个硬件和软件组合到一起的处理过程。异常的前半生,也就是异常的发生和捕捉,是在硬件层面完成的。但是异常的后半生,也就是说,异常的处理,其实是由软件来完成的。** 计算机会为每一种可能会发生的异常,分配一个异常代码(Exception Number)。有些教科书会把异常代码叫作中断向量(Interrupt Vector)。异常发生的时候,通常是 CPU 检测到了一个特殊的信号。比如,你按下键盘上的按键,输入设备就会给 CPU 发一个信号。或者,正在执行的指令发生了加法溢出,同样,我们可以有一个进位溢出的信号。这些信号呢,在组成原理里面,我们一般叫作发生了一个事件(Event)。CPU 在检测到事件的时候,其实也就拿到了对应的异常代码。 **这些异常代码里,I/O 发出的信号的异常代码,是由操作系统来分配的,也就是由软件来设定的。而像加法溢出这样的异常代码,则是由 CPU 预先分配好的,也就是由硬件来分配的。这又是另一个软件和硬件共同组合来处理异常的过程。** 拿到异常代码之后,CPU 就会触发异常处理的流程。计算机在内存里,会保留一个异常表(Exception Table)。也有地方,把这个表叫作中断向量表(Interrupt Vector Table),好和上面的中断向量对应起来。这个异常表有点儿像我们在[第 10 讲](https://time.geekbang.org/column/article/95244)里讲的 GOT 表,存放的是不同的异常代码对应的异常处理程序(Exception Handler)所在的地址。
+**关于异常,最有意思的一点就是,它其实是一个硬件和软件组合到一起的处理过程。异常的前半生,也就是异常的发生和捕捉,是在硬件层面完成的。但是异常的后半生,也就是说,异常的处理,其实是由软件来完成的。** 计算机会为每一种可能会发生的异常,分配一个异常代码(Exception Number)。有些教科书会把异常代码叫作中断向量(Interrupt Vector)。异常发生的时候,通常是 CPU 检测到了一个特殊的信号。比如,你按下键盘上的按键,输入设备就会给 CPU 发一个信号。或者,正在执行的指令发生了加法溢出,同样,我们可以有一个进位溢出的信号。这些信号呢,在组成原理里面,我们一般叫作发生了一个事件(Event)。CPU 在检测到事件的时候,其实也就拿到了对应的异常代码。**这些异常代码里,I/O 发出的信号的异常代码,是由操作系统来分配的,也就是由软件来设定的。而像加法溢出这样的异常代码,则是由 CPU 预先分配好的,也就是由硬件来分配的。这又是另一个软件和硬件共同组合来处理异常的过程。** 拿到异常代码之后,CPU 就会触发异常处理的流程。计算机在内存里,会保留一个异常表(Exception Table)。也有地方,把这个表叫作中断向量表(Interrupt Vector Table),好和上面的中断向量对应起来。这个异常表有点儿像我们在[第 10 讲](https://time.geekbang.org/column/article/95244)里讲的 GOT 表,存放的是不同的异常代码对应的异常处理程序(Exception Handler)所在的地址。
 
 我们的 CPU 在拿到了异常码之后,会先把当前的程序执行的现场,保存到程序栈里面,然后根据异常码查询,找到对应的异常处理程序,最后把后续指令执行的指挥权,交给这个异常处理程序。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25429\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25429\350\256\262.md"
index 390899521..b77a95b17 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25429\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25429\350\256\262.md"
@@ -42,7 +42,7 @@ RISC 的 CPU 里完成指令的电路变得简单了,于是也就腾出了更
 
 程序的 CPU 执行时间 = 指令数 × CPI × Clock Cycle Time
 
-CISC 的架构,其实就是通过优化 **指令数** ,来减少 CPU 的执行时间。而 RISC 的架构,其实是在优化 CPI。因为指令比较简单,需要的时钟周期就比较少。
+CISC 的架构,其实就是通过优化 **指令数**,来减少 CPU 的执行时间。而 RISC 的架构,其实是在优化 CPI。因为指令比较简单,需要的时钟周期就比较少。
 
 因为 RISC 降低了 CPU 硬件的设计和开发难度,所以从 80 年代开始,大部分新的 CPU 都开始采用 RISC 架构。从 IBM 的 PowerPC,到 SUN 的 SPARC,都是 RISC 架构。所有人看到仍然采用 CISC 架构的 Intel CPU,都可以批评一句“Complex and messy”。但是,为什么无论是在 PC 上,还是服务器上,仍然是 Intel 成为最后的赢家呢?
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25430\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25430\350\256\262.md"
index 1575c21e9..6cb490dea 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25430\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25430\350\256\262.md"
@@ -44,7 +44,7 @@ GPU 是随着我们开始在计算机里面需要渲染三维图形的出现,
 
 图形渲染的第一步是顶点处理。构成多边形建模的每一个多边形呢,都有多个顶点(Vertex)。这些顶点都有一个在三维空间里的坐标。但是我们的屏幕是二维的,所以在确定当前视角的时候,我们需要把这些顶点在三维空间里面的位置,转化到屏幕这个二维空间里面。这个转换的操作,就被叫作顶点处理。
 
-如果你稍微学过一点图形学的话,应该知道,这样的转化都是通过线性代数的计算来进行的。可以想见,我们的建模越精细,需要转换的顶点数量就越多,计算量就越大。 **而且,这里面每一个顶点位置的转换,互相之间没有依赖,是可以并行独立计算的。** ![img](assets/04c3da62c382e45b8f891cfa046169de.jpeg)
+如果你稍微学过一点图形学的话,应该知道,这样的转化都是通过线性代数的计算来进行的。可以想见,我们的建模越精细,需要转换的顶点数量就越多,计算量就越大。**而且,这里面每一个顶点位置的转换,互相之间没有依赖,是可以并行独立计算的。**![img](assets/04c3da62c382e45b8f891cfa046169de.jpeg)
 
 顶点处理就是在进行线性变换
 
@@ -56,11 +56,11 @@ GPU 是随着我们开始在计算机里面需要渲染三维图形的出现,
 
 ### 栅格化
 
-在图元处理完成之后呢,渲染还远远没有完成。我们的屏幕分辨率是有限的。它一般是通过一个个“像素(Pixel)”来显示出内容的。所以,对于做完图元处理的多边形,我们要开始进行第三步操作。这个操作就是把它们转换成屏幕里面的一个个像素点。这个操作呢,就叫作栅格化。 **这个栅格化操作,有一个特点和上面的顶点处理是一样的,就是每一个图元都可以并行独立地栅格化。** ![img](assets/e60a58e632fc05dbf96eaa5cbb7fb2a6.jpeg)
+在图元处理完成之后呢,渲染还远远没有完成。我们的屏幕分辨率是有限的。它一般是通过一个个“像素(Pixel)”来显示出内容的。所以,对于做完图元处理的多边形,我们要开始进行第三步操作。这个操作就是把它们转换成屏幕里面的一个个像素点。这个操作呢,就叫作栅格化。**这个栅格化操作,有一个特点和上面的顶点处理是一样的,就是每一个图元都可以并行独立地栅格化。**![img](assets/e60a58e632fc05dbf96eaa5cbb7fb2a6.jpeg)
 
 ### 片段处理
 
-在栅格化变成了像素点之后,我们的图还是“黑白”的。我们还需要计算每一个像素的颜色、透明度等信息,给像素点上色。这步操作,就是片段处理。 **这步操作,同样也可以每个片段并行、独立进行,和上面的顶点处理和栅格化一样。** ![img](assets/490f298719e81beb1871c10566d56308.jpeg)
+在栅格化变成了像素点之后,我们的图还是“黑白”的。我们还需要计算每一个像素的颜色、透明度等信息,给像素点上色。这步操作,就是片段处理。**这步操作,同样也可以每个片段并行、独立进行,和上面的顶点处理和栅格化一样。**![img](assets/490f298719e81beb1871c10566d56308.jpeg)
 
 ### 像素操作
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25431\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25431\350\256\262.md"
index 99edb072a..3ff0b2d6b 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25431\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25431\350\256\262.md"
@@ -24,13 +24,13 @@
 
 一开始的可编程管线呢,仅限于顶点处理(Vertex Processing)和片段处理(Fragment Processing)部分。比起原来只能通过显卡和 Direct3D 这样的图形接口提供的固定配置,程序员们终于也可以开始在图形效果上开始大显身手了。
 
-这些可以编程的接口,我们称之为 **Shader** ,中文名称就是 **着色器** 。之所以叫“着色器”,是因为一开始这些“可编程”的接口,只能修改顶点处理和片段处理部分的程序逻辑。我们用这些接口来做的,也主要是光照、亮度、颜色等等的处理,所以叫着色器。
+这些可以编程的接口,我们称之为 **Shader**,中文名称就是 **着色器** 。之所以叫“着色器”,是因为一开始这些“可编程”的接口,只能修改顶点处理和片段处理部分的程序逻辑。我们用这些接口来做的,也主要是光照、亮度、颜色等等的处理,所以叫着色器。
 
 这个时候的 GPU,有两类 Shader,也就是 Vertex Shader 和 Fragment Shader。我们在上一讲看到,在进行顶点处理的时候,我们操作的是多边形的顶点;在片段操作的时候,我们操作的是屏幕上的像素点。对于顶点的操作,通常比片段要复杂一些。所以一开始,这两类 Shader 都是独立的硬件电路,也各自有独立的编程接口。因为这么做,硬件设计起来更加简单,一块 GPU 上也能容纳下更多的 Shader。
 
 不过呢,大家很快发现,虽然我们在顶点处理和片段处理上的具体逻辑不太一样,但是里面用到的指令集可以用同一套。而且,虽然把 Vertex Shader 和 Fragment Shader 分开,可以减少硬件设计的复杂程度,但是也带来了一种浪费,有一半 Shader 始终没有被使用。在整个渲染管线里,Vertext Shader 运行的时候,Fragment Shader 停在那里什么也没干。Fragment Shader 在运行的时候,Vertext Shader 也停在那里发呆。
 
-本来 GPU 就不便宜,结果设计的电路有一半时间是闲着的。喜欢精打细算抠出每一分性能的硬件工程师当然受不了了。于是, **统一着色器架构** (Unified Shader Architecture)就应运而生了。
+本来 GPU 就不便宜,结果设计的电路有一半时间是闲着的。喜欢精打细算抠出每一分性能的硬件工程师当然受不了了。于是,**统一着色器架构** (Unified Shader Architecture)就应运而生了。
 
 既然大家用的指令集是一样的,那不如就在 GPU 里面放很多个一样的 Shader 硬件电路,然后通过统一调度,把顶点处理、图元处理、片段处理这些任务,都交给这些 Shader 去处理,让整个 GPU 尽可能地忙起来。这样的设计,就是我们现代 GPU 的设计,就是统一着色器架构。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25433\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25433\350\256\262.md"
index 74897b7cd..b07824494 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25433\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25433\350\256\262.md"
@@ -14,7 +14,7 @@
 
 不过你有没有想过,在深度学习热起来之后,计算量最大的是什么呢?并不是进行深度学习的训练,而是深度学习的推断部分。
 
-所谓 **推断部分** ,是指我们在完成深度学习训练之后,把训练完成的模型存储下来。这个存储下来的模型,是许许多多个向量组成的参数。然后,我们根据这些参数,去计算输入的数据,最终得到一个计算结果。这个推断过程,可能是在互联网广告领域,去推测某一个用户是否会点击特定的广告;也可能是我们在经过高铁站的时候,扫一下身份证进行一次人脸识别,判断一下是不是你本人。
+所谓 **推断部分**,是指我们在完成深度学习训练之后,把训练完成的模型存储下来。这个存储下来的模型,是许许多多个向量组成的参数。然后,我们根据这些参数,去计算输入的数据,最终得到一个计算结果。这个推断过程,可能是在互联网广告领域,去推测某一个用户是否会点击特定的广告;也可能是我们在经过高铁站的时候,扫一下身份证进行一次人脸识别,判断一下是不是你本人。
 
 虽然训练一个深度学习的模型需要花的时间不少,但是实际在推断上花的时间要更多。比如,我们上面说的高铁,去年(2018 年)一年就有 20 亿人次坐了高铁,这也就意味着至少进行了 20 亿次的人脸识别“推断“工作。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25434\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25434\350\256\262.md"
index 67adddc22..a7340dbb0 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25434\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25434\350\256\262.md"
@@ -64,7 +64,7 @@ MAME 模拟器的界面
 
 所以,我们希望我们的虚拟化技术,能够克服上面的模拟器方式的两个缺陷。同时,我们可以放弃掉模拟器方式能做到的跨硬件平台的这个能力。因为毕竟对于我们想要做的云服务里的“服务器租赁”业务来说,中小客户想要租的也是一个 x86 的服务器。而另外一方面,他们希望这个租用的服务器用起来,和直接买一台或者租一台物理服务器没有区别。作为出租方的我们,也希望服务器不要因为用了虚拟化技术,而在中间损耗掉太多的性能。
 
-所以,首先我们需要一个“全虚拟化”的技术,也就是说,我们可以在现有的物理服务器的硬件和操作系统上,去跑一个完整的、不需要做任何修改的客户机操作系统(Guest OS)。那么,我们怎么在一个操作系统上,再去跑多个完整的操作系统呢?答案就是,我们自己做软件开发中很常用的一个解决方案,就是加入一个中间层。在虚拟机技术里面,这个中间层就叫作 **虚拟机监视器** ,英文叫 VMM(Virtual Machine Manager)或者 Hypervisor。
+所以,首先我们需要一个“全虚拟化”的技术,也就是说,我们可以在现有的物理服务器的硬件和操作系统上,去跑一个完整的、不需要做任何修改的客户机操作系统(Guest OS)。那么,我们怎么在一个操作系统上,再去跑多个完整的操作系统呢?答案就是,我们自己做软件开发中很常用的一个解决方案,就是加入一个中间层。在虚拟机技术里面,这个中间层就叫作 **虚拟机监视器**,英文叫 VMM(Virtual Machine Manager)或者 Hypervisor。
 
 ![img](assets/e09b64e035a3b1bd664b0584a7b52fbf.jpeg)
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25435\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25435\350\256\262.md"
index c406c24a4..b14c84195 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25435\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25435\350\256\262.md"
@@ -26,7 +26,7 @@ SRAM 之所以被称为“静态”存储器,是因为只要处在通电状态
 
 6 个晶体管组成 SRAM 的一个比特
 
-在 CPU 里,通常会有 L1、L2、L3 这样三层高速缓存。每个 CPU 核心都有一块属于自己的 L1 高速缓存,通常分成 **指令缓存** 和 **数据缓存** ,分开存放 CPU 使用的指令和数据。
+在 CPU 里,通常会有 L1、L2、L3 这样三层高速缓存。每个 CPU 核心都有一块属于自己的 L1 高速缓存,通常分成 **指令缓存** 和 **数据缓存**,分开存放 CPU 使用的指令和数据。
 
 不知道你还记不记得我们在[第 22 讲](https://time.geekbang.org/column/article/100569)讲过的哈佛架构,这里的指令缓存和数据缓存,其实就是来自于哈佛架构。L1 的 Cache 往往就嵌在 CPU 核心的内部。
 
@@ -50,7 +50,7 @@ DRAM 被称为“动态”存储器,是因为 DRAM 需要靠不断地“刷新
 
 我们自己的书房和书桌(也就是内存)空间一般是有限的,没有办法放下所有书(也就是数据)。如果想要扩大空间的话,就相当于要多买几平方米的房子,成本就会很高。于是,想要放下更多的书,我们就要寻找更加廉价的解决方案。
 
-没错,我们想到了公共图书馆。对于内存来说, **SSD** (Solid-state drive 或 Solid-state disk,固态硬盘)、 **HDD** (Hard Disk Drive,硬盘)这些被称为 **硬盘** 的外部存储设备,就是公共图书馆。于是,我们就可以去家附近的图书馆借书了。图书馆有更多的空间(存储空间)和更多的书(数据)。
+没错,我们想到了公共图书馆。对于内存来说,**SSD** (Solid-state drive 或 Solid-state disk,固态硬盘)、 **HDD** (Hard Disk Drive,硬盘)这些被称为 **硬盘** 的外部存储设备,就是公共图书馆。于是,我们就可以去家附近的图书馆借书了。图书馆有更多的空间(存储空间)和更多的书(数据)。
 
 你应该也在自己的个人电脑上用过 SSD 硬盘。过去几年,SSD 这种基于 NAND 芯片的高速硬盘,价格已经大幅度下降。
 
@@ -62,7 +62,7 @@ DRAM 被称为“动态”存储器,是因为 DRAM 需要靠不断地“刷新
 
 存储器的层次关系图
 
-从 Cache、内存,到 SSD 和 HDD 硬盘,一台现代计算机中,就用上了所有这些存储器设备。其中,容量越小的设备速度越快,而且,CPU 并不是直接和每一种存储器设备打交道,而是每一种存储器设备,只和它相邻的存储设备打交道。比如,CPU Cache 是从内存里加载而来的,或者需要写回内存,并不会直接写回数据到硬盘,也不会直接从硬盘加载数据到 CPU Cache 中,而是先加载到内存,再从内存加载到 Cache 中。 **这样,各个存储器只和相邻的一层存储器打交道,并且随着一层层向下,存储器的容量逐层增大,访问速度逐层变慢,而单位存储成本也逐层下降,也就构成了我们日常所说的存储器层次结构。**
+从 Cache、内存,到 SSD 和 HDD 硬盘,一台现代计算机中,就用上了所有这些存储器设备。其中,容量越小的设备速度越快,而且,CPU 并不是直接和每一种存储器设备打交道,而是每一种存储器设备,只和它相邻的存储设备打交道。比如,CPU Cache 是从内存里加载而来的,或者需要写回内存,并不会直接写回数据到硬盘,也不会直接从硬盘加载数据到 CPU Cache 中,而是先加载到内存,再从内存加载到 Cache 中。**这样,各个存储器只和相邻的一层存储器打交道,并且随着一层层向下,存储器的容量逐层增大,访问速度逐层变慢,而单位存储成本也逐层下降,也就构成了我们日常所说的存储器层次结构。**
 
 ## 使用存储器的时候,该如何权衡价格和性能?
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25437\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25437\350\256\262.md"
index 82b0bb4c5..c60eaaf0a 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25437\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25437\350\256\262.md"
@@ -72,7 +72,7 @@ Cache 采用 mod 的方式,把内存块映射到对应的 CPU Cache 中
 
 CPU 在读取数据的时候,并不是要读取一整个 Block,而是读取一个他需要的整数。这样的数据,我们叫作 CPU 里的一个字(Word)。具体是哪个字,就用这个字在整个 Block 里面的位置来决定。这个位置,我们叫作偏移量(Offset)。
 
-总结一下, **一个内存的访问地址,最终包括高位代表的组标记、低位代表的索引,以及在对应的 Data Block 中定位对应字的位置偏移量。** ![img](assets/1313fe1e4eb3b5c949284c8b215af8d4.png)
+总结一下,**一个内存的访问地址,最终包括高位代表的组标记、低位代表的索引,以及在对应的 Data Block 中定位对应字的位置偏移量。**![img](assets/1313fe1e4eb3b5c949284c8b215af8d4.png)
 
 内存地址到 Cache Line 的关系
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25438\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25438\350\256\262.md"
index 2cf6a15b5..59c7cd77e 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25438\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25438\350\256\262.md"
@@ -130,7 +130,7 @@ Incrementing COUNTER to : 5
 Sleep 5ms, Got Change for COUNTER : 5
 ```
 
-这些有意思的现象,其实来自于我们的 Java 内存模型以及关键字 volatile 的含义。 **那 volatile 关键字究竟代表什么含义呢?它会确保我们对于这个变量的读取和写入,都一定会同步到主内存里,而不是从 Cache 里面读取。** 该怎么理解这个解释呢?我们通过刚才的例子来进行分析。
+这些有意思的现象,其实来自于我们的 Java 内存模型以及关键字 volatile 的含义。**那 volatile 关键字究竟代表什么含义呢?它会确保我们对于这个变量的读取和写入,都一定会同步到主内存里,而不是从 Cache 里面读取。** 该怎么理解这个解释呢?我们通过刚才的例子来进行分析。
 
 刚刚第一个使用了 volatile 关键字的例子里,因为所有数据的读和写都来自主内存。那么自然地,我们的 ChangeMaker 和 ChangeListener 之间,看到的 COUNTER 值就是一样的。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25439\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25439\350\256\262.md"
index ecf176b52..da20be0d5 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25439\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25439\350\256\262.md"
@@ -18,7 +18,7 @@
 
 比方说,iPhone 降价了,我们要把 iPhone 最新的价格更新到内存里。为了性能问题,它采用了上一讲我们说的写回策略,先把数据写入到 L2 Cache 里面,然后把 Cache Block 标记成脏的。这个时候,数据其实并没有被同步到 L3 Cache 或者主内存里。1 号核心希望在这个 Cache Block 要被交换出去的时候,数据才写入到主内存里。
 
-如果我们的 CPU 只有 1 号核心这一个 CPU 核,那这其实是没有问题的。不过,我们旁边还有一个 2 号核心呢!这个时候,2 号核心尝试从内存里面去读取 iPhone 的价格,结果读到的是一个错误的价格。这是因为,iPhone 的价格刚刚被 1 号核心更新过。但是这个更新的信息,只出现在 1 号核心的 L2 Cache 里,而没有出现在 2 号核心的 L2 Cache 或者主内存里面。 **这个问题,就是所谓的缓存一致性问题,1 号核心和 2 号核心的缓存,在这个时候是不一致的。** 为了解决这个缓存不一致的问题,我们就需要有一种机制,来同步两个不同核心里面的缓存数据。那这样的机制需要满足什么条件呢?我觉得能够做到下面两点就是合理的。
+如果我们的 CPU 只有 1 号核心这一个 CPU 核,那这其实是没有问题的。不过,我们旁边还有一个 2 号核心呢!这个时候,2 号核心尝试从内存里面去读取 iPhone 的价格,结果读到的是一个错误的价格。这是因为,iPhone 的价格刚刚被 1 号核心更新过。但是这个更新的信息,只出现在 1 号核心的 L2 Cache 里,而没有出现在 2 号核心的 L2 Cache 或者主内存里面。**这个问题,就是所谓的缓存一致性问题,1 号核心和 2 号核心的缓存,在这个时候是不一致的。** 为了解决这个缓存不一致的问题,我们就需要有一种机制,来同步两个不同核心里面的缓存数据。那这样的机制需要满足什么条件呢?我觉得能够做到下面两点就是合理的。
 
 第一点叫 **写传播** (Write Propagation)。写传播是说,在一个 CPU 核心里,我们的 Cache 数据更新,必须能够传播到其他的对应节点的 Cache Line 里。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25441\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25441\350\256\262.md"
index ad3fbb591..09187d078 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25441\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25441\350\256\262.md"
@@ -1,8 +1,8 @@
 # 41 理解内存(下):解析TLB和内存保护
 
-机器指令里面的内存地址都是虚拟内存地址。程序里面的每一个进程,都有一个属于自己的虚拟内存地址空间。我们可以通过地址转换来获得最终的实际物理地址。我们每一个指令都存放在内存里面,每一条数据都存放在内存里面。因此,“地址转换”是一个非常高频的动作,“地址转换”的性能就变得至关重要了。这就是我们今天要讲的 **第一个问题** ,也就是 **性能问题** 。
+机器指令里面的内存地址都是虚拟内存地址。程序里面的每一个进程,都有一个属于自己的虚拟内存地址空间。我们可以通过地址转换来获得最终的实际物理地址。我们每一个指令都存放在内存里面,每一条数据都存放在内存里面。因此,“地址转换”是一个非常高频的动作,“地址转换”的性能就变得至关重要了。这就是我们今天要讲的 **第一个问题**,也就是 **性能问题** 。
 
-因为我们的指令、数据都存放在内存里面,这里就会遇到我们今天要谈的 **第二个问题** ,也就是 **内存安全问题** 。如果被人修改了内存里面的内容,我们的 CPU 就可能会去执行我们计划之外的指令。这个指令可能是破坏我们服务器里面的数据,也可能是被人获取到服务器里面的敏感信息。
+因为我们的指令、数据都存放在内存里面,这里就会遇到我们今天要谈的 **第二个问题**,也就是 **内存安全问题** 。如果被人修改了内存里面的内容,我们的 CPU 就可能会去执行我们计划之外的指令。这个指令可能是破坏我们服务器里面的数据,也可能是被人获取到服务器里面的敏感信息。
 
 现代的 CPU 和操作系统,会通过什么样的方式来解决这两个问题呢?别着急,等讲完今天的内容,你就知道答案了。
 
@@ -20,7 +20,7 @@
 
 ![img](assets/ef754d9b2c816acff1dad63875ffea27.jpeg)
 
-于是,计算机工程师们专门在 CPU 里放了一块缓存芯片。这块缓存芯片我们称之为 **TLB** ,全称是 **地址变换高速缓冲** (Translation-Lookaside Buffer)。这块缓存存放了之前已经进行过地址转换的查询结果。这样,当同样的虚拟地址需要进行地址转换的时候,我们可以直接在 TLB 里面查询结果,而不需要多次访问内存来完成一次转换。
+于是,计算机工程师们专门在 CPU 里放了一块缓存芯片。这块缓存芯片我们称之为 **TLB**,全称是 **地址变换高速缓冲** (Translation-Lookaside Buffer)。这块缓存存放了之前已经进行过地址转换的查询结果。这样,当同样的虚拟地址需要进行地址转换的时候,我们可以直接在 TLB 里面查询结果,而不需要多次访问内存来完成一次转换。
 
 TLB 和我们前面讲的 CPU 的高速缓存类似,可以分成指令的 TLB 和数据的 TLB,也就是 **ITLB** 和 **DTLB** 。同样的,我们也可以根据大小对它进行分级,变成 L1、L2 这样多层的 TLB。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25445\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25445\350\256\262.md"
index e885b36e4..937ba4e5e 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25445\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25445\350\256\262.md"
@@ -24,7 +24,7 @@
 
 盘面本身通常是用的铝、玻璃或者陶瓷这样的材质做成的光滑盘片。然后,盘面上有一层磁性的涂层。我们的数据就存储在这个磁性的涂层上。盘面中间有一个受电机控制的转轴。这个转轴会控制我们的盘面去旋转。
 
-我们平时买硬盘的时候经常会听到一个指标,叫作这个硬盘的 **转速** 。我们的硬盘有 5400 转的、7200 转的,乃至 10000 转的。这个多少多少转,指的就是盘面中间电机控制的转轴的旋转速度,英文单位叫 **RPM** ,也就是 **每分钟的旋转圈数** (Rotations Per Minute)。所谓 7200 转,其实更准确地说是 7200RPM,指的就是一旦电脑开机供电之后,我们的硬盘就可以一直做到每分钟转上 7200 圈。如果折算到每一秒钟,就是 120 圈。
+我们平时买硬盘的时候经常会听到一个指标,叫作这个硬盘的 **转速** 。我们的硬盘有 5400 转的、7200 转的,乃至 10000 转的。这个多少多少转,指的就是盘面中间电机控制的转轴的旋转速度,英文单位叫 **RPM**,也就是 **每分钟的旋转圈数** (Rotations Per Minute)。所谓 7200 转,其实更准确地说是 7200RPM,指的就是一旦电脑开机供电之后,我们的硬盘就可以一直做到每分钟转上 7200 圈。如果折算到每一秒钟,就是 120 圈。
 
 说完了盘面,我们来看 **磁头** (Drive Head)。我们的数据并不能直接从盘面传输到总线上,而是通过磁头,从盘面上读取到,然后再通过电路信号传输给控制电路、接口,再到总线上的。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25446\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25446\350\256\262.md"
index cdf98779f..0c9373cb2 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25446\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25446\350\256\262.md"
@@ -24,7 +24,7 @@ SSD 没有像机械硬盘那样的寻道过程,所以它的随机读写都更
 
 ### SLC、MLC、TLC 和 QLC
 
-能够记录一个比特很容易理解。给电容里面充上电有电压的时候就是 1,给电容放电里面没有电就是 0。采用这样方式存储数据的 SSD 硬盘,我们一般称之为 **使用了 SLC 的颗粒** ,全称是 Single-Level Cell,也就是一个存储单元中只有一位数据。
+能够记录一个比特很容易理解。给电容里面充上电有电压的时候就是 1,给电容放电里面没有电就是 0。采用这样方式存储数据的 SSD 硬盘,我们一般称之为 **使用了 SLC 的颗粒**,全称是 Single-Level Cell,也就是一个存储单元中只有一位数据。
 
 ![img](assets/0698c240459faa11254932905675dba7.jpeg)
 
@@ -44,7 +44,7 @@ SSD 没有像机械硬盘那样的寻道过程,所以它的随机读写都更
 
 首先,自然和其他的 I/O 设备一样,它有对应的 **接口和控制电路** 。现在的 SSD 硬盘用的是 SATA 或者 PCI Express 接口。在控制电路里,有一个很重要的模块,叫作 **FTL** (Flash-Translation Layer),也就是 **闪存转换层** 。这个可以说是 SSD 硬盘的一个核心模块,SSD 硬盘性能的好坏,很大程度上也取决于 FTL 的算法好不好。现在容我卖个关子,我们晚一会儿仔细讲 FTL 的功能。
 
-接下来是 **实际 I/O 设备** ,它其实和机械硬盘很像。现在新的大容量 SSD 硬盘都是 3D 封装的了,也就是说,是由很多个裸片(Die)叠在一起的,就好像我们的机械硬盘把很多个盘面(Platter)叠放再一起一样,这样可以在同样的空间下放下更多的容量。
+接下来是 **实际 I/O 设备**,它其实和机械硬盘很像。现在新的大容量 SSD 硬盘都是 3D 封装的了,也就是说,是由很多个裸片(Die)叠在一起的,就好像我们的机械硬盘把很多个盘面(Platter)叠放再一起一样,这样可以在同样的空间下放下更多的容量。
 
 ![img](assets/0eee44535a925825b657bcac6afb72d3.jpeg)
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25448\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25448\350\256\262.md"
index b7acfd99c..6464bc042 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25448\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25448\350\256\262.md"
@@ -36,7 +36,7 @@
 
 - 首先是 **源地址的初始值以及传输时候的地址增减方式** 。 所谓源地址,就是数据要从哪里传输过来。如果我们要从内存里面写入数据到硬盘上,那么就是要读取的数据在内存里面的地址。如果是从硬盘读取数据到内存里,那就是硬盘的 I/O 接口的地址。 我们讲过总线的时候说过,I/O 的地址可以是一个内存地址,也可以是一个端口地址。而地址的增减方式就是说,数据是从大的地址向小的地址传输,还是从小的地址往大的地址传输。
 - 其次是 **目标地址初始值和传输时候的地址增减方式** 。目标地址自然就是和源地址对应的设备,也就是我们数据传输的目的地。
-- 第三个自然是 **要传输的数据长度** ,也就是我们一共要传输多少数据。
+- 第三个自然是 **要传输的数据长度**,也就是我们一共要传输多少数据。
 
 \\3. 设置完这些信息之后,DMAC 就会变成一个空闲的状态(Idle)。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25451\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25451\350\256\262.md"
index 222225201..9db51b534 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25451\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25451\350\256\262.md"
@@ -4,7 +4,7 @@
 
 把计算机这一系列组件组合起来,我们就拿到了一台完整的计算机。现在我们每天在用的个人 PC、智能手机,乃至云上的服务器,都是这样一台计算机。
 
-但是,一台计算机在数据中心里是不够的。因为如果只有一台计算机,我们会遇到三个核心问题。第一个核心问题,叫作 **垂直扩展和水平扩展的选择问题** ,第二问题叫作 **如何保持高可用性** (High Availability),第三个问题叫作 **一致性问题** (Consistency)。
+但是,一台计算机在数据中心里是不够的。因为如果只有一台计算机,我们会遇到三个核心问题。第一个核心问题,叫作 **垂直扩展和水平扩展的选择问题**,第二问题叫作 **如何保持高可用性** (High Availability),第三个问题叫作 **一致性问题** (Consistency)。
 
 围绕这三个问题,其实就是我们今天要讲的主题,分布式计算。当然,短短的一讲肯定讲不完这么大一个主题。分布式计算拿出来单开一门专栏也绰绰有余。我们今天这一讲的目标,是让你能理解水平扩展、高可用性这两个核心问题。对于分布式系统带来的一致性问题,我们会留在我们的实战篇里面,再用案例来为大家分析。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25452\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25452\350\256\262.md"
index b9f525464..fce8af762 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25452\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25452\350\256\262.md"
@@ -50,7 +50,7 @@ MongoDB 的设计听起来特别厉害,不需要预先数据 Schema,访问
 
 上面我们已经讲过 DMP 的 KV 数据库期望的应用场景和性能要求了,这里我们就来看一下 **数据管道** 和 **数据仓库** 的性能取舍。
 
-对于数据管道来说,我们需要的是高吞吐量,它的并发量虽然和 KV 数据库差不多,但是在响应时间上,要求就没有那么严格了,1-2 秒甚至再多几秒的延时都是可以接受的。而且,和 KV 数据库不太一样,数据管道的数据读写都是顺序读写,没有大量的随机读写的需求。 **数据仓库** 就更不一样了,数据仓库的数据读取的量要比管道大得多。管道的数据读取就是我们当时写入的数据,一天有 10TB 日志数据,管道只会写入 10TB。下游的数据仓库存放数据和实时数据模块读取的数据,再加上个 2 倍的 10TB,也就是 20TB 也就够了。
+对于数据管道来说,我们需要的是高吞吐量,它的并发量虽然和 KV 数据库差不多,但是在响应时间上,要求就没有那么严格了,1-2 秒甚至再多几秒的延时都是可以接受的。而且,和 KV 数据库不太一样,数据管道的数据读写都是顺序读写,没有大量的随机读写的需求。**数据仓库** 就更不一样了,数据仓库的数据读取的量要比管道大得多。管道的数据读取就是我们当时写入的数据,一天有 10TB 日志数据,管道只会写入 10TB。下游的数据仓库存放数据和实时数据模块读取的数据,再加上个 2 倍的 10TB,也就是 20TB 也就够了。
 
 但是,数据仓库的数据分析任务要读取的数据量就大多了。一方面,我们可能要分析一周、一个月乃至一个季度的数据。这一次分析要读取的数据可不是 10TB,而是 100TB 乃至 1PB。我们一天在数据仓库上跑的分析任务也不是 1 个,而是成千上万个,所以数据的读取量是巨大的。另一方面,我们存储在数据仓库里面的数据,也不像数据管道一样,存放几个小时、最多一天的数据,而是往往要存上 3 个月甚至是 1 年的数据。所以,我们需要的是 1PB 乃至 5PB 这样的存储空间。
 
diff --git "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25456\350\256\262.md" "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25456\350\256\262.md"
index 48877750e..6cc750187 100644
--- "a/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25456\350\256\262.md"
+++ "b/docs/Basic/\346\267\261\345\205\245\346\265\205\345\207\272\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\347\254\25456\350\256\262.md"
@@ -16,7 +16,7 @@ Facebook 的文化里面喜欢用各种小标语,其中有一条我很喜欢
 
 不知道正在读结束语的你,有没有在过去 5 个月里坚持学习这个专栏呢?有没有认真阅读我每一节后的推荐阅读呢?有没有尝试去做一做每一讲后面的思考题呢?
 
-如果你能够坚持下来,那首先要恭喜你,我相信能够学完的同学并不太多。如果你还没有学完,也不要紧,先跟着整个课程走一遍,有个大致印象。与其半途而费,不如先囫囵吞枣,硬着头皮看完再说。 **新的知识第一遍没有百分百看懂,而随着时间的推移,慢慢领悟成长了,这才是人生的常态。而我所见到的优秀的工程师大都会经历这样的成长过程。**
+如果你能够坚持下来,那首先要恭喜你,我相信能够学完的同学并不太多。如果你还没有学完,也不要紧,先跟着整个课程走一遍,有个大致印象。与其半途而费,不如先囫囵吞枣,硬着头皮看完再说。**新的知识第一遍没有百分百看懂,而随着时间的推移,慢慢领悟成长了,这才是人生的常态。而我所见到的优秀的工程师大都会经历这样的成长过程。**
 
 我们这个行业,经常喜欢把软件开发和建筑放在一起类比,所以才会有经典的《设计模式》这样的书。甚至有不少人干脆从《建筑的永恒之道》里面去寻找灵感。然而,建筑能够在历史上留下长久的刻印,但是软件却完全不同。无论多么完美的代码都会不断迭代,就好像新陈代谢一样。几年过去之后,最初那些代码的踪影早已经没有了。软件工程师放弃了追求永恒,而是投身在创作的快乐之中。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25400\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25400\350\256\262.md"
index 770809fb3..403564a90 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25400\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25400\350\256\262.md"
@@ -6,13 +6,13 @@
 
 ### 程序员为什么要注重数学?
 
-在\[《数据结构与算法》\]课程中,许多留言问题高频集中在:复杂度如何计算、某个代码优化是否降低了时间复杂度,或者是动态规划的状态转移方程问题,等等。这的确是在学习数据结构中遇到的困难,但剥离了外壳之后, **你会发现本质上都是数学问题。** 举个例子,对于一个有序数组中查找目标值的问题,应该采用二分查找算法。而且随着数组元素越来越多,二分查找相对全局遍历而言,性能上的优势会越来越明显。从数学视角来看,这是因为当 x 很大时,lnx \<\ “为了保证系统的稳定过渡, **并且** 保证在过渡期,各个使用方的需求正常迭代, **因此** 系统拟定共分为三期:过渡期、实验期、切换期。其中,过渡期采用某技术,保证数据系统打通;实验期通过 AB 实验,验证流程正确。”
+> “为了保证系统的稳定过渡,**并且** 保证在过渡期,各个使用方的需求正常迭代,**因此** 系统拟定共分为三期:过渡期、实验期、切换期。其中,过渡期采用某技术,保证数据系统打通;实验期通过 AB 实验,验证流程正确。”
 
 从字面来看,我们能脑补出说话者要做什么事情,以及做这些事情的目的和方法。但是,从逻辑的视角来看,上面一段话至少包含了以下几个问题:
 
@@ -63,7 +63,7 @@ final_price = discount * price
 
 显然,当 age 为 10 的时候,程序不会走任何一个策略分支,于是代码会出现错误。
 
-在解决类似的逻辑问题时, **一定要注意所有边界值的可能性** 。原则上,每个可行值(尤其是边界值)能且只能落在一个策略分支中。
+在解决类似的逻辑问题时,**一定要注意所有边界值的可能性** 。原则上,每个可行值(尤其是边界值)能且只能落在一个策略分支中。
 
 一个常用的分析方法就是画线法,如下图所示。画一根数轴,代表所有的可行值,再使用 if 语句分解问题,空心点表示开区间,实心点表示闭区间。
 
@@ -98,7 +98,7 @@ final_price = discount * price
 
 #### 2.从文氏图看“异或” **“异或”在 Python 语言中也记作A^B** 。命题 A 和命题 B 的真假不同时,则 AB 为真,否则为假。一个好的记忆方式是,异为 1,即 A 和 B 的真假性相异(不同),则结果为 1(为真)
 
-一个形象判断逻辑关系的方法是,便是 **文氏图** ,如下图所示,假设在文氏图中有两个命题 A 和 B,用椭圆形的区域表示一个命题为真的地方,而椭圆区域外则表示这个命题为假的区域。
+一个形象判断逻辑关系的方法是,便是 **文氏图**,如下图所示,假设在文氏图中有两个命题 A 和 B,用椭圆形的区域表示一个命题为真的地方,而椭圆区域外则表示这个命题为假的区域。
 
 ![图片4.png](assets/Ciqc1F-X_Y6AWx9PAACybmhz670044.png)
 
@@ -136,7 +136,7 @@ not A 文氏图
 
 AB 文氏图
 
-你会发现, **“A^B”的蓝色区域,就是上面“A or B”区域减去“A and B”区域,即A^B = (A or B) - (A and B)** 。
+你会发现,**“A^B”的蓝色区域,就是上面“A or B”区域减去“A and B”区域,即A^B = (A or B) - (A and B)** 。
 
 讲完命题的逻辑运算后,我们进入工作实践场景,向你讲解工作中的命题逻辑处理问题。
 
@@ -146,7 +146,7 @@ AB 文氏图
 
 #### 1.不遗漏原则
 
-当你在处理逻辑关系时,不管有多少个可能的 if 语句,哪怕你觉得你已经在 if 中穷举了所有的可能性,也尽可能用 **else** 进行一个兜底, **这是对代码潜在风险的规避** 。
+当你在处理逻辑关系时,不管有多少个可能的 if 语句,哪怕你觉得你已经在 if 中穷举了所有的可能性,也尽可能用 **else** 进行一个兜底,**这是对代码潜在风险的规避** 。
 
 例如,下面一段代码从结构来看,它虽然没有错误,但不利于解读、维护。
 
@@ -172,7 +172,7 @@ def fun(x):
 
 #### 2.不重复原则
 
-就说明 **每个可能的输入,只能进入唯一 一个策略分支** ,否则就有可能造成结果不受控制。这就说明,在代码开发中,尽可能少用多个 if 语句,而改用 elif 语句。
+就说明 **每个可能的输入,只能进入唯一 一个策略分支**,否则就有可能造成结果不受控制。这就说明,在代码开发中,尽可能少用多个 if 语句,而改用 elif 语句。
 
 > elif 是 else if 的合体,功能上他们二者完全可以互相替代,从逻辑的表达来看,elif 更像是对 if 的兜底。
 
@@ -218,11 +218,11 @@ def fun(x,y):
 
 “或者”,顾名思义,就是 A or B。例如,这个暑期,小琳打算去海南,否则小琳就打算去辽宁。经过逻辑运算后,得到这个暑假,小琳打算去海南或者辽宁(A or B)。
 
-你可以发现“漂亮”和“聪明”,“海南”和“辽宁”都是相互独立的。所以你在使用“而且”和“或者”沟通时,要注意命题 A 和命题 B 也最好是相互独立的, **也就是 A 与 B 应符合上文讲的 MECE 中的不重复原则。**
+你可以发现“漂亮”和“聪明”,“海南”和“辽宁”都是相互独立的。所以你在使用“而且”和“或者”沟通时,要注意命题 A 和命题 B 也最好是相互独立的,**也就是 A 与 B 应符合上文讲的 MECE 中的不重复原则。**
 
 下面我将通过三个反例说明问题:
 
-- 例1,小琳很聪明漂亮(A), **而且** 小琳很聪明(B)。
+- 例1,小琳很聪明漂亮(A),**而且** 小琳很聪明(B)。
 
 > 虽然语义上无误,读者也能理解,但从沟通的角度来看,这句话非常不妥帖。
 
@@ -230,7 +230,7 @@ def fun(x,y):
 
 > 此时,命题 A 显然包括了命题 B,与例1 如出一辙。
 
-- 例3,小琳是东北人(A), **或者** 小琳是北方人(B)。
+- 例3,小琳是东北人(A),**或者** 小琳是北方人(B)。
 
 > “北方”包含了“东北”,相互重复,在表达上绕了一个大弯,仅表达小琳是北方人。
 
@@ -270,7 +270,7 @@ def fun(x,y):
 
 > 虽然小琳学习成绩不好,但她一直很努力。
 
-在人们的潜意识中,成绩好的人一定是努力的人,这就是“ **因为** 她成绩好(A), **所以** 她是个努力的人(B)”的默认关系;反之,努力的人(B),学习成绩不一定很好(非 A),这就构成了转折,于是得到“ **虽然** 小琳成绩不好(非 A), **但是** 她很努力(B)”。
+在人们的潜意识中,成绩好的人一定是努力的人,这就是“ **因为** 她成绩好(A),**所以** 她是个努力的人(B)”的默认关系;反之,努力的人(B),学习成绩不一定很好(非 A),这就构成了转折,于是得到“ **虽然** 小琳成绩不好(非 A),**但是** 她很努力(B)”。
 
 在这一例子的逻辑过程中,你会发现 **“虽然(非A)…但是(B)…”** 这个关联词与 **“因为(A)...所以(B)...”** 刚好相反。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25403\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25403\350\256\262.md"
index 06fd2cdb8..e5416b0a2 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25403\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25403\350\256\262.md"
@@ -4,7 +4,7 @@
 
 那么怎么我们应该怎么“算账”呢?算完账后又应该如何决策呢?
 
-下面我会先讲一个我的 **算账定律** ,带你在麻将局中认识算账的关键三要素:系统、指标、兑换;然后再带你回到学生时代的“补习场景”,认识 **转化漏斗分析法** ,看到外部力量向指标的转化路径;最后,还是回归各位程序员的现实工作场景中,通过三个案例看到不同的转化路径,深入理解“投入”“转化”“产出”三者的关系。
+下面我会先讲一个我的 **算账定律**,带你在麻将局中认识算账的关键三要素:系统、指标、兑换;然后再带你回到学生时代的“补习场景”,认识 **转化漏斗分析法**,看到外部力量向指标的转化路径;最后,还是回归各位程序员的现实工作场景中,通过三个案例看到不同的转化路径,深入理解“投入”“转化”“产出”三者的关系。
 
 本课时的内容梗概如下图所示,可供你参考学习。
 
@@ -28,7 +28,7 @@
 
 ### 关键要素:系统、指标和兑换
 
-利用算账定律时,你需要把握好以下几个关键要素,分别是系统、指标和兑换。我们以大漂亮的学习成绩为例展开讨论。 **系统** ,就是一个个对象,它包括了你研究的目标对象,也包括了影响你研究目标的外部系统。对于大漂亮的学习而言,大漂亮就是一个系统,老师也是一个系统。 **指标** ,是评价系统运转结果的数学变量,即总账。例如,对于大漂亮的系统而言,指标包括但不限于考试成绩、生活愉悦度、日均自习时长、日均参加补习班的时长、日均娱乐时长等。 **兑换** ,是个动作,也是个结果,即你在用什么来换取什么。算账定律(算账版的能量守恒定律)说到,对于一个没有外部力量作用的系统,它的总账为零;反过来说,要想指标(总账)有提高,就需要借助外部力量,并把它兑换为指标的提高。
+利用算账定律时,你需要把握好以下几个关键要素,分别是系统、指标和兑换。我们以大漂亮的学习成绩为例展开讨论。**系统**,就是一个个对象,它包括了你研究的目标对象,也包括了影响你研究目标的外部系统。对于大漂亮的学习而言,大漂亮就是一个系统,老师也是一个系统。**指标**,是评价系统运转结果的数学变量,即总账。例如,对于大漂亮的系统而言,指标包括但不限于考试成绩、生活愉悦度、日均自习时长、日均参加补习班的时长、日均娱乐时长等。**兑换**,是个动作,也是个结果,即你在用什么来换取什么。算账定律(算账版的能量守恒定律)说到,对于一个没有外部力量作用的系统,它的总账为零;反过来说,要想指标(总账)有提高,就需要借助外部力量,并把它兑换为指标的提高。
 
 我们以大漂亮想要提升考试成绩为例,通过两种方式来看看系统情况:
 
@@ -50,7 +50,7 @@
 
 有了外部力量之后,就要开始分析外部力量作用在系统中的效率,这就需要 **转化漏斗分析法** 。
 
-- **转化** ,是一个动作,表示的是外部力量转化为指标提高的动作过程。
+- **转化**,是一个动作,表示的是外部力量转化为指标提高的动作过程。
 - 漏斗,代表了效率,即转化过程的 **投入** 和 **产出** 分别是多少。
 
 转化漏斗分析,能够辅助你看清转化路径,并寻找瓶颈予以突破。
@@ -103,7 +103,7 @@
 
 有一天,大聪明调整了点击率阈值,由 0.8 提高到了 0.9,其余影响因素都没有变,你来帮大聪明算算账,看他这样的动作对这个推荐系统是否有帮助。
 
-分析:先看一下我们的分析对象,也就是 **系统** 。此时,我们的系统可以是这个推荐系统。 **指标** 自然就是这个系统在用户身上产生的 PV。大聪明的动作是调整了点击率阈值,这很显然是个系统内部的改动,并没有外部力量注入这个系统。接下来我们对比分析一下两种不同阈值的转化路径。
+分析:先看一下我们的分析对象,也就是 **系统** 。此时,我们的系统可以是这个推荐系统。**指标** 自然就是这个系统在用户身上产生的 PV。大聪明的动作是调整了点击率阈值,这很显然是个系统内部的改动,并没有外部力量注入这个系统。接下来我们对比分析一下两种不同阈值的转化路径。
 
 ![图片8.png](assets/Ciqc1F-dG7OAa-eAAAF4X8VCPPc965.png)
 
@@ -114,7 +114,7 @@
 
 因此,曝光量由 500 人次降低到 470 人次。
 
-但运气比较好,提高了阈值之后, **由于文章匹配度更高,反而带来了更多的页面点击量** (430 人次)。对于这个推荐系统而言, **在没有外力的情况下,通过折损了曝光量,兑换到了 PV 的提高** 。
+但运气比较好,提高了阈值之后,**由于文章匹配度更高,反而带来了更多的页面点击量** (430 人次)。对于这个推荐系统而言,**在没有外力的情况下,通过折损了曝光量,兑换到了 PV 的提高** 。
 
 这个兑换是否合理,或者说是否划算,可能要综合公司业务的现状来考量。
 
@@ -126,7 +126,7 @@
 
 ![图片9.png](assets/Ciqc1F-dA_aAPKelAAFJs4gJXVg325.png)
 
-当 bug 修复之后,注册用户数、打开 App 用户数、文章的曝光量都没有改变。但是因为 **产品交互体验变好了** ,用户点击文章的 PV 由 420 提高到了 430。
+当 bug 修复之后,注册用户数、打开 App 用户数、文章的曝光量都没有改变。但是因为 **产品交互体验变好了**,用户点击文章的 PV 由 420 提高到了 430。
 
 对于这个推荐系统而言,在有外力的情况下,外力换来了 PV 的提高。这个功能迭代就是合理的、划算的,毫无疑问是有价值的。
 
@@ -138,7 +138,7 @@
 
 ![图片10.png](assets/Ciqc1F-dG52ASoJ7AAFBQB3WdUg225.png)
 
-当 CTR 模型准确率提高后,注册用户数、打开 App 用户数、文章的曝光量都没有改变。 **但是因为模型更准了,用户曝光的文章跟用户的兴趣更匹配** ,PV 由 430 提高到了 450。
+当 CTR 模型准确率提高后,注册用户数、打开 App 用户数、文章的曝光量都没有改变。**但是因为模型更准了,用户曝光的文章跟用户的兴趣更匹配**,PV 由 430 提高到了 450。
 
 对于这个推荐系统而言,在有外力的情况下,外力换来了 PV 的提高。这个功能迭代就是合理的、划算的,毫无疑问是有价值的。可见大迷糊,不仅不迷糊,反而很有业务能力。
 
@@ -146,7 +146,7 @@
 
 ### 小结
 
-这一课时,我们重点讲述了两方面的内容,一是算账定律,另一个是转化漏斗分析。 **算账定律** 告诉我们,在没有外力注入的情况下,总账为零。即通过牺牲系统内的某个指标,换取另一个指标的提高。 **转化漏斗分析** 是在有外力的情况下,根据外力向指标的转化路径,寻找转化效率和转化瓶颈的分析方法。
+这一课时,我们重点讲述了两方面的内容,一是算账定律,另一个是转化漏斗分析。**算账定律** 告诉我们,在没有外力注入的情况下,总账为零。即通过牺牲系统内的某个指标,换取另一个指标的提高。**转化漏斗分析** 是在有外力的情况下,根据外力向指标的转化路径,寻找转化效率和转化瓶颈的分析方法。
 
 这两方面的思维,非常利于我们看到生活中很多事情的本质。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25404\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25404\350\256\262.md"
index ceac791b3..92ae9e216 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25404\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25404\350\256\262.md"
@@ -24,9 +24,9 @@
 
 在做好了系统开发工作后,大漂亮在用户的维度上,上线了灰度实验。即一半用户被随机地划到了实验组,享受红包优惠;剩下的另一半用户,被划分到了对照组,不享受红包优惠。实验过后的所有数据记录如下表,围绕 GMV(营收额),帮大漂亮算一下这次双十一投放红包的 ROI 吧。 ![1.png](assets/CgqCHl-hPM6Ae76AAADKSy4HAUg281.png) 根据 ROI 的定义式很容易得到,ROI=(80万-65万)/10万=1.5。
 
-值得一提的是,如果回报定义为实际的营收额,ROI 一般不会小于 1。因为满减红包这样的投入,是不会被白白浪费的, **每一笔投入一定会转化为核销,并计算在营收额中** 。换句话说,你不花满满减金额,也不会核销掉这 10 元的红包。
+值得一提的是,如果回报定义为实际的营收额,ROI 一般不会小于 1。因为满减红包这样的投入,是不会被白白浪费的,**每一笔投入一定会转化为核销,并计算在营收额中** 。换句话说,你不花满满减金额,也不会核销掉这 10 元的红包。
 
-简单总结下,如果你负责某个“资源投入换产出”模式下的项目,例如投入补贴换营业额,那么业务指标上涨是显而易见的事情。毕竟对这个系统而言,是有资源投入的。此时,最关键的指标就是资源投入与业务产出的 **兑换效率** ,也就是资源的投资回报率 ROI。你的工作方向将会是,在算账体系下的 ROI 提高或优化的工作。
+简单总结下,如果你负责某个“资源投入换产出”模式下的项目,例如投入补贴换营业额,那么业务指标上涨是显而易见的事情。毕竟对这个系统而言,是有资源投入的。此时,最关键的指标就是资源投入与业务产出的 **兑换效率**,也就是资源的投资回报率 ROI。你的工作方向将会是,在算账体系下的 ROI 提高或优化的工作。
 
 讲完“钱”后,我们再讲下“人”吧。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25405\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25405\350\256\262.md"
index 908581fd6..9c7ec98d8 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25405\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25405\350\256\262.md"
@@ -61,7 +61,7 @@ def getSubsidy(k,b,m,c):
 
 ### 梯度下降法
 
-对于一个函数,它的 **导数的含义是斜率** ,这也是高中数学知识之一。例如某个函数 f(x),在某个点 x0 的 导数为 f'(x0) = k0。那么 k0,就是函数 f(x) 在 x0 处切线的斜率,如下图:
+对于一个函数,它的 **导数的含义是斜率**,这也是高中数学知识之一。例如某个函数 f(x),在某个点 x0 的 导数为 f'(x0) = k0。那么 k0,就是函数 f(x) 在 x0 处切线的斜率,如下图:
 
 ![2.png](assets/Ciqc1F-qVZKAS8jJAABcDXkImPQ292.png)
 
@@ -143,7 +143,7 @@ if __name__ == '__main__':
 
 ![图片7.png](assets/CgqCHl-lLlqAYAhhAAEvzMkgSOE792.png)
 
-从计算过程而言,两种方法都需要对目标函数进行求导(求梯度)。 **求导法** 的计算量虽然少,但它的难度就在于 **必须求解出导数为零的方程** 。这样,在无法写出解析解的场景下,求导法就无能为力了。 **梯度下降法** 需要进行多轮的迭代计算,显然计算量是更多的。但每一轮的计算仅仅是带入梯度函数求个梯度值,再更新下自变量。计算量虽然多,难度却很低。对于无法写出解析解的方程,它一样是适用的。
+从计算过程而言,两种方法都需要对目标函数进行求导(求梯度)。**求导法** 的计算量虽然少,但它的难度就在于 **必须求解出导数为零的方程** 。这样,在无法写出解析解的场景下,求导法就无能为力了。**梯度下降法** 需要进行多轮的迭代计算,显然计算量是更多的。但每一轮的计算仅仅是带入梯度函数求个梯度值,再更新下自变量。计算量虽然多,难度却很低。对于无法写出解析解的方程,它一样是适用的。
 
 相对求导法,梯度下降法显然是更厉害的算法。不过,它也有一些 **局限性** :
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25406\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25406\350\256\262.md"
index c2251fb76..2b9ab1b79 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25406\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25406\350\256\262.md"
@@ -44,9 +44,9 @@
 
 ![图片8.png](assets/Ciqc1F-qW36AaufQAACCC2N6w4Y661.png)
 
-需要注意的是,矩阵的乘法对维数有严格要求。 **第一个矩阵的列数与第二个的行数必须相等** 。所以, **矩阵的乘法并不满足交换律** 。 ![WechatIMG846.png](assets/CgqCHl-zPpaAdglhAACpwikeCDc307.png)
+需要注意的是,矩阵的乘法对维数有严格要求。**第一个矩阵的列数与第二个的行数必须相等** 。所以,**矩阵的乘法并不满足交换律** 。 ![WechatIMG846.png](assets/CgqCHl-zPpaAdglhAACpwikeCDc307.png)
 
-- **哈达玛积** 哈达玛积在对海量数据预处理中会被高频使用,它的计算方式相对简单很多。哈达玛积 **要求两个矩阵的行列维数完全相同** ,计算方式是对应位置元素的乘积,例如:
+- **哈达玛积** 哈达玛积在对海量数据预处理中会被高频使用,它的计算方式相对简单很多。哈达玛积 **要求两个矩阵的行列维数完全相同**,计算方式是对应位置元素的乘积,例如:
 
 ![图片4.png](assets/CgqCHl-qW5CASFf5AAB7d4ZJSIo496.png)
 
@@ -62,7 +62,7 @@
 
 ### 向量的求导
 
-前面说过,在对复杂业务问题进行形式化定义后,再求解最优值的过程中,不管是用求导法还是梯度下降法,都是逃不开要对目标函数进行求导的。复杂业务环境中, **自变量肯定不止一个,这就需要我们在向量或矩阵的环境中,掌握求导的运算。** 实际工作中,矩阵的求导用得非常少,掌握向量的求导就足够了。因此,我们重点学习“向量关于向量”的导数计算。
+前面说过,在对复杂业务问题进行形式化定义后,再求解最优值的过程中,不管是用求导法还是梯度下降法,都是逃不开要对目标函数进行求导的。复杂业务环境中,**自变量肯定不止一个,这就需要我们在向量或矩阵的环境中,掌握求导的运算。** 实际工作中,矩阵的求导用得非常少,掌握向量的求导就足够了。因此,我们重点学习“向量关于向量”的导数计算。
 
 我们先给出向量关于向量的导数的计算方法。向量 _ **y** _ 关于向量 _ **w** _ 的求导结果是个矩阵,标记为\_ **A** \_。矩阵 _ **A** _ 中第 i 行第 j 列的元素 aij,为向量 _ **y** _ 中第 i 个元素关于向量 _ **w** _ 中第 j 个元素的导数。例如,如果向量 _ **w** _ 的维数为 n×1,向量 _ **y** _ 的维数是 m×1,则 y 关于 w 的求导结果矩阵维数就是 n×m,其中第 i 行第 j 列的元素为:
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25407\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25407\350\256\262.md"
index fb9e7a00f..692fc27b7 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25407\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25407\350\256\262.md"
@@ -28,17 +28,17 @@ y =_ **k** _T_ **x** _+b。
 
 我们先前总结过解决问题的通用方法,包括两步:首先要进行形式化定义,接着对形式化定义的问题进行最优化求解。
 
-形式化定义,是要用数学语言来描述清楚问题的目标是什么。我们前面分析到, **问题的目标是尽可能把数据样本点“串”在一起** 。那么如何用数学语言来描述呢?
+形式化定义,是要用数学语言来描述清楚问题的目标是什么。我们前面分析到,**问题的目标是尽可能把数据样本点“串”在一起** 。那么如何用数学语言来描述呢?
 
 在线性回归中,通常用平方误差来衡量拟合的效果。平方误差的定义是,真实值和预测值之差的平方,即 (ŷ-y)2。值得一提的是,我们采用 ŷ 来代表真实值,用 y 来代表回归拟合的预测值。
 
 有了这些背景知识后,我们回到大漂亮的问题。大漂亮想用一个线性函数去拟合购买率和折扣率,不妨用 y 表示购买率,x 表示折扣率,那么线性函数的表达式就是 y = kx + b。
 
-此时,大漂亮面对的是一元线性回归问题,要做的事情就是求解出 k 和 b 的值。假设大漂亮已经有了 k 和 b,那么就能根据输入的 x,拟合出 y 的值了, **而线性回归的目标是尽可能让“串”在一起的平方误差最小** 。因此,平方误差函数在这里的形式就是:
+此时,大漂亮面对的是一元线性回归问题,要做的事情就是求解出 k 和 b 的值。假设大漂亮已经有了 k 和 b,那么就能根据输入的 x,拟合出 y 的值了,**而线性回归的目标是尽可能让“串”在一起的平方误差最小** 。因此,平方误差函数在这里的形式就是:
 
 ![图片1.png](assets/CgqCHl-uZk2AHa4KAABgsVJP-X4148.png)
 
-其中求和的 1 到 7,表示的是大漂亮获得的数据集中 7 个样本。公式的含义就是,每个样本的预测值和真实值的平方误差,再求和。大漂亮遇到的问题定性描述是,通过线性回归,让数据尽可能“串”在一起。 **其形式化定义,就是找到能让平方误差函数最小的 k 和 b 的值。** ### 线性回归的求解方法
+其中求和的 1 到 7,表示的是大漂亮获得的数据集中 7 个样本。公式的含义就是,每个样本的预测值和真实值的平方误差,再求和。大漂亮遇到的问题定性描述是,通过线性回归,让数据尽可能“串”在一起。**其形式化定义,就是找到能让平方误差函数最小的 k 和 b 的值。** ### 线性回归的求解方法
 
 有了形式化定义的问题之后,就是求解问题的最优化过程。根据形式化定义,你会发现,这不就是个求解最值的问题嘛,我们已经学过了很多求解方法了。是的,绝大多数的问题,只要形式化定义清楚之后,就是个求解最值的过程。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25408\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25408\350\256\262.md"
index 1a0def8a9..0933e2b6f 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25408\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25408\350\256\262.md"
@@ -96,7 +96,7 @@ print obj/10000
 
 那么,如果我们无法得到独立的事件,而都是耦合在一起的事件,又该如何计算概率呢?这就需要用到条件概率的知识了。
 
-**条件概率** ,指事件 A 在另外一个事件 B 已经发生条件下的发生概率,记作 P(A|B),读作“B 条件下 A 的概率”。条件概率的定义式为 P(A|B) = P(AB) / P(B),将其变换一下就是 P(AB) = P(A|B) × P(B)。
+**条件概率**,指事件 A 在另外一个事件 B 已经发生条件下的发生概率,记作 P(A|B),读作“B 条件下 A 的概率”。条件概率的定义式为 P(A|B) = P(AB) / P(B),将其变换一下就是 P(AB) = P(A|B) × P(B)。
 
 > 条件概率的特殊性,在于事件 A 和事件 B 有千丝万缕的联系。如果二者为毫无关联的独立事件的话,事件 A 的发生则与 B 毫无关系,则有 P(A|B) = P(A)。
 
@@ -143,7 +143,7 @@ print 1.0*fenzi/fenmu
 
 试着分析一下,这里的事件之间的概率关系,以及投放红包到底产生了怎样的概率刺激效果?
 
-可以想象,用户购买某个商品的动作顺序是,点击商品详情页,再付款购买。很显然“点击详情页”和“付款购买”并不是独立的事件,原因在于不点击详情页是无法完成购买动作的,二者存在先后关系。因此 **P(点击并购买) = P(购买|点击) × P(点击)** ,这个公式对所有的用户都生效。
+可以想象,用户购买某个商品的动作顺序是,点击商品详情页,再付款购买。很显然“点击详情页”和“付款购买”并不是独立的事件,原因在于不点击详情页是无法完成购买动作的,二者存在先后关系。因此 **P(点击并购买) = P(购买|点击) × P(点击)**,这个公式对所有的用户都生效。
 
 接下来,大漂亮的红包投放条件是,用户在商品的详情页停留了 1 分钟以上。此时,产生购买行为的用户就有两部分,分别是使用红包的购买用户和未使用红包的购买用户。很显然,使用红包和不使用红包是两个并行的逻辑,可以采用加法原理进行概率计算,因此有 **P(点击并购买) = P(点击并使用红包购买) + P(点击并未使用红包购买)** 。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25409\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25409\350\256\262.md"
index c1106cece..110f41d37 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25409\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25409\350\256\262.md"
@@ -26,9 +26,9 @@
 
 极大似然估计的流程可以分为 3 步,分别是似然、极大和估计。
 
-- 第一步 **似然** ,即根据观测的样本建立似然函数,也是概率函数或可能性函数。 这个步骤的数学表达如下:假设观测的样本或集合为 D,待估计的参数为 θ。则观察到样本集合的概率,就是在参数 θ 条件下,D 发生的条件概率 P(D|θ)。这就是似然函数,也是极大似然估计中最难的一步。
-- 第二步 **极大** ,也就是求解似然函数的极大值。 你可以通过求导法、梯度下降法等方式求解。这个步骤的数学表达就简单许多,即 max P(D|θ)。
-- 第三步 **估计** ,利用求解出的极大值,对未知参数进行估计。 ![图片1.png](assets/CgqCHl-3jnCAQHn-AADNrmqedmI922.png)
+- 第一步 **似然**,即根据观测的样本建立似然函数,也是概率函数或可能性函数。 这个步骤的数学表达如下:假设观测的样本或集合为 D,待估计的参数为 θ。则观察到样本集合的概率,就是在参数 θ 条件下,D 发生的条件概率 P(D|θ)。这就是似然函数,也是极大似然估计中最难的一步。
+- 第二步 **极大**,也就是求解似然函数的极大值。 你可以通过求导法、梯度下降法等方式求解。这个步骤的数学表达就简单许多,即 max P(D|θ)。
+- 第三步 **估计**,利用求解出的极大值,对未知参数进行估计。 ![图片1.png](assets/CgqCHl-3jnCAQHn-AADNrmqedmI922.png)
 
 利用这 3 步就完成了极大似然估计的整个流程。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25410\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25410\350\256\262.md"
index 1ed4df5b2..f26f16986 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25410\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25410\350\256\262.md"
@@ -2,7 +2,7 @@
 
 你好,欢迎来到第 10 课时——信息熵:事件的不确定性如何计算?
 
-从加乘法则开始,我们基于事情的不确定性出发,尝试计算事情发生的可能性。然而, **对于事件与事件之间的不确定性如何相比和衡量,单独靠概率就无法说清楚了** 。我说的这句话是什么意思呢?下面我举个例子来说明。
+从加乘法则开始,我们基于事情的不确定性出发,尝试计算事情发生的可能性。然而,**对于事件与事件之间的不确定性如何相比和衡量,单独靠概率就无法说清楚了** 。我说的这句话是什么意思呢?下面我举个例子来说明。
 
 假设有两场足球赛,也就是两个事件。第一场足球赛,对阵的双方是老挝队和巴西队,标记为事件 A;第二场足球赛,对阵的双方是阿根廷队和葡萄牙队,标记为事件 B。显然,在比赛开始前,这两个事件的比赛结果都具备一定的不确定性。人们也会根据历史数据,分别计算两场足球赛结果的概率。
 
@@ -94,7 +94,7 @@ if __name__ == '__main__':
 这个结论的证明会很复杂,感兴趣的同学可以自己试着推导下。我们借助刚刚的足球比赛的例子,来验证这个结论。先通过这个表格,利用“08 | 加乘法则:如何计算复杂事件发生的概率?”中的加乘法则,分别计算出巴西队获胜和不胜的概率: ![图片5.png](assets/Ciqc1F-81gqARFRnAACRW_tlOqQ382.png) 接下来,我们将上表算出的巴西队获胜和不胜的概率,代入刚刚已经开发好的代码,计算出比赛结果的熵。执行 print entropy(0.75,0.25),结果如下图,即 H(Y) = 0.8113。 ![图片6.png](assets/CgqCHl-81h6AFDzcAAHlH6OsqoM644.png) 而刚刚我们已经计算了条件熵为 H(Y|X) = 0.7200。可见,由于掌握了球员健康或患病这个条件,让比赛结果的不确定性由 0.8113 降低为 0.7200。这个差值,就来自于外部条件的引入,带来事物不确定性的下降,这就称之为 **信息增益** 。
 
 - 信息增益,顾名思义就是信息量增加了多少;换句话说,也是不确定性降低了多少。标记为 g(X,Y),定义式为 g(X,Y) = H(Y) - H(Y|X)。
-- 有时候,除了看这个差值以外,还会同时观察降幅的比值。此时为 **信息增益率** ,定义式为 gr(X,Y) = g(X,Y) / H(Y)。
+- 有时候,除了看这个差值以外,还会同时观察降幅的比值。此时为 **信息增益率**,定义式为 gr(X,Y) = g(X,Y) / H(Y)。
 
 回到刚刚足球比赛的例子,它的信息增益为 g(X,Y) = H(Y) - H(Y|X) = 0.8113 - 0.7200 = 0.0913;信息增益率为 gr(X,Y) = g(X,Y) / H(Y) = 0.0913 / 0.8113 = 11.25%
 
@@ -121,7 +121,7 @@ if __name__ == '__main__':
 
 我们说过,熵的由高到低,就是信息量的由高到低,也就是不确定性的由高到低。也就是,熵越低的事情,越接近废话,也就越有把握。那么我们在调节资源投入的时候,就应该尽量避免在熵低的事情上的投入;相反,应该投入到熵比较高的事情上。
 
-所以,当明确了要在熵高的事情上投入资源后,就要想办法让这个事情的熵逐步降低,让它的不确定性降低,你可以理解为解决问题的过程就是让熵减少的过程。而要让熵减少,就需要不断地有外部条件输入。通过外部条件输入,获得 **信息增益** ,来不断降低熵。
+所以,当明确了要在熵高的事情上投入资源后,就要想办法让这个事情的熵逐步降低,让它的不确定性降低,你可以理解为解决问题的过程就是让熵减少的过程。而要让熵减少,就需要不断地有外部条件输入。通过外部条件输入,获得 **信息增益**,来不断降低熵。
 
 上面的描述很抽象,我们用一个具体的例子来说明,假设大漂亮是某公司的总监。在下个月,有两个同等重要的技术方向,分别标记为 A 和 B。按照现在的发展趋势来看,A 方向在下个月成功解决的概率为 0.9,无法解决的概率为 0.1;B 方向在下个月成功解决的概率为 0.6,无法解决的概率为 0.4。
 
@@ -133,11 +133,11 @@ if __name__ == '__main__':
 
 我们再回顾一下“概率”和“熵”的区别。对于一个事件而言,它可能有很多个结果。例如,“老挝队和巴西队的足球比赛”这是一个事件,而这个事件有很多可能的结果,例如巴西队胜、巴西队不胜。
 
-- **概率** ,描述的是某个事件的结果,发生的可能性。有时候,在不刻意强调区分“事件”和“事件结果”的时候,也被简称为事件发生的可能性。
-- **熵** ,描述的则是事件背后蕴含的信息量和不确定性。
+- **概率**,描述的是某个事件的结果,发生的可能性。有时候,在不刻意强调区分“事件”和“事件结果”的时候,也被简称为事件发生的可能性。
+- **熵**,描述的则是事件背后蕴含的信息量和不确定性。
 
 你也可以理解为,“可能性”探讨的是事件某个结果的发生;而“不确定性”探讨的是一个事情下的不同结果发生的情况。
 
-最后总结一下这一讲的要点。 **熵** 是描述事物不确定性的量。在定量描述了事物的不确定性之后,可以辅助人们做出更加合理的资源分配决策。 **条件熵,是指引入了某个外部条件后的熵;条件引入,必然会带来信息增益,也就是会让熵变小,这个变小的幅度可以用信息增益** 或 **信息增益率** 来描述。
+最后总结一下这一讲的要点。**熵** 是描述事物不确定性的量。在定量描述了事物的不确定性之后,可以辅助人们做出更加合理的资源分配决策。**条件熵,是指引入了某个外部条件后的熵;条件引入,必然会带来信息增益,也就是会让熵变小,这个变小的幅度可以用信息增益** 或 **信息增益率** 来描述。
 
 这四个关键概念的定义式如下,你可以通过定义式去反复领悟它们之间的区别和意义。 ![图片8.png](assets/Ciqc1F-81lCASNNaAAB7GUMjSxE886.png) 我们给一个练习题,假设韩国和日本要踢一场友谊赛,比赛当天天气存在一定的不确定性。已知,比赛当天有 0.3 的概率会下雨。如果下雨,韩国队获胜的概率可以达到 0.7;如果晴天,则韩国队获胜的概率只有 0.3。假设 Y 为比赛结果,X 为天气状况,试着求条件熵 H(Y|X)。
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25411\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25411\350\256\262.md"
index b418bb277..1eddc4365 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25411\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25411\350\256\262.md"
@@ -6,7 +6,7 @@
 
 ### 灰度实验
 
-在实际的工作中,通常需要进行灰度实验来验证某个新系统相对于旧系统的收益。 **灰** 是介于黑和白之间的颜色,可以理解为是个中间态。灰度实验,也可以称作为 AB 实验、灰度发布,名称虽然不同,但本质上是没有什么区别的。
+在实际的工作中,通常需要进行灰度实验来验证某个新系统相对于旧系统的收益。**灰** 是介于黑和白之间的颜色,可以理解为是个中间态。灰度实验,也可以称作为 AB 实验、灰度发布,名称虽然不同,但本质上是没有什么区别的。
 
 AB 实验的理念,是构造一个平行世界,去观察两个世界的不同。具体来说就是,把线上的流量随机地拆分为具有同样分布的实验组和对照组,然后将新旧两个系统分别作用在这两组流量上,去观察业务指标的变化。
 
@@ -111,7 +111,7 @@ print len(con)
 
 接下来,如何衡量实验效果的好坏呢?
 
-> 一个误区,是实验组点击量为 9000 小于对照组的 16000。于是得到结论,新系统效果不如老系统。这很显然是不对的,因为实验组只有 290 人,而对照组有 710 人。流量的不平衡天然就会造成点击量的不同。 **因此 AB 实验的指标中有这样一个原则:“量”指标一定要对流量进行归一化,得到“率”指标后,才可以对比。**
+> 一个误区,是实验组点击量为 9000 小于对照组的 16000。于是得到结论,新系统效果不如老系统。这很显然是不对的,因为实验组只有 290 人,而对照组有 710 人。流量的不平衡天然就会造成点击量的不同。**因此 AB 实验的指标中有这样一个原则:“量”指标一定要对流量进行归一化,得到“率”指标后,才可以对比。**
 
 基于这个原则,我们可以重新设计如下几个实验评估指标:
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25412\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25412\350\256\262.md"
index 9c513d533..152fff0e7 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25412\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25412\350\256\262.md"
@@ -20,7 +20,7 @@
 
 中心极限定理是统计学中的圣经级定理,它的内容为:假设从均值为 μ,方差为 σ2 的任意一个总体中,抽取样本量为 n 的样本,当 n 充分大时,样本均值x̅的分布近似服从均值为 μ、方差为 σ2/n 的正态分布。通常认为 n≥30 为大样本。
 
-**中心极限定理的厉害之处,在于它实现了任意一个分布向正态分布的转换** ,如下图:
+**中心极限定理的厉害之处,在于它实现了任意一个分布向正态分布的转换**,如下图:
 
 > 至于为什么实现了正态分布就很厉害,下文会为你讲解。
 
@@ -82,7 +82,7 @@ print var
 
 这就需要去运用中心极限定理了,一旦有了实验组、对照组两个总体的均值和方差,就可以利用一些检验手段,来计算显著性了。
 
-所以接下来,我们便需要将中心极限定理应用在 AB 实验中,去 **论证实验是不是随机得到的** ,这就需要用到统计学“均值假设检验“的知识了。
+所以接下来,我们便需要将中心极限定理应用在 AB 实验中,去 **论证实验是不是随机得到的**,这就需要用到统计学“均值假设检验“的知识了。
 
 ### 均值假设检验 **均值假设检验,就是要验证通过 AB 实验得到的某个均值是否存在显著的差异。** 这里显著的含义是,结果是真实、客观的规律,并非偶然得到
 
@@ -103,7 +103,7 @@ print var
 
 > 例如,利用第 2 行、第 3 列的数值,可以计算出 Z 为 0.12 的显著性水平(Z 统计量分布表中绿框部分)。
 
-通常,人们选择表中 **0.9750** 作为临界值(图中上面的红色框);也就是说, **Z 统计量的临界值是 1.96** 。人们常常根据 Z 统计量的绝对值与 1.96 的关系来判断是否显著,即绝对值大于 1.96 则认为显著,反之亦然。
+通常,人们选择表中 **0.9750** 作为临界值(图中上面的红色框);也就是说,**Z 统计量的临界值是 1.96** 。人们常常根据 Z 统计量的绝对值与 1.96 的关系来判断是否显著,即绝对值大于 1.96 则认为显著,反之亦然。
 
 > 之所以选择 0.9750,是因为此时的显著性为 0.05 时,即观测结果是偶然发生的概率为 5%。这里 0.05 计算而来的公式是 (1-0.9750)×2 = 0.05,这个公式背后的含义涉及正态分布的累积概率的计算,在此我们不展开说明,感兴趣的同学可以自己查阅相关的统计学教材。
 
@@ -119,7 +119,7 @@ print var
 
 ### 利用“均值假设检验”论证实验结果是否为偶然得到
 
-刚刚讲解的 **“均值假设检验”可以论证“两个均值”的偏差是否为偶然得到的** 。我们将它对应到 AB 实验中,会发现其中一个“均值”是总体的均值,就像是 AB 实验中的对照组;另一个“均值”是抽样的均值,就像是 AB 实验中的实验组。 **所以有了“均值假设检验”的理论基础,你就可以论证并回答,实验组相对对照组的差异是否为偶然得到的。**
+刚刚讲解的 **“均值假设检验”可以论证“两个均值”的偏差是否为偶然得到的** 。我们将它对应到 AB 实验中,会发现其中一个“均值”是总体的均值,就像是 AB 实验中的对照组;另一个“均值”是抽样的均值,就像是 AB 实验中的实验组。**所以有了“均值假设检验”的理论基础,你就可以论证并回答,实验组相对对照组的差异是否为偶然得到的。**
 
 我们继续以大漂亮的推荐系统 v2.0 为例。下面是先前的实验观测数据,但很容易被人质疑是否为偶然得到。接下来,我们就来用均值假设检验,来论证实验结果是否显著。我们以人均点击量为例展开论述。 ![图片8.png](assets/CgqCHl_GGqKAWGFCAAIqdcBeYMY198.png) 围绕刚刚讲过的 Z 统计量的公式,我们先需要帮助大漂亮找到这些参数的值。 ![图片5.png](assets/Ciqc1F_GGwGAUXmSAAB8xqFr90c154.png) 从公式出发,光有个实验组人均点击量为 31,对照组人均点击量为 23,肯定是不够的,至少是需要构建 n 个人均点击量才行。因此,我们考虑把为期一周的实验,切分为每一天来统计 7 个指标。
 
@@ -137,8 +137,8 @@ print var
 
 这一讲,我们学习了统计学的知识“中心极限定理”和“均值假设检验”,并将它应用到工作中,用来论证 AB 实验的结果是否为偶然得到。
 
-我们了解到, **中心极限定理** 构建了样本和总体之间的桥梁,让我们找到抽样的统计量和总体的统计量之间的关系。
+我们了解到,**中心极限定理** 构建了样本和总体之间的桥梁,让我们找到抽样的统计量和总体的统计量之间的关系。
 
-然后“ **均值假设检验** ”又可以论证“两个均值”的偏差是否为偶然得到。我们将其对应到 AB 实验中,会发现其中一个“均值”是总体的均值,就像是 AB 实验中的对照组;另一个“均值”是抽样的均值,就像是 AB 实验中的实验组。 **所以便可以论证并回答,实验组相对对照组的差异是否为偶然得到的** 。这时的关键步骤,就是根据公式来计算 Z 统计量的值,并判断。
+然后“ **均值假设检验** ”又可以论证“两个均值”的偏差是否为偶然得到。我们将其对应到 AB 实验中,会发现其中一个“均值”是总体的均值,就像是 AB 实验中的对照组;另一个“均值”是抽样的均值,就像是 AB 实验中的实验组。**所以便可以论证并回答,实验组相对对照组的差异是否为偶然得到的** 。这时的关键步骤,就是根据公式来计算 Z 统计量的值,并判断。
 
 最后,我们给出一个练习题:利用下面的数据,计算 CTR 的差异是否显著。 ![图片13.png](assets/Ciqc1F_GG02ABKmuAAJ9_ZkgZnM820.png)
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25413\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25413\350\256\262.md"
index ee8628035..8c525a9b7 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25413\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25413\350\256\262.md"
@@ -6,7 +6,7 @@
 
 ### 程序的时间损耗
 
-**程序** 就是计算机执行运算动作的指令, **运算** 就是对数据进行的处理。
+**程序** 就是计算机执行运算动作的指令,**运算** 就是对数据进行的处理。
 
 例如,1+2 这样的加法运算,就是对两个数据 1 和 2 执行加法的处理。同样地,加法运算还可以针对更多的数据,比如 1+2+3+...+50,这就是对 1~50 这 50 个数据,执行加法运算的处理。
 
@@ -46,7 +46,7 @@ print t2 - t1
 
 > 在实际工作中,通常会重点关注时间方面的复杂度,也叫时间复杂度。这一讲,我们为了简便行文,就把时间复杂度简称为复杂度。
 
-从本质上来看, **复杂度描述的是程序时间损耗和数据总量之间的变化关系** 。
+从本质上来看,**复杂度描述的是程序时间损耗和数据总量之间的变化关系** 。
 
 【例 1】我们先举一个例子说明,看下面这段代码:
 
@@ -129,7 +129,7 @@ print result
 
 - 而第 14~15 行,根据前面所学是 O(n) 的时间复杂度。所以,整个代码的时间复杂度就是 O(n2+n)。仍然可以继续使用刚刚平方公式的化简方法,得到最终的时间复杂度是 O(n2)。
 
-从这个例子,我们可以发现, **多项式级的复杂度相加时,可以选择高者作为结果。** 例如,O(n2+n) 的时间复杂度,可以直接写为 O(n2)。
+从这个例子,我们可以发现,**多项式级的复杂度相加时,可以选择高者作为结果。** 例如,O(n2+n) 的时间复杂度,可以直接写为 O(n2)。
 
 复杂度的性质都来自数学的推导,与此同时,复杂度的计算还与程序的结构有着密切关系。通常而言,一个 **顺序结构** 或 **选择结构** 的代码的执行时间与数据量无关,复杂度就是 O(1);而对于 **循环结构** 而言,如果循环的次数与输入数据量的多少有关,就会产生复杂度了。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25414\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25414\350\256\262.md"
index 6a02d7c62..df804e4a1 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25414\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25414\350\256\262.md"
@@ -33,7 +33,7 @@
 最简单常见的数学归纳法是,用来证明当 n 等于任意一个自然数时某个命题成立,其证明步骤可以分下面两步:
 
 - 第一,当 n=1 时,命题成立;
-- 第二,假设对于任意一个数字 i 命题成立,可以推导出在对于 **i+1** ,命题依然成立。
+- 第二,假设对于任意一个数字 i 命题成立,可以推导出在对于 **i+1**,命题依然成立。
 
 只要这两个条件都满足,命题就得证。
 
@@ -251,7 +251,7 @@ while(s2)
 
 根据 while 语句的执行顺序可知,这段代码的执行顺序为 s1、(s2,s4,s3)、(s2,s4,s3)...(s2,s4,s3)、s2,因此可以得知,两段代码的功能结果完全一致。
 
-而如果非要采用 **do while 循环** ,可以按照如下方式实现:
+而如果非要采用 **do while 循环**,可以按照如下方式实现:
 
 ```plaintext
 s1;
@@ -269,14 +269,14 @@ do {
 
 以上面的代码为例,一旦第 3 行的条件判断通过,则需要执行 break 语句。break 语句会帮助程序跳出当前循环,这样程序就会从第 4 行跳转至第 10 行继续执行。基于 break 语句,再根据 do while 语句的执行顺序可知,这段代码的执行顺序为 s1、(s2,s4,s3)、(s2,s4,s3)...(s2,s4,s3)、s2,因此可以得知两段代码的功能结果完全一致。
 
-这里要给大家提个醒:如果是在技术面试时, **千万不要说某某功能的开发,只能用 for 循环、while 循环或 do while 循环,这一定是错的** 。因为,功能上这三种循环的实现是完全可以实现互换的;只不过,三者在代码美观上可能是有所区别。
+这里要给大家提个醒:如果是在技术面试时,**千万不要说某某功能的开发,只能用 for 循环、while 循环或 do while 循环,这一定是错的** 。因为,功能上这三种循环的实现是完全可以实现互换的;只不过,三者在代码美观上可能是有所区别。
 
 ### **数学归纳法与循环结构** 数学归纳法和循环结构有很多相似之处,它们都是 **从某个起点开始,不断地重复执行某个或某组相似的动作集合**
 
 不过,二者也有一些区别:
 
-- 数学归纳法 **不关注归纳过程的结束** ,它就是用一种重复动作,由有穷尽朝着无穷尽的方向去前进;
-- 而循环结构作为一种程序开发逻辑,则 **必须要关注循环过程的结束** ,否则就会造成系统陷入死循环或死机。
+- 数学归纳法 **不关注归纳过程的结束**,它就是用一种重复动作,由有穷尽朝着无穷尽的方向去前进;
+- 而循环结构作为一种程序开发逻辑,则 **必须要关注循环过程的结束**,否则就会造成系统陷入死循环或死机。
 
 接下来,我们试着把一个数学归纳法的计算过程,用循环结构改写。为了让二者没有区别,我们对数学归纳法的问题增加一个截止条件的限制,那就是 k 小于 100 时。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25415\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25415\350\256\262.md"
index c6eabea2e..c1230ce16 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25415\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25415\350\256\262.md"
@@ -90,17 +90,17 @@ hanoi(3, 'a', 'b', 'c')
 
 汉诺塔问题解法的核心步骤就是:移动全部盘子,等价于移动“合并盘”,加上移动“大盘子”,加上再移动“合并盘”,然后你需要重复执行这个步骤。
 
-**用函数表达这个过程,就是 f(全部盘子) = f(合并盘) + f(大盘子) + f(合并盘)。** 为了代码实现这个功能,我们定义这个函数为 **hanoi(N,x,y,z),** 并且在这个函数中,需要调用自己才能完成“合并盘”的移动, **这种会调用自己的编码方式在程序开发中,就叫作递归** 。 **严格意义来说,递归并不是个算法,它是一种重要的程序开发思想,是某个算法的实现方式。**
+**用函数表达这个过程,就是 f(全部盘子) = f(合并盘) + f(大盘子) + f(合并盘)。** 为了代码实现这个功能,我们定义这个函数为 **hanoi(N,x,y,z),** 并且在这个函数中,需要调用自己才能完成“合并盘”的移动,**这种会调用自己的编码方式在程序开发中,就叫作递归** 。**严格意义来说,递归并不是个算法,它是一种重要的程序开发思想,是某个算法的实现方式。**
 
 在使用递归进行程序开发时,需要注意下面两个关键问题。
 
-- 第一个问题,递归必须要有 **终止条件** ,否则程序就会进入不停调用自己的死循环。
+- 第一个问题,递归必须要有 **终止条件**,否则程序就会进入不停调用自己的死循环。
 
 > 有这样一个故事:从前有座山,山里有个庙,庙里有个和尚讲故事;故事是,从前有座山,山里有个庙,庙里有个和尚讲故事;故事是...
 
 这就是一个典型的没有终止条件的递归。在汉诺塔问题中,我们的终止条件,就是当盘子数量为 1 时,直接从 x 移动到 z,而不用再递归调用自身。
 
-- 第二个问题,写代码之前需要先写出 **递归公式** 。 在汉诺塔问题中,递归公式是 **H(N)=H(N-1)+1+H(N-1)** ,这也是递归函数代码中除了终止条件以外的部分。
+- 第二个问题,写代码之前需要先写出 **递归公式** 。 在汉诺塔问题中,递归公式是 **H(N)=H(N-1)+1+H(N-1)**,这也是递归函数代码中除了终止条件以外的部分。
 
 > 对应于“循环结构”中的循环体,这部分代码对于“递归”而言,偶尔也被人称作“ **递归体** ”。
 
@@ -225,7 +225,7 @@ def fib(n):
 
 学完这一讲,你可能会发现,递归和循环比较 **相像** 。确实,递归和循环都是通过解决若干个简单问题来解决复杂问题的,它们也都有自己的终止条件和循环体/递归体,都是重复进行某个步骤。
 
-然而,它们也有很多 **差异性** ,主要体现在以下两方面。
+然而,它们也有很多 **差异性**,主要体现在以下两方面。
 
 **迭代次数**
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25416\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25416\350\256\262.md"
index 9f894837d..ab13745f7 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25416\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25416\350\256\262.md"
@@ -62,7 +62,7 @@ print times
 
 在计算机中,上面的现象也被称作“指数爆炸”。你可以理解为,某个看似不起眼的任务,每次以翻倍的速度进行增长,很快就会达到“星星之火可以燎原”的爆炸式效果和影响面。显然,指数爆炸性质的问题如果在程序中发生,会让系统迅速瘫痪。
 
-**不过,如果可以把指数爆炸的思想反过来用,就能对程序的效率进行优化。** 具体而言,某个任务虽然很庞大、很复杂, **但是每次我们都让这个任务的复杂性减半,那么用不了多久,这个庞大而又复杂的任务就会变成一个非常简单的任务了** 。
+**不过,如果可以把指数爆炸的思想反过来用,就能对程序的效率进行优化。** 具体而言,某个任务虽然很庞大、很复杂,**但是每次我们都让这个任务的复杂性减半,那么用不了多久,这个庞大而又复杂的任务就会变成一个非常简单的任务了** 。
 
 所以,指数爆炸思想的反向应用就是分治法,而分治法中的一个经典案例就是 **二分查找** 。
 
@@ -211,7 +211,7 @@ logb1 = 0
 
 如果密码的长度为 n,则密码的搜索空间为 S = 10n。假设 n 为 5,则密码共有 105 = 1 万种可能性。要想破译密码,无异于万里挑一。
 
-可见,要想把密码做得很复杂,一个可行的方法是,利用指数爆炸不断增加 **位数** ,来获得更大的搜索空间;除了增加密码尾数的方式外,将单个密码位上的构成可能增加也是一种提升安全性的手段。
+可见,要想把密码做得很复杂,一个可行的方法是,利用指数爆炸不断增加 **位数**,来获得更大的搜索空间;除了增加密码尾数的方式外,将单个密码位上的构成可能增加也是一种提升安全性的手段。
 
 例如,如果把每一位的密码,由先前的数字调整为数字或区分大小写的字母,则意味着密码的搜索空间由 S = 10n,提高到 S = 62n。
 
@@ -223,6 +223,6 @@ logb1 = 0
 
 这一课时,我们了解了指数爆炸(运算)与对数运算,以及它们在程序和生活中的应用。而指数爆炸的思维过程就是“折纸,分奔到月球”的过程,其正向应用就是密码学。
 
-而指数爆炸的反向应用有二分查找算法(也就是基于对数函数性质),二分查找算法是提高程序效率的重要手段,其前提条件是搜索空间有序,其实现方法需要采用上一讲所学的 **递归思想** ,需要预先定义递归的终止条件和递归体。
+而指数爆炸的反向应用有二分查找算法(也就是基于对数函数性质),二分查找算法是提高程序效率的重要手段,其前提条件是搜索空间有序,其实现方法需要采用上一讲所学的 **递归思想**,需要预先定义递归的终止条件和递归体。
 
 最后,我们留个课后习题,在上面的内容中,我们介绍了对数和指数的一些关键性质,你可以试着从数学的角度来证明这些性质的成立。
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25417\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25417\350\256\262.md"
index 5fb35c4fc..89d8f91f4 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25417\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25417\350\256\262.md"
@@ -1,6 +1,6 @@
 # 17 动态规划:如何利用最优子结构解决问题?
 
-动态规划是运筹学问题,运筹学又是数学的一个分支,与“运筹帷幄、决胜千里”的含义很接近;同时,动态规划也是计算机技术的问题,对于程序员而言,能灵活运用动态规划解决复杂问题是一项进阶的能力。 **在一线互联网公司的程序员面试中,动态规划的考核绝对是一大难点。**
+动态规划是运筹学问题,运筹学又是数学的一个分支,与“运筹帷幄、决胜千里”的含义很接近;同时,动态规划也是计算机技术的问题,对于程序员而言,能灵活运用动态规划解决复杂问题是一项进阶的能力。**在一线互联网公司的程序员面试中,动态规划的考核绝对是一大难点。**
 
 这一讲,我们就从数学的视角学习动态规划,并通过代码完成动态规划问题的开发。
 
@@ -70,7 +70,7 @@
 
 过程结点图
 
-接着就是最关键的内容了。我们提到过 **最优子结构特点** ,含义是假设 A 到 G 的最优路线要经过 B1,最优路线就可以以 B1 为分割点,前后分解为 Path 1 和 Path 2。
+接着就是最关键的内容了。我们提到过 **最优子结构特点**,含义是假设 A 到 G 的最优路线要经过 B1,最优路线就可以以 B1 为分割点,前后分解为 Path 1 和 Path 2。
 
 - Path 1 是 A 到 B1 的最优路线;
 - Path 2 也是 B1 到 G 的最优路线。
@@ -133,7 +133,7 @@ print minPath(m, 15)
 - 第 5 行,设置某个最大距离值为 999。接下来我们要遍历从 0 到 i,如果 matrix\[j\]\[i\] 不是 0,则说明结点是可抵达的。
 - 则需要计算某个子结构,即第 8 行。
 - 第 9 行,对于每个可能的子结构,寻找最优子结构。如果发现更近,则修改 distance 变量。
-- 第 2 行, **终止条件** 中如果 i 为 0,说明走到了终点,就要跳出递归。
+- 第 2 行,**终止条件** 中如果 i 为 0,说明走到了终点,就要跳出递归。
 
 这段代码理解难度很大,需要你仔细思考,最好把一些过程结果也打印出来。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25418\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25418\350\256\262.md"
index 5e7e5d367..b161d916d 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25418\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25418\350\256\262.md"
@@ -22,7 +22,7 @@
 
 在这个例子中,我们对前三对父子身高关系进行计算,得到了“儿子的身高 = 父亲的身高 + 3”的经验;再用这个经验,对身高为 182 的父亲的孩子身高做出更合理、智能的决策结果。
 
-可见,人工智能的目标就是要做出更合理、智能的决策。它的途径是对数据的计算,当然数据量越多越好,这也是“大数据”的核心优势。它的产出结果就是经验,有时候也叫作模型。换句话说, **人工智能就是要根据输入的数据,来建立出效果最好的模型** 。
+可见,人工智能的目标就是要做出更合理、智能的决策。它的途径是对数据的计算,当然数据量越多越好,这也是“大数据”的核心优势。它的产出结果就是经验,有时候也叫作模型。换句话说,**人工智能就是要根据输入的数据,来建立出效果最好的模型** 。
 
 ### 人工智能建模框架的基本步骤
 
@@ -96,7 +96,7 @@ L(_ **w** _) = (y1 - ŷ1)2 + (y2 - ŷ2)2 + (y3 - ŷ3)2。其中\_ **w** \_= \[k,
 
 最后,我们对这一讲进行总结。这一讲是模块四的开胃菜,我们通过一个预测身高这样一个最简单的例子,以小见大,认识了人工智能模型的建模过程和基本本质。
 
-人工智能的目标是做出更合理、更智能的决策,它的途径是对数据进行计算,从而输出结果,并将这一结果叫作模型。 **用一句话来概括,人工智能就是要根据输入的数据,来建立出效果最好的模型。**
+人工智能的目标是做出更合理、更智能的决策,它的途径是对数据进行计算,从而输出结果,并将这一结果叫作模型。**用一句话来概括,人工智能就是要根据输入的数据,来建立出效果最好的模型。**
 
 人工智能的建模过程通常包括下面三个步骤:
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25419\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25419\350\256\262.md"
index b37fef44d..1387191f6 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25419\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25419\350\256\262.md"
@@ -41,7 +41,7 @@
 
 此时的预测值 y 还是个 0~1 之间的连续值,这是因为 Sigmoid 函数的值域是 (0,1)。逻辑回归是个二分类模型,它的最终输出值只能是两个类别标签之一。通常,我们习惯于用“0”和“1”来分别标记二分类的两个类别。
 
-在逻辑回归中,常用预测值 y 和 0.5 的大小关系,来判断样本的类别归属。 **具体地,预测值 y 如果大于 0.5,则认为预测的类别为 1;反之,则预测的类别为 0。** 我们把上面的描述进行总结,来汇总一下逻辑回归输入向量、预测值和类别标签之间的关系,则有下面的流程图。
+在逻辑回归中,常用预测值 y 和 0.5 的大小关系,来判断样本的类别归属。**具体地,预测值 y 如果大于 0.5,则认为预测的类别为 1;反之,则预测的类别为 0。** 我们把上面的描述进行总结,来汇总一下逻辑回归输入向量、预测值和类别标签之间的关系,则有下面的流程图。
 
 ![图片1.png](assets/Cip5yF_lxJuAJjk2AAE7vKJA3Cg656.png)
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25420\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25420\350\256\262.md"
index d447c6974..0ecb1640f 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25420\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25420\350\256\262.md"
@@ -14,7 +14,7 @@
 
 我们可以发现决策树有以下特点。
 
-决策树由结点和边组成。最上边的结点称作 **根结点** ,最下边的结点称作 **叶子结点** 。除了叶子结点外,每个结点都根据某个变量及其分界阈值,决定了是向左走或向右走。每个叶子结点代表了某个分类的结果。
+决策树由结点和边组成。最上边的结点称作 **根结点**,最下边的结点称作 **叶子结点** 。除了叶子结点外,每个结点都根据某个变量及其分界阈值,决定了是向左走或向右走。每个叶子结点代表了某个分类的结果。
 
 - 当使用决策树模型去预测某个样本的归属类别时,需要将这个样本从根结点输入;
 - 接着就要“按图索骥”,根据决策树中的规则,一步步找到向左走或向右走的路径;
@@ -93,7 +93,7 @@ NP 难问题,指最优参数无法在多项式时间内被计算出来,这
 
 ![Drawing 1.png](assets/CgpVE1_lymqAcPlvAAA5o6I0_vQ040.png)
 
-根据当前的决策树,可以把原数据集切分为两个子集,分别是 D1 和 D2。 **X** 1 **= 0 时,子数据集是 D** 1
+根据当前的决策树,可以把原数据集切分为两个子集,分别是 D1 和 D2。**X** 1 **= 0 时,子数据集是 D** 1
 
 ![Lark20201229-160955.png](assets/Ciqc1F_q5tKAO7TMAABDmYKSNK0106.png) 在 D1 中,所有样本的类别标签都是“0”,满足了决策树建模的终止条件,则直接输出类别标签“0”,决策树更新为
 
@@ -137,7 +137,7 @@ NP 难问题,指最优参数无法在多项式时间内被计算出来,这
 
 ![Drawing 5.png](assets/Ciqc1F_lyouAEIX-AAAjk2gZiFs803.png)
 
-根据当前的决策树,可以将数据集分割为 D1 和 D2 两部分,并建立决策树。 **X1 为 0 时,子数据集为 D1**
+根据当前的决策树,可以将数据集分割为 D1 和 D2 两部分,并建立决策树。**X1 为 0 时,子数据集为 D1**
 
 ![Lark20201229-161007.png](assets/CgqCHl_q6USARgYZAABBkd-FmD0667.png)
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25421\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25421\350\256\262.md"
index 75dd45eff..6226a9fdd 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25421\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25421\350\256\262.md"
@@ -1,6 +1,6 @@
 # 21 神经网络与深度学习:计算机是如何理解图像、文本和语音的?
 
-在上一讲的最后,我们提到过“浅层模型”和“深层模型”。其实,人工智能的早期并没有“浅层模型”的概念,浅层模型是深度学习出现之后,与之对应而形成的概念。在浅层模型向深层模型转变的过程中, **神经网络算法无疑是个催化剂** ,并在此基础上诞生了深度学习。
+在上一讲的最后,我们提到过“浅层模型”和“深层模型”。其实,人工智能的早期并没有“浅层模型”的概念,浅层模型是深度学习出现之后,与之对应而形成的概念。在浅层模型向深层模型转变的过程中,**神经网络算法无疑是个催化剂**,并在此基础上诞生了深度学习。
 
 这一讲,我们就来学习一下神经网络和深度学习。
 
@@ -40,7 +40,7 @@
 
 #### 2.层次化将“神经元”构成神经网络
 
-我们说过,层次化地把多个神经元组织在一起,才构成了神经网络。在这里, **层次化** 的含义是,每一层有若干个神经元结点,层与层之间通过带权重的边互相连接。如下图,就是一个简单的神经网络。
+我们说过,层次化地把多个神经元组织在一起,才构成了神经网络。在这里,**层次化** 的含义是,每一层有若干个神经元结点,层与层之间通过带权重的边互相连接。如下图,就是一个简单的神经网络。
 
 ![图片4.png](assets/CgpVE1_wgbSARHK6AAElMQEDF1Q333.png)
 
@@ -102,7 +102,7 @@ ym=sigmoid\[sigmoid(xm1w111+xm2w121+xm3w131)·w211+sigmoid(xm1w112+xm2w122+xm3w1
 
 在这个例子中,我们有 8 个 wijk 变量,分别是 w111、w121、w131、w211、w112、w122、w132、w221,因此需要求分别计算损失函数关于这 8 个变量的导数。
 
-既然表达式都有了,我们就利用大学数学求导的 **链式法则** ,耐着性子来求解一下吧。
+既然表达式都有了,我们就利用大学数学求导的 **链式法则**,耐着性子来求解一下吧。
 
 > 别忘了,y=sigmoid(x) 的一阶导数是 y·(1-y)。
 
diff --git "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25424\350\256\262.md" "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25424\350\256\262.md"
index bc92f11e3..35bbf763d 100644
--- "a/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25424\350\256\262.md"
+++ "b/docs/Basic/\347\250\213\345\272\217\345\221\230\347\232\204\346\225\260\345\255\246\350\257\276/\347\254\25424\350\256\262.md"
@@ -4,7 +4,7 @@
 
 在回答这个问题之前,我们先看几个跟数学有关的案例或桥段。
 
-**美剧《危机边缘》第三季的第三集** 一个年轻男子在邮筒上放置了一支笔,紧接着发生了一系列的连锁案件。先是笔掉在地上,导致一个老人去弯腰捡拾;接着,骑车而过的路人撞倒了老人,导致一群人围观;最后,围观群众过多,让公交车司机没注意红绿灯变化,导致撞死了一个手捧鲜花的女子。原来,这个年轻男子是个智商极高的人,通过各种精准的计算,对事情有了准确预判,完成了自己的杀人计划。 **电影《决胜 21 点》** 几个数学高才生,利用假期时间,在赌城拉斯维加斯,玩他们再熟悉不过的“21点”,最终狂赢三百多万的美金。他们靠记住扑克牌的分布状况推算获胜概率,并调整自己的下注策略,谋求统计上收益期望最大的策略。 **综艺节目《相声有新人》**
+**美剧《危机边缘》第三季的第三集** 一个年轻男子在邮筒上放置了一支笔,紧接着发生了一系列的连锁案件。先是笔掉在地上,导致一个老人去弯腰捡拾;接着,骑车而过的路人撞倒了老人,导致一群人围观;最后,围观群众过多,让公交车司机没注意红绿灯变化,导致撞死了一个手捧鲜花的女子。原来,这个年轻男子是个智商极高的人,通过各种精准的计算,对事情有了准确预判,完成了自己的杀人计划。**电影《决胜 21 点》** 几个数学高才生,利用假期时间,在赌城拉斯维加斯,玩他们再熟悉不过的“21点”,最终狂赢三百多万的美金。他们靠记住扑克牌的分布状况推算获胜概率,并调整自己的下注策略,谋求统计上收益期望最大的策略。**综艺节目《相声有新人》**
 
 有一对博士夫妻尝试在相声中加入数学公式元素。他们认为,人类的情感可以被公式化计算,并进一步利用这些公式创作出让人产生情感共鸣的相声。虽然他们的相声并没有让我发笑,但这的确算得上是数学与相声融合的大胆尝试。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25400\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25400\350\256\262.md"
index ce38c32d5..72a01da75 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25400\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25400\350\256\262.md"
@@ -32,7 +32,7 @@
 
 这个回答同样适用于软件开发领域。在软件开发领域,有哪些知识十年前很重要,现在仍然重要,未来可能同样重要?
 
-其实仔细分析,这些知识不外乎: **数据结构、算法、面向对象思想、设计模式、软件工程** 。如果范围不局限于程序开发,还要算上测试、产品设计、项目管理、运维这些岗位。 **你会发现,无论你是什么岗位,只要你从事软件开发相关领域,都绕不开“软件工程”,因为现代软件项目开发,多多少少都离不开软件工程知识的应用。**
+其实仔细分析,这些知识不外乎: **数据结构、算法、面向对象思想、设计模式、软件工程** 。如果范围不局限于程序开发,还要算上测试、产品设计、项目管理、运维这些岗位。**你会发现,无论你是什么岗位,只要你从事软件开发相关领域,都绕不开“软件工程”,因为现代软件项目开发,多多少少都离不开软件工程知识的应用。**
 
 想象下在日常工作中,不管你用什么开发语言,不管是前端和后端:
 
@@ -72,7 +72,7 @@
 
 这些丰富的经历,帮助我更好地理解了软件工程的知识,也知道如何应用它,可以发挥最大的效用。
 
-因此,在这个专栏中,我会结合自身在软件开发中的经历, **将软件工程中的知识点和我所看到的国内外前沿的、典型的项目案例结合起来讲解,也会和你一起分享我对这些知识背后的思考** 。和你一起去软件工程学中,寻找软件项目中问题的答案。
+因此,在这个专栏中,我会结合自身在软件开发中的经历,**将软件工程中的知识点和我所看到的国内外前沿的、典型的项目案例结合起来讲解,也会和你一起分享我对这些知识背后的思考** 。和你一起去软件工程学中,寻找软件项目中问题的答案。
 
 我希望最终,你能把软件工程知识和项目经验有机地结合起来,转换成你自身能力的一部分。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25401\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25401\350\256\262.md"
index b6ee4319e..6f47c2d62 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25401\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25401\350\256\262.md"
@@ -74,7 +74,7 @@ OS/360 操作系统是上世纪 60 年代最复杂的软件系统之一,也是
 
 ## 如果你去搜索一下“软件工程定义”,你还能找到很多其他定义,这里就不一一列举。我们没必要花太多时间在这些字面解释上,关键是要抓住这些定义的本质: **就是要用工程化方法去规范软件开发,让项目可以按时完成、成本可控、质量有保证。** 软件工程的演化史
 
-对比传统的工程学科,和软件工程最接近的就是建筑工程了。设想一下建一座房子:首先要先立项、设定预算,然后画设计图,再是施工,施工完成后,由专业人士进行质量检查,质检合格后入住。 **开发软件本质上也是像盖房子一样,是从无到有创造的过程。工程化的方式,就是你分步骤(过程),采用科学的方法,借助工具来做产品。** 于是参考建筑工程,整个软件开发过程也被分成了几个阶段:需求定义与分析、设计、实现、测试、交付和维护,这也就是我们常说的软件项目生命周期。
+对比传统的工程学科,和软件工程最接近的就是建筑工程了。设想一下建一座房子:首先要先立项、设定预算,然后画设计图,再是施工,施工完成后,由专业人士进行质量检查,质检合格后入住。**开发软件本质上也是像盖房子一样,是从无到有创造的过程。工程化的方式,就是你分步骤(过程),采用科学的方法,借助工具来做产品。** 于是参考建筑工程,整个软件开发过程也被分成了几个阶段:需求定义与分析、设计、实现、测试、交付和维护,这也就是我们常说的软件项目生命周期。
 
 当然,各个阶段都会有人的参与,于是产生了软件项目里的各种角色:项目经理、产品经理、架构师、程序员、测试工程师、运维工程师。而对这整个过程的管理,我们通常称之为“项目管理”。
 
@@ -96,7 +96,7 @@ OS/360 操作系统是上世纪 60 年代最复杂的软件系统之一,也是
 
 ## 一个公式
 
-当你大致了解整个软件工程的演变发展史,你会发现,软件工程的知识,都是建立在软件项目的过程,或者说软件项目生命周期之上的。 **基于软件过程,我们有了角色分工,有了对过程的管理和工具,对过程中每个阶段细分的方法学和工具。** 现在,如果再回头看看我们的问题“什么是软件工程?”其实可以总结为:软件工程就是用工程化的方法来开发维护软件。也可以说软件工程就是用一定的过程,采用科学的方法,借助工具来开发软件。
+当你大致了解整个软件工程的演变发展史,你会发现,软件工程的知识,都是建立在软件项目的过程,或者说软件项目生命周期之上的。**基于软件过程,我们有了角色分工,有了对过程的管理和工具,对过程中每个阶段细分的方法学和工具。** 现在,如果再回头看看我们的问题“什么是软件工程?”其实可以总结为:软件工程就是用工程化的方法来开发维护软件。也可以说软件工程就是用一定的过程,采用科学的方法,借助工具来开发软件。
 
 如果用一个简单的公式表达,那就是: **软件工程 = 过程 + 方法 + 工具** 。
 
@@ -106,4 +106,4 @@ OS/360 操作系统是上世纪 60 年代最复杂的软件系统之一,也是
 
 从 1968 年提出软件工程到现在,正好是 50 年。在 2002 年,我最开始学软件工程专业的时候,还只有瀑布模型、需求分析、系统设计等这些传统软件工程内容,但是经过十几年的发展,在软件项目中,敏捷开发、持续集成、微服务等这些新兴内容已经开始在软件项目中占据越来越重要的位置。
 
-可以预见,未来软件工程领域还会有新的概念、新的知识诞生。但是万变不离其宗,只要你抓住软件工程的本质,无论将来如何变化,你总能很快掌握新的知识内容。 **而软件工程的核心,就是围绕软件项目开发,对开发过程的组织,对方法的运用,对工具的使用。** 
\ No newline at end of file
+可以预见,未来软件工程领域还会有新的概念、新的知识诞生。但是万变不离其宗,只要你抓住软件工程的本质,无论将来如何变化,你总能很快掌握新的知识内容。**而软件工程的核心,就是围绕软件项目开发,对开发过程的组织,对方法的运用,对工具的使用。** 
\ No newline at end of file
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25402\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25402\350\256\262.md"
index 5d65dcb80..eb1012df6 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25402\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25402\350\256\262.md"
@@ -8,7 +8,7 @@
 
 这句话对我影响很大。我真的开始在日常生活中尝试应用“Everything is a project”的概念,小到做作业,大到完成工作中的复杂项目。
 
-解决这些问题的方式,就是参考软件生命周期和瀑布模型,把一件事情分成几个阶段: **分析、设计、实施、测试、完成** ,然后制定相应的计划。这种方法不仅非常有效,让我的做事效率大幅提高,而且让我在看待事情上,能够更全面地、站在更高的角度去思考。
+解决这些问题的方式,就是参考软件生命周期和瀑布模型,把一件事情分成几个阶段: **分析、设计、实施、测试、完成**,然后制定相应的计划。这种方法不仅非常有效,让我的做事效率大幅提高,而且让我在看待事情上,能够更全面地、站在更高的角度去思考。
 
 2010 年在上海的时候,我机缘巧合参加了一个关于产品设计与用户体验的线下活动,我可能是与会人员中,为数不多的非专业产品设计的同学。
 
@@ -56,7 +56,7 @@
 
 1. **有一个被有效论证过的方法论指导你,可以帮助你提高成功概率,也可以提高效率。** 2. **当你用工程方法去思考的时候,你会更多的站在整体而非局部去思考,更有大局观。** 前面提到的“设计一个老年机”的游戏就是个很好的例子,后来我在不同场合、不同人群中都组织过这个游戏,无论我如何强调时间限制(30 分钟)和产出(必须要演示结果),绝大部分人还是会把时间和注意力放在各种稀奇古怪的想法上,并沉浸其中。等到时间快到了,他们才发现还来不及把方案画到纸上,甚至还没确定该选哪个方案。
 
-这种现象其实很常见,我们在日常处理事务时,天然地会选择自己感兴趣的、擅长的那部分,而容易无视整体和其他部分。 **所以问题的核心并不在于是不是用工程方法,而是有没有把这件事当作一个项目,是不是能看到这件事的全貌,而不是只看到局部。**
+这种现象其实很常见,我们在日常处理事务时,天然地会选择自己感兴趣的、擅长的那部分,而容易无视整体和其他部分。**所以问题的核心并不在于是不是用工程方法,而是有没有把这件事当作一个项目,是不是能看到这件事的全貌,而不是只看到局部。**
 
 在工作分工越来越细致的今天,一个项目里面有产品设计、开发、测试、运维等诸多岗位,每个岗位都有自己的价值追求,测试人员关注找出更多 Bug、开发人员关注技术和高效开发功能、运维关心系统稳定。
 
@@ -76,7 +76,7 @@
 
 这样的场景问题还有很多,为什么会出现这种情况呢?事实上,这在很大程度上都归因于大家只是站在自己岗位的角度来看问题,没有站在项目的整体角度来看。
 
-如果能站在项目整体来看问题,你就会去关注 **项目的质量、项目的进度、项目的成本、项目的最终用户** ,那么上面这些场景将变成:
+如果能站在项目整体来看问题,你就会去关注 **项目的质量、项目的进度、项目的成本、项目的最终用户**,那么上面这些场景将变成:
 
 - 为了项目整体的效率和避免返工浪费,产品经理会及早和开发人员确认技术可行性,并对产品设计先行验证;
 
@@ -92,12 +92,12 @@
 
 肯定有人会想,我又不是项目经理,干嘛要操这心呀?在这个问题上,我的看法是: **每个项目成员,如果能多站在项目的角度去考虑,那么这样不仅对项目有利,更对自己有好处。** 项目做成了,大家脸上都有光,也得到了更多的锻炼;项目没做成,不仅脸上无光,甚至可能面临丢工作的危险。很多人都有技术升管理的理想,能多站在项目整体角度去考虑的人,在日常工作中,也一定会有更多的锻炼机会,自然会多一些提升的空间。
 
-我把这种思维方式称为“工程思维”。如果给一个定义的话,工程思维, **本质上是一种思考问题的方式,在解决日常遇到的问题时,尝试从一个项目的角度去看待问题、尝试用工程方法去解决问题、站在一个整体而不是局部的角度去看问题。** 在我的职业生涯中,一直习惯于用“工程思维”去思考问题,遇到问题,会尽可能把它当成一个项目,用工程方法有计划、有步骤地去解决它,这让我积累了不少的工程方法实践经验。
+我把这种思维方式称为“工程思维”。如果给一个定义的话,工程思维,**本质上是一种思考问题的方式,在解决日常遇到的问题时,尝试从一个项目的角度去看待问题、尝试用工程方法去解决问题、站在一个整体而不是局部的角度去看问题。** 在我的职业生涯中,一直习惯于用“工程思维”去思考问题,遇到问题,会尽可能把它当成一个项目,用工程方法有计划、有步骤地去解决它,这让我积累了不少的工程方法实践经验。
 
 同时,我也更多站在整体的角度思考,这让我在项目中能更好地和其他同事合作,有更多的晋升机会。我还记得,我第一次开始管项目的时候,并没有慌张,而是把项目任务按阶段一拆分,然后按阶段制定好计划,再按照计划一点点执行、调整,很快就上手了项目管理的工作。
 
 总结
--- **改变,最有效的是方式是改变思想,这往往也是最难的部分。** 当你开始学习这个软件工程专栏,我希望你不仅仅学到软件工程的理论知识,更希望你能用“工程思维”来思考你遇到的各类问题。 **你不需要现在是一个项目经理或者管理者,也一样可以在日常生活中应用“工程思维”** 。比如学习这个专栏,你会制定一个什么样的计划?每个阶段达到一个什么样的成果?比如你今年有没有去旅行的计划?你会怎么制定你的旅行计划?
+-- **改变,最有效的是方式是改变思想,这往往也是最难的部分。** 当你开始学习这个软件工程专栏,我希望你不仅仅学到软件工程的理论知识,更希望你能用“工程思维”来思考你遇到的各类问题。**你不需要现在是一个项目经理或者管理者,也一样可以在日常生活中应用“工程思维”** 。比如学习这个专栏,你会制定一个什么样的计划?每个阶段达到一个什么样的成果?比如你今年有没有去旅行的计划?你会怎么制定你的旅行计划?
 
 如果有兴趣的话,你还可以看看我以前写过的一篇文章[记录下两个孩子在 MineCraft 里面还原公寓的经历](https://zhuanlan.zhihu.com/p/21314651)。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25403\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25403\350\256\262.md"
index fdc08bb04..57290584c 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25403\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25403\350\256\262.md"
@@ -60,7 +60,7 @@
 
 **也是从那时开始,有了“软件生命周期”(Software Life Cycle,SLC) 的概念。** > 软件生命周期是软件的产生直到报废或停止使用的生命周期。而像瀑布模型这样,通过把整个软件生命周期划分为若干阶段来管理软件开发过程的方法,叫软件生命周期模型。
 
-虽然现在瀑布模型已经不是最主流的开发模式,那为什么我们现在还要学习瀑布模型呢? **因为不管什么软件项目,不管采用什么开发模式,有四种活动是必不可少的,那就是需求、设计、编码和测试。而这四项活动,都是起源自瀑布模型,也是瀑布模型中核心的部分。**
+虽然现在瀑布模型已经不是最主流的开发模式,那为什么我们现在还要学习瀑布模型呢?**因为不管什么软件项目,不管采用什么开发模式,有四种活动是必不可少的,那就是需求、设计、编码和测试。而这四项活动,都是起源自瀑布模型,也是瀑布模型中核心的部分。**
 
 学好瀑布模型,才可以帮助你更好的理解这些内容。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25404\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25404\350\256\262.md"
index 1bdfc87cf..925edb4f9 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25404\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25404\350\256\262.md"
@@ -22,7 +22,7 @@
 
 我还偷摸着花了很多时间想把代码写好,结果发现,这个快速做好的简单版本,主要目的就是为了给客户演示,跟客户确认需求,然后把客户的反馈记录下来,再去优化。这样几个小版本下来,基本上就把需求确定了,而我当时写的好多代码根本就用不上。
 
-后来我才知道,这就是快速原型模型。 **快速原型模型,就是为了要解决客户的需求不明确和需求多变的问题。** 先迅速建造一个可以运行的软件原型,然后收集用户反馈,再反复修改确认,使开发出的软件能真正反映用户需求,这种开发模型就叫快速原型模型,也叫原型模型。
+后来我才知道,这就是快速原型模型。**快速原型模型,就是为了要解决客户的需求不明确和需求多变的问题。** 先迅速建造一个可以运行的软件原型,然后收集用户反馈,再反复修改确认,使开发出的软件能真正反映用户需求,这种开发模型就叫快速原型模型,也叫原型模型。
 
 这就好比客户想要盖房子,但是他没想好要盖成什么样子,于是施工方就先搭了一栋彩钢房(就像工地里面搭的临时房子),让客户先用起来,然后再给反馈调整。
 
@@ -30,7 +30,7 @@
 
 不过,这样做也有一个问题,用彩钢房这种方式盖房子虽然快,但是房子质量不会太好,住的不算舒服,想有点个性化的风格也难。
 
-同样的道理,也适用于软件项目。彩钢房就像是软件原型,重点是反映软件核心功能和交互,功能可以是不完整的,可靠性和性能要求不高,但开发速度可以很快。 **原型模型因为能快速修改,所以能快速对用户的反馈和变更作出响应,同时原型模型注重和客户的沟通,所以最终开发出来的软件能够真正反映用户的需求。**  **但这种快速原型开发往往是以牺牲质量为代价的。** 在原型开发过程中,没有经过严谨的系统设计和规划,可靠性和性能都难以保障。所以在实际的软件项目中,针对原型模型的这种快速、低质量的特点,通常有两种处理策略: **抛弃策略和附加策略。** 抛弃策略是将原型只应用于需求分析阶段,在确认完需求后,原型会被抛弃,实际开发时,将重新开发所有功能。类似于用彩钢房盖房子,确认完客户需求后,拆掉重新建。
+同样的道理,也适用于软件项目。彩钢房就像是软件原型,重点是反映软件核心功能和交互,功能可以是不完整的,可靠性和性能要求不高,但开发速度可以很快。**原型模型因为能快速修改,所以能快速对用户的反馈和变更作出响应,同时原型模型注重和客户的沟通,所以最终开发出来的软件能够真正反映用户的需求。**  **但这种快速原型开发往往是以牺牲质量为代价的。** 在原型开发过程中,没有经过严谨的系统设计和规划,可靠性和性能都难以保障。所以在实际的软件项目中,针对原型模型的这种快速、低质量的特点,通常有两种处理策略: **抛弃策略和附加策略。** 抛弃策略是将原型只应用于需求分析阶段,在确认完需求后,原型会被抛弃,实际开发时,将重新开发所有功能。类似于用彩钢房盖房子,确认完客户需求后,拆掉重新建。
 
 附加策略则是将原型应用于整个开发过程,原型一直在完善,不断增加新功能新需求,直到满足客户所有需求,最终将原型变成交付客户的软件。类似于用彩钢房盖房子,最后还要做一番精装修,交付客户。
 
@@ -45,7 +45,7 @@
 
 基于这种思路,产生了很多开发模型,比较典型的主要是: **增量模型** 和 **迭代模型** 。
 
-- **增量模型——按模块分批次交付** 增量模型是把待开发的软件系统模块化,然后在每个小模块的开发过程中,应用一个小瀑布模型,对这个模块进行需求分析、设计、编码和测试。相对瀑布模型而言,增量模型周期更短,不需要一次性把整个软件产品交付给客户,而是分批次交付。 **如果拿盖房子来比喻的话,就是先盖卫生间,然后盖厨房,再是卧室。**
+- **增量模型——按模块分批次交付** 增量模型是把待开发的软件系统模块化,然后在每个小模块的开发过程中,应用一个小瀑布模型,对这个模块进行需求分析、设计、编码和测试。相对瀑布模型而言,增量模型周期更短,不需要一次性把整个软件产品交付给客户,而是分批次交付。**如果拿盖房子来比喻的话,就是先盖卫生间,然后盖厨房,再是卧室。**
 
 盖卫生间的时候,也要先分析需求,然后设计,再实施,最后验收。有时候也可以多模块并行,例如同时盖卫生间和厨房,前提是模块之间不能有依赖关系,比如,你不可能先盖二楼再盖一楼。
 
@@ -53,7 +53,7 @@
 
 ![img](assets/20d7896e4a52e8043defff6eedb9869b.jpg)
 
-因为增量模型的根基是模块化,所以, **如果系统不能模块化,那么将很难采用增量模型的模式来开发。** 另外,对模块的划分很抽象,这本身对于系统架构的水平是要求很高的。
+因为增量模型的根基是模块化,所以,**如果系统不能模块化,那么将很难采用增量模型的模式来开发。** 另外,对模块的划分很抽象,这本身对于系统架构的水平是要求很高的。
 
 基于这样的特点,增量模型主要适用于: **需求比较清楚,能模块化的软件系统,并且可以按模块分批次交付。**
 
@@ -67,9 +67,9 @@
 
 在迭代模型中,整个项目被拆分成一系列小的迭代。通常一个迭代的时间都是固定的,不会太长,例如 2~4 周。每次迭代只实现一部分功能,做能在这个周期内完成的功能。
 
-在一个迭代中都会包括需求分析、设计、实现和测试,类似于一个小瀑布模型。 **迭代结束时要完成一个可以运行的交付版本。** ![img](assets/9abe6230baeb7a92a95b65dd7c383d10.jpg)
+在一个迭代中都会包括需求分析、设计、实现和测试,类似于一个小瀑布模型。**迭代结束时要完成一个可以运行的交付版本。**![img](assets/9abe6230baeb7a92a95b65dd7c383d10.jpg)
 
-迭代模型和增量模型很容易混淆,因为都是把大瀑布拆成小瀑布。这两种模型的主要差别在于如何拆分项目功能上。 **增量模型是按照功能模块来拆分;而迭代模型则是按照时间来拆分,看单位时间内能完成多少功能。**
+迭代模型和增量模型很容易混淆,因为都是把大瀑布拆成小瀑布。这两种模型的主要差别在于如何拆分项目功能上。**增量模型是按照功能模块来拆分;而迭代模型则是按照时间来拆分,看单位时间内能完成多少功能。**
 
 还是用盖房子来理解,增量模型则是先盖厨房,再是卧室,这样一个个模块来完成。而迭代模型则是先盖一个简单的茅草房,有简易的土灶和土床,然后再升级成小木屋,有更好的灶和更好的卧室,这样一步步迭代成最终的房子。
 
@@ -95,13 +95,13 @@
 
 ![img](assets/c015252d6ae984b667499ee5b8c76ab1.jpg)
 
-这个模型就是 V 模型,本质上它还是瀑布模型,只不过它是更重视对每个阶段验收测试的过程模型。 **场景二:项目风险高,随时可能会中断** 如果你现在要做一个风险很高的项目,客户可能随时不给你钱了。这种情况下,如果采用传统瀑布模型,无疑风险很高,可能做完的时候才发现客户给不了钱,损失就很大了!
+这个模型就是 V 模型,本质上它还是瀑布模型,只不过它是更重视对每个阶段验收测试的过程模型。**场景二:项目风险高,随时可能会中断** 如果你现在要做一个风险很高的项目,客户可能随时不给你钱了。这种情况下,如果采用传统瀑布模型,无疑风险很高,可能做完的时候才发现客户给不了钱,损失就很大了!
 
 这种情况,基于增量模型或者迭代模型进行开发,就可以有效降低风险。你需要注意的是,在每次交付的时候,要同时做一个风险评估,如果风险过大就不继续后续开发了,及时止损。
 
 ![img](assets/5c1f2444754f3ce5ce68e0a790da2bcc.png)
 
-这种强调风险,以风险驱动的方式完善项目的开发模型就是螺旋模型。 **场景三:山寨一款软件产品,希望能快速上线发布** 其实软件行业山寨的案例不少,山寨项目的特点是,项目需求是明确的,不会有什么变化,这时候就可以选择增量模型,划分好模块,先实现核心模块,发布可运行版本,再增量发布其他模块。多模块可以同步开发。 **场景四:客户都没想清楚想要什么,但是个大单子** 很多项目,客户一开始都没想清楚想要的是什么,需要花很长时间去分析定义需求,但是单子很大,值得认真去做好。
+这种强调风险,以风险驱动的方式完善项目的开发模型就是螺旋模型。**场景三:山寨一款软件产品,希望能快速上线发布** 其实软件行业山寨的案例不少,山寨项目的特点是,项目需求是明确的,不会有什么变化,这时候就可以选择增量模型,划分好模块,先实现核心模块,发布可运行版本,再增量发布其他模块。多模块可以同步开发。**场景四:客户都没想清楚想要什么,但是个大单子** 很多项目,客户一开始都没想清楚想要的是什么,需要花很长时间去分析定义需求,但是单子很大,值得认真去做好。
 
 那么这样的项目,你可以考虑拆分成四个阶段:
 
@@ -125,7 +125,7 @@
 
 ![img](assets/b0091341a7fa31cd26d8a02e7d63e2fc.png)
 
-上面这种开发方式来源自统一软件开发过程(Rational Unified Process,RUP),适用于复杂和需求不明确的软件系统。 **场景五:我的产品已经上线,但是需要持续更新维护**
+上面这种开发方式来源自统一软件开发过程(Rational Unified Process,RUP),适用于复杂和需求不明确的软件系统。**场景五:我的产品已经上线,但是需要持续更新维护**
 
 很多产品在上线后,还在保持不停的更新维护,修复 Bug、增加新功能,每个月甚至每周更新。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25405\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25405\350\256\262.md"
index 93c869445..001f307f1 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25405\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25405\350\256\262.md"
@@ -66,7 +66,7 @@
 
 结合敏捷开发提出的背景,你其实不难发现,敏捷开发就是想解决瀑布模型这样的重型软件开发方法存在的问题,用一种轻量的、敏捷的方法来改善甚至是替代它。
 
-这些年敏捷开发也是一直这么做的。 **瀑布模型的典型问题就是周期长、发布烦、变更难,敏捷开发就是快速迭代、持续集成、拥抱变化。**
+这些年敏捷开发也是一直这么做的。**瀑布模型的典型问题就是周期长、发布烦、变更难,敏捷开发就是快速迭代、持续集成、拥抱变化。**
 
 ## 如果用敏捷的方式盖房子
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25407\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25407\350\256\262.md"
index 385fd2577..707353ab3 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25407\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25407\350\256\262.md"
@@ -32,7 +32,7 @@
 
 - **每周一部署生产环境** 没有人愿意星期五部署,那意味着如果部署后发现故障,可能周末都没法好好休息了。所以即使程序早已经测试好了,除非特别紧急,否则都会留在下一周再部署。所以部署放在上半周,这样后面遇到问题还有足够的时间去应对。
 
-部署很简单,按照流程执行几个命令就可以完成生产环境部署。部署完成后,需要对线上监控的图表进行观察,如果有问题需要及时甄别,必要的话对部署进行回滚操作。 **但轻易不会打补丁马上重新上线,因为仓促之间的修复可能会导致更大的问题。**
+部署很简单,按照流程执行几个命令就可以完成生产环境部署。部署完成后,需要对线上监控的图表进行观察,如果有问题需要及时甄别,必要的话对部署进行回滚操作。**但轻易不会打补丁马上重新上线,因为仓促之间的修复可能会导致更大的问题。**
 
 像敏捷开发这样一周一个 Sprint 的好处之一就是,即使这一周的部署回滚了,下周再一起部署也不会有太大影响。
 
@@ -52,7 +52,7 @@
 
 团队每个成员都要对候选的下个 Sprint Backlog 中的 Ticket 从 1-5 分进行打分,1 分表示容易 1 天以内可以完成的工作量,2 分表示 2 天内可以完成的工作,5 分表示非常复杂,需要 5 天以上的工作量。
 
-这里需要注意,打分时,要大家一起亮分,而不是挨个表态,不然结果很容易被前面亮分的人影响。 **评估每条 Ticket 工作量的大概流程如下:**
+这里需要注意,打分时,要大家一起亮分,而不是挨个表态,不然结果很容易被前面亮分的人影响。**评估每条 Ticket 工作量的大概流程如下:**
 
 - 会议组织者阅读一条 Ticket,可能是用户故事,可能是 Bug,可能是优化任务。同时会询问大家对内容有没有疑问。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25408\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25408\350\256\262.md"
index 6bf294462..eae7ab24a 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25408\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25408\350\256\262.md"
@@ -34,7 +34,7 @@
 
 为什么四个要素,是“质量”放在三角形的中间?
 
-因为 **软件工程的目标就是要构建和维护高质量的软件** ,所以项目的质量是高于一切的。也就是说,“质量”这个因素一般不会妥协,因此把“质量”放在三角形中间,然后在时间、成本、范围这三条边之间寻求平衡。
+因为 **软件工程的目标就是要构建和维护高质量的软件**,所以项目的质量是高于一切的。也就是说,“质量”这个因素一般不会妥协,因此把“质量”放在三角形中间,然后在时间、成本、范围这三条边之间寻求平衡。
 
 质量往往也是其他三个因素平衡后结果的体现,想要做的快、成本低、功能多,最后一定是个质量很差的产品。
 
@@ -42,7 +42,7 @@
 
 我在专栏中常用“道术器”来比喻软件工程中的各个知识点,“金三角”无疑就是“道”级别的。
 
-**项目管理其实就是项目中一系列问题的平衡和妥协** ,而“金三角”理论则为我们的平衡提供了理论指导,了解这三个因素分别对项目其他方面产生的影响,可以帮助你在做决策时进行权衡取舍。
+**项目管理其实就是项目中一系列问题的平衡和妥协**,而“金三角”理论则为我们的平衡提供了理论指导,了解这三个因素分别对项目其他方面产生的影响,可以帮助你在做决策时进行权衡取舍。
 
 当你接手一个项目,项目的进度、成本和范围指标很容易可以跟踪到。有了这些信息,你就可以及时发现问题,调整“金三角”的边,及时解决,以防止这些小问题发展成大问题。
 
@@ -74,7 +74,7 @@
 
 ![img](assets/27e916733d013fa85b2964a2b1051ea0.jpg)
 
-我们再来看敏捷开发,敏捷开发中,是采用固定时间周期的开发模式,例如每两周一个 Sprint,团队人数也比较少。所以, **在敏捷开发中,时间和成本两条边是固定,就只有范围这条边是变量。** 这就是为什么在敏捷开发中,每个 Sprint 开始前都要开 Sprint 计划会,大家一起选择下个 Sprint 能做完的任务,甚至于在 Sprint 结束时,没能完成的任务会放到下个 Sprint 再做。
+我们再来看敏捷开发,敏捷开发中,是采用固定时间周期的开发模式,例如每两周一个 Sprint,团队人数也比较少。所以,**在敏捷开发中,时间和成本两条边是固定,就只有范围这条边是变量。** 这就是为什么在敏捷开发中,每个 Sprint 开始前都要开 Sprint 计划会,大家一起选择下个 Sprint 能做完的任务,甚至于在 Sprint 结束时,没能完成的任务会放到下个 Sprint 再做。
 
 ![img](assets/1yy45e28893d0b4652e780d47f0a2873.jpg)
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25409\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25409\350\256\262.md"
index bac64c9f3..8e7140aa6 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25409\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25409\350\256\262.md"
@@ -52,7 +52,7 @@ BSD 的许可协议本身是开放的、没有限制的,但 Facebook 在此基
 
 而可行性研究是基于问题和解决方案来分析的,因此这有点像“先有鸡还是先有蛋”的问题:你得先立项才能慢慢搞明白需求是什么,然后才能有解决方案;而你只有搞明白需求是什么,以及解决方案是什么,才能去做可行性研究。
 
-但“我们很特殊”,不能成为不做可行性分析的借口,可能项目需求最开始是模糊不清的,还不具备可行性研究的条件,那么等到项目有了一定的进展,需求逐步明确后,要继续对可行性做研究。 **如果发现方案不具备可行性,也应及时调整方案或停止项目以止损。** 2. **“老板拍板的项目,明知道不可行也得硬着头皮干呀!”** 这个问题要分类讨论,有两种情况。
+但“我们很特殊”,不能成为不做可行性分析的借口,可能项目需求最开始是模糊不清的,还不具备可行性研究的条件,那么等到项目有了一定的进展,需求逐步明确后,要继续对可行性做研究。**如果发现方案不具备可行性,也应及时调整方案或停止项目以止损。** 2. **“老板拍板的项目,明知道不可行也得硬着头皮干呀!”** 这个问题要分类讨论,有两种情况。
 
 第一种情况,多半是由于老板或者项目负责人控制决策权,且对于不同意见容忍度较低。底下人不敢提不同意见,明知道不对也只能执行。
 
@@ -74,7 +74,7 @@ BSD 的许可协议本身是开放的、没有限制的,但 Facebook 在此基
 
 前面,我们讲了可行性研究在软件工程中的重要性,也帮你厘清了几个常见的困惑,接下来我们来看看“如何做”的问题。
 
-其实, **当你决定要做可行性研究,你就已经成功一半了,怎么做反而是相对简单的部分!**
+其实,**当你决定要做可行性研究,你就已经成功一半了,怎么做反而是相对简单的部分!**
 
 软件工程的教材里面,通常会讲如何写可行性研究报告,很繁琐,要撰写诸如引言、背景、定义等内容。在这里,我们关注的重点是,软件工程中是如何去做可行性研究的。如文章开头所说的,通常从三个方面着手做:
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25410\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25410\350\256\262.md"
index bed46a379..bea329513 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25410\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25410\350\256\262.md"
@@ -24,7 +24,7 @@
 
 程序员总是想着如何技术实现、用什么语言框架、怎么提高效率……要钻研技术,这些是非常好的优点,但是要转管理,这反而会是一种障碍。
 
-**因为管理** ,最重要的一点就是大局观,要能从整个项目的角度,从整个团队的角度去思考,去确定方向,去发现问题,对问题及时解决及时调整。
+**因为管理**,最重要的一点就是大局观,要能从整个项目的角度,从整个团队的角度去思考,去确定方向,去发现问题,对问题及时解决及时调整。
 
 但是当你把注意力都放在技术细节上,就容易忽视其他事情,例如和其他人之间的沟通、不关心当前项目进展。
 
@@ -70,7 +70,7 @@
 
 2. **用流程和规范让项目成员一起紧密协作** 项目成员,也就是帮助你一起完成项目的人。
 
-对于项目成员的管理,不需要过多依赖人的管理,否则项目经理就会成为项目管理的瓶颈。所以更多要落实到流程和工具上。 **好的项目管理,不需要直接去管人,而是管理好流程规范;项目成员不需要按照项目经理的指令做事,而是遵循流程规范。** 合适的项目管理工具,也可以简化流程,保障流程的执行,提高效率。
+对于项目成员的管理,不需要过多依赖人的管理,否则项目经理就会成为项目管理的瓶颈。所以更多要落实到流程和工具上。**好的项目管理,不需要直接去管人,而是管理好流程规范;项目成员不需要按照项目经理的指令做事,而是遵循流程规范。** 合适的项目管理工具,也可以简化流程,保障流程的执行,提高效率。
 
 关于具体怎样制定流程规范,我会后续更新的文章《12 流程和规范:红绿灯不是约束,而是用来提高效率》中有更多介绍。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25411\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25411\350\256\262.md"
index d90be30af..958583ccb 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25411\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25411\350\256\262.md"
@@ -10,7 +10,7 @@
 
 这样大家重整旗鼓,很快就完成了第一个里程碑。达到第一个里程碑的目标后,团队成员很受鼓舞,士气很快就上来了,后面按照新的计划,并没有太多加班加点,就完成了一个个的里程碑,最后顺利完成项目。
 
-你看, **如果没有计划,你的项目可能会陷入一种无序和混乱中。** 计划,就像我们出行用的导航,你可以清楚地看到项目整体的安排,同时它还时刻提醒我们目标是什么,不要偏离方向。
+你看,**如果没有计划,你的项目可能会陷入一种无序和混乱中。** 计划,就像我们出行用的导航,你可以清楚地看到项目整体的安排,同时它还时刻提醒我们目标是什么,不要偏离方向。
 
 执行计划的项目成员,就像使用导航的司机,可以知道什么时间做什么事情,保证任务得以执行。执行计划的过程,就像我们沿着导航前进,可以了解是不是项目过程中出现了偏差,及时的调整。
 
@@ -32,7 +32,7 @@
 
 当然,有时候你可能确实是没机会参与到当前的项目计划中。不过,万事皆项目,你一样要学会做计划,因为学会做计划,会对你工作生活的方方面面起到积极的作用。
 
-比如很多人都有一些目标:要转型做管理、要移民、要写一个业余项目,然而很多目标都无疾而终了。 **这是因为光有目标还不够的,必须得要付诸行动。而要行动,就需要对目标进行分解,进而变成可以执行的计划。**
+比如很多人都有一些目标:要转型做管理、要移民、要写一个业余项目,然而很多目标都无疾而终了。**这是因为光有目标还不够的,必须得要付诸行动。而要行动,就需要对目标进行分解,进而变成可以执行的计划。**
 
 ## 如何制定计划?
 
@@ -58,7 +58,7 @@
 
 我们写程序的时候都有经验,就是要把复杂的问题拆分成简单的问题,大的模块拆成小的模块,在工程里面这个叫“分而治之”。做计划也是一样,第一步就是要对任务进行分解。
 
-在项目管理中,对任务分解有个专业的词汇叫 WBS,它意思是工作分解结构(Work Breakdown Structure, WBS)。 **就是把要做的事情,按照一个树形结构去组织,逐级分解,分割成小而具体的可交付结果,直到不能再拆分为止。**
+在项目管理中,对任务分解有个专业的词汇叫 WBS,它意思是工作分解结构(Work Breakdown Structure, WBS)。**就是把要做的事情,按照一个树形结构去组织,逐级分解,分割成小而具体的可交付结果,直到不能再拆分为止。**
 
 下图就是“留言飞语”项目按照 WBS 拆分的结果。
 
@@ -96,7 +96,7 @@
 
 但这不意味着项目经理对估算不需要控制,通常来说,项目经理需要自己有一个估算,然后再请开发人员一起评估。如果结果和自己的估算差不多,那就可以达成一致,如果估算不一致,那怎么办呢?
 
-其实很简单, **就是要双方一起沟通,消除偏差。** 特别要注意的是,开发人员预估工作量通常会很乐观,所以最后时间会偏紧,这种情况一样要去沟通消除偏差。估算的主要目的是尽可能得到准确的时间。
+其实很简单,**就是要双方一起沟通,消除偏差。** 特别要注意的是,开发人员预估工作量通常会很乐观,所以最后时间会偏紧,这种情况一样要去沟通消除偏差。估算的主要目的是尽可能得到准确的时间。
 
 但是在沟通中也要注意技巧,不要采用质问的方式:“这么简单一个模块居然要 5 天?”这只会让听者产生逆反心理,无法有效的沟通。可以恰当的提一些问题来达到有效沟通的目的,比如我通常会问两个问题:
 
@@ -104,7 +104,7 @@
 
 - “能不能简单介绍一下这个模块你是打算如何实现的?”
 
-估算出现偏差,可能是由于开发人员没想清楚,或者是项目经理自己低估了其难度。 **提问可以帮助双方搞清楚真实的情况是什么样的,而且也不会招致反感。** 同时项目经理还可以给予一些建议和支持。
+估算出现偏差,可能是由于开发人员没想清楚,或者是项目经理自己低估了其难度。**提问可以帮助双方搞清楚真实的情况是什么样的,而且也不会招致反感。** 同时项目经理还可以给予一些建议和支持。
 
 沟通最好的方式就是倾听和恰当的提问。
 
@@ -116,7 +116,7 @@
 
 我们知道,项目中有些任务是可以并行做的,而有些任务之间则是有依赖关系的。比如说“留言飞语”项目中,编码和测试方案是可以同时进行的,而 Code Review,要在编码完成后进行。
 
-所以, **排路径就是要根据任务之间的关系,资源的占用情况,排出合适的顺序。** 例如下图。
+所以,**排路径就是要根据任务之间的关系,资源的占用情况,排出合适的顺序。** 例如下图。
 
 ![img](assets/62cb0d16d486b8a0b8084d23262e01ad.png)
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25412\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25412\350\256\262.md"
index 33ddd8ae5..36ebbc5bf 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25412\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25412\350\256\262.md"
@@ -74,7 +74,7 @@
 
 如果你有一个大家认可的需求变更流程,就不再需要靠项目经理一个人决定该不该加需求,而是通过流程,来大家一起决策是不是要加这个流程。
 
-## 所以你看, **流程规范,看起来是约束,实际上你用的好的话,不仅可以提高团队效率,还可以将好的实践标准化流程化,让大家可以共享经验,还可以有效的管理项目。** 如何制定好流程规范?
+## 所以你看,**流程规范,看起来是约束,实际上你用的好的话,不仅可以提高团队效率,还可以将好的实践标准化流程化,让大家可以共享经验,还可以有效的管理项目。** 如何制定好流程规范?
 
 在项目管理中,难免要去制定流程规范。即使你不是管理者,也可以提出合理的流程规范,帮助把项目管理好。
 
@@ -82,11 +82,11 @@
 
 ### 制定流程规范的四个步骤
 
-对于流程规范的制定,可以通过四个步骤来开展。 **第一步:明确要解决的问题** 要制定一个流程规范,第一步就是明确你是要解决什么样的问题。项目中很多问题,都可以思考是不是能通过流程解决。
+对于流程规范的制定,可以通过四个步骤来开展。**第一步:明确要解决的问题** 要制定一个流程规范,第一步就是明确你是要解决什么样的问题。项目中很多问题,都可以思考是不是能通过流程解决。
 
 比如说有程序员在生产环境操作,误删了数据表,造成了严重问题。如果只是对程序员进行处罚,寄希望于小心谨慎避免类似问题,那么下一次还有可能会有类似的事情发生。
 
-如果说在流程上规范起来,例如:数据库操作之前先备份数据库,事先写好 SQL 语句,需要有人审查,测试环境先测试通过,最后再生产环境执行,那么就可以避免以后再出现不小心删除数据表的事情发生。 **第二步:提出解决方案**
+如果说在流程上规范起来,例如:数据库操作之前先备份数据库,事先写好 SQL 语句,需要有人审查,测试环境先测试通过,最后再生产环境执行,那么就可以避免以后再出现不小心删除数据表的事情发生。**第二步:提出解决方案**
 
 对于问题,也不用着急马上就想着用流程规范,可以先思考解决的方法,有了方法后再进一步思考是否能提炼流程规范。
 
@@ -112,7 +112,7 @@
 
 对于大家都认可、很重要的流程规范,一定要让大家严格遵守,必要的时候需要配合一些奖惩制度,以保障其执行。
 
-比如说流程规范的执行和绩效考评挂钩,对于没有执行的需要私下沟通提醒,严重的需要批评教育。否则流程规范会形同虚设,没有太大的意义。 **第四步: 持续优化,不断改进** 流程制定后,在实际执行的时候,难免发现一些不合理或者不科学的地方,这时候就需要对其进行调整。
+比如说流程规范的执行和绩效考评挂钩,对于没有执行的需要私下沟通提醒,严重的需要批评教育。否则流程规范会形同虚设,没有太大的意义。**第四步: 持续优化,不断改进** 流程制定后,在实际执行的时候,难免发现一些不合理或者不科学的地方,这时候就需要对其进行调整。
 
 还有一些流程规范,随着时间推移,可能已经不能符合要求了,也需要考虑改进甚至放弃,不然反而会成为一种阻碍。
 
@@ -122,7 +122,7 @@
 
 ### 将流程规范工具化
 
-如果说,以前我还是人为去推动一些流程规范的执行,近些年,我越来越感觉到, **应该尽可能借助技术手段来推动甚至替代流程规范。**
+如果说,以前我还是人为去推动一些流程规范的执行,近些年,我越来越感觉到,**应该尽可能借助技术手段来推动甚至替代流程规范。**
 
 例如说代码规范,以前代码规范的执行,主要靠反复的教育宣传和代码审查中一个个去检查。而现在,借助 VSCode 这种强大的 IDE,以及 ESLint 这种代码检查工具,可以方便的检测出不符合规范的代码,甚至于可以帮你直接格式化成满足代码规范的格式。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25413\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25413\350\256\262.md"
index afdcc57ce..dada87ff7 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25413\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25413\350\256\262.md"
@@ -46,7 +46,7 @@
 
 我觉得像每日站立会这样的会议更有效率,其时间短、人数少,所以成本低,创造的价值高于其成本。而人数多,又偏离会议主题的讨论会则没有价值,这是因为人数多时间长,导致会议成本高,而其创造的价值远远不及成本。
 
-那为什么还有那么多低效率的会议? **因为有的会议,就不是为了创造价值。** 比如说有的会议,花的成本不是组织者的,对他来说,得到他的会议价值就可以了。
+那为什么还有那么多低效率的会议?**因为有的会议,就不是为了创造价值。** 比如说有的会议,花的成本不是组织者的,对他来说,得到他的会议价值就可以了。
 
 > 你是砍柴的,他是放羊的,你和他聊了一天,他的羊吃饱了,你的柴呢?
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25415\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25415\350\256\262.md"
index d82ebc1ab..94bc47e37 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25415\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25415\350\256\262.md"
@@ -75,7 +75,7 @@
 
 #### 2. 管理风险
 
-软件项目风险管理,通常分四步来做。 **第一步:风险识别,识别可能的风险** 风险识别,就是看项目中有哪些可能的风险,因为只有找出来有可能存在的风险,才会有后续的步骤。
+软件项目风险管理,通常分四步来做。**第一步:风险识别,识别可能的风险** 风险识别,就是看项目中有哪些可能的风险,因为只有找出来有可能存在的风险,才会有后续的步骤。
 
 识别风险这种事,经验很重要,因为大部分风险其实都是相似的。以前看 CSDN 总裁蒋涛发过一条微博,内容引发了很多人的共鸣,每一条无不应对着软件项目中的常见风险。
 
@@ -101,7 +101,7 @@
 >
 > 1. 你根本无法解决的大问题。
 
-一个识别风险的方法叫 **检查表法** ,就是可以把类似于上面这些常见风险收集整理起来,分类列成清单,按照清单去检查对照。
+一个识别风险的方法叫 **检查表法**,就是可以把类似于上面这些常见风险收集整理起来,分类列成清单,按照清单去检查对照。
 
 软件项目的风险主要分成以下几类:
 
@@ -165,7 +165,7 @@
 
 比如说前面说的采用新技术 React 导致进度延迟的案例,这个风险虽然有很大概率发生,但是进度延迟的影响可以接受,并且让团队今后在技术栈上多了新的选择,长远来看对项目更有利,那么这个风险就是可以接受的,还是可以继续做下去。
 
-回避风险、转移风险、缓解风险、接受风险,以上就是针对风险提前准备的一些应对策略,实际项目中,可以根据实际情况来灵活运用以上策略,有效应对风险,减少可能损失。 **第四步:风险监控,对风险进行监控预警**
+回避风险、转移风险、缓解风险、接受风险,以上就是针对风险提前准备的一些应对策略,实际项目中,可以根据实际情况来灵活运用以上策略,有效应对风险,减少可能损失。**第四步:风险监控,对风险进行监控预警**
 
 风险在没发生的时候并不会变成问题也不会造成损失,如果风险可以监控,可以预知风险即将发生,或者可以在风险发生后,第一时间知道,那么就可以马上对风险进行干预,避免变成更大的问题。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25416\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25416\350\256\262.md"
index 3d62b2475..036b6bd9f 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25416\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25416\350\256\262.md"
@@ -96,7 +96,7 @@
 
 为什么我不一开始就写很细的文档呢?
 
-一个原因是太难写,要花很多时间精力,甚至可能写不下去;另一个原因就是在收集反馈的过程中,会有很多修改。 **写得越细则无用功越多,最后,你甚至会因为不想改文档而抵触不同的意见。** 而从粗到细逐步迭代的方式就好多了,一开始的目的是为了梳理清楚思路,只要脑图这种级别的内容就好了,然后进行调整。因为文档很粗,调整也方便,等到基本确定后再写细节,就不会有大的反复。
+一个原因是太难写,要花很多时间精力,甚至可能写不下去;另一个原因就是在收集反馈的过程中,会有很多修改。**写得越细则无用功越多,最后,你甚至会因为不想改文档而抵触不同的意见。** 而从粗到细逐步迭代的方式就好多了,一开始的目的是为了梳理清楚思路,只要脑图这种级别的内容就好了,然后进行调整。因为文档很粗,调整也方便,等到基本确定后再写细节,就不会有大的反复。
 
 4. **一些基本的画图的技巧**
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25417\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25417\350\256\262.md"
index 3ed671ef7..1a289bdde 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25417\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25417\350\256\262.md"
@@ -22,7 +22,7 @@
 
 - 产品经理对架构师说:我们现在有一个需求,在树上拴两绳子,再吊一块板子,你做一下设计。
 
-很明显,这两个需求的意思不一样, **前面这个需求是用户需求,后面这个需求是产品需求。** 用户需求是由用户提出来的,期望满足自身一定需要的要求,例如用户说:“想要一个给三个孩子玩的秋千。”这种原始的用户需求通常是不能直接做成产品的,需要对其进行分析提炼,最终形成产品需求。
+很明显,这两个需求的意思不一样,**前面这个需求是用户需求,后面这个需求是产品需求。** 用户需求是由用户提出来的,期望满足自身一定需要的要求,例如用户说:“想要一个给三个孩子玩的秋千。”这种原始的用户需求通常是不能直接做成产品的,需要对其进行分析提炼,最终形成产品需求。
 
 产品需求就是在分析提炼用户真实需求后,提出的符合产品定位的解决方案。就像上面“在树上栓两绳子,再吊一块板子”,就是产品经理针对用户需求提出的解决方案。
 
@@ -30,7 +30,7 @@
 
 其实对用户需求的分析,不是一个动作,而是一个过程。需求分析,就是对用户需求进行提炼分析,最终形成产品需求的过程。
 
-而针对每个用户需求的需求分析过程,需要经过三个步骤。 **第一步:挖掘真实需求** 大部分用户提的需求,都不见得是其真实的需求,需要透过现象看本质,去挖掘其背后真实的需求。就像福特汽车创始人亨利福特说过的:
+而针对每个用户需求的需求分析过程,需要经过三个步骤。**第一步:挖掘真实需求** 大部分用户提的需求,都不见得是其真实的需求,需要透过现象看本质,去挖掘其背后真实的需求。就像福特汽车创始人亨利福特说过的:
 
 如果我最初是问消费者他们想要什么,他们应该是会告诉我,“要一辆更快的马车!”
 
@@ -46,11 +46,11 @@
 
 而现实项目中,大多数人需求分析的不正确,就是因为没有挖掘出用户的真实需求。
 
-我们再看之前的秋千项目,目标用户是三个孩子,使用场景是一起户外玩耍,想解决的问题其实是能有一起玩的娱乐设施。 **第二步:提出解决方案** 我们知道了目标用户,其使用场景和想要解决的问题,就可以结合产品定位,提出相应的解决方案。
+我们再看之前的秋千项目,目标用户是三个孩子,使用场景是一起户外玩耍,想解决的问题其实是能有一起玩的娱乐设施。**第二步:提出解决方案** 我们知道了目标用户,其使用场景和想要解决的问题,就可以结合产品定位,提出相应的解决方案。
 
 比如针对想要“更快更舒适的出行方式”日常出行的乘客,我们就可以提出汽车的解决方案,而不一定要局限于马车,汽车能更好的满足用户需求。
 
-针对三个孩子想有一个在户外一起玩的娱乐设施这个需求,我们可以提供一个轮胎式的秋千,就可以很好的满足他们的需求,我们甚至可以建一个小型游乐园。 **第三步:筛选和验证方案**
+针对三个孩子想有一个在户外一起玩的娱乐设施这个需求,我们可以提供一个轮胎式的秋千,就可以很好的满足他们的需求,我们甚至可以建一个小型游乐园。**第三步:筛选和验证方案**
 
 在提出方案后,我们需要对方案进行筛选,比如对于秋千项目,建小型游乐园的方案虽然能满足需求,但是成本太高,需要排除掉。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25418\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25418\350\256\262.md"
index 160bb56dc..8e0c46e10 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25418\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25418\350\256\262.md"
@@ -32,7 +32,7 @@
 
 第三个阶段就是完成最终的后台服务,接入真正的数据库或者其他后台服务,完成整个网站的开发。由于前面两个阶段,产品经理已经把需求和交互确认清楚,所以这个阶段的开发,就没有太多需求上的反复和修改,可以高效的完成设计和开发。
 
-简单来说快速原型模型就是, **第一阶段确认界面布局和内容,第二阶段确认交互,第三阶段实现。**
+简单来说快速原型模型就是,**第一阶段确认界面布局和内容,第二阶段确认交互,第三阶段实现。**
 
 通过快速原型模型来开发,可以低成本、快速地确认好需求。但也有一个问题: **整个过程单靠产品经理是无法完成的,必须要有开发人员配合才能完成。** 而对产品经理来说,要开发人员配合还是一件高成本的事情。
 
@@ -105,11 +105,11 @@
 
 ![img](assets/c6fecc876ce699fb7331d5f54238c661.png)
 
-这样你就可以一步步将整体信息结构从粗到细,一点点整理清楚。 **画产品使用流程图**
+这样你就可以一步步将整体信息结构从粗到细,一点点整理清楚。**画产品使用流程图**
 
 用户在使用产品时,会在不同的模块之间跳转,比如说你从极客时间进入到一个没订阅过的专栏,还可以点击订阅按钮进入订阅界面,订阅成功又可以返回专栏界面。
 
-所以, **需要用流程图把这些界面之间跳转的逻辑梳理清楚。** 在设计流程图的时候,要重点考虑用户的使用场景,结合使用场景设计好流程。
+所以,**需要用流程图把这些界面之间跳转的逻辑梳理清楚。** 在设计流程图的时候,要重点考虑用户的使用场景,结合使用场景设计好流程。
 
 举例来说,如果当前用户进入到专栏首页,如果用户没有订阅,最重要的就是让用户可以方便的订阅,然后继续阅读;如果用户已经订阅,就没必要显示订阅相关内容,直接可以看到文章列表,选取想看的文章直接阅读。
 
@@ -125,7 +125,7 @@
 
 在设计好整体的信息架构和使用流程图后,就可以开始对每个界面画流程图了。
 
-在具体到界面时, **要优先考虑满足产品需求,然后是让界面好看好用。**
+在具体到界面时,**要优先考虑满足产品需求,然后是让界面好看好用。**
 
 比如说阅读专栏文章这个界面,在 iPhone 上,屏幕很小,显示的信息有限,到 iPad 上,有了更大的屏幕,就可以增加更多的内容。但是注意不能造成太多的信息干扰,要突出重点,增强体验。
 
@@ -169,7 +169,7 @@
 
 这里推荐几款主要的原型设计工具,供参考。
 
-**Axure RP** :Axure RP 曾一度是原型设计工具的代名词,历史悠久功能强大,可以制作网站、桌面软件、移动 App 的原型。 缺点是专业度较高,价格高。 **墨刀** :墨刀 是一款优秀的国产原型设计工具,可以制作网站、桌面软件、移动 App 的原型。上手相对容易,价钱也较 Axure 便宜很多。 **Adobe XD** :Adobe XD 是 Adebe 出的一款设计兼原型设计工具,可以制作出高保真原型,对于设计师尤其容易上手。 **ProtoPie** :ProtoPie 是一款高保真原型设计工具,不需要编程基础,可以做出逼真强大的交互效果。 **Framer X** :Framer X是一款高保真的原型设计工具,功能很强大,但是需要一定的编程基础,尤其适合程序员使用。
+**Axure RP** :Axure RP 曾一度是原型设计工具的代名词,历史悠久功能强大,可以制作网站、桌面软件、移动 App 的原型。 缺点是专业度较高,价格高。**墨刀** :墨刀 是一款优秀的国产原型设计工具,可以制作网站、桌面软件、移动 App 的原型。上手相对容易,价钱也较 Axure 便宜很多。**Adobe XD** :Adobe XD 是 Adebe 出的一款设计兼原型设计工具,可以制作出高保真原型,对于设计师尤其容易上手。**ProtoPie** :ProtoPie 是一款高保真原型设计工具,不需要编程基础,可以做出逼真强大的交互效果。**Framer X** :Framer X是一款高保真的原型设计工具,功能很强大,但是需要一定的编程基础,尤其适合程序员使用。
 
 关于原型设计工具更多的资料,可以到“人人都是产品经理”网站的原型设计分类下,可以找到很多有价值的资料。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25419\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25419\350\256\262.md"
index 06d47e3df..953bc71ce 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25419\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25419\350\256\262.md"
@@ -20,7 +20,7 @@
 
 这也解释了为什么同一个公司内,负责热门产品的部门,奖金都能多分一点;在效益好的公司,不但不担心裁员,反而钱也拿的多。这些年程序员的待遇相对于其他行业要高,也主要是因为软件和互联网行业的产品估值高。
 
-所以说,程序员的价值,并不完全是体现在技术上的,而在于用技术做成了产品,产品创造了价值,再回过头来成就了程序员的价值。 **第二,你的价值体现在团队中的稀缺性。**
+所以说,程序员的价值,并不完全是体现在技术上的,而在于用技术做成了产品,产品创造了价值,再回过头来成就了程序员的价值。**第二,你的价值体现在团队中的稀缺性。**
 
 很多时候程序员其实没机会去选择产品的。但即使在同一个产品中,技术水平相当的程序员,价值也有差别。那些价值高的程序员通常在技术上或者技术之外都有一技之长:
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25420\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25420\350\256\262.md"
index 4836b5ac4..1582cfe75 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25420\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25420\350\256\262.md"
@@ -14,14 +14,14 @@
 
 既然大家都不满意,那么我们就需要想办法去改善,这也是软件工程这门学科存在的目的和意义。
 
-目前也已经有很多管理需求变更的解决方案,比如这两个常见的解决方案。 **方案一:增强需求变更流程,让需求变更规范起来。** 这个方案简单来说,就是通过严格的流程,来避免一些没有意义的变更,从而达到管理需求变更的目的。 **方案二:快速迭代,缩短版本周期。** 这个方案是另一个思路,就是将大的功能拆分,每个版本周期仅实现一部分功能需求,周期较短,这样需求发生变更时,就可以快速响应。
+目前也已经有很多管理需求变更的解决方案,比如这两个常见的解决方案。**方案一:增强需求变更流程,让需求变更规范起来。** 这个方案简单来说,就是通过严格的流程,来避免一些没有意义的变更,从而达到管理需求变更的目的。**方案二:快速迭代,缩短版本周期。** 这个方案是另一个思路,就是将大的功能拆分,每个版本周期仅实现一部分功能需求,周期较短,这样需求发生变更时,就可以快速响应。
 
 不过,在看到这两个方案后,我还是希望你不满足于当前的答案,自己停下来思考一下这两个问题:
 
 1. 这些方案是否解决了你当前项目的问题?
 1. 如果换一个项目环境,当前方案是否依然适用?
 
-之所以要思考这样的问题,是因为对于像软件工程这样偏理论知识的学习,你一定不要仅仅停留在了解有什么样的解决方案上, **而是要追本溯源,研究问题背后的原因,研究理论背后的来龙去脉。** 因为,就算你记住了再多的解决方案,换个项目环境可能就不适用了。所以我们要多去思考和分析逻辑,这样未来遇到类似的问题时,才可以做到对症下药,选择合适的解决方案,甚至在没有现成解决方案的情况下,能自己创造合适的解决方案。
+之所以要思考这样的问题,是因为对于像软件工程这样偏理论知识的学习,你一定不要仅仅停留在了解有什么样的解决方案上,**而是要追本溯源,研究问题背后的原因,研究理论背后的来龙去脉。** 因为,就算你记住了再多的解决方案,换个项目环境可能就不适用了。所以我们要多去思考和分析逻辑,这样未来遇到类似的问题时,才可以做到对症下药,选择合适的解决方案,甚至在没有现成解决方案的情况下,能自己创造合适的解决方案。
 
 ## 为什么建筑工程中少有需求变更?
 
@@ -85,13 +85,13 @@
 
 创业初期,加龙同学真的是不容易,每天和几个程序员一起加班加点,就是为了应对客户这种频繁变更的需求。如果你是加龙,参考前面总结的几种解决方案,你会怎么做?
 
-加龙作为软件工程专业毕业的学生,我觉得他当时运用软件工程知识去改善需求变更问题上是做得非常好的。他其实并没有采用一个单一的解决方案,而是分阶段逐步改进。 **第一步:规范变更流程,提升客户变更成本。** 加龙其实也知道,通过提升需求确定性,做好需求分析,和客户多沟通确认,是可以有效减少需求变更的。但是他当时确实人手太有限,也没有专业的产品经理,不能短时间内去提升需求分析、产品设计的水平,所以他第一步选择提升客户变更需求的成本,这样可以马上产生效果。
+加龙作为软件工程专业毕业的学生,我觉得他当时运用软件工程知识去改善需求变更问题上是做得非常好的。他其实并没有采用一个单一的解决方案,而是分阶段逐步改进。**第一步:规范变更流程,提升客户变更成本。** 加龙其实也知道,通过提升需求确定性,做好需求分析,和客户多沟通确认,是可以有效减少需求变更的。但是他当时确实人手太有限,也没有专业的产品经理,不能短时间内去提升需求分析、产品设计的水平,所以他第一步选择提升客户变更需求的成本,这样可以马上产生效果。
 
 于是在后面的项目中,在和客户签订合同时,他会和客户约定,如果有需求变更,先统一提交到他那里,然后他甄别后再决定是否做,什么时候做,是否要重新签订新的附加合同(增加额外费用)。通过制定一系列标准,让双方合作的流程变得更规范。
 
-这样,程序员就可以专注于开发,也不会因为频繁的需求变更影响进度,大家不用那么累,收入也在稳步上升。但是需求变更的情况还是时有发生。 **第二步:用原型设计低成本响应需求变更;做好需求分析和确认,减少需求变更。** 加龙在挺过最艰难的创业初期后,雇佣了一个全职的产品经理,专门去和客户确认需求。这个产品经理很专业,每次在了解完客户的需求后,不急于让程序员马上去写代码,而是自己先用 Axure 这样的原型设计工具,做一个简单的交互原型,给客户演示。
+这样,程序员就可以专注于开发,也不会因为频繁的需求变更影响进度,大家不用那么累,收入也在稳步上升。但是需求变更的情况还是时有发生。**第二步:用原型设计低成本响应需求变更;做好需求分析和确认,减少需求变更。** 加龙在挺过最艰难的创业初期后,雇佣了一个全职的产品经理,专门去和客户确认需求。这个产品经理很专业,每次在了解完客户的需求后,不急于让程序员马上去写代码,而是自己先用 Axure 这样的原型设计工具,做一个简单的交互原型,给客户演示。
 
-于是客户会针对原型的效果提出一些修改意见,他再快速地修改原型,这样反复确认,等到客户没有什么修改意见后,再让开发着手实现。 **通过原型设计的方式,不仅可以方便地与客户沟通需求,还可以灵活响应需求变更。** 通过提升需求确定性,加龙的公司进一步降低了需求变更的情况发生,营收又上了一个台阶,又增加了几个程序员和产品经理。 **第三步:通过灵活的架构和强大的配置,低成本响应客户需求变更。**
+于是客户会针对原型的效果提出一些修改意见,他再快速地修改原型,这样反复确认,等到客户没有什么修改意见后,再让开发着手实现。**通过原型设计的方式,不仅可以方便地与客户沟通需求,还可以灵活响应需求变更。** 通过提升需求确定性,加龙的公司进一步降低了需求变更的情况发生,营收又上了一个台阶,又增加了几个程序员和产品经理。**第三步:通过灵活的架构和强大的配置,低成本响应客户需求变更。**
 
 加龙公司经过两年的发展后,敏锐地发现其实大部分企业网站的功能都是很相似的,主要差别还是在界面样式上。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25421\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25421\350\256\262.md"
index 561b974cb..9fa51592d 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25421\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25421\350\256\262.md"
@@ -26,13 +26,13 @@
 
 4. **要让软件稳定运行是复杂的** 软件在开发完成后,要发布运行,但运行时也充满了各种不确定性。比如说明星发布八卦可能会导致微博宕机;阿里云宕机导致很多基于阿里云的系统也跟着一起无法提供服务。
 
-因为技术的这些复杂性,会导致软件开发变得很复杂,开发成本很高。而架构设计恰恰可以在这些方面很好地解决技术复杂的问题。 **首先,架构设计可以降低满足需求和需求变化的开发成本。** 对于复杂的需求,架构设计通过对系统抽象和分解,把复杂系统拆分成若干简单的子系统。就像淘宝这样复杂的网站,最终拆分成一个个小的微服务后,单个微服务开发的难度,其实和个人博客网站的难度已经差不太多了,普通程序员都可以完成,降低了人力成本。
+因为技术的这些复杂性,会导致软件开发变得很复杂,开发成本很高。而架构设计恰恰可以在这些方面很好地解决技术复杂的问题。**首先,架构设计可以降低满足需求和需求变化的开发成本。** 对于复杂的需求,架构设计通过对系统抽象和分解,把复杂系统拆分成若干简单的子系统。就像淘宝这样复杂的网站,最终拆分成一个个小的微服务后,单个微服务开发的难度,其实和个人博客网站的难度已经差不太多了,普通程序员都可以完成,降低了人力成本。
 
-对于需求的变化,已经有一些成熟的架构实践,比如说像分层架构这样把 UI 界面和业务逻辑分离,可以让 UI 上的改动,不会影响业务逻辑的代码;像 Wordpress 这样基于插件和定制化的设计,可以满足绝大部分内容类网站的需求,降低了时间成本。 **其次,架构设计可以帮助组织人员一起高效协作。** 通过对系统抽象,再拆分,可以把复杂的系统分拆。分拆后,开发人员可以各自独立完成功能模块,最后通过约定好的接口协议集成。
+对于需求的变化,已经有一些成熟的架构实践,比如说像分层架构这样把 UI 界面和业务逻辑分离,可以让 UI 上的改动,不会影响业务逻辑的代码;像 Wordpress 这样基于插件和定制化的设计,可以满足绝大部分内容类网站的需求,降低了时间成本。**其次,架构设计可以帮助组织人员一起高效协作。** 通过对系统抽象,再拆分,可以把复杂的系统分拆。分拆后,开发人员可以各自独立完成功能模块,最后通过约定好的接口协议集成。
 
-比如说前后端分拆后,有的开发人员就负责前端 UI 相关的开发,有的开发人员就负责后端服务的开发。根据团队规模还可以进一步细分,比如说前端可以有的程序员负责 iOS,有的程序员负责网站,这样最终各个开发小组规模都不大,既能有效协作,又能各自保证战斗力。 **再次,架构设计可以帮助组织好各种技术。** 架构设计可以用合适的编程语言和协议,把框架、技术组件、数据库等技术或者工具有效的组织起来,一起实现需求目标。
+比如说前后端分拆后,有的开发人员就负责前端 UI 相关的开发,有的开发人员就负责后端服务的开发。根据团队规模还可以进一步细分,比如说前端可以有的程序员负责 iOS,有的程序员负责网站,这样最终各个开发小组规模都不大,既能有效协作,又能各自保证战斗力。**再次,架构设计可以帮助组织好各种技术。** 架构设计可以用合适的编程语言和协议,把框架、技术组件、数据库等技术或者工具有效的组织起来,一起实现需求目标。
 
-比如说经典的分层架构,UI 层通过选择合适的前端框架,例如 React/Vue 实现复杂的界面逻辑,服务层利用 Web 框架提供稳定的网络服务,数据访问层通过数据库接口读写数据库,数据库则负责记录数据结果。 **最后,架构设计可以保障服务稳定运行。** 现在有很多成熟的架构设计方案,可以保障服务的稳定运行。比如说分布式的架构,可以把高访问量分摊到不同的服务器,这样即使流量很大,分流到单台服务器的压力并不大;还有像[异地多活](https://developer.aliyun.com/article/57715)这样的架构方案可以保证即使一个机房宕机,还可以继续提供服务。
+比如说经典的分层架构,UI 层通过选择合适的前端框架,例如 React/Vue 实现复杂的界面逻辑,服务层利用 Web 框架提供稳定的网络服务,数据访问层通过数据库接口读写数据库,数据库则负责记录数据结果。**最后,架构设计可以保障服务稳定运行。** 现在有很多成熟的架构设计方案,可以保障服务的稳定运行。比如说分布式的架构,可以把高访问量分摊到不同的服务器,这样即使流量很大,分流到单台服务器的压力并不大;还有像[异地多活](https://developer.aliyun.com/article/57715)这样的架构方案可以保证即使一个机房宕机,还可以继续提供服务。
 
 其实,满足需求和需求变化、满足软件稳定运行是架构的目标,对人员和技术的组织是手段。架构设计,就是要控制这些技术不确定问题。
 
@@ -46,7 +46,7 @@
 
 比如说有人把一个小网站拆分成几十个微服务运行,也是一种架构设计,但是这样,无论是开发成本还是运行成本都很高。
 
-所以架构设计的目标, **是用最小的人力成本来满足需求的开发和响应需求的变化,用最小的运行成本来保障软件的运行。**
+所以架构设计的目标,**是用最小的人力成本来满足需求的开发和响应需求的变化,用最小的运行成本来保障软件的运行。**
 
 架构设计,已经有很多成熟的方法。比如说:
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25422\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25422\350\256\262.md"
index af8a1e1ed..4e8731bf8 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25422\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25422\350\256\262.md"
@@ -10,13 +10,13 @@
 
 包括我们自己做技术选型时,也会有很多个人偏好在里面。比如我以前对微软技术栈特别熟悉,也特别喜欢,做技术方案就会偏向微软技术栈;我喜欢 React,做前端技术选型,也会偏向 React 的方案。
 
-通过上一篇架构设计的学习,我们知道, **架构设计的主要目标,是要能低成本地满足需求和需求变化,低成本地保障软件运行。** 然而对技术的个人偏好,很可能让你在技术选型时,忽略架构设计的目标,导致满足需求的成本变高,或者运行成本居高不下。
+通过上一篇架构设计的学习,我们知道,**架构设计的主要目标,是要能低成本地满足需求和需求变化,低成本地保障软件运行。** 然而对技术的个人偏好,很可能让你在技术选型时,忽略架构设计的目标,导致满足需求的成本变高,或者运行成本居高不下。
 
 所以今天,我们一起来探讨一下,在软件工程中,怎么样才能避免这种选型的倾向性,科学客观地做好技术选型。
 
 ## 技术选型就是项目决策
 
-技术选型,就是在两个或者多个技术方案中选择适合当时项目情况的方案。 **技术选型看起来是个技术的选择,但其实是一个和项目情况密切相关的项目决策。**
+技术选型,就是在两个或者多个技术方案中选择适合当时项目情况的方案。**技术选型看起来是个技术的选择,但其实是一个和项目情况密切相关的项目决策。**
 
 在项目中,除了技术上的选型,类似的选择也有很多,比如说产品设计中:某个功能该不该加?该选哪种动画效果?比如制定测试方案的时候,选择哪一种压力测试工具?选择哪个测试框架?这些选择,本质上就是一种项目决策。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25424\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25424\350\256\262.md"
index 2490a7f93..583079112 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25424\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25424\350\256\262.md"
@@ -22,7 +22,7 @@
 
 ![img](assets/7fa5c8351b4590a2bc8a482955c133f7.jpg)
 
-范围不减,成本不增加,还想节约时间走捷径,就会影响到质量。这个“质量”,不只是产品质量,还有架构质量和代码质量。这种对质量的透支,就是一种债务。 **而技术债务,就是软件项目中对架构质量和代码质量的透支。**
+范围不减,成本不增加,还想节约时间走捷径,就会影响到质量。这个“质量”,不只是产品质量,还有架构质量和代码质量。这种对质量的透支,就是一种债务。**而技术债务,就是软件项目中对架构质量和代码质量的透支。**
 
 技术债务确实是个形象生动的比喻,让你意识到它是和成本挂钩的,而且技术债务也有金融债务的一些特点,比如有利息,再比如技术债务也有好的一面。
 
@@ -109,7 +109,7 @@ Martin Fowler 画过一张图,来形象的描述了设计、时间和开发速
 
 如果是现实中的债务,查查银行账户就很容易知道是不是欠债了,而技术债务却没那么直观,但识别技术债务是很关键一步,只有发现系统中的技术债务,才能去找到合适的方案解决它。
 
-你要是细心观察,还是可以通过很多指标来发现软件项目存在的技术债务。比如说: **开发速度降低** :通常项目正常情况下,在相同的时间间隔下,完成的任务是接近的。尤其是使用敏捷开发的团队,每个任务会评估故事分数,每个 Sprint 能完成的故事分数是接近的。但是如果单位时间内能完成的任务数明显下降,那很可能是技术债务太多导致的。 **单元测试代码覆盖率低** :现在大部分语言都有单元测试覆盖率的检测工具,通过工具可以很容易知道当前项目单元测试覆盖率如何,如果覆盖率太低或者下降厉害,就说明存在技术债务了。 **代码规范检查的错误率高** :现在主流的语言也有各种规范和错误检查工具,也叫 lint 工具,比如 Javascript 就有 eslint,Swift 有 SwiftLint,python 有 pylint。通过各种 lint 工具,可以有效发现代码中潜在的错误和不规范之处,如果错误率高,则说明代码质量不够好。 **Bug 数量越来越多** :正常情况下,如果没有新功能开发,Bug 数量会越来越少。但是如果 Bug 数量下降很慢,甚至有增多的迹象,那说明代码质量或者架构可能存在比较大问题。
+你要是细心观察,还是可以通过很多指标来发现软件项目存在的技术债务。比如说: **开发速度降低** :通常项目正常情况下,在相同的时间间隔下,完成的任务是接近的。尤其是使用敏捷开发的团队,每个任务会评估故事分数,每个 Sprint 能完成的故事分数是接近的。但是如果单位时间内能完成的任务数明显下降,那很可能是技术债务太多导致的。**单元测试代码覆盖率低** :现在大部分语言都有单元测试覆盖率的检测工具,通过工具可以很容易知道当前项目单元测试覆盖率如何,如果覆盖率太低或者下降厉害,就说明存在技术债务了。**代码规范检查的错误率高** :现在主流的语言也有各种规范和错误检查工具,也叫 lint 工具,比如 Javascript 就有 eslint,Swift 有 SwiftLint,python 有 pylint。通过各种 lint 工具,可以有效发现代码中潜在的错误和不规范之处,如果错误率高,则说明代码质量不够好。**Bug 数量越来越多** :正常情况下,如果没有新功能开发,Bug 数量会越来越少。但是如果 Bug 数量下降很慢,甚至有增多的迹象,那说明代码质量或者架构可能存在比较大问题。
 
 除了上面这些指标,其实你还能找到一些其他指标,比如你用的语言或者框架的版本是不是太老,早已无人更新维护了;比如开发人员总是需要加班加点才能赶上进度,如果架构良好、代码质量良好,这些加班本是可以避免的。
 
@@ -133,7 +133,7 @@ Martin Fowler 画过一张图,来形象的描述了设计、时间和开发速
 
 每次只是改进系统其中一部分功能,在不改变功能的情况下,只对内部结构和代码进行重新整理,不断调整优化系统的结构,最终完全偿还技术债务。这种方式优点很多,例如不会导致系统不稳定,对业务影响很小。缺点就是整个过程耗时相对更久。
 
-这三种策略并没有绝对好坏,需要根据当前项目场景灵活选择。有个简单原则可以帮助你选择, **那就是看哪一种策略投入产出比更好。**
+这三种策略并没有绝对好坏,需要根据当前项目场景灵活选择。有个简单原则可以帮助你选择,**那就是看哪一种策略投入产出比更好。**
 
 无论选择哪种策略,都是要有投入的,也就是要有人、要花时间,而人和时间就是成本;同样,对于选择的策略,也是有收益的,比如带来开发效率的提升,节约了人和时间,这就是收益。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25426\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25426\350\256\262.md"
index 9c163b6f7..f6ee36327 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25426\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25426\350\256\262.md"
@@ -28,7 +28,7 @@
 
 图片来源:Understanding the Difference Between CI and CD
 
-瀑布模型开发的集成,或者说传统的集成,都是在开发阶段整体完成的差不多了,才开始集成。而持续集成的做法,则是每次有代码合并入主干之前,都进行集成,持续的集成。 **代码集成到主干之前,必须通过自动化测试,只要有一个测试用例失败,就不能集成。**
+瀑布模型开发的集成,或者说传统的集成,都是在开发阶段整体完成的差不多了,才开始集成。而持续集成的做法,则是每次有代码合并入主干之前,都进行集成,持续的集成。**代码集成到主干之前,必须通过自动化测试,只要有一个测试用例失败,就不能集成。**
 
 持续集成的好处很明显:
 
@@ -164,11 +164,11 @@ Azure Pipelines是微软的持续集成平台,可以自己搭建也可以使
 
 今天我带你一起学习了与持续交付相关的一些概念:
 
-- **持续集成** ,就是持续频繁地将代码从分支集成到主干,并且要保证在合并到主干之前,必须要通过所有的自动化测试。
+- **持续集成**,就是持续频繁地将代码从分支集成到主干,并且要保证在合并到主干之前,必须要通过所有的自动化测试。
 
-- **持续交付** ,则是基于持续集成,在自动化测试完成后,同时构建生成各个环境的发布包,部署到测试环境,但生产环境的部署需要手动确认。
+- **持续交付**,则是基于持续集成,在自动化测试完成后,同时构建生成各个环境的发布包,部署到测试环境,但生产环境的部署需要手动确认。
 
-- **持续部署** ,是在持续交付的基础上,对生产环境的部署也采用自动化。
+- **持续部署**,是在持续交付的基础上,对生产环境的部署也采用自动化。
 
 要搭建持续交付环境,首先需要做好准备工作,例如自动化测试代码和自动部署脚本;然后要选择好持续集成工具;最后按照选择的持续集成工具来实施。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25427\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25427\350\256\262.md"
index 9a6de5dae..c47771eb0 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25427\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25427\350\256\262.md"
@@ -36,7 +36,7 @@
 1. 修复 Bug——改 Bug 最大的挑战其实是重现问题,也就是发现问题,然后再分析问题,最后解决问题;
 1. 重构代码、优化性能——对代码重构,优化性能,最难的地方其实在于发现代码问题在哪,发现性能的瓶颈,后面再去寻找解决方案,最后再解决。
 
-也就是说, **软件工程师这些日常开发工作的核心还是在发现问题、分析问题和解决问题,在这里我统称为解决问题的能力。** 这几个能力看起来没什么稀奇,但是要仔细分析,其实软件工程师的水平高低,恰恰就体现在解决问题的能力上面。
+也就是说,**软件工程师这些日常开发工作的核心还是在发现问题、分析问题和解决问题,在这里我统称为解决问题的能力。** 这几个能力看起来没什么稀奇,但是要仔细分析,其实软件工程师的水平高低,恰恰就体现在解决问题的能力上面。
 
 #### 发现问题
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25428\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25428\350\256\262.md"
index 68b6b30a1..295a23bc3 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25428\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25428\350\256\262.md"
@@ -16,7 +16,7 @@
 
 - **首先需要在一个技术领域深耕** 每个人精力其实很有限的,一开始专注在一个技术领域容易在短时间取得成绩,同时也相当于建立起了最初的知识体系,在未来的知识森林里种下的第一棵大树,这样当你开始学习新的技术的时候,已有的知识就可以直接借用,相当于这棵大树可以帮助新的知识树的成长提供很好的养分,快速培养出新的大树。
 
-如果一开始就同时涉猎多个领域,每个领域的知识又没有掌握好,这样的知识是没法共享的,就相当于你种的只是一片知识的灌木,最终只能收获像灌木丛的知识体系。 **只有一个领域的知识你真正吃透,才能有效地共享到其他领域,构成一个知识领域的森林。**
+如果一开始就同时涉猎多个领域,每个领域的知识又没有掌握好,这样的知识是没法共享的,就相当于你种的只是一片知识的灌木,最终只能收获像灌木丛的知识体系。**只有一个领域的知识你真正吃透,才能有效地共享到其他领域,构成一个知识领域的森林。**
 
 我在毕业之后,整整有 6 年时间,是一直专注于 Asp.Net 技术领域,让我能成为这个领域的专家,也帮我构建了最基础的知识体系。所以在后来我去学 iOS 开发时,发现像面向对象、网络协议、数据存储、MVC 开发模式这些知识,我就不需要再去学习,因为我的知识体系已经有了这部分知识。
 
@@ -54,11 +54,11 @@
 
 这其实很像我在专栏开头就提到的工程思维,遇到一个项目,哪怕不是软件项目,我也可以借鉴瀑布模型,用工程方法去解决。所以你要提高解决问题的能力,就要形成自己的一套解决问题的方法论。
 
-在这里分享我解决问题的一套方法论,其实很简单,只有三步。 **第一步:明确问题** 解决问题,最重要的一步就是要明确问题是什么,这其实就跟做项目需要先做需求分析一样,搞清楚目标是什么,才能做到有的放矢。
+在这里分享我解决问题的一套方法论,其实很简单,只有三步。**第一步:明确问题** 解决问题,最重要的一步就是要明确问题是什么,这其实就跟做项目需要先做需求分析一样,搞清楚目标是什么,才能做到有的放矢。
 
-同时这一步也要透过现象看本质,去明确问题背后是不是还有其他问题。就像我在前一篇文章中举例的抽奖项目,不能光看到功能需求,还需要看到安全上的需求;网络异常的问题,不能光想着应用程序错误,还要看看网络是不是有问题。 **第二步:拆分和定位问题** 前面我们学习架构思维的时候,也提到了,一个复杂的问题,只有经过拆分,才好找到本质的问题。
+同时这一步也要透过现象看本质,去明确问题背后是不是还有其他问题。就像我在前一篇文章中举例的抽奖项目,不能光看到功能需求,还需要看到安全上的需求;网络异常的问题,不能光想着应用程序错误,还要看看网络是不是有问题。**第二步:拆分和定位问题** 前面我们学习架构思维的时候,也提到了,一个复杂的问题,只有经过拆分,才好找到本质的问题。
 
-就像上面举的解决网络异常的例子,我首先拆分成程序问题和网络问题,通过回滚观察,我就可以基本上断定不是程序问题,那就说明是网络问题。然后对于网络问题,将整个请求过程拆分,最终就可以定位到有问题的环节。 **第三步:提出解决方案并总结**
+就像上面举的解决网络异常的例子,我首先拆分成程序问题和网络问题,通过回滚观察,我就可以基本上断定不是程序问题,那就说明是网络问题。然后对于网络问题,将整个请求过程拆分,最终就可以定位到有问题的环节。**第三步:提出解决方案并总结**
 
 发现并分析完问题后,找到解决方案是容易的,但很有必要总结一下。总结要做的就是两点:
 
@@ -68,7 +68,7 @@
 
 通过总结,就可以进一步提升解决问题的经验。如果你对于解决技术上的问题还没有总结出来自己的方法论,不妨可以先参考这套方法,一步步去发现问题,分析问题和解决问题。
 
-尤其是在解决完问题后,再想一想如何预防以后类似问题的发生。 **如果每次解决完问题,你还能提出一个预防问题发生的方案,一定会让大家印象深刻的。**
+尤其是在解决完问题后,再想一想如何预防以后类似问题的发生。**如果每次解决完问题,你还能提出一个预防问题发生的方案,一定会让大家印象深刻的。**
 
 ## 如何提升影响力?
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25430\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25430\350\256\262.md"
index 0fa4aafe4..6e19dd3ba 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25430\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25430\350\256\262.md"
@@ -128,7 +128,7 @@ Coding是国内一个不错的代码托管平台,5 人以下的私有库免费
 
 #### 原则三:提交的代码要有人审查
 
-代码审查是自动化测试之外,一种非常行之有效的提高质量的手段,通过代码审查,可以发现代码中潜在的问题。通过代码审查,也可以加强团队的技术交流,让水平高的开发人员 Review,可以帮助提升整体代码水平;Review 高水平的代码也是一种非常有效的学习方法。 **怎么做好代码审查呢?**
+代码审查是自动化测试之外,一种非常行之有效的提高质量的手段,通过代码审查,可以发现代码中潜在的问题。通过代码审查,也可以加强团队的技术交流,让水平高的开发人员 Review,可以帮助提升整体代码水平;Review 高水平的代码也是一种非常有效的学习方法。**怎么做好代码审查呢?**
 
 我的经验是,在审查别人代码的时候,先了解清楚这个提交的代码要解决的是什么问题,想象一下如果是自己来写这个代码会怎么写。这样在审查的时候,就能发现一些和自己不一样的地方,别人好的地方我们可以学习,不对的地方应该指出。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25431\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25431\350\256\262.md"
index a3dc31265..553a90c31 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25431\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25431\350\256\262.md"
@@ -8,7 +8,7 @@
 
 上线后,如果因为有测试漏测导致的 Bug,测试人员还要为质量问题背锅,受到责备。上面这样的场景到现在也还在很多软件项目中上演。但这对测试人员其实是不公平的。
 
-因为软件开发是多个环节组成的,从最开始的需求,到后面的设计、开发,每个环节都可能会导致质量问题, **而测试只能对已经开发完成的软件产品进行检测,并不能干预整个过程。** 比如说测试是无法对开发写的代码直接测试的,只能基于软件功能去测试,也就是说对于代码的质量,测试人员其实是没有什么办法的。
+因为软件开发是多个环节组成的,从最开始的需求,到后面的设计、开发,每个环节都可能会导致质量问题,**而测试只能对已经开发完成的软件产品进行检测,并不能干预整个过程。** 比如说测试是无法对开发写的代码直接测试的,只能基于软件功能去测试,也就是说对于代码的质量,测试人员其实是没有什么办法的。
 
 那到底谁应该为产品质量负责呢?在回答这个问题之前,你不妨先思考一个更本质的问题:什么是软件产品质量?
 
@@ -18,7 +18,7 @@
 
 因为不同的人对软件质量好坏的评判角度是不同的。比如对用户来说,更看重产品是不是满足需求,是不是美观好用;对开发来说,看重的是代码质量是不是高,是不是好维护;对于软件测试人员而言,看重的是 Bug 数量、安全、性能等指标;对于项目负责人,看重的是整个开发过程的质量,是不是成本可控、如期完成。
 
-在这个问题上,我比较认同《The Three Aspects of Software Quality: Functional, Structural, and Process》这篇文章作者 David Chappell 的观点,他把软件质量分成了三个考量方面:功能、结构和流程。对于他提的“结构质量”,我认为定义为“代码质量”更贴切,也就是说, **功能质量、代码质量和过程质量这三个方面组合在一起,很好地概括了软件质量。**
+在这个问题上,我比较认同《The Three Aspects of Software Quality: Functional, Structural, and Process》这篇文章作者 David Chappell 的观点,他把软件质量分成了三个考量方面:功能、结构和流程。对于他提的“结构质量”,我认为定义为“代码质量”更贴切,也就是说,**功能质量、代码质量和过程质量这三个方面组合在一起,很好地概括了软件质量。**
 
 所有的软件开发都是从一个想法开始的,用户需要一个软件,有人出钱,然后开发团队实施,把想法变成需求,需求变成设计,设计变成代码,代码变成软件。
 
@@ -48,11 +48,11 @@
 
 过程质量虽然也是用户不能直接感知的,但是过程质量会直接影响代码质量和功能质量,甚至是产品的成败。
 
-以上就是软件质量的三个方面, **软件质量从来不是单方面质量决定的,通常是几方面质量因素相互影响,共同决定的。** 比如说改进流程,增加了自动化测试的覆盖,应用了持续集成,这样可以提高代码质量和功能质量。或者说对代码质量过于追求,又可能会影响过程质量,例如时间延期,成本超标。
+以上就是软件质量的三个方面,**软件质量从来不是单方面质量决定的,通常是几方面质量因素相互影响,共同决定的。** 比如说改进流程,增加了自动化测试的覆盖,应用了持续集成,这样可以提高代码质量和功能质量。或者说对代码质量过于追求,又可能会影响过程质量,例如时间延期,成本超标。
 
 ## 谁该为产品质量负责?
 
-在梳理清楚产品质量的问题后,我们就可以来讨论谁该为产品质量负责的话题了。 **既然产品质量是由功能质量、代码质量和过程质量共同决定的,那么对产品质量负责,意味着要对这三方面共同负责。**
+在梳理清楚产品质量的问题后,我们就可以来讨论谁该为产品质量负责的话题了。**既然产品质量是由功能质量、代码质量和过程质量共同决定的,那么对产品质量负责,意味着要对这三方面共同负责。**
 
 在说到责任之前,我想补充一下权责对等的问题。责任和权力是需要对等的,比如说你让开发人员对软件开发过程负责,那么前提是他必须有权力去影响和控制开发过程,否则离开权力谈责任就是耍流氓了。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25433\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25433\350\256\262.md"
index 05e4efb66..6359f5cba 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25433\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25433\350\256\262.md"
@@ -34,7 +34,7 @@
 
 - 不能直观的了解当前项目的 Bug 状态,比如说:修复了多少,还有多少没有修复,近期 Bug 数量是增加了还是减少了。
 
-不难看出,通过 QQ 等方式报告的 Bug,都是文字配合图片等信息,很难检索和分类, **而 Bug 跟踪工具,采用结构化的数据来定义 Bug,每一个 Bug 都有一些关键的信息可以对 Bug 进行分类和检索。**
+不难看出,通过 QQ 等方式报告的 Bug,都是文字配合图片等信息,很难检索和分类,**而 Bug 跟踪工具,采用结构化的数据来定义 Bug,每一个 Bug 都有一些关键的信息可以对 Bug 进行分类和检索。**
 
 在 Bug 跟踪工具使用中,一个基本的 Bug 信息包括:
 
@@ -58,7 +58,7 @@
 
 这样对于开发人员来说,可以直观的看到自己有哪些 Bug 需要处理,Bug 的描述信息也可以帮助重现 Bug、快速定位到 Bug 的原因;对于项目经理或者测试人员来说,可以直观的看到哪些 Bug 还没解决,及时了解项目进展。
 
-另外,我在《12 流程和规范:红绿灯不是约束,而是用来提高效率》这篇文章中提到了项目中的流程和规范, **在软件项目中,要把好的实践流程化,把好的流程工具化。Bug 跟踪工具则很好的贯彻了这一点,将 Bug 的解决过程流程化。** 你平时在 Bug 跟踪系统中看到的 Bug 状态,看起来只是一个有限的状态列表,但背后其实是一套解决 Bug 的流程。就像下面这张图表示的这样,一个 Bug 从创建到最后结束,其实是有一个完整的流程的。
+另外,我在《12 流程和规范:红绿灯不是约束,而是用来提高效率》这篇文章中提到了项目中的流程和规范,**在软件项目中,要把好的实践流程化,把好的流程工具化。Bug 跟踪工具则很好的贯彻了这一点,将 Bug 的解决过程流程化。** 你平时在 Bug 跟踪系统中看到的 Bug 状态,看起来只是一个有限的状态列表,但背后其实是一套解决 Bug 的流程。就像下面这张图表示的这样,一个 Bug 从创建到最后结束,其实是有一个完整的流程的。
 
 ![img](assets/4f56bf4d43b652ab2b92318775a850dd.png)
 
@@ -80,7 +80,7 @@ Bug 跟踪系统的主要功能是用来跟踪 Bug 的,不是用来讨论和
 
 ## 自动化测试工具
 
-除了 Bug 跟踪工具,软件测试中还有很重要的一个工具就是自动化测试工具,虽然我在《29 自动化测试:如何把 Bug 杀死在摇篮里?》中已经有了较多篇幅说明,但这里还是想继续提一下,因为我觉得, **未来自动化测试会占据越来越多的比例,很多手工测试的工作会逐步被自动化测试代替。** 像美国 Facebook、Google、Amazon 这些大厂,单纯的手工测试职位在减少,一些手工执行测试用例检查的工作外包到了人力成本更低的像中国、印度、罗马尼亚等国家,而美国本土主要招聘的都是能写自动化测试的软件测试人员,或者直接就是开发人员来写这些自动化测试代码。
+除了 Bug 跟踪工具,软件测试中还有很重要的一个工具就是自动化测试工具,虽然我在《29 自动化测试:如何把 Bug 杀死在摇篮里?》中已经有了较多篇幅说明,但这里还是想继续提一下,因为我觉得,**未来自动化测试会占据越来越多的比例,很多手工测试的工作会逐步被自动化测试代替。** 像美国 Facebook、Google、Amazon 这些大厂,单纯的手工测试职位在减少,一些手工执行测试用例检查的工作外包到了人力成本更低的像中国、印度、罗马尼亚等国家,而美国本土主要招聘的都是能写自动化测试的软件测试人员,或者直接就是开发人员来写这些自动化测试代码。
 
 这就意味着对于软件测试人员来说,要求越来越高了,不仅要会设计测试用例,还要能写自动化测试脚本。同时对于开发人员来说,不仅要写功能代码,还需要实现一定量的自动化测试代码。
 
@@ -120,11 +120,11 @@ Bug 跟踪系统的主要功能是用来跟踪 Bug 的,不是用来讨论和
 
 #### Bug 跟踪工具
 
-在项目管理工具那一篇文章中,我已经给你介绍了一些任务跟踪系统,比如说Jira、禅道、TAPD、云效等,都可以用来跟踪 Bug。 **Bugzilla** Bugzilla 是由 Mazilla 公司提供的一款开源免费的 bug 跟踪系统。这是一款历史很悠久的产品。 **MantisBT** MantisBT 是一个简单但功能强大的开源 bug 跟踪系统,可以通过各种插件来扩展其功能。 **Redmine** Redmine 是一款开源的综合性的项目管理工具,不仅可以用于 Bug 跟踪,还可以用来跟踪项目进度。
+在项目管理工具那一篇文章中,我已经给你介绍了一些任务跟踪系统,比如说Jira、禅道、TAPD、云效等,都可以用来跟踪 Bug。**Bugzilla** Bugzilla 是由 Mazilla 公司提供的一款开源免费的 bug 跟踪系统。这是一款历史很悠久的产品。**MantisBT** MantisBT 是一个简单但功能强大的开源 bug 跟踪系统,可以通过各种插件来扩展其功能。**Redmine** Redmine 是一款开源的综合性的项目管理工具,不仅可以用于 Bug 跟踪,还可以用来跟踪项目进度。
 
 #### 自动化测试工具
 
-除了传统的桌面应用外,现在移动设备的普及,要测试的终端也越来越多。借助一些自动化测试工具,可以帮助简化多设备的测试。下面简单介绍几个自动化测试工具。 **Selenium** Selenium 是一个 Web 端的自动化测试工具,直接运行在浏览器中,用来模拟用户操作。类似的还有WebDriverIO 和 Nightwatch.js ,支持 Javascript,API 更简单更方便。 **Appium** Appium 是一个开源、跨平台的自动化测试工具,用于测试移动原生应用,支持 iOS, Android 系统。 **Macaca** Macaca 是阿里巴巴开源的一款面向多端的自动化测试工具,支持桌面端、Web、移动端、真实设备和模拟器。
+除了传统的桌面应用外,现在移动设备的普及,要测试的终端也越来越多。借助一些自动化测试工具,可以帮助简化多设备的测试。下面简单介绍几个自动化测试工具。**Selenium** Selenium 是一个 Web 端的自动化测试工具,直接运行在浏览器中,用来模拟用户操作。类似的还有WebDriverIO 和 Nightwatch.js ,支持 Javascript,API 更简单更方便。**Appium** Appium 是一个开源、跨平台的自动化测试工具,用于测试移动原生应用,支持 iOS, Android 系统。**Macaca** Macaca 是阿里巴巴开源的一款面向多端的自动化测试工具,支持桌面端、Web、移动端、真实设备和模拟器。
 
 更多自动化测试工具可以参考:[Best Automation Testing Tools for 2019 (Top 10 reviews)](https://medium.com/@briananderson2209/best-automation-testing-tools-for-2018-top-10-reviews-8a4a19f664d2),([中文版](https://segmentfault.com/a/1190000012016234))。
 
@@ -132,21 +132,21 @@ Bug 跟踪系统的主要功能是用来跟踪 Bug 的,不是用来讨论和
 
 很多软件在上线后,需要面对巨大的用户访问量,但如果等到上线后才发现程序性能不行,访问量一大就会导致服务崩溃,那就太晚了。所以最好是在测试阶段,就能测试出来程序的性能如何,瓶颈在哪里,然后在发布前对程序进行优化,确保能满足性能要求。
 
-对程序性能的测试,就需要借助压力测试工具来模拟大量用户并发访问的场景。下面简单介绍一下几款常用的性能测试工具。 **Apache JMeter** JMeter 是一款开源的压力测试工具,纯 Java 应用程序。 **LoadRunner** LoadRunner 是惠普旗下的一款商业自动负载测试工具,可以通过录制的方式制作测试脚本,上手容易功能强大,可以方便的监控和分析性能测试结果。 **阿里云性能测试 PTS** 阿里云性能测试 PTS 是基于云端的压力测试服务,可以模拟从全国各地域运营商网络发起的流量,真实地反映使用情况,生成有价值的性能测试报告。 **WebPageTest** WebPageTest 是一个可以用来测试和分析网页性能的在线工具,支持不同浏览器,支持 API。可参考《WebPagetest H5 性能测试工具入门详解》。
+对程序性能的测试,就需要借助压力测试工具来模拟大量用户并发访问的场景。下面简单介绍一下几款常用的性能测试工具。**Apache JMeter** JMeter 是一款开源的压力测试工具,纯 Java 应用程序。**LoadRunner** LoadRunner 是惠普旗下的一款商业自动负载测试工具,可以通过录制的方式制作测试脚本,上手容易功能强大,可以方便的监控和分析性能测试结果。**阿里云性能测试 PTS** 阿里云性能测试 PTS 是基于云端的压力测试服务,可以模拟从全国各地域运营商网络发起的流量,真实地反映使用情况,生成有价值的性能测试报告。**WebPageTest** WebPageTest 是一个可以用来测试和分析网页性能的在线工具,支持不同浏览器,支持 API。可参考《WebPagetest H5 性能测试工具入门详解》。
 
 更多性能测试工具介绍可以参考:《10 大主流压力 / 负载 / 性能测试工具推荐》。
 
 #### 安全性测试工具
 
-软件的安全性是非常重要的指标,有时候开发人员缺乏安全意识,就可能会导致程序存在安全漏洞。安全领域也是开发和测试之外的一个技术领域,中小公司一般不会有自己专业的安全团队,就需要借助一些安全性测试工具来帮助对软件进行安全性检测。 **HP Fortify On Demand** Fortify On Demand 是惠普旗下的一款安全检测工具,可以通过分析源代码、二进制程序或者应用程序 URL 检测程序安全漏洞。 **Sqlmap** Sqlmap是一款开源免费的检测 SQL 注入的工具。 **IBM Application Security APPScan** APPScan 是 IBM 旗下的一款漏洞扫描工具,支持网站和移动 App。
+软件的安全性是非常重要的指标,有时候开发人员缺乏安全意识,就可能会导致程序存在安全漏洞。安全领域也是开发和测试之外的一个技术领域,中小公司一般不会有自己专业的安全团队,就需要借助一些安全性测试工具来帮助对软件进行安全性检测。**HP Fortify On Demand** Fortify On Demand 是惠普旗下的一款安全检测工具,可以通过分析源代码、二进制程序或者应用程序 URL 检测程序安全漏洞。**Sqlmap** Sqlmap是一款开源免费的检测 SQL 注入的工具。**IBM Application Security APPScan** APPScan 是 IBM 旗下的一款漏洞扫描工具,支持网站和移动 App。
 
 更多安全性测试工具介绍可以参考:[11 款常用的安全测试工具](https://blog.csdn.net/lb245557472/article/details/88572607), [安全测试工具篇(开源 & 商业)](http://codeshold.me/2016/09/security_tolls.html), [最受欢迎的软件安全性测试工具有哪些?](https://mp.weixin.qq.com/s/OZp7Q8Jq_voTAkHj3CAMMQ?)。
 
 #### 浏览器兼容性测试工具
 
-网站开发最苦恼的问题之一就是浏览器兼容问题,不仅要兼容 Chrome、IE/Edge、Firefox 三大主流浏览器,还得考虑桌面设备和移动设备上的不同表现。如果人工对所有浏览器做兼容性测试,工作量比较大。好在也有一些不错的工具可以帮助做兼容性测试。 **Browsera** Browsera 可以对不同浏览器下的布局提供报告,包括截图和 Javascript 错误。 **Browslering** Browslering 可以针对不同浏览器进行测试,它在虚拟机中运行真实桌面浏览器,还可以人工进行交互。
+网站开发最苦恼的问题之一就是浏览器兼容问题,不仅要兼容 Chrome、IE/Edge、Firefox 三大主流浏览器,还得考虑桌面设备和移动设备上的不同表现。如果人工对所有浏览器做兼容性测试,工作量比较大。好在也有一些不错的工具可以帮助做兼容性测试。**Browsera** Browsera 可以对不同浏览器下的布局提供报告,包括截图和 Javascript 错误。**Browslering** Browslering 可以针对不同浏览器进行测试,它在虚拟机中运行真实桌面浏览器,还可以人工进行交互。
 
-更多浏览器兼容性测试工具可参考《10 个免费的顶级跨浏览器测试工具》 **测试用例管理工具** 我们在上一篇里面已经学习了,设计测试用例是软件测试很重要的工作,有专业的工具帮助管理测试用例,也可以起到事半功倍的效果。 **TestRail** TestRail 是 TestRail 是一个专注于管理测试用例的工具,可以用它来创建测试用例和用例集,跟踪测试用例的执行和生成报告。 **飞蛾**
+更多浏览器兼容性测试工具可参考《10 个免费的顶级跨浏览器测试工具》 **测试用例管理工具** 我们在上一篇里面已经学习了,设计测试用例是软件测试很重要的工作,有专业的工具帮助管理测试用例,也可以起到事半功倍的效果。**TestRail** TestRail 是 TestRail 是一个专注于管理测试用例的工具,可以用它来创建测试用例和用例集,跟踪测试用例的执行和生成报告。**飞蛾**
 
 飞蛾 是 Coding 旗下的测试管理工具,对中文支持好,界面美观。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25434\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25434\350\256\262.md"
index 0aa301af2..affbb7e5b 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25434\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25434\350\256\262.md"
@@ -42,7 +42,7 @@
 
 还有 CSDN,对用户密码明文存储到数据库中,数据库泄露后,用户密码也跟着一起泄露了,而大多数用户习惯于在不同的网站也用相同的密码,导致在其他网站的密码也一起泄露了。
 
-对于软件来说, **我们不能假设数据存储是安全的,而是要考虑到数据是有泄露的可能,提前做好预防措施,对敏感数据进行加密。**
+对于软件来说,**我们不能假设数据存储是安全的,而是要考虑到数据是有泄露的可能,提前做好预防措施,对敏感数据进行加密。**
 
 在了解了这些常见的安全问题来源和可能带来的后果之后,我们在软件开发时,就可以对薄弱环节、重点问题进行提前预防,在开发时就考虑到可能的安全漏洞,做出科学的应对方案。
 
@@ -60,7 +60,7 @@
 
 - **需求阶段**
 
-需求是软件项目的源头, **在确定需求,做产品设计的时候,不仅要考虑到功能上的需求,还要同时考虑到安全方面的要求。** 这样当你在架构设计的时候,就不会轻易遗漏安全方面的架构设计。
+需求是软件项目的源头,**在确定需求,做产品设计的时候,不仅要考虑到功能上的需求,还要同时考虑到安全方面的要求。** 这样当你在架构设计的时候,就不会轻易遗漏安全方面的架构设计。
 
 尤其是对于一些外包项目,如果在需求中没有提出安全需求,大概率外包公司是不会帮你考虑这些需求的。
 
@@ -112,7 +112,7 @@
 
 > 不要只反馈是否 OK,同时也把支付的金额和 OK 一起返回过去。是支付 2000 元 OK 还是 1 元 OK。这就解决了问题,现在的电商都改成这个设计了。
 
-通过这样一些好的安全设计原则,就可以在设计阶段把很多安全问题预防好。 **开发阶段**
+通过这样一些好的安全设计原则,就可以在设计阶段把很多安全问题预防好。**开发阶段**
 
 只是设计阶段做好安全相关的设计还不好,很多安全问题其实都是编码阶段时,没有好的编码习惯、没有良好的安全意识导致的。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25435\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25435\350\256\262.md"
index 515b42b51..9af5adfa6 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25435\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25435\350\256\262.md"
@@ -30,7 +30,7 @@
 
 而实际上,并不代表你需要完成所有的功能,或者没有任何 Bug,有一个完美的版本才能上线。毕竟追求完美是没有止境的,这世界上也不存在完美的软件,很多著名的软件,比如 Windows、Office、iOS 都免不了在发布后还要打补丁。
 
-这里的关键在于, **要在用户(或客户)的心理预期和你软件的实际情况之间,达到一种平衡,让软件的功能和质量,满足好用户的预期。**
+这里的关键在于,**要在用户(或客户)的心理预期和你软件的实际情况之间,达到一种平衡,让软件的功能和质量,满足好用户的预期。**
 
 要合理管理好用户的预期,达到好的发布效果,就需要在版本发布前先做好版本发布的规划。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25436\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25436\350\256\262.md"
index 745d1796a..b14848c33 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25436\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25436\350\256\262.md"
@@ -106,11 +106,11 @@ DevOps 看起来很美好,也许你迫不及待想去实施,但 DevOps 这
 
 对于应急响应流程,首先应该能第一时间通知最合适的人去处理,比如负责这个服务值班的开发人员,然后对于怎么第一时间恢复应该有准备,涉及跨部门协作也应该有相应的配合流程;最后对于故障应该有总结,避免类似情况再次发生。
 
-有关监控和日志分析,我还会在我们专栏后续文章《 监控和日志分析:如何借助工具快速发现和定位产品问题 ?》中有更多介绍。 **然后,要构建基于云计算和虚拟化技术的基础设施。** 虽然并非每一个软件项目都是基于云计算或虚拟化技术来搭建的,但云计算和虚拟化技术方面的技术,其实是横跨开发和运维的,可能对于大部分开发和运维来说,都只了解其中一部分知识,这就需要有人能同时懂软件开发和云计算或虚拟化技术,或者一起协作,才能搭建出真正适合云计算或虚拟化技术的架构。
+有关监控和日志分析,我还会在我们专栏后续文章《 监控和日志分析:如何借助工具快速发现和定位产品问题 ?》中有更多介绍。**然后,要构建基于云计算和虚拟化技术的基础设施。** 虽然并非每一个软件项目都是基于云计算或虚拟化技术来搭建的,但云计算和虚拟化技术方面的技术,其实是横跨开发和运维的,可能对于大部分开发和运维来说,都只了解其中一部分知识,这就需要有人能同时懂软件开发和云计算或虚拟化技术,或者一起协作,才能搭建出真正适合云计算或虚拟化技术的架构。
 
 构建出来基于云计算和虚拟化技术的基础设施后,对于开发人员来说,只要通过 API 或脚本即可搭建应用,对于运维来说,也只要通过脚本和工具即可管理。
 
-这其实也是 DevOps 中的[基础设施即代码](https://insights.thoughtworks.cn/nfrastructure-as-code/)的概念。 **最后,要形成 DevOps 的文化。**
+这其实也是 DevOps 中的[基础设施即代码](https://insights.thoughtworks.cn/nfrastructure-as-code/)的概念。**最后,要形成 DevOps 的文化。**
 
 DevOps 最核心的本质就是工作方式和协作的文化,而这样的文化需要有人引领,一点点去形成。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25439\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25439\350\256\262.md"
index 68ed66201..2bf62987a 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25439\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25439\350\256\262.md"
@@ -57,11 +57,11 @@
 
 **第一步:回顾项目目标** 每个项目在最开始的时候都会确定项目的目标,所以复盘的第一步,就是要回顾最初的项目目标,方便对最终结果进行评估。
 
-在这个环节,需要你描述清楚当初定的项目目标是什么?项目计划中制定的里程碑是什么?其中的关键就在于,对目标的描述要尽可能准确和客观。 **因为只有做到准确和客观,在后续你才能对目标的完成情况进行准确地评估。** 比如说:“我们的目标是做一款伟大的产品”,就不算是准确客观,因为“伟大”是一个根据主观评判的形容词,每个人对伟大的理解是不同的。
+在这个环节,需要你描述清楚当初定的项目目标是什么?项目计划中制定的里程碑是什么?其中的关键就在于,对目标的描述要尽可能准确和客观。**因为只有做到准确和客观,在后续你才能对目标的完成情况进行准确地评估。** 比如说:“我们的目标是做一款伟大的产品”,就不算是准确客观,因为“伟大”是一个根据主观评判的形容词,每个人对伟大的理解是不同的。
 
 你需要将这类形容词换成具体可考核的检查项,比如,可以总结出类似于这样的目标:“三个月时间完成一款在线学习网站产品,包括登录、在线学习、留言等主要功能模块,上线后的 Bug 比例低于上一款产品。”
 
-最后再加上最初定的里程碑,比如说:“两个月开始内部测试,三个月正式上线。”这样,大家就可以对目标的完成情况有清晰地认识。 **第二步:评估项目结果**
+最后再加上最初定的里程碑,比如说:“两个月开始内部测试,三个月正式上线。”这样,大家就可以对目标的完成情况有清晰地认识。**第二步:评估项目结果**
 
 在对项目的目标进行回顾后,就可以来看看项目的实际结果和当初的目标有多少差异了。这里需要列出两方面的差异:好的差异和坏的差异。
 
@@ -101,7 +101,7 @@
 
 设计时没有考虑到需求的变更,导致需求变更发生后,很多设计需要修改,最终导致延期。
 
-在分析的时候,可以营造一个宽松的氛围,让团队成员能畅所欲言,讨论时要做到对事不对人,尽可能客观地分析清楚成功和失败的原因。 **只有分析清楚原因,才能总结出规律。**  **第四步:总结规律,落实行动**
+在分析的时候,可以营造一个宽松的氛围,让团队成员能畅所欲言,讨论时要做到对事不对人,尽可能客观地分析清楚成功和失败的原因。**只有分析清楚原因,才能总结出规律。**  **第四步:总结规律,落实行动**
 
 分析出原因后还不够,最重要的是,还需要去总结背后的规律,才能真正把成功或失败的经验变成个人和团队的能力。这里也可以充分运用你在《软件工程之美》专栏中学习到的知识,去帮助你总结规律。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25440\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25440\350\256\262.md"
index 8993eb6a5..1730aeb27 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25440\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25440\350\256\262.md"
@@ -62,7 +62,7 @@
 
 团队建设,绕不开几件事:招人、培养人、管理人和开除人。
 
-- **小团队如何招人** 小团队招人,难点在于成本有限,开不出很高的工资,品牌也不够吸引人,招人的时候相对选择有限,能否直接招到技术大牛就得看运气了。 **但这不意味着就要大幅降低标准,比较现实的方法就是招有潜力的程序员培养。**
+- **小团队如何招人** 小团队招人,难点在于成本有限,开不出很高的工资,品牌也不够吸引人,招人的时候相对选择有限,能否直接招到技术大牛就得看运气了。**但这不意味着就要大幅降低标准,比较现实的方法就是招有潜力的程序员培养。**
 
 那么怎么知道候选人是不是有培养潜力呢?可以参考我们专栏《27 软件工程师的核心竞争力是什么?(上)》这篇文章,考察候选人的学习能力、解决问题能力。
 
@@ -70,7 +70,7 @@
 
 但我在这种方式上也犯过错误,就是新人的比例太高,中间断层,日常的技术指导和代码审查一度跟不上,导致代码质量低下。所以在招人时,也不能一味节约成本,还要注意梯队的建设,中间要有几个有经验的技术骨干帮助把控好代码质量。
 
-- **小团队如何培养人** 在培养人方面,相对来说,小团队不像大公司有完善的培训制度,资源也有限,难以请到外面的人来讲课, **所以培养人主要还是要靠内部形成好的学习分享的机制。**
+- **小团队如何培养人** 在培养人方面,相对来说,小团队不像大公司有完善的培训制度,资源也有限,难以请到外面的人来讲课,**所以培养人主要还是要靠内部形成好的学习分享的机制。**
 
 在大厂,新人加入,通常会指定一个 Mentor,也就是导师或者师傅,可以帮助新人快速融入环境,新人有问题也可以随时请教。这种师傅带新人的机制其实对小团队一样适用,对新人来说可以快速融入,及时获得指导,对于师傅来说,通过带人,也能促进自身的成长。
 
@@ -86,7 +86,7 @@
 
 - 小团队如何管理人
 
-因为小团队人数不多,对人的管理上,可以不需要像大公司一样用复杂的组织结构,用复杂的管理制度。 **小团队的管理,核心在于营造好的氛围,鼓励成员自我驱动去做事。**
+因为小团队人数不多,对人的管理上,可以不需要像大公司一样用复杂的组织结构,用复杂的管理制度。**小团队的管理,核心在于营造好的氛围,鼓励成员自我驱动去做事。**
 
 其实这个理念和敏捷开发的理念是吻合的。在专栏文章《05 敏捷开发到底是想解决什么问题?》中,我也提到了:敏捷开发的实施,离不开扁平化的组织结构,更少的控制,更多的发挥项目组成员的主动性。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25442\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25442\350\256\262.md"
index bcc54e2e9..f9120d1e6 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25442\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25442\350\256\262.md"
@@ -82,7 +82,7 @@ IEEE(电气和电子工程师协会)有一个专门的网页,把过去十
 
 #### 案例 1. 来自地狱的项目
 
-**案例描述:** 这个案例来自法国政府,当时参与项目的一名项目成员专门为这个项目开了一个博客叫ProjectFailures,将这个项目描述为来自地狱的项目。原计划 2-3 年开发,结果干了十几年都没有完成,最终以项目负责人被以欺诈罪关进监狱而告终。详细内容可以查看中文版本:《[开发 12 年 整整 6 百万行代码:史上最烂的开发项目长这样](https://zhuanlan.zhihu.com/p/39827365)》。 **案例分析:**
+**案例描述:** 这个案例来自法国政府,当时参与项目的一名项目成员专门为这个项目开了一个博客叫ProjectFailures,将这个项目描述为来自地狱的项目。原计划 2-3 年开发,结果干了十几年都没有完成,最终以项目负责人被以欺诈罪关进监狱而告终。详细内容可以查看中文版本:《[开发 12 年 整整 6 百万行代码:史上最烂的开发项目长这样](https://zhuanlan.zhihu.com/p/39827365)》。**案例分析:**
 
 - **外部环境** :法国政府官员腐败,对于项目进度并没有施加压力;
 
@@ -92,7 +92,7 @@ IEEE(电气和电子工程师协会)有一个专门的网页,把过去十
 
 - **组织文化** :禁止超过 9 点打卡,禁止喝咖啡等奇葩要求。
 
-#### 案例 2. 美国联邦调查局虚拟案件文档系统 **案例描述:** FBI(美国联邦调查局)虚拟案件文档系统的项目开始与 2001 年,项目初始目标是 3 年内将原有的 FBI 案件文档管理系统升级,但因为 911 恐怖袭击事件爆发,项目目标从升级变成了重写。最终 2005 年项目宣布废弃,而此时已经在这个项目上花费了 1.7 亿美元。有关项目的细节可以参考:《[著名豆腐渣软件项目:美国联邦调查局虚拟案件文档系统](https://linux.cn/article-2307-1.html)》。 **案例分析:**
+#### 案例 2. 美国联邦调查局虚拟案件文档系统 **案例描述:** FBI(美国联邦调查局)虚拟案件文档系统的项目开始与 2001 年,项目初始目标是 3 年内将原有的 FBI 案件文档管理系统升级,但因为 911 恐怖袭击事件爆发,项目目标从升级变成了重写。最终 2005 年项目宣布废弃,而此时已经在这个项目上花费了 1.7 亿美元。有关项目的细节可以参考:《[著名豆腐渣软件项目:美国联邦调查局虚拟案件文档系统](https://linux.cn/article-2307-1.html)》。**案例分析:**
 
 - **外部环境** :FBI 没有真正懂技术的负责人领导和管控项目,对承包商缺少控制;
 
@@ -104,7 +104,7 @@ IEEE(电气和电子工程师协会)有一个专门的网页,把过去十
 
 #### 案例 3. 微软 Vista 项目 **案例描述:** 微软的 Windows Vista 项目开始与 2001 年 7 月,预计 2003 年发布。比尔盖茨为 Vista 提出了三大目标:1. 完全使用 C# 提升开发效率;2. 使用数据库作为新的文件系统 WinFS;3. 使用全新的显示技术 Avalon (后来改名为 WPF),打破桌面软件和网站的用户界面界限,提升微软竞争力
 
-目标非常好,但技术难度非常大,结果三年后也未能开发完成,不得不在 2004 年对目标进行调整:不用 C#、取消 WinFS、删改 Avalon ,一开始的三大目标就这样被完全否决,最终 2007 年才发布 Vista。参考文章:《[五年磨砺: 微软 Vista 开发过程全记录](https://blog.51cto.com/jiayu/22476)》。 **案例分析:**
+目标非常好,但技术难度非常大,结果三年后也未能开发完成,不得不在 2004 年对目标进行调整:不用 C#、取消 WinFS、删改 Avalon ,一开始的三大目标就这样被完全否决,最终 2007 年才发布 Vista。参考文章:《[五年磨砺: 微软 Vista 开发过程全记录](https://blog.51cto.com/jiayu/22476)》。**案例分析:**
 
 - **外部环境** :在目标的设定上,主要不是为了满足用户需求,而是为了商业上的竞争需要;
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25444\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25444\350\256\262.md"
index d9d6006d0..a46803b99 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25444\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25444\350\256\262.md"
@@ -12,7 +12,7 @@
 
 然而这些带有各自文化特色的部分,却是我们很难学习借鉴的,因为这样的文化都只适合各自的公司。假设让微软去学习 Facebook 的黑客精神,发布带有很多 Bug 的 Windows 系统,那么用户是不能忍受的;而让普通公司去学 Google 的工程师文化,项目没有严格的 Dead Line,系统隔几年重写一遍,那公司恐怕都要撑不住了。
 
-所以,要学习大厂, **你要多去关注大厂们对软件工程实践共通的地方,可以应用在你自己项目的地方,另外还要去看大厂对软件工程实践的变化趋势,在朝什么方向发展。** 通常这些大厂的很多实践都是业界的风向标,一旦一些实践大厂都在应用,那么很多中小厂就会跟风,最终变成行业标准。
+所以,要学习大厂,**你要多去关注大厂们对软件工程实践共通的地方,可以应用在你自己项目的地方,另外还要去看大厂对软件工程实践的变化趋势,在朝什么方向发展。** 通常这些大厂的很多实践都是业界的风向标,一旦一些实践大厂都在应用,那么很多中小厂就会跟风,最终变成行业标准。
 
 在上一篇《43 以 VS Code 为例,看大型开源项目是如何应用软件工程的?》中,我从项目的开发迭代过程,团队的角色分工和项目开发各个阶段来分析了 VS Code 对软件工程的应用。类似的,我也将从大厂的开发团队组成、开发工具的使用、项目开发流程这几个方面来分析一下大厂对软件工程的应用中,有哪些共同点?有哪些变化趋势?有什么地方可以借鉴?
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25445\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25445\350\256\262.md"
index 6aa5b8ce1..f24bdcf27 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25445\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25445\350\256\262.md"
@@ -42,7 +42,7 @@
 
 (图片来源:Conway’s Law)
 
-看完康威定律再回过头来看微服务, **你会发现,微服务架构的设计,不仅仅是一个对服务拆分的架构设计,同时也是对组织架构拆分的设计。**
+看完康威定律再回过头来看微服务,**你会发现,微服务架构的设计,不仅仅是一个对服务拆分的架构设计,同时也是对组织架构拆分的设计。**
 
 当你在做架构设计,在考虑你的微服务拆分粒度的时候,不妨先想一想:你团队的组织结构是什么样的?真的大到需要用微服务了吗?你能按照微服务的设计去重新设计和调整你的组织结构吗?
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25446\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25446\350\256\262.md"
index 8104e8876..5b67053b2 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25446\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25446\350\256\262.md"
@@ -4,7 +4,7 @@
 
 在专栏更新之初我就提到,把每件事都当作一个项目来推进,现在我也用“万事皆项目”来作为我们专栏的结束。
 
-我学习软件工程最大的收获,就是在看问题的时候, **不再局限于从技术层面或者是一个局部去思考问题,而是站在整体,用软件工程的方法去指导自己的思考和决策。**
+我学习软件工程最大的收获,就是在看问题的时候,**不再局限于从技术层面或者是一个局部去思考问题,而是站在整体,用软件工程的方法去指导自己的思考和决策。**
 
 所以在整个专栏的讲述中,我也希望能给你带来这样的转变:在做一件事情之前你可以先考虑一下,这是不是可以当作一个项目来推进,站在整体思考,有目的、有计划、有步骤地解决问题。
 
@@ -60,7 +60,7 @@
 
 ## 埋下一颗种子
 
-日常生活中很多事情,就像去写一个专栏,并不是一个软件项目,但是你应用软件工程的知识去指导去推进,一样可以帮助你有计划、有步骤地完成它,一样可以让你有很多机会去实践软件工程的知识。 **你对软件工程知识实践的越多,你对它的理解也会越深刻,这样当你在做软件项目时,你就能更好地应用这些知识,帮助你高质量地完成项目。**
+日常生活中很多事情,就像去写一个专栏,并不是一个软件项目,但是你应用软件工程的知识去指导去推进,一样可以帮助你有计划、有步骤地完成它,一样可以让你有很多机会去实践软件工程的知识。**你对软件工程知识实践的越多,你对它的理解也会越深刻,这样当你在做软件项目时,你就能更好地应用这些知识,帮助你高质量地完成项目。**
 
 通过对大家留言的观察,我也发现那些已经有丰富的项目经验的同学收获是最大的,因为他们很容易将丰富的项目经验和软件工程的知识串起来,把零散的知识点借助专栏的学习一点点构建成了完整的软件工程知识体系。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25447\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25447\350\256\262.md"
index 7d93bc82f..62f32c405 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25447\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25447\350\256\262.md"
@@ -4,7 +4,7 @@
 
 2019 年 1 月,任正非的那封[《全面提升软件工程能力与实践,打造可信的高质量产品》](https://xinsheng.huawei.com/cn/index.php?app=forum&mod=Detail&act=index&id=4134815)公开信在朋友圈刷屏了。作为软件工程专业出身的程序员,这封公开信自然是引起了我的好奇,仔细阅读之下,确实让我大吃一惊。
 
-于是,我从软件工程的角度对这封公开信进行了解读。 **在我们专栏内容正式更新前,我将它作为特别放送分享给你,希望可以帮助你更好地理解软件工程。**
+于是,我从软件工程的角度对这封公开信进行了解读。**在我们专栏内容正式更新前,我将它作为特别放送分享给你,希望可以帮助你更好地理解软件工程。**
 
 这封信看似像八股文一般,但细看之下,可以发现作者对于软件工程的理解确实非常深刻,各种专业术语信手拈来,比喻恰到好处。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25448\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25448\350\256\262.md"
index f51459a26..c4e24fdaa 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25448\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25448\350\256\262.md"
@@ -18,27 +18,27 @@
 
 **那“过程”指的是什么呢?** 要构建高质量软件,则要解决软件过程中的混乱,将软件开发过程中的沟通、计划、建模、构建和部署等活动有效地组织起来。而软件过程,就是在软件项目的生命周期内,也就是软件从诞生到结束这期间,在开发与构建系统时要遵循的步骤。
 
-有两种过程框架你一定经常听到,那就是瀑布模型和敏捷开发。这是在软件工程多年的发展中,逐步形成的两种主流的软件过程指导框架。 **那么,何为“方法”?** 方法是指在整个过程中,如何构建系统的方法学。比如说,如何分析用户需求;如何对产品进行测试验收;如何进行系统架构设计等。 **知道了过程,掌握了方法,那么具体落到操作层面,就会涉及到工具的使用。** 我们需要工具来辅助方法的执行,提高效率。通过工具,可以把一些手动的工作自动化,比如自动化测试工具,自动构建部署工具;通过工具,可以帮助把一些流程规范起来,比如 Bug 跟踪、源代码管理;还可以通过工具,帮助提高编码效率,比如各种编辑器 IDE、各种高级语言。
+有两种过程框架你一定经常听到,那就是瀑布模型和敏捷开发。这是在软件工程多年的发展中,逐步形成的两种主流的软件过程指导框架。**那么,何为“方法”?** 方法是指在整个过程中,如何构建系统的方法学。比如说,如何分析用户需求;如何对产品进行测试验收;如何进行系统架构设计等。**知道了过程,掌握了方法,那么具体落到操作层面,就会涉及到工具的使用。** 我们需要工具来辅助方法的执行,提高效率。通过工具,可以把一些手动的工作自动化,比如自动化测试工具,自动构建部署工具;通过工具,可以帮助把一些流程规范起来,比如 Bug 跟踪、源代码管理;还可以通过工具,帮助提高编码效率,比如各种编辑器 IDE、各种高级语言。
 
-如果现在再回头总结一下,软件工程的核心知识点, **就是围绕软件开发过程,产生的方法学和工具。** 你可以用一个简单的公式来理解软件工程,那就是: **软件工程 = 工具 + 方法 + 过程。** 根据这个公式,我将软件工程的知识结构做成了思维导图,方便你对知识点有更好地理解,高效学习。
+如果现在再回头总结一下,软件工程的核心知识点,**就是围绕软件开发过程,产生的方法学和工具。** 你可以用一个简单的公式来理解软件工程,那就是: **软件工程 = 工具 + 方法 + 过程。** 根据这个公式,我将软件工程的知识结构做成了思维导图,方便你对知识点有更好地理解,高效学习。
 
 ![img](assets/9926b79ecc91a4e664933c587f630199.jpg)
 
 ## 如何学习软件工程?
 
-我给了你软件工程学的公式,也对软件工程有了更为全面的了解,看起来软件工程学很简单,但这些内容一下子要吃透也不容易。在开篇词中,我介绍了会从“道、术和器”三个维度去讲这个专栏,这其实对应了学习软件工程的四重境界。 **学习软件工程的四重境界**  **第一重:用器** “器”就是工具,工具规则简单,上手就可以用,也很快就能看到效果。比如,原型设计工具可以帮助你确定需求,持续集成工具可以帮助你简化测试和部署的流程。对工具的学习是最为简单的,也是最基础的。 **第二重:学术** “术”就是方法,学会方法,你就能应用方法去完成一个任务,例如用需求分析的方法,你去搞清楚用户想要什么,用 Scrum 去组织项目开发过程。
+我给了你软件工程学的公式,也对软件工程有了更为全面的了解,看起来软件工程学很简单,但这些内容一下子要吃透也不容易。在开篇词中,我介绍了会从“道、术和器”三个维度去讲这个专栏,这其实对应了学习软件工程的四重境界。**学习软件工程的四重境界**  **第一重:用器** “器”就是工具,工具规则简单,上手就可以用,也很快就能看到效果。比如,原型设计工具可以帮助你确定需求,持续集成工具可以帮助你简化测试和部署的流程。对工具的学习是最为简单的,也是最基础的。**第二重:学术** “术”就是方法,学会方法,你就能应用方法去完成一个任务,例如用需求分析的方法,你去搞清楚用户想要什么,用 Scrum 去组织项目开发过程。
 
-掌握了术,甚至是可以脱离器的,例如你没用原型设计工具,你用纸和笔,用白板,一样可以去沟通确认需求。 **第三重:悟道** “道”就是本源,软件工程知识的核心思想和本质规律。就像敏捷开发,本身并不是一种方法,而是一套价值观和原则,领悟了这个道,就可以成为你在处理项目过程中各种问题决策的依据。道是可以产生术的,你掌握了敏捷开发的道,你就可以领悟出 Scrum、极限编程这样的术。 **第四重: 传道** 当你能把复杂的知识通过浅显易懂的方式传授给别人,那就说明你对知识的领悟已经到了更高的境界。同时,教学也是最好的学习方式,通过传授别人知识,可以让你对知识本身有更深入的理解。
+掌握了术,甚至是可以脱离器的,例如你没用原型设计工具,你用纸和笔,用白板,一样可以去沟通确认需求。**第三重:悟道** “道”就是本源,软件工程知识的核心思想和本质规律。就像敏捷开发,本身并不是一种方法,而是一套价值观和原则,领悟了这个道,就可以成为你在处理项目过程中各种问题决策的依据。道是可以产生术的,你掌握了敏捷开发的道,你就可以领悟出 Scrum、极限编程这样的术。**第四重: 传道** 当你能把复杂的知识通过浅显易懂的方式传授给别人,那就说明你对知识的领悟已经到了更高的境界。同时,教学也是最好的学习方式,通过传授别人知识,可以让你对知识本身有更深入的理解。
 
 ## 做中学和教中学
 
 你可能会问,怎样学,才能到达以上这四重境界?我在做技术管理的工作中,经常要做一些培训的工作,在这过程中我总结了两套行之有效的方法:“做中学”和“教中学”。
 
-![img](assets/38203f9726c63858c230e1947768f019.jpg) **“做中学”,是一种自下而上的学习方法** ,通过实践,从使用工具到学习方法,再从方法中提炼出道。
+![img](assets/38203f9726c63858c230e1947768f019.jpg) **“做中学”,是一种自下而上的学习方法**,通过实践,从使用工具到学习方法,再从方法中提炼出道。
 
-在学习本专栏的时候,你可以采用“做中学”的方式,把专栏中的知识应用起来,在实践的过程中去巩固你学到的知识,去思考背后的道。把已经积累的项目经验和软件工程的知识点关联起来,这样才能加深你的理解,学以致用,把经验和知识转化为能力。 **“教中学”,是一种自上而下的学习方法** ,通过教学,去进一步深入领会别人总结出来的道,去模仿推导方法,去学习如何使用工具。
+在学习本专栏的时候,你可以采用“做中学”的方式,把专栏中的知识应用起来,在实践的过程中去巩固你学到的知识,去思考背后的道。把已经积累的项目经验和软件工程的知识点关联起来,这样才能加深你的理解,学以致用,把经验和知识转化为能力。**“教中学”,是一种自上而下的学习方法**,通过教学,去进一步深入领会别人总结出来的道,去模仿推导方法,去学习如何使用工具。
 
-比如,你学习完一篇专栏文章后,把学到的知识进行输出,写成微博或博客分享出去;在公司内部讲给你的同事们听等。在教学分享的过程中,去进一步深化吸收知识内容,构建你的知识体系。 **“做中学”和“教中学”** ,这两种方法你可以配合起来使用。
+比如,你学习完一篇专栏文章后,把学到的知识进行输出,写成微博或博客分享出去;在公司内部讲给你的同事们听等。在教学分享的过程中,去进一步深化吸收知识内容,构建你的知识体系。**“做中学”和“教中学”**,这两种方法你可以配合起来使用。
 
 ## 参考书目
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25449\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25449\350\256\262.md"
index fffa29c22..6fcb46e09 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25449\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25449\350\256\262.md"
@@ -12,7 +12,7 @@
 
 ### No.1
 
-**hua168** :学这个专栏需要哪些基础知识为前提的?开发都要学哪些基础东西? **宝玉** :学习这个专栏,不需要你有特别的基础,当然有一些项目经验可以帮助你更好的理解。至于要学什么基础的东西,其实你可以从另一个角度思考一下: **开发的价值是体现在哪的?**
+**hua168** :学这个专栏需要哪些基础知识为前提的?开发都要学哪些基础东西?**宝玉** :学习这个专栏,不需要你有特别的基础,当然有一些项目经验可以帮助你更好的理解。至于要学什么基础的东西,其实你可以从另一个角度思考一下: **开发的价值是体现在哪的?**
 
 开发的价值是通过在项目中创造价值体现的,所以你要考虑学什么能帮助到你更好的在项目中创造价值。比如说除了具体的编程技能外,还可以从这些方面思考:
 
@@ -24,19 +24,19 @@
 
 ### No.2
 
-**hua168** :软件工程在游戏项目上,是不是也一样呢? **zhxilin℃** :对游戏行业的过程模型有什么理解? **宝玉** :万变不离其宗。游戏项目一样离不开软件工程,游戏开发本身也是软件开发,只是有些名字换了,比如产品经理变成了游戏策划,产品设计变成了游戏策划案。游戏开发一样要有需求分析、架构设计、编码、测试等关键活动。只是游戏项目的需求变更频繁、节奏快,用增量或者迭代要好很多!另外也可以试试敏捷开发。
+**hua168** :软件工程在游戏项目上,是不是也一样呢?**zhxilin℃** :对游戏行业的过程模型有什么理解?**宝玉** :万变不离其宗。游戏项目一样离不开软件工程,游戏开发本身也是软件开发,只是有些名字换了,比如产品经理变成了游戏策划,产品设计变成了游戏策划案。游戏开发一样要有需求分析、架构设计、编码、测试等关键活动。只是游戏项目的需求变更频繁、节奏快,用增量或者迭代要好很多!另外也可以试试敏捷开发。
 
-### No.3 **于欣磊** :现在已经进入云计算时代,基本上大中小企业都在上云,复杂逻辑都在云端处理,真的还需要软件工程里讲的开发要搞这么多流程么? **宝玉** :是的,云计算的兴起可以减少很多劳动,但不代表你就什么都不用做了,还是要做需求分析,再去做架构设计,做完架构设计你才能清楚哪些可以用云计算,那些需要自己去实现。最后编码完了,一样还要测试的
+### No.3 **于欣磊** :现在已经进入云计算时代,基本上大中小企业都在上云,复杂逻辑都在云端处理,真的还需要软件工程里讲的开发要搞这么多流程么?**宝玉** :是的,云计算的兴起可以减少很多劳动,但不代表你就什么都不用做了,还是要做需求分析,再去做架构设计,做完架构设计你才能清楚哪些可以用云计算,那些需要自己去实现。最后编码完了,一样还要测试的
 
-### No.4 **hua168** :现在运维的前景怎么样?感觉竞争很激烈,很多小公司都不招,开发兼职了运维,各大云出了一些维护监控工具,对他们来说够用了,感觉发展空间变小了,运维开发招也少了……也有人提到运维职业会消失,难道要转开发?那运维开发能力也争不过真正的开发啊。 **宝玉** :你这个问题很有代表性,现在云服务兴起后,传统运维的职位在减少,所以 DevOps 在兴起。DevOps 和运维的主要差别就是 DevOps 不仅有运维能力,还有开发能力,可以站在运维和开发更高的角度去看问题,帮助自动化的,稳定的交付部署产品。你不用完全转开发,但是应该要学习一些开发知识,尤其是自动化脚本相关的
+### No.4 **hua168** :现在运维的前景怎么样?感觉竞争很激烈,很多小公司都不招,开发兼职了运维,各大云出了一些维护监控工具,对他们来说够用了,感觉发展空间变小了,运维开发招也少了……也有人提到运维职业会消失,难道要转开发?那运维开发能力也争不过真正的开发啊。**宝玉** :你这个问题很有代表性,现在云服务兴起后,传统运维的职位在减少,所以 DevOps 在兴起。DevOps 和运维的主要差别就是 DevOps 不仅有运维能力,还有开发能力,可以站在运维和开发更高的角度去看问题,帮助自动化的,稳定的交付部署产品。你不用完全转开发,但是应该要学习一些开发知识,尤其是自动化脚本相关的
 
 ### No.5 **行者无疆** :传统瀑布模型前期进行了完整的需求评估,在技术选型,系统架构,实施路径上可以做好全面的规划,虽然周期长,不必要的反复工作会少很多,目标也更容易控制
 
-那敏捷模型的迭代方式并不会把需求都考虑全面,未来的迭代可能会造成前面的技术架构或者实施细节等都不能满足新需求的要求。所有工作都要重来的问题,会存在大量的重复工作和资源浪费。 敏捷模型是如何有效地规避这些问题的呢? **宝玉** :你说的问题确实存在,导致常说的技术债务问题,所以需要定期去重构,改进这些问题。迭代过程中的重复工作确实存在,但是软件开发中的浪费其实主要不是在于迭代过程中的重复工作,而是在于需求不明确和需求变更导致的返工或失败。
+那敏捷模型的迭代方式并不会把需求都考虑全面,未来的迭代可能会造成前面的技术架构或者实施细节等都不能满足新需求的要求。所有工作都要重来的问题,会存在大量的重复工作和资源浪费。 敏捷模型是如何有效地规避这些问题的呢?**宝玉** :你说的问题确实存在,导致常说的技术债务问题,所以需要定期去重构,改进这些问题。迭代过程中的重复工作确实存在,但是软件开发中的浪费其实主要不是在于迭代过程中的重复工作,而是在于需求不明确和需求变更导致的返工或失败。
 
 敏捷开发持续发布稳定版本的理念还是利大于弊。有一些项目其实是瀑布模型和敏捷开发的结合,需求分析和系统设计的时候用瀑布模型,开发和测试阶段用敏捷,也是个不错的选择。
 
-### No.6 **Geek_85f782** :如果说软件工程 = 过程 + 方法 + 工具,其中过程是否就是具体指软件的生命周期?方法是指选用的生命周期模型,比如瀑布、螺旋、迭代、敏捷? **宝玉** :我认为过程应该包含过程模型采用的方法。而方法是指基于过程模型之下的方法。因为过程模型决定了软件开发过程是什么样的,进而决定了采用什么开发方法
+### No.6 **Geek_85f782** :如果说软件工程 = 过程 + 方法 + 工具,其中过程是否就是具体指软件的生命周期?方法是指选用的生命周期模型,比如瀑布、螺旋、迭代、敏捷?**宝玉** :我认为过程应该包含过程模型采用的方法。而方法是指基于过程模型之下的方法。因为过程模型决定了软件开发过程是什么样的,进而决定了采用什么开发方法
 
 比如你选择了瀑布模型,整个软件开发过程就是按照瀑布模型的分阶段来进行,对应的方法就是瀑布模型中的方法,例如需求分析、架构设计;如果你选择了敏捷开发,则整个开发过程就是一种敏捷迭代方式,后面的方法对应的就是敏捷开发的一套方法体系,例如 Scrum、用户故事、持续集成等。
 
@@ -44,54 +44,54 @@
 
 宝玉:软件开发,核心就是人,如果没有人,规范和文档都没意义的。要留住人,一个是得舍得给钱,另一个得有个好的环境,还有就是要有梯队,能把新人培养上去。饭店里只有一个大厨,大厨当然敢乱提要求,如果大厨多几个,就不担心了。还是得要舍得下本钱招优秀的人。
 
-### No.8 **白发青年** :如果是外包项目,作为项目的乙方,如果采用敏捷开发,最初的工作量就很难完整估计,不利于双方的合同签订。不知老师是否有好的建议? **宝玉** :这个问题通常有两种解决方案供参考
+### No.8 **白发青年** :如果是外包项目,作为项目的乙方,如果采用敏捷开发,最初的工作量就很难完整估计,不利于双方的合同签订。不知老师是否有好的建议?**宝玉** :这个问题通常有两种解决方案供参考
 
 1. 你按照瀑布模型的方式去估算工作量,然后签订合同。开发的时候你需求分析和架构设计还是用瀑布模型的方式,但是编码和测试用敏捷开发。这是一种不错的折中方案;
 
 1. 你把所有需求拆分成用户故事,对用户故事进行打分(了解下计划扑克之类的打分方案),然后可以算出来一个总分数。另外按照你以前敏捷开发的经验,可以知道每个 Sprint 大概能完成多少分,这样你就能大致推算出来工期。
 
-### No.9 **Charles** :瀑布模型非常考验人的能力,会造成互相扯皮推卸责任,上线以后有什么问题,还会互相推锅背,这种情况下管理者有啥好的方式去解决? **宝玉** :虽然我觉得甩锅不是什么好事,但是如果你真要甩锅,最简单有效就是设置流程去划分责任。上线后有问题其实很正常的,重要的是要有合理的机制
+### No.9 **Charles** :瀑布模型非常考验人的能力,会造成互相扯皮推卸责任,上线以后有什么问题,还会互相推锅背,这种情况下管理者有啥好的方式去解决?**宝玉** :虽然我觉得甩锅不是什么好事,但是如果你真要甩锅,最简单有效就是设置流程去划分责任。上线后有问题其实很正常的,重要的是要有合理的机制
 
 1. 及时发现问题,监控报警、用户投诉反馈等;
 1. 马上解决问题,对线上版本有专门的代码分支,可以随时打补丁修复,测试上线;
 1. 避免后续再犯同样的错误。要分析原因,看什么导致问题,然后改进流程。
 
-### No.10 **Linuxer** :小公司有很多项目就是一两个人,没有那么多角色,怎么做到按这种流程去开发项目呢? 比如常常在代码编写过程中发现很多问题都没考虑全面又感觉在交流需求的时候根本就没想到,要怎么在之后的项目中不再犯这种问题呢? **宝玉** :即使只有一个人,建议也要做简单的需求分析和设计,做完后,形成简单的文档,找人评审一下,提一些意见。因为你写文档的过程,给别人讲的过程,其实是在帮助你思考,帮助你梳理清楚逻辑,避免在实现的时候发现好多问题没想清楚
+### No.10 **Linuxer** :小公司有很多项目就是一两个人,没有那么多角色,怎么做到按这种流程去开发项目呢? 比如常常在代码编写过程中发现很多问题都没考虑全面又感觉在交流需求的时候根本就没想到,要怎么在之后的项目中不再犯这种问题呢?**宝玉** :即使只有一个人,建议也要做简单的需求分析和设计,做完后,形成简单的文档,找人评审一下,提一些意见。因为你写文档的过程,给别人讲的过程,其实是在帮助你思考,帮助你梳理清楚逻辑,避免在实现的时候发现好多问题没想清楚
 
 还有一个思路就是快一点迭代,每一个迭代解决优先级最高的问题,然后下一个迭代中改进上一个迭代的问题。项目中犯错误其实很正常的,重要的时候要总结,看看通过什么方式能改进,避免犯类似的错误。
 
-### No.11 **bearlu** :是不是每种模型,有其应用场景,不能只追求最新,要适用才行? **宝玉** :你说的太对了!举个例子来说,敏捷开发肯定又新又好,但是如果成员没一个懂敏捷开发,强行照葫芦画瓢,可能结果还不如用瀑布模型
+### No.11 **bearlu** :是不是每种模型,有其应用场景,不能只追求最新,要适用才行?**宝玉** :你说的太对了!举个例子来说,敏捷开发肯定又新又好,但是如果成员没一个懂敏捷开发,强行照葫芦画瓢,可能结果还不如用瀑布模型
 
-### No.12 **clever_P** :瀑布模型难以快速响应需求变化,那有没有可能通过对业务领域的深入研究,对业务的发展和变化做出一些前瞻性预测,在软件设计的时候将这些预测考虑进去,以此来减小后期需求变化对整个项目的影响呢? **宝玉** :你说的是一个方向,但是要预测其实是很难的,结果很可能是过度设计,设计了很多可能最终完全用不上的架构,反倒不如快速迭代快速响应来得实际
+### No.12 **clever_P** :瀑布模型难以快速响应需求变化,那有没有可能通过对业务领域的深入研究,对业务的发展和变化做出一些前瞻性预测,在软件设计的时候将这些预测考虑进去,以此来减小后期需求变化对整个项目的影响呢?**宝玉** :你说的是一个方向,但是要预测其实是很难的,结果很可能是过度设计,设计了很多可能最终完全用不上的架构,反倒不如快速迭代快速响应来得实际
 
-### No.13 **一年** :开发一款测试市场反应的产品,使用快速原型模型是不是好点呢? **宝玉** :恐怕不行,因为快速原型模型是牺牲质量的。质量差的软件,去测试市场,你不知道是因为质量问题不行还是需求没抓住不行。这种情况,可以考虑用迭代模型,先开发核心需求,然后再逐步迭代
+### No.13 **一年** :开发一款测试市场反应的产品,使用快速原型模型是不是好点呢?**宝玉** :恐怕不行,因为快速原型模型是牺牲质量的。质量差的软件,去测试市场,你不知道是因为质量问题不行还是需求没抓住不行。这种情况,可以考虑用迭代模型,先开发核心需求,然后再逐步迭代
 
-### No.14 **凯纳软件** :感觉自己之前做任何事情都没有章法,觉得只要做了就可以。通篇学完之后,知道自己哪里欠缺,应该怎样去学习及工作。 **宝玉** :谋定而后动。还有一点经验就是:如果你想更有章法,更有大局观,做一件事情前先做个计划,可以帮助你更好的思考,也更容易执行
+### No.14 **凯纳软件** :感觉自己之前做任何事情都没有章法,觉得只要做了就可以。通篇学完之后,知道自己哪里欠缺,应该怎样去学习及工作。**宝玉** :谋定而后动。还有一点经验就是:如果你想更有章法,更有大局观,做一件事情前先做个计划,可以帮助你更好的思考,也更容易执行
 
-### No.15 **Joey** :我们公司是比较大的国企(多个业务部门对一个开发部门),对质量要求较高,现在业务条线也比较多,业务部门基本都嫌我们开发部门效率低,对于我们研发部门,组织架构还是按照瀑布模型设计的,开发模型基本是迭代 + 增量,如果想推行敏捷,肯定需要调整组织架构,一旦调整,就会触发一些利益关系,在这种背景下,有没有什么好的招数,既可以提高研发效率,又可以保证质量? **宝玉** :如果你想推行敏捷,可以先找个小项目,组个小团队试点,成了可以作为一个参考,领导可以去邀功,以后可以更大规模尝试;失败了也损失不大,领导也不用担责任
+### No.15 **Joey** :我们公司是比较大的国企(多个业务部门对一个开发部门),对质量要求较高,现在业务条线也比较多,业务部门基本都嫌我们开发部门效率低,对于我们研发部门,组织架构还是按照瀑布模型设计的,开发模型基本是迭代 + 增量,如果想推行敏捷,肯定需要调整组织架构,一旦调整,就会触发一些利益关系,在这种背景下,有没有什么好的招数,既可以提高研发效率,又可以保证质量?**宝玉** :如果你想推行敏捷,可以先找个小项目,组个小团队试点,成了可以作为一个参考,领导可以去邀功,以后可以更大规模尝试;失败了也损失不大,领导也不用担责任
 
 不管用不用敏捷开发,你都可以学习其中好的实践,例如持续集成用起来,帮助你高效的集成部署;自动化测试代码写起来,帮助你提高项目质量;迭代快起来,以前 3 个月变成 1 个月,以前 1 个月的变 2 周。有些事情即使只是程序员都是可控范围内的,做着做着其实你就“敏捷”起来了。
 
 No.16
------ **一步** :最小可行性产品 MVP 应该就是迭代开发了? **宝玉** :MVP 更多的是需求定义上的概念,和开发模型并没有关系。但是你使用迭代开发或者敏捷开发,必然要优先选择最核心最重要的功能需求先开发。所以通常 MVP 的方式选择核心需求,用迭代模型或敏捷开发开发需求。
+----- **一步** :最小可行性产品 MVP 应该就是迭代开发了?**宝玉** :MVP 更多的是需求定义上的概念,和开发模型并没有关系。但是你使用迭代开发或者敏捷开发,必然要优先选择最核心最重要的功能需求先开发。所以通常 MVP 的方式选择核心需求,用迭代模型或敏捷开发开发需求。
 
 ### No.17 **龙哥** :有依赖交叉的用户故事应该怎么做,比如用户系统的数据库该由谁搭建。毕竟注册、登录、修改这些都可能基于一个数据表。表字段这些需要统一,不能一个程序员改一次字段名吧 **宝玉** :敏捷开发中有一个迭代 0,也就是第一个迭代,就是做这些准备工作、基础架构搭建的。敏捷团队小,有个好处就在于遇到你说的这种情况,在做之前,大家都在一起开个小会一商量就可以定下来了
 
-### No.18 **阿神** :敏捷开发里开发也要写集成测试用例吗,那么测试人员主要做手工测试? **宝玉** :对,开发不仅要写单元测试,还要写集成测试。但开发都是用模拟数据,假的 API。而测试的自动化测试会用真实的数据,调用真实的 API,而且也要做一部分手动测试。至于比例多少,还得看项目特点
+### No.18 **阿神** :敏捷开发里开发也要写集成测试用例吗,那么测试人员主要做手工测试?**宝玉** :对,开发不仅要写单元测试,还要写集成测试。但开发都是用模拟数据,假的 API。而测试的自动化测试会用真实的数据,调用真实的 API,而且也要做一部分手动测试。至于比例多少,还得看项目特点
 
-### No.19 **holylin** :如果合同金额一开始就是根据商务阶段了解的情况评估的工作量而确定的,那么在合同执行过程中,如果按敏捷开发的思路,客户不断改需求我们不断地响应,然后工作量甚至已经超过了原先合同的金额,这个时候要如何处理? **宝玉** :这是个好问题,我对这个问题上没有什么经验,但我可以试着帮你分析一下
+### No.19 **holylin** :如果合同金额一开始就是根据商务阶段了解的情况评估的工作量而确定的,那么在合同执行过程中,如果按敏捷开发的思路,客户不断改需求我们不断地响应,然后工作量甚至已经超过了原先合同的金额,这个时候要如何处理?**宝玉** :这是个好问题,我对这个问题上没有什么经验,但我可以试着帮你分析一下
 
 你的合同是按照当时的需求签订的,如果后期客户变更需求或者增加新需求,那相当于需要重新签订变更这部分的补充合同。
 
 应用敏捷开发的时候,你也可以让产品经理或者项目经理充当客户的角色,这样他们会更偏重产品需求的解读,而不是重新提出新的需求。还有一点,合同执行的时候,这时候你不需要太过于纠结是不是用敏捷还是迭代还是瀑布,而是哪一种开发模式,可以让你高质量高效率的完成,那就是最好的最适合你的开发模式。
 
-### No.20 **长眉 _ 张永** :作为一个电商 ERP 服务商,既要关注产品的研发进度,又要对产品做维护。人员一旦离职,发现没有较为详细的文档,就需要去猜测,之前的业务了。敏捷后上线,留下的技术债务应该归谁负责呢? **宝玉** :敏捷还是要写必要的文档,只是会简化。尤其是这种涉及交接的、维护的,文档不能省。技术债务应该团队成员集体负责,大家在迭代计划会上应该将技术重构列入后续的 Sprint
+### No.20 **长眉 _ 张永** :作为一个电商 ERP 服务商,既要关注产品的研发进度,又要对产品做维护。人员一旦离职,发现没有较为详细的文档,就需要去猜测,之前的业务了。敏捷后上线,留下的技术债务应该归谁负责呢?**宝玉** :敏捷还是要写必要的文档,只是会简化。尤其是这种涉及交接的、维护的,文档不能省。技术债务应该团队成员集体负责,大家在迭代计划会上应该将技术重构列入后续的 Sprint
 
-### No.21 **刘晓林** :敏捷开发这么强调扁平化,这么重视人,这么强调开放而弱化约束,那和最初没有软件工程时期的开发主要区别是啥呀? **宝玉** :好问题,你难倒我了。前面介绍过,没有软件工程的时候呢,开发就是边写边改模式,没有需求分析、没有架构设计、没有测试,就导致很多问题
+### No.21 **刘晓林** :敏捷开发这么强调扁平化,这么重视人,这么强调开放而弱化约束,那和最初没有软件工程时期的开发主要区别是啥呀?**宝玉** :好问题,你难倒我了。前面介绍过,没有软件工程的时候呢,开发就是边写边改模式,没有需求分析、没有架构设计、没有测试,就导致很多问题
 
 ### No.22 **邢爱明** :对于企业管理的软件,核心需求涉及多个部门,需要反复沟通确认周期很长,这种情况下是否还适合使用用户故事的方式做需求分析呢?
 
-另外,我按照瀑布开发模式的习惯分析,开发人员和 po 沟通需求后,如果没有文档作为输出物,在开发和测试的时候就没有标准,反而会造成工作返工。这是否意味着,团队成员需要高度的协同和配合? 以完成任务为导向,而不是强调各自的分工? **宝玉** :好问题!敏捷开发这种方式,需要客户紧密配合,也就是可以方便确认需求,否则还是少不了要写需求文档。另外我在文章中描述用户故事,有些描写不清楚或者歧义的地方,其实用户故事还应该包括验收标准,这样可以解决你说的开发和测试没有标准的问题。
+另外,我按照瀑布开发模式的习惯分析,开发人员和 po 沟通需求后,如果没有文档作为输出物,在开发和测试的时候就没有标准,反而会造成工作返工。这是否意味着,团队成员需要高度的协同和配合? 以完成任务为导向,而不是强调各自的分工?**宝玉** :好问题!敏捷开发这种方式,需要客户紧密配合,也就是可以方便确认需求,否则还是少不了要写需求文档。另外我在文章中描述用户故事,有些描写不清楚或者歧义的地方,其实用户故事还应该包括验收标准,这样可以解决你说的开发和测试没有标准的问题。
 
 团队成员需要高度的协同和配合那是一定的,尤其是架构和需求两部分。需求简化后,就意味着开发过程中需要反复沟通确认;没有专门的设计阶段,也就意味着每个 Sprint 开始前,团队要商量有没有要设计或者修改架构的,有就需要有个简单可行的方案对架构进行修改。如果各自分工,这样的目标就很难达到。
 
@@ -105,35 +105,35 @@ No.16
 
 ### No.24
 
-**dancer** :对比瀑布模型来说,敏捷开发在需求分析和软件设计上要薄弱一些,这会导致越向后迭代,软件越难以变更和维护,请问老师有什么好的方法和建议吗? **宝玉** :需求分析是在 Sprint 进行中同步进行,也就是开发具体的用户故事之前要和客户或产品经理充分沟通了解需求。如果用户故事不是特别大,这并不是很大的问题。另外并非只能用用户故事,也可以用传统的产品设计文档代替用户故事,也一样是很不错的实践。
+**dancer** :对比瀑布模型来说,敏捷开发在需求分析和软件设计上要薄弱一些,这会导致越向后迭代,软件越难以变更和维护,请问老师有什么好的方法和建议吗?**宝玉** :需求分析是在 Sprint 进行中同步进行,也就是开发具体的用户故事之前要和客户或产品经理充分沟通了解需求。如果用户故事不是特别大,这并不是很大的问题。另外并非只能用用户故事,也可以用传统的产品设计文档代替用户故事,也一样是很不错的实践。
 
 对于架构设计,架构只设计当前迭代的,所以迭代到一定阶段,是要考虑重构的。通常重构代码也是 Sprint 的工作任务的一部分。
 
-### No.25 **Dora** :瀑布对人员要求不高 (各自负责各自的工作,比如需求只管需求),而敏捷流程,一个人什么都要过一遍。这样理解,对吗? **宝玉** :瀑布对人员也不说要求不高,但分工确实更细一点,比如像你说的,需求只管需求;开发一般就不操心怎么测试,写完等着测试报 bug;敏捷开发里面,分工没那么细,需求不仅要写需求文档或者用户故事,还要和团队成员紧密合作,及时讲解需求;开发也要自己写很多自动化测试代码;敏捷团队也不是没有测试,但是会用自动化测试分担一部分测试任务
+### No.25 **Dora** :瀑布对人员要求不高 (各自负责各自的工作,比如需求只管需求),而敏捷流程,一个人什么都要过一遍。这样理解,对吗?**宝玉** :瀑布对人员也不说要求不高,但分工确实更细一点,比如像你说的,需求只管需求;开发一般就不操心怎么测试,写完等着测试报 bug;敏捷开发里面,分工没那么细,需求不仅要写需求文档或者用户故事,还要和团队成员紧密合作,及时讲解需求;开发也要自己写很多自动化测试代码;敏捷团队也不是没有测试,但是会用自动化测试分担一部分测试任务
 
-### No.26 **Tiger** :在敏捷里面,开发写自动化脚本测试,那是不是就不需要测试这个角色了啊?感觉在敏捷里面,只需要开发这一个角色就可以了啊? **宝玉** :在《07 大厂都在用哪些敏捷方法?(下)》我有谈到这个问题。自动化测试是辅助的,还是离不开人工的测试。而且开发写的集成测试和测试写的自动化测试还是有一点差别的,一个是用程序模拟操作的固定数据,而测试用的是真实的数据环境。举个例子来说,网页的自动化测试,开发只会用 Chrome Headless,数据都是事先写好的模拟数据;测试的话会用主流的 Chrome、Safari、Firefox、Edge 分别测试(自动化或手动),数据都是测试环境的真实数据
+### No.26 **Tiger** :在敏捷里面,开发写自动化脚本测试,那是不是就不需要测试这个角色了啊?感觉在敏捷里面,只需要开发这一个角色就可以了啊?**宝玉** :在《07 大厂都在用哪些敏捷方法?(下)》我有谈到这个问题。自动化测试是辅助的,还是离不开人工的测试。而且开发写的集成测试和测试写的自动化测试还是有一点差别的,一个是用程序模拟操作的固定数据,而测试用的是真实的数据环境。举个例子来说,网页的自动化测试,开发只会用 Chrome Headless,数据都是事先写好的模拟数据;测试的话会用主流的 Chrome、Safari、Firefox、Edge 分别测试(自动化或手动),数据都是测试环境的真实数据
 
-### No.27 **一路向北** :对于小公司小团队的项目,因为项目经理,产品经理都是身兼数职,是否有更好的实施方式呢? **宝玉** :项目经理、产品经理兼多个项目是正常的,也没大问题。但是让程序员同时兼做开发和项目经理工作就很不好,因为项目经理需要更多全局掌控,而一旦要花精力在开发上,很难跳出具体的开发工作,会极大影响项目管理工作;项目管理工作也会频繁打断开发,造成进度延迟
+### No.27 **一路向北** :对于小公司小团队的项目,因为项目经理,产品经理都是身兼数职,是否有更好的实施方式呢?**宝玉** :项目经理、产品经理兼多个项目是正常的,也没大问题。但是让程序员同时兼做开发和项目经理工作就很不好,因为项目经理需要更多全局掌控,而一旦要花精力在开发上,很难跳出具体的开发工作,会极大影响项目管理工作;项目管理工作也会频繁打断开发,造成进度延迟
 
 所以我建议应该有专职的项目经理,不应该让程序员兼职项目管理。新旧项目交织并不是问题,可以放在一个项目一个 Sprint 里面一起管理,也就是同一个 Sprint 里面有维护的 Ticket,也有新需求的 Ticket,只要保证开发人员同一时间只是做一件事,而不要几件事并行,就可以最大化发挥敏捷优势。
 
-### No.28 **天之大舒** :怎样培养团队成员? **宝玉** :有一些建议仅供参考
+### No.28 **天之大舒** :怎样培养团队成员?**宝玉** :有一些建议仅供参考
 
 1. 招人和开人都很重要,招优秀的,开掉没有责任心,没能力的。这两点都不容易做到,不过得坚持做;
 1. 设置合理的流程,配合一定的奖惩制度;你奖励什么,团队就会往哪方面发展;
 1. 团队要有梯队,不能都是资历浅的也不能都是资深的,保持一个合适的比例是比较健康的;
 1. 实战中锻炼,实战中磨合;给他们有挑战的任务,给予合适的指导(这就是有梯队的原因,需要高一级别的待低一级别的)。
 
-### No.29 **星星童鞋** :请问老师,对于需求更新极快,基本上每周都需要迭代更新上线的项目,在架构设计和项目部署上会不会有什么特殊的要求? **宝玉** :架构设计上,一定要定期需要重构,优化设计,不然后续新需求效率会降低,包括代码上也会越来越臃肿。比如我现在所在项目组,每 1-2 年会有一次大的架构升级调整,日常每隔几周会有小的架构优化,这样基本上可以保证快速迭代不会受太大影响
+### No.29 **星星童鞋** :请问老师,对于需求更新极快,基本上每周都需要迭代更新上线的项目,在架构设计和项目部署上会不会有什么特殊的要求?**宝玉** :架构设计上,一定要定期需要重构,优化设计,不然后续新需求效率会降低,包括代码上也会越来越臃肿。比如我现在所在项目组,每 1-2 年会有一次大的架构升级调整,日常每隔几周会有小的架构优化,这样基本上可以保证快速迭代不会受太大影响
 
 部署的话,一个是要自动化,可以快速方便的部署,另外一个部署后,需要有配套的数据监控和高于阈值报警的机制,因为上线后可能会有严重问题,需要及时发现,及时处理。
 
-### No.30 **alva_xu** :如果一个迭代里没有评审会,怎么知道我上线的系统是符合要求的? **宝玉** :没有评审会,但是有专职测试针对最初提的需求进行测试,另外产品经理也会验收,如果验收不合格会提交 Ticket。也就是说是有验收,只是没有专门的会议
+### No.30 **alva_xu** :如果一个迭代里没有评审会,怎么知道我上线的系统是符合要求的?**宝玉** :没有评审会,但是有专职测试针对最初提的需求进行测试,另外产品经理也会验收,如果验收不合格会提交 Ticket。也就是说是有验收,只是没有专门的会议
 
 精选留言
 ---- **阿杜:** 软件过程不是搞科研,不是搞艺术,而是解决多人合作将一个想法落地的学科,其中包括严谨的过程步骤、规范,用于提高效率或防范风险的工具。软件工程的主体是工程,这就要求我们具备基本的工程思维:模块化思维、抽象思维;具备一些关键的意识:质量意识、风险意识、交付意识。
 
-相关阅读:01 | 到底应该怎样理解软件工程? **alva_xu:** 对于大型系统的建设,可否用敏捷方法来实现,一直是个问题。
+相关阅读:01 | 到底应该怎样理解软件工程?**alva_xu:** 对于大型系统的建设,可否用敏捷方法来实现,一直是个问题。
 
 敏捷方法,适合于小团队(比如两个披萨团队)、小架构。对于大型单体应用的开发,至少在架构设计上是不适合用敏捷迭代方式的。
 
@@ -141,11 +141,11 @@ No.16
 
 所以,微服务、容器、devops 这三剑客和敏捷方法一起,互为依存、互相促进,成为了软件工程中最有生命力的技术工具和流程,使软件开发在质量和效率上得到极大提升。
 
-相关阅读:01 | 到底应该怎样理解软件工程? **老张:** 在今天没有不可替代的硬件,却有无数不可替代的软件。硬件早已不是共享的壁垒,而曾经被认为有很强可塑性的却已经是最硬的壁垒。一台服务器、一块磁盘、一根内存以及交换机、防火墙等网络设备,更遑论鼠标、键盘、显示器,在冗余、复用、虚拟化等等技术之下,更换、替代、扩容如此之方便,经过简单培训的工人就可以轻松完成。
+相关阅读:01 | 到底应该怎样理解软件工程?**老张:** 在今天没有不可替代的硬件,却有无数不可替代的软件。硬件早已不是共享的壁垒,而曾经被认为有很强可塑性的却已经是最硬的壁垒。一台服务器、一块磁盘、一根内存以及交换机、防火墙等网络设备,更遑论鼠标、键盘、显示器,在冗余、复用、虚拟化等等技术之下,更换、替代、扩容如此之方便,经过简单培训的工人就可以轻松完成。
 
 可是即便是美国国会图书馆,依然认为纸质是保存资料最好的方式,因为大量资料电子化后存放在不同介质,需要当时定制的软件才能读取这些格式。今天的软件就是这么硬。也许有一天,有人会写写如何开发真正的软件。
 
-相关阅读:01 | 到底应该怎样理解软件工程? **阿银:** 软件工程的本质在于工程。利用工程理论来保证高质量软件产品的产出。工程讲究效率,成本,质量,除此之外,容易忽略的是工作量与效益的权衡,这一点尤为关键。
+相关阅读:01 | 到底应该怎样理解软件工程?**阿银:** 软件工程的本质在于工程。利用工程理论来保证高质量软件产品的产出。工程讲究效率,成本,质量,除此之外,容易忽略的是工作量与效益的权衡,这一点尤为关键。
 
 相关阅读:01 | 到底应该怎样理解软件工程?
 
@@ -155,7 +155,7 @@ hyeebeen:
 
 学习对应的工程技巧,内化为自身素质,在项目过程中既能预防工程风险,也能建设面对风险的反应机制。这种人,各个企业都喜欢。
 
-相关阅读:01 | 到底应该怎样理解软件工程? **阿杜:** 1.  做任何事情都要按照一定的理论指导来,例如,依靠系统化、结构化的“工程思维”,将生活和工作中的每个事情都看做一个项目,可以提高做事的成功率和效率,虽然不用这些理论指导也能做成事情,但是相对来说是偶然性的,不是常规性的。这就是常说的认知(意识)先行,持有高级的认知去跟低认知的人竞争,是一种降维打击。
+相关阅读:01 | 到底应该怎样理解软件工程?**阿杜:** 1.  做任何事情都要按照一定的理论指导来,例如,依靠系统化、结构化的“工程思维”,将生活和工作中的每个事情都看做一个项目,可以提高做事的成功率和效率,虽然不用这些理论指导也能做成事情,但是相对来说是偶然性的,不是常规性的。这就是常说的认知(意识)先行,持有高级的认知去跟低认知的人竞争,是一种降维打击。
 
 2. 工程思维的核心有两点:系统化,也就是全局观,要从站在整个项目的高度去看问题,不能做井底之蛙;结构化,也就是有步骤、有节奏得做事情的意识。
 
@@ -223,15 +223,15 @@ ps:学习自动化测试其实不等于一定能缩短测试周期,“测试
 
 相关阅读:03 | 瀑布模型:像工厂流水线一样把软件开发分层化
 
-**纯洁的憎恶:** 稳定、可靠、一步到位的瀑布模型,不太适用于违约风险大、需求不明确、快速见效的场景。 **快速原型模型** :不见兔子不撒鹰。期初不考虑质量、架构,用最快的速度见效,并向用户确认需求。经过几轮直观、快速的反馈,把需求确定下来。接下来,既可以抛弃原型用瀑布精密重构,也可以在模型基础上完善。优点是快速有效地确认需求。不足难以有效应对后续的需求变更。 **增量模型** :分而治之。将大系统横向拆分成相对独立的若干小模块,每个模块采用瀑布模式分批次交付。优点是较快见到成果,且能够及时了解项目进展。不足是存在需求明确、系统可拆分、交付可分批等适用条件。 **迭代模型** :罗马不是一天建成。把软件项目纵向划分成若干阶段,从核心功能入手,逐渐深化、细化,直到满足用户的全部需求。每个阶段都是一个瀑布,都要在前一阶段成果基础上加工、打磨。优点是快速满足基本需要,并体会软件演进的快感。不足是需求演化具有不确定性,会导致代码冗余、系统重构风险、项目周期不可控。
+**纯洁的憎恶:** 稳定、可靠、一步到位的瀑布模型,不太适用于违约风险大、需求不明确、快速见效的场景。**快速原型模型** :不见兔子不撒鹰。期初不考虑质量、架构,用最快的速度见效,并向用户确认需求。经过几轮直观、快速的反馈,把需求确定下来。接下来,既可以抛弃原型用瀑布精密重构,也可以在模型基础上完善。优点是快速有效地确认需求。不足难以有效应对后续的需求变更。**增量模型** :分而治之。将大系统横向拆分成相对独立的若干小模块,每个模块采用瀑布模式分批次交付。优点是较快见到成果,且能够及时了解项目进展。不足是存在需求明确、系统可拆分、交付可分批等适用条件。**迭代模型** :罗马不是一天建成。把软件项目纵向划分成若干阶段,从核心功能入手,逐渐深化、细化,直到满足用户的全部需求。每个阶段都是一个瀑布,都要在前一阶段成果基础上加工、打磨。优点是快速满足基本需要,并体会软件演进的快感。不足是需求演化具有不确定性,会导致代码冗余、系统重构风险、项目周期不可控。
 
 我做甲方管过不少外包项目,大 V 模型再熟悉不过了。整个过程冗长繁琐,走流程比建软件更累心。而且等项目结束的时候,需求早就变得面目全非了。乙方只能硬着头皮做,不然连业绩都没有,真是血本无归。在增量或迭代模型的每次交付后都做一次风险评估,演进为螺旋模型,可以及时止损。
 
 项目做成这样,更深远的原因是业务都是在摸着石头过河,需求不变更才怪呢。但每年几个亿的信息化预算还是非常诱人的,投标单位络绎不绝。RUB 看起来不错,但需求快速演化会依然带来无法回避的系统重构压力,终归还要具体问题具体分析。
 
-相关阅读:04 | 瀑布模型之外,还有哪些开发模型? **西西弗与卡夫卡:** 当前不够明确、后期可能有较大变化的需求,准确说首先要考虑的不是用哪种开发方法,而是最好避免一开始就投入开发资源。开发的代价非常高,推倒重新开发的代价更高。最好是先想别的办法,验证需求是否真实存在之后再动手写代码。
+相关阅读:04 | 瀑布模型之外,还有哪些开发模型?**西西弗与卡夫卡:** 当前不够明确、后期可能有较大变化的需求,准确说首先要考虑的不是用哪种开发方法,而是最好避免一开始就投入开发资源。开发的代价非常高,推倒重新开发的代价更高。最好是先想别的办法,验证需求是否真实存在之后再动手写代码。
 
-相关阅读:04 | 瀑布模型之外,还有哪些开发模型? **alva_xu:**
+相关阅读:04 | 瀑布模型之外,还有哪些开发模型?**alva_xu:**
 
 对于增量或迭代开发,大型企业需要考虑这些不适应点:
 
@@ -255,7 +255,7 @@ ps:学习自动化测试其实不等于一定能缩短测试周期,“测试
 
 敏捷开发具有快速迭代、持续集成、拥抱变化等诱人的特点,但也有苛刻的条件要求。不过,即使无法推行完整的敏捷开发,依旧可以在传统模式下,有针对性的应用敏捷开发的实践方法。
 
-相关阅读:05 | 敏捷开发打底是想解决什么问题? **alva_xu:**
+相关阅读:05 | 敏捷开发打底是想解决什么问题?**alva_xu:**
 
 我们现在着手的一个项目,是一个软件框架建设项目,外包给供应商做的。在签合同时,基本需求已经梳理得差不多了。所以按理是可以采用瀑布式开发来进行的。但由于以下原因,所以我们结合了增量开发和 Scrum 项目管理的模式进行系统建设。
 
@@ -319,7 +319,7 @@ ps:学习自动化测试其实不等于一定能缩短测试周期,“测试
 
 作者回复:迭代模型和 MVP 是非常好的组合,因为迭代的时候,会优先选取最重要的功能,慢慢的那些不重要的功能甚至永远不会被加入迭代中,就因为不需要浪费时间在上面了
 
-相关阅读:08 | 怎样平衡软件质量与时间成本范围的关系? **alva_xu:** 传统的大企业(不是指 BAT 这类大企业),比如我们企业,IT 项目牵涉到三个部门,一个是业务需求部门,一个是 IT 部门,一个是财务预算审批部门,采取的形式一般都是采用外包方式,而且往往是固定合同,也就是合同价格是确定的,需求范围也是确定的,这样的话,金三角的两条边就定下来了,剩下来的就是时间和质量的关系问题了。
+相关阅读:08 | 怎样平衡软件质量与时间成本范围的关系?**alva_xu:** 传统的大企业(不是指 BAT 这类大企业),比如我们企业,IT 项目牵涉到三个部门,一个是业务需求部门,一个是 IT 部门,一个是财务预算审批部门,采取的形式一般都是采用外包方式,而且往往是固定合同,也就是合同价格是确定的,需求范围也是确定的,这样的话,金三角的两条边就定下来了,剩下来的就是时间和质量的关系问题了。
 
 按照金三角的理论,我们就可以知道前面所述的场景下项目组该重点抓什么了:作为甲方项目经理,重点抓的就是质量和时间了。如何通过提高效率,使单位时间的产出比原来的多(相当于增加了时间),来提高项目的交付质量,是我们甲方 IT 项目经理最关心的事。
 
@@ -336,7 +336,7 @@ ps:学习自动化测试其实不等于一定能缩短测试周期,“测试
 最佳思辨
 ---- **林云:** 文中提出可以借鉴软件开发模型中的特点,这一点并不是普通软件开发成员可以使用的。任何一个软件开发模式都有对应的主要问题。就像你把飞机的引擎放在拖拉机上一样。需要对模型进行总体考虑。而且不同的软件开发模式都有对交付团队有能力的要求。
 
-举个不恰当的例子,组合软件开发模式的特点就像让一个摩托车驾驶员开着安装了飞机引擎的拖拉机。这并不是软件工程想达到的结果。希望作者对组合研发模式的前提和应用过程进行描述,以减少软件工程方法使用的随意性。 **宝玉:**
+举个不恰当的例子,组合软件开发模式的特点就像让一个摩托车驾驶员开着安装了飞机引擎的拖拉机。这并不是软件工程想达到的结果。希望作者对组合研发模式的前提和应用过程进行描述,以减少软件工程方法使用的随意性。**宝玉:**
 
 谢谢指正,结合最近波音 747Max 的案例,确实不能乱用,不能说飞机的软件也用敏捷这种快速上线快速迭代的模式。我觉得组合研发模式的前提还是质量,软件工程的目标就是要构建和维护高质量的软件,无论怎么组合开发模式,都不能牺牲质量。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25450\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25450\350\256\262.md"
index 0677f87b4..8a27389e5 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25450\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25450\350\256\262.md"
@@ -10,7 +10,7 @@
 
 ### No.1
 
-**Charles** :可行性分析形同虚设,小公司岗位职责不清晰,互相照顾面子怕得罪人,谁都怕犯错背锅,感觉谁都对,最终就导致谁是“老板”谁拍板!我感觉这个问题挺严重的,很影响决策正确性,只能等所谓的市场反馈。也用类似项目成员“扑克牌”打分的方式可以解决吗?核心问题出在哪里? **宝玉** :这个问题已经不是可行性研究的问题了!核心问题在于没有一套合理的类似于扑克牌打分的机制和流程。
+**Charles** :可行性分析形同虚设,小公司岗位职责不清晰,互相照顾面子怕得罪人,谁都怕犯错背锅,感觉谁都对,最终就导致谁是“老板”谁拍板!我感觉这个问题挺严重的,很影响决策正确性,只能等所谓的市场反馈。也用类似项目成员“扑克牌”打分的方式可以解决吗?核心问题出在哪里?**宝玉** :这个问题已经不是可行性研究的问题了!核心问题在于没有一套合理的类似于扑克牌打分的机制和流程。
 
 扑克牌为什么是个好机制:
 
@@ -19,7 +19,7 @@
 
 可行性研究是不是也可以形成类似机制?有专门会议,大家提前准备,会议上一起讨论结果,不用背锅,根据讨论结果形成最终决议。项目结束后在回顾对比当初的分析,作为下一次的参考。
 
-### No.2 **川杰** :架构师是否也属于管理者的范畴?因为他需要对产品的整个框架的负责,进而涉及到对每个人的代码的管理,必要时还要给带领团队成员去做重难点问题的攻坚。那么对于架构师而言,是更偏向技术还是管理呢? **宝玉** :我觉得架构师和管理有相通的也有不同的,简单说一下我的观点: **相同之处:**
+### No.2 **川杰** :架构师是否也属于管理者的范畴?因为他需要对产品的整个框架的负责,进而涉及到对每个人的代码的管理,必要时还要给带领团队成员去做重难点问题的攻坚。那么对于架构师而言,是更偏向技术还是管理呢?**宝玉** :我觉得架构师和管理有相通的也有不同的,简单说一下我的观点: **相同之处:**
 
 - 都需要大局观;
 
@@ -39,19 +39,19 @@
 
 ### No.3
 
-**天之大舒** :目标的一致性是遇到的困难,公司没有激励制度,导致项目经理和组员目标不一致,如何解决这个问题很挠头。 **宝玉** :解决目标一致性问题,一个方法是多一对一沟通,你了解组员想法,组员知道你的期望;另一个方法就是不必依赖于公司现有制度,自己创造激励制度,激励制度并不一定要花钱或者花很多钱,有时候正式的表扬比钱还有价值。
+**天之大舒** :目标的一致性是遇到的困难,公司没有激励制度,导致项目经理和组员目标不一致,如何解决这个问题很挠头。**宝玉** :解决目标一致性问题,一个方法是多一对一沟通,你了解组员想法,组员知道你的期望;另一个方法就是不必依赖于公司现有制度,自己创造激励制度,激励制度并不一定要花钱或者花很多钱,有时候正式的表扬比钱还有价值。
 
-### No.4 **titan** :小公司如何进行技术管理的问题?我所在的公司,开发人员多的 40、50 人,少的 10 多个人,这个阶段,是用制度来进行管理,还是人来管理比较合适? **宝玉** :我觉得无论大小公司,一定都要多用合理制度流程,多用工具,摆脱对人的过度依赖,只是在设计流程规范时,要充分结合公司特点、项目特点
+### No.4 **titan** :小公司如何进行技术管理的问题?我所在的公司,开发人员多的 40、50 人,少的 10 多个人,这个阶段,是用制度来进行管理,还是人来管理比较合适?**宝玉** :我觉得无论大小公司,一定都要多用合理制度流程,多用工具,摆脱对人的过度依赖,只是在设计流程规范时,要充分结合公司特点、项目特点
 
 比如说小公司老板权力很大,有些流程普通员工有效,老板直接无视了,你还得做好隔离措施,让他不要破坏流程。比如说大公司很多工具、系统都是自建,小公司就不如买来的合算。
 
 大公司各种会议和文档相对多很多,小公司这方面就可以多精简,但必要的也不能少;大公司用瀑布模型开发,一个项目几年耗得起,小公司还是敏捷一点,早点能看到产出更好。将来有一天,小公司也会变成大公司,如果你之前没有做好制度建设,将来团队壮大,项目多了,可能就会成为你的管理瓶颈。
 
-### No.5 **风翱** :“团队的成功,才是你的成功“,以前也坚信这个观点,但自身的例子,让我有些动摇。把下级培养起来了,结果不是升职,而是上级越来越把我边沿化。对于这种情况,怎么调整自己呢? **宝玉** :心情完全能理解,但建议还是看长远些。人生不只是一个下属,不只是一个老板,也不只是一个项目。以前我也纠结过这问题,现在不纠结了。因为我不止能培养好一个下属还能培养更多的下属,我能做好一个项目还能做好更多项目,我不需要靠一个老板的赏识与否来证明自己
+### No.5 **风翱** :“团队的成功,才是你的成功“,以前也坚信这个观点,但自身的例子,让我有些动摇。把下级培养起来了,结果不是升职,而是上级越来越把我边沿化。对于这种情况,怎么调整自己呢?**宝玉** :心情完全能理解,但建议还是看长远些。人生不只是一个下属,不只是一个老板,也不只是一个项目。以前我也纠结过这问题,现在不纠结了。因为我不止能培养好一个下属还能培养更多的下属,我能做好一个项目还能做好更多项目,我不需要靠一个老板的赏识与否来证明自己
 
 ### No.6 **冰封血影** :针对一些曾经贡献大的技术怎么管理呢?然而传统思维模式和产品迭代模式遗留的一些诟病,很难用新环境、新模式让他们去做改变。(这里并不是否定以前的模式)
 
-比如:对新推出的 KPI 这类漠不关心、对整个团队表现不出积极的面,反而带来了一些不好的点和面,但是做东西质量相比其他又高;这类怎么去处理和更好的提升整个团队的战斗力、协作力? **宝玉** :几点建议:
+比如:对新推出的 KPI 这类漠不关心、对整个团队表现不出积极的面,反而带来了一些不好的点和面,但是做东西质量相比其他又高;这类怎么去处理和更好的提升整个团队的战斗力、协作力?**宝玉** :几点建议:
 
 - 多一对一沟通,了解他的诉求,让他了解你的期望;
 
@@ -65,7 +65,7 @@
 
 ### No.7
 
-**Dora** :技术人员呢一般很傲,所以做项目管理,可能要面对被技术人员心里瞧不起,甚至不听话。怎么办? **宝玉** :几点建议吧:
+**Dora** :技术人员呢一般很傲,所以做项目管理,可能要面对被技术人员心里瞧不起,甚至不听话。怎么办?**宝玉** :几点建议吧:
 
 - 如果管理者技术牛,或者懂一点技术,那么就容易很多;
 
@@ -79,17 +79,17 @@
 
 ### No.8
 
-**tcny** :如果因为开发不紧不慢耽误了时间,如何处理呢。应该设置什么样的奖惩制度呢? **宝玉** :这是个好问题!计划恰恰就是为了预防类似于开发不紧不慢耽误了时间的问题。具体例子,一个模块,正常估算(开发和 PM 都认可)需要 5 天,但是如果你的计划粒度是 5 天,那么你到最后一天才能知道是不是会延迟,这时候补救已经晚了。
+**tcny** :如果因为开发不紧不慢耽误了时间,如何处理呢。应该设置什么样的奖惩制度呢?**宝玉** :这是个好问题!计划恰恰就是为了预防类似于开发不紧不慢耽误了时间的问题。具体例子,一个模块,正常估算(开发和 PM 都认可)需要 5 天,但是如果你的计划粒度是 5 天,那么你到最后一天才能知道是不是会延迟,这时候补救已经晚了。
 
 如果你能把粒度设置到半天一天,那么第二或第三天你大概就能知道进度是不是有问题,然后马上作出调整,要么加班,要么找人帮忙,要么换人,要么改计划。这样才可以做到防患未然!至于奖惩制度,只是手段,而不是目的!
 
-### No.9 **一路向北** :计划是否也会有一个迭代的过程呢? **宝玉** :计划一定是个迭代的过程,计划也是个粗到细的过程。一开始不建议做特别细的计划,整体粗一点,定好大的时间节点,也就是里程碑,然后对于下一阶段的计划细化
+### No.9 **一路向北** :计划是否也会有一个迭代的过程呢?**宝玉** :计划一定是个迭代的过程,计划也是个粗到细的过程。一开始不建议做特别细的计划,整体粗一点,定好大的时间节点,也就是里程碑,然后对于下一阶段的计划细化
 
 细化过程中要拉上具体参与的人一起制定,这样结果才科学也不会导致抵触。里程碑定了后不要轻易变,不然就失去了 DeadLine 的意义,即使变也不能过于随意和频繁。
 
-### No.10 **Geek_85f782** :如果是采用敏捷方法的项目,项目计划是否应该就是迭代计划?在这种情况下 WBS 的结构其实就是一轮接着一轮的“规划 - 分析 - 编码 - 测试 - 集成发布 - 与敏捷配套的一系列总结”?每一轮迭代的成果就是项目的里程碑? **宝玉** :敏捷的项目计划确实有些不一样,WBS 分解后会变成 backlog,backlog 的项会被打分(参考扑克牌打分),根据分数大致可以算出来需要多少 Sprint。因为敏捷开发磨合好后,每个 Sprint 能做的任务分数大致相当。算出来多少 Sprint,就能大概知道需要多少时间。通常里程碑不会那么密集的,一般会几个 Sprint 一个里程碑
+### No.10 **Geek_85f782** :如果是采用敏捷方法的项目,项目计划是否应该就是迭代计划?在这种情况下 WBS 的结构其实就是一轮接着一轮的“规划 - 分析 - 编码 - 测试 - 集成发布 - 与敏捷配套的一系列总结”?每一轮迭代的成果就是项目的里程碑?**宝玉** :敏捷的项目计划确实有些不一样,WBS 分解后会变成 backlog,backlog 的项会被打分(参考扑克牌打分),根据分数大致可以算出来需要多少 Sprint。因为敏捷开发磨合好后,每个 Sprint 能做的任务分数大致相当。算出来多少 Sprint,就能大概知道需要多少时间。通常里程碑不会那么密集的,一般会几个 Sprint 一个里程碑
 
-### No.11 **纯洁的憎恶** :制定计划最好能让项目相关各方充分参与,这样计划更可行,偏差低,结果更可控、可预期。但我的经历却是需求、开发、运营、用户等角色几乎不参与制定计划,就连需求分析、功能设计、测试、验收也以工作忙为借口很少介入。项目管理人员主动拉他们,也遭到厌恶与不配合。在观念与体制不支持的环境里,如何能更好的调动各方充分参与、支持项目呢? **宝玉** :你这种性质的单位我确实没经历过,缺少经验。不过我可以帮你从另一个角度分析下,就是如果我不愿意参与计划可能有这些方面原因
+### No.11 **纯洁的憎恶** :制定计划最好能让项目相关各方充分参与,这样计划更可行,偏差低,结果更可控、可预期。但我的经历却是需求、开发、运营、用户等角色几乎不参与制定计划,就连需求分析、功能设计、测试、验收也以工作忙为借口很少介入。项目管理人员主动拉他们,也遭到厌恶与不配合。在观念与体制不支持的环境里,如何能更好的调动各方充分参与、支持项目呢?**宝玉** :你这种性质的单位我确实没经历过,缺少经验。不过我可以帮你从另一个角度分析下,就是如果我不愿意参与计划可能有这些方面原因
 
 1. 跟我利益不相关,做了没好处,不做没损失;
 1. 你已经做的够好够细了,没什么好发挥的;
@@ -97,27 +97,27 @@
 
 所以你可以看看能不能让这事变成一个跟大家利益相关的事,跟绩效考评啥的扯上关系,必要的话拉上领导狐假虎威一番。拉他们参与时不用太细,让他们有机会参与制定,制定时能平衡好他们的利益关系。尤其是里程碑的确定,我觉得应该是和大部分人利益相关的,至少这个点得让他们参与进去。
 
-### No.12 **bearlu** :我一直想自己私下做一个项目,但是不知道如何开始,是不是第一步要确定做个什么软件? **宝玉** :对的,第一步先想好做什么。给你的建议是
+### No.12 **bearlu** :我一直想自己私下做一个项目,但是不知道如何开始,是不是第一步要确定做个什么软件?**宝玉** :对的,第一步先想好做什么。给你的建议是
 
 1. 做个小的;
 1. 做个实用的,最好自己能用或者身边人能用;
 1. 迭代开发,第一版本只做核心功能。
 
-### No.13 **哥本** :在做里程碑的时候需要花时间整合做集成测试吗?就比如像您说的,服务端开发完成后需要与 pc 客户端联调,那这就涉及到发布,环境搭建,部署…做 WBS 时要把这些时间也算进去吗? **宝玉** :做里程碑要不要整合做集成测试,取决于里程碑的目标,比如说如果目标是具备测试条件可以联调,只要能调就可以;也可以定义目标是要测试验收通过,这就需要做集成测试的
+### No.13 **哥本** :在做里程碑的时候需要花时间整合做集成测试吗?就比如像您说的,服务端开发完成后需要与 pc 客户端联调,那这就涉及到发布,环境搭建,部署…做 WBS 时要把这些时间也算进去吗?**宝玉** :做里程碑要不要整合做集成测试,取决于里程碑的目标,比如说如果目标是具备测试条件可以联调,只要能调就可以;也可以定义目标是要测试验收通过,这就需要做集成测试的
 
 这种需要人需要时间去做的事情,都应该放到计划里面。文章中的计划表没有放,是考虑不周。
 
-### No.14 **alva_xu** :需求文档和测试用例怎么验收?对于性能测试是否合格问题,你们是怎么解决测试环境和生产环境可比性问题的? **宝玉** :需求文档验收可以通过需求评审会议,评审时开发和测试都要有代表参加,一个是提出反馈,另一个是及早了解需求。评审会议通常要开几次才能最终定下来。测试用例通常是产品经理协助验收或者辅助确认
+### No.14 **alva_xu** :需求文档和测试用例怎么验收?对于性能测试是否合格问题,你们是怎么解决测试环境和生产环境可比性问题的?**宝玉** :需求文档验收可以通过需求评审会议,评审时开发和测试都要有代表参加,一个是提出反馈,另一个是及早了解需求。评审会议通常要开几次才能最终定下来。测试用例通常是产品经理协助验收或者辅助确认
 
 原来我们在飞信时,会有一个模拟生产环境的压力测试环节,从生产环境同步真实数据过去,规模按生产环境比例缩放。还有的压力测试是直接在生产环境做的,在半夜人流量少的时候。
 
-### No.15 **张驰** :在日常工作中,流程应该由谁来制定呢?普通开发人员还是领导者,亦或者是公司有这种专职专岗的人?往往很多人都能够发现问题,甚至也有一些自己解决问题的方式方法,但是要想具体流程化对公司整体产生作用,往往感觉是有力无门,没有一个好的渠道。 **宝玉** :一个好的流程经常是跟问题切实相关的人员提出来的,或者把问题反馈出来,大家一起想办法,最后由项目经理或者部门负责人帮助落实推广
+### No.15 **张驰** :在日常工作中,流程应该由谁来制定呢?普通开发人员还是领导者,亦或者是公司有这种专职专岗的人?往往很多人都能够发现问题,甚至也有一些自己解决问题的方式方法,但是要想具体流程化对公司整体产生作用,往往感觉是有力无门,没有一个好的渠道。**宝玉** :一个好的流程经常是跟问题切实相关的人员提出来的,或者把问题反馈出来,大家一起想办法,最后由项目经理或者部门负责人帮助落实推广
 
 其实像敏捷开发每次迭代结束后的 Sprint 回顾会议就是一个很好的讨论问题的方式。可以考虑参考 Sprint 回顾会议的做法,定期有专门的会议讨论这样的问题。另外如果有组员之间的“1-1”会议,也是讨论问题和解决方案的途径。也可以通过邮件、聊天工具讨论解决。
 
-### No.16 **bearlu** :能不能说说开会要留意些什么内容,我是个新手,每次开完会议,到开发的时候又找产品确认具体功能。 **宝玉** :我想你说的应该是需求评审会议或者需求讲解会议,对于这类会议,建议你会议前读一下文档,这样心中有数,同时对于文档中觉得不清楚或者有疑惑的地方记录下来,在会议中提出。不同的会议重点不一样,开会之前你都可以实现了解下这次会议的主要目的是什么,然后事先准备一下,这样开会就会更有效率一些
+### No.16 **bearlu** :能不能说说开会要留意些什么内容,我是个新手,每次开完会议,到开发的时候又找产品确认具体功能。**宝玉** :我想你说的应该是需求评审会议或者需求讲解会议,对于这类会议,建议你会议前读一下文档,这样心中有数,同时对于文档中觉得不清楚或者有疑惑的地方记录下来,在会议中提出。不同的会议重点不一样,开会之前你都可以实现了解下这次会议的主要目的是什么,然后事先准备一下,这样开会就会更有效率一些
 
-### No.17 **hua168** :整个项目开始前到项目完全结束,一般都要开那些会议呀?目的是什么? **宝玉** :整理如下。 **项目启动会议:**
+### No.17 **hua168** :整个项目开始前到项目完全结束,一般都要开那些会议呀?目的是什么?**宝玉** :整理如下。**项目启动会议:**
 
 通常在项目启动后,会有一个正式项目启动会议,俗称 Kick off meeting,通过这个项目,你可以了解几个关键信息:
 
@@ -129,9 +129,9 @@
 
 - 流程规范:项目开发的主要流程是什么,基于瀑布还是敏捷。
 
-**瀑布模型各个阶段的评审会议** 瀑布模型因为阶段划分清楚,每个阶段都有明确的产出,所以通常每个阶段都有评审会议,典型的像需求评审会议和架构设计评审会议。这种会议主要目的是用来收集意见。 **瀑布模型各阶段的说明会议** 在评审会议结束后,需求设计、架构设计最终确定后,通常还会组织会议对需求设计和架构设计做说明,所以会有:需求设计说明会和架构设计说明会。 **进度报告会议** 无论是采用瀑布模型还是敏捷开发,通常都少不了进度报告会议。只是瀑布模型通常是以周为单位的周例会,而敏捷开发是以天为单位的每日站会。可以通过会议,了解项目的进展,了解当前的困难和瓶颈,及时调整计划,解决问题。另外在会议上,每个人都要当众讲一下做过的事情和计划要做的事情,也是一种无形的监督和约束。 **项目计划会** 在敏捷开发中,每个 Sprint 开始前都会有一个 Sprint 计划会(Sprint Planning),决定当前 Sprint 要做哪些内容。在瀑布模型中,每个版本开始之前也会有项目计划会。有所不同的是,瀑布模型通常是项目经理和开发经理、测试经理等少数几个人决定的,而敏捷开发中则是全体成员一起针对 Backlog 的内容进行选取和打分。 **产品演示验收会** 在瀑布模型中,项目在测试通过后,会对客户有一个产品演示验收的会议,向客户展示工作成功。敏捷开发中也有 Sprint 评审会(Sprint Review),在每个 Sprint 结束后,向客户演示当前 Sprint 成果。 **项目总结会议** 在项目结束,通常项目经理需要组织一个总结会议,希望大家能在会议上总结一下项目的得失,把经验总结下来,帮助下一次做的更好。在敏捷开发中,更是每个 Sprint 都会有一个 Sprint 回顾会议(Sprint Retrospective)。 **一对一会议** 虽然项目中有每日站会或者周例会这种让项目成员可以反馈问题的方式,但是对于很多人来说,并不愿意在很多人面前说太多,但是如果是一对一的私人对话,则更愿意反馈一些更实质性的内容,从而项目经理或者管理者能了解到更真实更准确的信息。
+**瀑布模型各个阶段的评审会议** 瀑布模型因为阶段划分清楚,每个阶段都有明确的产出,所以通常每个阶段都有评审会议,典型的像需求评审会议和架构设计评审会议。这种会议主要目的是用来收集意见。**瀑布模型各阶段的说明会议** 在评审会议结束后,需求设计、架构设计最终确定后,通常还会组织会议对需求设计和架构设计做说明,所以会有:需求设计说明会和架构设计说明会。**进度报告会议** 无论是采用瀑布模型还是敏捷开发,通常都少不了进度报告会议。只是瀑布模型通常是以周为单位的周例会,而敏捷开发是以天为单位的每日站会。可以通过会议,了解项目的进展,了解当前的困难和瓶颈,及时调整计划,解决问题。另外在会议上,每个人都要当众讲一下做过的事情和计划要做的事情,也是一种无形的监督和约束。**项目计划会** 在敏捷开发中,每个 Sprint 开始前都会有一个 Sprint 计划会(Sprint Planning),决定当前 Sprint 要做哪些内容。在瀑布模型中,每个版本开始之前也会有项目计划会。有所不同的是,瀑布模型通常是项目经理和开发经理、测试经理等少数几个人决定的,而敏捷开发中则是全体成员一起针对 Backlog 的内容进行选取和打分。**产品演示验收会** 在瀑布模型中,项目在测试通过后,会对客户有一个产品演示验收的会议,向客户展示工作成功。敏捷开发中也有 Sprint 评审会(Sprint Review),在每个 Sprint 结束后,向客户演示当前 Sprint 成果。**项目总结会议** 在项目结束,通常项目经理需要组织一个总结会议,希望大家能在会议上总结一下项目的得失,把经验总结下来,帮助下一次做的更好。在敏捷开发中,更是每个 Sprint 都会有一个 Sprint 回顾会议(Sprint Retrospective)。**一对一会议** 虽然项目中有每日站会或者周例会这种让项目成员可以反馈问题的方式,但是对于很多人来说,并不愿意在很多人面前说太多,但是如果是一对一的私人对话,则更愿意反馈一些更实质性的内容,从而项目经理或者管理者能了解到更真实更准确的信息。
 
-### No.18 **kirogiyi** :能否把项目管理每个阶段用到的典型工具分享一下? **宝玉** :我们专栏每个阶段都有关于工具的章节
+### No.18 **kirogiyi** :能否把项目管理每个阶段用到的典型工具分享一下?**宝玉** :我们专栏每个阶段都有关于工具的章节
 
 - 需求分析篇的工具要讲原型设计,需求阶段还有需求收集管理工具,通常可以用 Ticket 管理系统(如 Jira)、源代码管理(如 git)或文档管理工具(如 Google Docs/ 石墨文档)来做。
 
@@ -145,9 +145,9 @@
 
 ### No.19
 
-**纯洁的憎恶** :我看燃尽图好像是根据 ticket 数量的历史变化情况,线性的预测未来的工作进展。但工作真实进展很可能不是线性的,这是否说明燃尽图的剩余工作预测存在天然偏差呢? **宝玉** :燃尽图是有天然偏差的,因为任务的复杂度其实不一样的,有的几小时就完了,有的得好几天,有时候你看只剩下一个任务了,但这个可能是最难耗时最长的。所以我个人更喜欢看板视图,可以直观看到当前 Sprint 具体什么任务还没完成。
+**纯洁的憎恶** :我看燃尽图好像是根据 ticket 数量的历史变化情况,线性的预测未来的工作进展。但工作真实进展很可能不是线性的,这是否说明燃尽图的剩余工作预测存在天然偏差呢?**宝玉** :燃尽图是有天然偏差的,因为任务的复杂度其实不一样的,有的几小时就完了,有的得好几天,有时候你看只剩下一个任务了,但这个可能是最难耗时最长的。所以我个人更喜欢看板视图,可以直观看到当前 Sprint 具体什么任务还没完成。
 
-### No.20 **busyStone** :请问新的这些工具还能看到并方便的编辑任务依赖么?有没有工具可以直接通过修改状态就自动换看板的? 另外,像同一个需求需要多端,安卓,苹果,PC 同时开发的,请问有没有好的方法来建立任务? 之前都是一样建一个,有点烦。 **宝玉** :以 Jira 为例
+### No.20 **busyStone** :请问新的这些工具还能看到并方便的编辑任务依赖么?有没有工具可以直接通过修改状态就自动换看板的? 另外,像同一个需求需要多端,安卓,苹果,PC 同时开发的,请问有没有好的方法来建立任务? 之前都是一样建一个,有点烦。**宝玉** :以 Jira 为例
 
 - Ticket 之间是可以建立关联的,好像不是强依赖。
 
@@ -175,11 +175,11 @@ oldlee:请问前后端开发分离工具有没有好产品推荐?现在遇
 
 ### No.22
 
-**alva_xu** :项目的不同时间节点,项目风险及其处理手段也是不一样的。所以,在讲风险管理的时候,还要加一个时间维度。老师能不能就这个维度来谈谈风险及管控处理方法? **宝玉** :其实要考虑时间维度,你只要把时间范围成本三要素的约束加上就好了。因为时间变了,这三要素的约束也在变。
+**alva_xu** :项目的不同时间节点,项目风险及其处理手段也是不一样的。所以,在讲风险管理的时候,还要加一个时间维度。老师能不能就这个维度来谈谈风险及管控处理方法?**宝玉** :其实要考虑时间维度,你只要把时间范围成本三要素的约束加上就好了。因为时间变了,这三要素的约束也在变。
 
 给你举个例子:一个创业公司,人少缺钱,这时候人就是个很大的风险,有人离职项目就很危险,这其实本质就是三要素的成本;等到熬过这阶段,进入发展阶段,活下来有钱了,人也多了,相对来说,成本就不是最大的约束了,人的风险就没那么大了,这时候就是求快,时间会变成约束,所以如果你的技术和架构跟不上开发的效率,就会成为新的风险。
 
-### No.23 **Bo** :已经写好项目文档,但想更另一步优化文档,老师可以分享一下项目中需求规格说明书、概要设计、详细设计、代码规范文档、测试文档、部署文档等的优秀具体案例吗? **宝玉** :有些内部文档不方便分享。我在文中附了一个开源项目的链接:
+### No.23 **Bo** :已经写好项目文档,但想更另一步优化文档,老师可以分享一下项目中需求规格说明书、概要设计、详细设计、代码规范文档、测试文档、部署文档等的优秀具体案例吗?**宝玉** :有些内部文档不方便分享。我在文中附了一个开源项目的链接:
 
 这个是一个组件使用文档,其实类似的有很多开源项目的文档都写得很好。比如:
 
@@ -195,7 +195,7 @@ Redux: 
 
 
 
-### No.24 **hua168** :老师能简单说一下项目前–> 项目中–> 项目完成,一般都需要哪些文档呀,有没有示例或链接或搜索关键词? **宝玉** :我大致列一下,可能有遗漏的。 **项目立项:**
+### No.24 **hua168** :老师能简单说一下项目前–> 项目中–> 项目完成,一般都需要哪些文档呀,有没有示例或链接或搜索关键词?**宝玉** :我大致列一下,可能有遗漏的。**项目立项:**
 
 - 原始需求文档;
 
@@ -233,14 +233,14 @@ Redux: 
 
 ### No.25
 
-**邢爱明** :对于详细设计文档的颗粒度一直有点疑问。是写到类图或者时序图这种级别,说明不同类和方法之间的关系?还是要细化到类似于伪代码级别,需要写操作哪个数据库表,和调用哪个 api 接口? **宝玉** :我们 2002 年学软件工程的时候,推荐的写设计文档就是你说的这种细化到为伪代码级别,当时初衷是学习建筑行业,把写代码变成像搬砖砌墙一样,招一堆蓝翔培训出来就可以写代码。据说当年日本软件产业就是这样的。
+**邢爱明** :对于详细设计文档的颗粒度一直有点疑问。是写到类图或者时序图这种级别,说明不同类和方法之间的关系?还是要细化到类似于伪代码级别,需要写操作哪个数据库表,和调用哪个 api 接口?**宝玉** :我们 2002 年学软件工程的时候,推荐的写设计文档就是你说的这种细化到为伪代码级别,当时初衷是学习建筑行业,把写代码变成像搬砖砌墙一样,招一堆蓝翔培训出来就可以写代码。据说当年日本软件产业就是这样的。
 
 实际上这些年下来,这种方法是不可行的(至少我没看到过成功案例),一个是设计文档写得太细,其实成本上已经跟写代码没差别了,不利于分工协作;另一个是写代码本身是一种创造性的劳动,当你把文档写到伪代码那么细,具体负责代码实现的没什么好发挥的空间了,都变成体力劳动了。
 
 推荐的做法是写设计文档时不要太细,同时应该把具体模块的设计交给负责这个模块开发的人去做,指导他完成设计。这样既可以更好地分工协作,也可以让程序员有机会成长和充分发挥其主观能动性。
 
 No.26
-===== **晓伟呢。☀** :需求分析之后是不是应该还有产品需求分析文档(PRD)和产品需求规格说明书? **宝玉** :其实不必困惑这个问题,因为这本身没有特别的标准的。如果用瀑布模型开发,确实会有你说的文档,但如果是敏捷开发,可能会是另外的形式存在,例如每个小功能一个独立的用户故事,或者是独立的产品设计文档,只是讲清楚一个功能。
+===== **晓伟呢。☀** :需求分析之后是不是应该还有产品需求分析文档(PRD)和产品需求规格说明书?**宝玉** :其实不必困惑这个问题,因为这本身没有特别的标准的。如果用瀑布模型开发,确实会有你说的文档,但如果是敏捷开发,可能会是另外的形式存在,例如每个小功能一个独立的用户故事,或者是独立的产品设计文档,只是讲清楚一个功能。
 
 虽然形式不一样,但其目的都是一样:让大家可以讨论需求,可以理解需求。之前我有回复过有哪些文档的问题:
 
@@ -252,7 +252,7 @@ No.26
 
 ### No.27
 
-**hua168** :什么是模块呀?模块有哪些分类?一般说的模块是业务模块?模块间“高内聚低耦合”,如果模块之间要进行通讯是不是用接口实现?如果有依赖关系越多的话的话那不是独立性越差?要实现“高内聚低耦合”,如果项目复杂一点,会有难度吧? **宝玉** :这个话题其实属于架构下面的。在技术里面,模块其实是对某一种类型需求的抽象。举个例子来说,一个博客系统,博客的帖子是一个模块,评论是一个模块,用户是一个模块,帖子和评论又可以进一步抽象成内容模块。
+**hua168** :什么是模块呀?模块有哪些分类?一般说的模块是业务模块?模块间“高内聚低耦合”,如果模块之间要进行通讯是不是用接口实现?如果有依赖关系越多的话的话那不是独立性越差?要实现“高内聚低耦合”,如果项目复杂一点,会有难度吧?**宝玉** :这个话题其实属于架构下面的。在技术里面,模块其实是对某一种类型需求的抽象。举个例子来说,一个博客系统,博客的帖子是一个模块,评论是一个模块,用户是一个模块,帖子和评论又可以进一步抽象成内容模块。
 
 模块的分类看你是从架构层面看还是从业务层面看。比如说从架构层面看,一个普通的博客网站可以看成三层:UI 层、业务逻辑层和数据访问层。其中 UI 层包含帖子列表模块和博客文章阅读模块;业务逻辑层则是帖子业务模块、用户业务模块。
 
@@ -276,7 +276,7 @@ kirogiyi:在敏捷开发中产品部门怎样参与产品设计会更好,或
 
 至于参与方式,主要还是看什么形式比较好。比如可以安排需求评审,关键节点让主要开发人员参与确认技术可行性和成本以及建议。比如有分批次的需求讲解会议向开发讲解产品设计,回答理解不清楚的问题。每个 Sprint 产品经理都要参与其中,及时和开发沟通确认需求不明确的问题!
 
-### No.29 **alva_xu** :在目前前后端分离、Restful 风格的应用架构下,是否更容易实现原型设计时的代码的重用率,以提高开发速度?具体是怎么做的? **宝玉** :以前在讨论开发模型的时候有介绍,快速原型开发模型有两种模式,一种是抛弃型的,就是用工具开发的这种;一种是演化型原型,就是类似于 MVP,先做简单核心功能,然后不断演化,变成最终产品
+### No.29 **alva_xu** :在目前前后端分离、Restful 风格的应用架构下,是否更容易实现原型设计时的代码的重用率,以提高开发速度?具体是怎么做的?**宝玉** :以前在讨论开发模型的时候有介绍,快速原型开发模型有两种模式,一种是抛弃型的,就是用工具开发的这种;一种是演化型原型,就是类似于 MVP,先做简单核心功能,然后不断演化,变成最终产品
 
 如果你要提升代码的重复率和开发速度,这种前后端分离的呀,我给你的建议是用一些第三方 API 云服务:
 
@@ -286,7 +286,7 @@ kirogiyi:在敏捷开发中产品部门怎样参与产品设计会更好,或
 
 这样你就完全不用考虑后端开发了,直接用它们定制就好了。等到产品开发出来,你再考虑后端迁移。
 
-### No.30 **LDxy** :Windows 系统已开始就是作为一个产品开发的,最初的项目团队应该是很有产品意识的;而 Linux 系统的开发者最初好像并不是把它作为产品开发的,这是不是也是造成如今 Linux 和 Windows 相比对大多数用户的易用性差别很大的原因?这是不是也是产品意识差异导致的结果?能不能作为一个说明产品意识的例子? **宝玉** :我觉得 Windows 和 Linux 产生的差别还是因为产品定位的不同导致的。前者是商业产品,面向普通用户;后者是开源产品,面向专业用户
+### No.30 **LDxy** :Windows 系统已开始就是作为一个产品开发的,最初的项目团队应该是很有产品意识的;而 Linux 系统的开发者最初好像并不是把它作为产品开发的,这是不是也是造成如今 Linux 和 Windows 相比对大多数用户的易用性差别很大的原因?这是不是也是产品意识差异导致的结果?能不能作为一个说明产品意识的例子?**宝玉** :我觉得 Windows 和 Linux 产生的差别还是因为产品定位的不同导致的。前者是商业产品,面向普通用户;后者是开源产品,面向专业用户
 
 精选留言:
 ----- **西西弗与卡夫卡:** 最近有个项目延期,原因之一就是用到的第三方库需要 https 绑定域名,测试环境因为用 http 所以没有发现该问题。事先的可行性研究,目的就是消除或者平衡项目中的技术风险、能力风险、协作成本、法律、部署等风险。
@@ -375,7 +375,7 @@ kirogiyi:在敏捷开发中产品部门怎样参与产品设计会更好,或
 
 相关阅读:12 | 流程和规范:红绿灯不是约束,而是用来提高效率 **青石:** 组织会议,一定要有会议时间、地点、人员、主题,会前要有准备、会中讨论要有结果(指定干系人)、会后要跟踪。没有主题、没有讨论结果、没有跟踪的会议,都属于无效会议。
 
-相关阅读:13 | 白天开会,加班写代码的节奏怎么破? **kirogiyi:** 完全手工方式管理的优点在于自由空间大、项目结构松散,比如临时添加需求、临时添加人员、临时改变策略等。一旦管理者没有足够的能力去驾驭项目的整体架构,随着项目时间的推移,项目不是越做越简单,而是越做越难,可能到处都是窟窿,根本没法持续下去,并且责任和义务大部分集中于项目管理者。
+相关阅读:13 | 白天开会,加班写代码的节奏怎么破?**kirogiyi:** 完全手工方式管理的优点在于自由空间大、项目结构松散,比如临时添加需求、临时添加人员、临时改变策略等。一旦管理者没有足够的能力去驾驭项目的整体架构,随着项目时间的推移,项目不是越做越简单,而是越做越难,可能到处都是窟窿,根本没法持续下去,并且责任和义务大部分集中于项目管理者。
 
 尽量采用软件工具管理的优点在于对需求、人员、进度、里程碑等可以进行事无巨细的分解或者组合,明确每个人的职责,明确每件事完成的要求,既可以让参与人员看到长期目标,也可以让他们看到短期目标,而不是遥遥无期。可以这样讲,没有路标的 100 公里总是比有路标的 100 公里来得费劲得多,还有就是很容易让参与者失去信心,丧失斗志。
 
@@ -395,11 +395,11 @@ kirogiyi:在敏捷开发中产品部门怎样参与产品设计会更好,或
 
 学的东西越多,记住的内容往往越少,将重复或可整理的内容写成文字,保存起来,使用的时候知道去哪里找就好。这也正是索引 / 缓存的妙处,利用大脑有限的 Cache 资源缓存常用的内容索引,将不常用的内容存盘,需要的时候再次加载。
 
-相关阅读:16 | 为什么你不爱写项目文档? **一路向北:** 我们的项目文档基本上是以协议和流程为主,写这类文档的时候,实际上已经把项目的每一个细节都考虑清楚了,经过几次的 review 之后,后面的项目实现就是根据文档的内容再继续细化,一旦遇到不太清楚的地方,再回头翻阅文档,也很容易知道当初设计的时候是怎么一回事。
+相关阅读:16 | 为什么你不爱写项目文档?**一路向北:** 我们的项目文档基本上是以协议和流程为主,写这类文档的时候,实际上已经把项目的每一个细节都考虑清楚了,经过几次的 review 之后,后面的项目实现就是根据文档的内容再继续细化,一旦遇到不太清楚的地方,再回头翻阅文档,也很容易知道当初设计的时候是怎么一回事。
 
 套用格式确实是一个比较好的方式,填空总是比直接写作文要简单的多,而一旦整个空都填满之后,再继续润色,细化那又会比一开始简单些。写文档,记笔记等,用对工具还是很重要的。
 
-相关阅读:16 | 为什么你不爱写项目文档? **青石:** 赞同老师的“价值体现在产品之上”。技术能力越强,增长曲线越缓慢。实际开发过程过程又大多是满足需求,而不关注质量。企业雇佣关系也更倾向于成本低、增长曲线高的程序员(大不了用你的薪水雇佣两个),所以就出现老程序员的无奈。那么技术在达到一定程度后(增长曲线减慢,收益比下降),同时横向扩展,丰富自己的知识体系结构,不失为一种保值方式。
+相关阅读:16 | 为什么你不爱写项目文档?**青石:** 赞同老师的“价值体现在产品之上”。技术能力越强,增长曲线越缓慢。实际开发过程过程又大多是满足需求,而不关注质量。企业雇佣关系也更倾向于成本低、增长曲线高的程序员(大不了用你的薪水雇佣两个),所以就出现老程序员的无奈。那么技术在达到一定程度后(增长曲线减慢,收益比下降),同时横向扩展,丰富自己的知识体系结构,不失为一种保值方式。
 
 技术通过努力都可以达到差不多的水平,不同的是思维方式和所处的高度。不断学习的过程,其实就是让自己了解的更多思考的越多,思考的越多站的高度自然更高。
 
@@ -430,7 +430,7 @@ InfoQ 上有篇文章供参考: 35 岁的程序员是“都挺好”还是“
 相关阅读:20 | 如何应对让人头疼的需求变更问题?
 
 思辨时刻
----- **dancer :** 管人和管事,言简意赅,受教了! 但是对是否写代码,我个人的看法是,对于一个一线技术管理,比如不到十人技术团队的 leader,我觉得时刻保持学习新技术,写写代码还是有必要的。好处一是做技术选型或者评审设计的时候,不会把团队带跑;好处二是做技术决策的时候,更有说服力。总儿言之,就是要有一定的技术领导力。 **宝玉:**
+---- **dancer :** 管人和管事,言简意赅,受教了! 但是对是否写代码,我个人的看法是,对于一个一线技术管理,比如不到十人技术团队的 leader,我觉得时刻保持学习新技术,写写代码还是有必要的。好处一是做技术选型或者评审设计的时候,不会把团队带跑;好处二是做技术决策的时候,更有说服力。总儿言之,就是要有一定的技术领导力。**宝玉:**
 
 你这个补充很好,我在文中说的有点绝对了,客观一点说法应该是尽可能保持一个合适的比例!但管理的团队越大,职责越多,那么要写的代码比例就要越少。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25451\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25451\350\256\262.md"
index 9044864af..335a70929 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25451\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25451\350\256\262.md"
@@ -12,7 +12,7 @@
 
 ### No.1
 
-**一路向北** :每次看这些架构的思想方法的时候,总是和实际的应用没能很好的结合起来,原因是不是架构设计的实践不够?或者是对各种实现的分析和思考太少? **宝玉** :我觉得不仅要有架构实践,还要有不同场景的实践。
+**一路向北** :每次看这些架构的思想方法的时候,总是和实际的应用没能很好的结合起来,原因是不是架构设计的实践不够?或者是对各种实现的分析和思考太少?**宝玉** :我觉得不仅要有架构实践,还要有不同场景的实践。
 
 举个例子来说,你平时做企业应用架构,没什么流量,没多少数据,复杂的地方都在业务逻辑,这时候你去看那些讲大数据、讲高并发的文章,很难带入到场景去。
 
@@ -22,7 +22,7 @@
 
 和实际应用想结合的问题,一方面说明你现有的架构可能并没有什么大问题,没有那么迫切的需求要改造;另一方面可能还是因为缺少实践经验,心里没底,不知道真用上了有没有用。
 
-### No.2 **小伟** :比较规范的文档有哪些,他们功能分别是什么? **宝玉** :对于瀑布模型,每个阶段结束后,都有相应的验收文档,而敏捷开发则没有那么多硬性的要求,而是根据项目需要,写必要的文档
+### No.2 **小伟** :比较规范的文档有哪些,他们功能分别是什么?**宝玉** :对于瀑布模型,每个阶段结束后,都有相应的验收文档,而敏捷开发则没有那么多硬性的要求,而是根据项目需要,写必要的文档
 
 有些团队对于测试阶段,会有测试用例文档、测试验收报告,发布前还会有部署文档、维护手册,但现在这类文档基本上被测试工具、部署脚本替代了,也没有什么存在必要。
 
@@ -48,13 +48,13 @@
 
 ### No.3 **邢爱明** :项目团队的开发人员,基本都是从外包公司临时找的,水平参差不齐,稳定性差,因此技术选型更多考虑技术的普及度的和是否容易学习掌握,从这方面看基本不太可能选择比较小众、但在特定领域很高效的技术
 
-加上是企业内部管理的系统,数据量和用户数量可控,因此存在技术瓶颈的可能性很小,综合下来看,最好的选择就是最成熟和通用的技术,比如说选择 java 技术栈,web 开发的 ssm 框架等,但这样长远看团队和个人的技术能力很难提升,请问老师在这方面有什么建议? **宝玉** :我觉得团队的技术提升和项目的技术选型要分开,不要总想着两个都兼顾,优先保证好项目稳定、低成本运行。
+加上是企业内部管理的系统,数据量和用户数量可控,因此存在技术瓶颈的可能性很小,综合下来看,最好的选择就是最成熟和通用的技术,比如说选择 java 技术栈,web 开发的 ssm 框架等,但这样长远看团队和个人的技术能力很难提升,请问老师在这方面有什么建议?**宝玉** :我觉得团队的技术提升和项目的技术选型要分开,不要总想着两个都兼顾,优先保证好项目稳定、低成本运行。
 
 技术提升这种事,需要让一部分人先成长起来,然后带动其他人。我自己工作之外会做一些业余项目,然后在这些项目中体验新的技术,体会其中优缺点,然后再逐步应用到工作的项目中,传授给同事们。
 
 我也鼓励其他同事这么做,去做一点自己的项目。但工作中的项目,我是很保守的。
 
-### No.4 **alva_xu** :对于开源技术方面,老师有没有什么经验来指导选型? **宝玉** :开源技术选型,我的经验一般是这样的
+### No.4 **alva_xu** :对于开源技术方面,老师有没有什么经验来指导选型?**宝玉** :开源技术选型,我的经验一般是这样的
 
 1. 先找朋友推荐,少走一点弯路。
 1. 没有推荐的话,就去网上搜索,找几个满足需求的备选。
@@ -72,7 +72,7 @@
 
 ### No.5
 
-**alva_xu** :有没有什么大的原则可以指导技术选型?比如技术成熟度等? **宝玉** :我认为在满足设计目标的前提下,大的原则还是在于项目约束,尤其是成本和时间,然后就是看技术可行性和风险是不是可控,其他看团队风格,有的偏保守有的追新。
+**alva_xu** :有没有什么大的原则可以指导技术选型?比如技术成熟度等?**宝玉** :我认为在满足设计目标的前提下,大的原则还是在于项目约束,尤其是成本和时间,然后就是看技术可行性和风险是不是可控,其他看团队风格,有的偏保守有的追新。
 
 比如说我自己的原则:
 
@@ -82,7 +82,7 @@
 1. 简单的好过复杂的;
 1. 开源的好过商业的(有时候也视情况而定)。
 
-### No.6 **Charles** :有着正常职位或头衔的架构师,对一个全新的项目理解产品需求后进行架构设计,一般会产出哪些“东西”,来满足后续的架构讲解和项目开发过程中的沟通? **宝玉** :互联网产品特点是用户多,企业产品特点是业务复杂,所以架构的侧重点不一样
+### No.6 **Charles** :有着正常职位或头衔的架构师,对一个全新的项目理解产品需求后进行架构设计,一般会产出哪些“东西”,来满足后续的架构讲解和项目开发过程中的沟通?**宝玉** :互联网产品特点是用户多,企业产品特点是业务复杂,所以架构的侧重点不一样
 
 架构师在架构设计后,产出首先是架构设计文档,让大家理解架构。然后还要写架构开发的文档,比如如何基于这个架构开发功能模块,有哪些公共 API 可以调用,怎么样是最佳实践,要遵守哪些规范等。
 
@@ -90,17 +90,17 @@
 
 还有就是在开发过程中,要答疑、解决架构中存在的问题,对架构做优化,还要做代码审查,对于不符合架构规范的地方要指出和修正。
 
-### No.7 **Dora** :互联网架构,要考虑互联网很快的迭代速度,所以对于扩展等特别注意。企业架构,内部 IT 系统相对稳定,对比互联网架构,更简单? **宝玉** :挺好的分析。帮你补充几点:互联网架构不仅迭代会快一些,用户规模通常更大,但业务也会单一些;企业应用通常业务比较复杂,尤其是和行业会有一些结合,但是用户规模要小很多。这些特点,都会影响架构设计的选择
+### No.7 **Dora** :互联网架构,要考虑互联网很快的迭代速度,所以对于扩展等特别注意。企业架构,内部 IT 系统相对稳定,对比互联网架构,更简单?**宝玉** :挺好的分析。帮你补充几点:互联网架构不仅迭代会快一些,用户规模通常更大,但业务也会单一些;企业应用通常业务比较复杂,尤其是和行业会有一些结合,但是用户规模要小很多。这些特点,都会影响架构设计的选择
 
-### No.8 **WL** :老师能不能具体讲讲重构有哪些原则和要注意的地方,感觉一直得不到要领。 **宝玉** :重构的要领我觉得两点
+### No.8 **WL** :老师能不能具体讲讲重构有哪些原则和要注意的地方,感觉一直得不到要领。**宝玉** :重构的要领我觉得两点
 
 第一:你要先写一部分自动化测试代码,保证重构后这些测试代码能帮助你检测出来问题;
 
 第二:在重构模块的时候,老的代码先保留,写新的代码,然后指向新代码,或者用特定开关控制新旧代码的指向(这样上线后可以自己先测试,有问题也可以及时关闭),然后让自动化测试通过,再部署测试,新代码没问题了,删除旧代码。
 
-### No.9 **bearlu** :有没有事情管理的工具?因为如果不记录下来,一会儿就忘记了。 **宝玉** :留言区 McCree 同学推荐了滴答清单。我个人的话,一般就用系统自带的记事本记一下,或者贴一个便签纸在显示器。如果时间跨度长,我就记到 Calendars 上,加上提醒。工作中的任务,我则会创建成 Ticket
+### No.9 **bearlu** :有没有事情管理的工具?因为如果不记录下来,一会儿就忘记了。**宝玉** :留言区 McCree 同学推荐了滴答清单。我个人的话,一般就用系统自带的记事本记一下,或者贴一个便签纸在显示器。如果时间跨度长,我就记到 Calendars 上,加上提醒。工作中的任务,我则会创建成 Ticket
 
-### No.10 **W.T** :现在还有一种说法:提倡基于主分支开发,效率更高;而不是您提到的每人基于自己的分支开发完再合并回主分支。您怎么看待这个问题? **宝玉** :我认为对于软件工程来说,很多问题,并不是只有唯一解,即使是最佳实践,也得看适用的场景和团队
+### No.10 **W.T** :现在还有一种说法:提倡基于主分支开发,效率更高;而不是您提到的每人基于自己的分支开发完再合并回主分支。您怎么看待这个问题?**宝玉** :我认为对于软件工程来说,很多问题,并不是只有唯一解,即使是最佳实践,也得看适用的场景和团队
 
 无论是基于主干还是分支开发,有两点需要注意的:
 
@@ -109,19 +109,19 @@
 
 上面两点才是核心。
 
-### No.11 **hua168** :如果一个项目有 5 个开发做,持续集成怎么保证不乱?比如开发 A 刚刚修复的 bug1,开发 B 把自己修复的 bug2 上传,之前的代码 bug1 没修复,怎么办?如果采用分支怎么合并?如果是直接更新 master 分支,那 A 不是白做了? **宝玉** :要注意是“合并”而不是“覆盖”。比如说 bug1 涉及 file1 和 file3 的修改,那么开发 A 合并的时候只合并 file1 和 file3
+### No.11 **hua168** :如果一个项目有 5 个开发做,持续集成怎么保证不乱?比如开发 A 刚刚修复的 bug1,开发 B 把自己修复的 bug2 上传,之前的代码 bug1 没修复,怎么办?如果采用分支怎么合并?如果是直接更新 master 分支,那 A 不是白做了?**宝玉** :要注意是“合并”而不是“覆盖”。比如说 bug1 涉及 file1 和 file3 的修改,那么开发 A 合并的时候只合并 file1 和 file3
 
 等到开发 B 修复了 bug2,修改了 file1 和 file2,file2 直接合并,file1 需要手动去修复合并冲突才能合并。
 
 每个人开发之前,都会从 master 获取最新版本,合并的时候,如果出现冲突,要先解决冲突才能合并进去。这些其实应该自己去动手试试,会体会更深刻。
 
-### No.12 **dancer** :在微服务架构中,一个服务在测试环境的交付验证,往往还依赖于其他相关服务的新版本,导致新的 feature 很难独立的交付。对于这种情况,有什么好的方法吗? **宝玉** :我觉得对于大部分时候,微服务之间应该是独立的,而不是依赖过于紧密,如果每一个新功能都会这样,那架构设计一定是有问题的,需要重新思考服务划分的合理性
+### No.12 **dancer** :在微服务架构中,一个服务在测试环境的交付验证,往往还依赖于其他相关服务的新版本,导致新的 feature 很难独立的交付。对于这种情况,有什么好的方法吗?**宝玉** :我觉得对于大部分时候,微服务之间应该是独立的,而不是依赖过于紧密,如果每一个新功能都会这样,那架构设计一定是有问题的,需要重新思考服务划分的合理性
 
 但你需要有更多上线或者场景我才能针对性提出一些意见。对于有一些确实需要跨服务合作的大 Feature,这样也是正常的,就是需要一起协作,实现商量好通信协议,分头开发,再联调。
 
-### No.13 **Gao** :老师所讲排查生产问题的案例,首先回滚版本,再看日志。这会引发更多的系统功能不可用吧,两个版本之间的功能差异尚不清楚就直接回滚,系统风险是否被进一步扩大? **宝玉** :这个确实要具体情况具体看,因为我日常的系统上线,都会有回滚方案,回滚也是自动化的很方便。有些跟数据库相关的,如果数据库结构发生变化又产生了新数据,确实没法直接回滚
+### No.13 **Gao** :老师所讲排查生产问题的案例,首先回滚版本,再看日志。这会引发更多的系统功能不可用吧,两个版本之间的功能差异尚不清楚就直接回滚,系统风险是否被进一步扩大?**宝玉** :这个确实要具体情况具体看,因为我日常的系统上线,都会有回滚方案,回滚也是自动化的很方便。有些跟数据库相关的,如果数据库结构发生变化又产生了新数据,确实没法直接回滚
 
-### No.14 **kirogiyi** :团队成员的能力和素质参差不齐,如何有效的去组织和管理项目的自动化测试,自动化集成? **宝玉** :首先,你要先搭建好自动化测试环境,让自动化测试代码能跑起来,最好要和 CI(持续集成工具)整合在一起,每次提交代码 CI 都会跑自动测试,然后能看到运行结果
+### No.14 **kirogiyi** :团队成员的能力和素质参差不齐,如何有效的去组织和管理项目的自动化测试,自动化集成?**宝玉** :首先,你要先搭建好自动化测试环境,让自动化测试代码能跑起来,最好要和 CI(持续集成工具)整合在一起,每次提交代码 CI 都会跑自动测试,然后能看到运行结果
 
 然后,把自动化测试作为开发流程的一部分,比如说要代码审查和自动化测试通过后才能合并代码。这部分工作如果和 CI 集成会容易很多。
 
@@ -129,26 +129,26 @@
 
 简单来说,就是代码审查 +CI+ 培训。
 
-### No.15 **探索无止境** :各种类型的测试覆盖率你们一般采用什么指标?个人感觉在理想的情况下最好是做到百分百覆盖率。 **宝玉** :100% 覆盖,这个我觉得可以作为一种理想追求,但是没必要追求极致,还是要在进度和质量之间有个平衡比较好,毕竟进度也很重要
+### No.15 **探索无止境** :各种类型的测试覆盖率你们一般采用什么指标?个人感觉在理想的情况下最好是做到百分百覆盖率。**宝玉** :100% 覆盖,这个我觉得可以作为一种理想追求,但是没必要追求极致,还是要在进度和质量之间有个平衡比较好,毕竟进度也很重要
 
 另外对于前端业务,我更重视集成测试的覆盖,对于主要业务场景集成测试覆盖到位后,单元测试也就有比较多的覆盖,相对性价比更高,然后再逐步补充单元测试的覆盖率。
 
-### No.16 **起而行** :持续集成怎么理解呢?我看知乎上说,有的团队成员在一天内多次进行编译,发布或自动化测试。 **宝玉** :狭义的持续集成不包括发布,主要指集成,持续的(每次提交代码变更都触发,频繁地提交)对代码进行集成(合并到主干),但集成前要确保自动化测试通过。广义的持续集成还包括部署,也就是集成后自动部署测试环境 (持续交付) 或者生产环境(持续部署)
+### No.16 **起而行** :持续集成怎么理解呢?我看知乎上说,有的团队成员在一天内多次进行编译,发布或自动化测试。**宝玉** :狭义的持续集成不包括发布,主要指集成,持续的(每次提交代码变更都触发,频繁地提交)对代码进行集成(合并到主干),但集成前要确保自动化测试通过。广义的持续集成还包括部署,也就是集成后自动部署测试环境 (持续交付) 或者生产环境(持续部署)
 
-### No.17 **小小** :请问下有没有介绍开发如何写好测试不错的书? **宝玉** :推荐:《how we test software at microsoft》中文版《微软的软件测试之道》。不过没有书其实你也可以找到很多资料的。比如我平时写前端程序,那么我会去 GitHub 或者 Google,通过关键字、语言找跟我项目类似的开源项目,然后看其中有没有自动化测试写得好的
+### No.17 **小小** :请问下有没有介绍开发如何写好测试不错的书?**宝玉** :推荐:《how we test software at microsoft》中文版《微软的软件测试之道》。不过没有书其实你也可以找到很多资料的。比如我平时写前端程序,那么我会去 GitHub 或者 Google,通过关键字、语言找跟我项目类似的开源项目,然后看其中有没有自动化测试写得好的
 
 找到了 (例如:reactstrap、electron-react-boilerplate、kitematic) 就照葫芦画瓢好了,因为都是真实项目,所以特别简单有效,建议你也可以试试。
 
 另外耐心一点,你也可以看到很多关于测试知识分享的技术文章,多看一看也有收获。
 
-### No.18 **hua168** :代码审核是纯手工做的吗?没有好的工具? **宝玉** :代码审查可以参考 GitHub 上一些开源项目的 PR Review,通常网页上可以清楚地标记出代码修改,针对代码行可以写 Review 的评论,这就已经很方便了
+### No.18 **hua168** :代码审核是纯手工做的吗?没有好的工具?**宝玉** :代码审查可以参考 GitHub 上一些开源项目的 PR Review,通常网页上可以清楚地标记出代码修改,针对代码行可以写 Review 的评论,这就已经很方便了
 
 其他工具主要是 Lint 检查代码规范、语法错误等,这个一般在 CI 里面就集成了。
 
 精选留言
 ---- **陈珙 :** 在没有特殊要求的情况下,项目中更加倾向选择更为熟悉的技术,因为我们需要对项目的质量与交付时间负责,可以做到可控的。而新技术有着新的设计思想与强大的功能,同时也伴随着无法预知的“坑”。在后续产品迭代的时间里,有针对性的升级或者选择更换同类技术里更优的。
 
-相关阅读: 22 | 如何为项目做好技术选型? **Y024:** Appfuse(一个 Web 开发基础平台)的作者 Matt Raible 曾总结了选择 Web 框架的 20 条标准。
+相关阅读: 22 | 如何为项目做好技术选型?**Y024:** Appfuse(一个 Web 开发基础平台)的作者 Matt Raible 曾总结了选择 Web 框架的 20 条标准。
 
 同时,他也整理了一份表格,你可以根据自己的权重进行调整,产生自己的分析。
 
@@ -156,7 +156,7 @@
 
 有位大佬说过,“这个世界是,你认为有很多选择,其实只是幻觉,大多数人只有很少的选项。技术研讨会,搞一个选型:hadoop + mysql + xx 时髦技术。架构师唾沫四溅吹一下午,结果老老实实上 Oracle 单例。”
 
-相关阅读: 22 | 如何为项目做好技术选型? **kirogiyi :** 架构师是一个概念性职位,没有明确的界定,需要具备的能力和素质也是千差万别,每个开发人员心目中的架构师画像也都不一样,神秘的 IT 牛人,高级的保姆,无休的恶魔…
+相关阅读: 22 | 如何为项目做好技术选型?**kirogiyi :** 架构师是一个概念性职位,没有明确的界定,需要具备的能力和素质也是千差万别,每个开发人员心目中的架构师画像也都不一样,神秘的 IT 牛人,高级的保姆,无休的恶魔…
 
 在我看来,一名优秀的架构师应该具备良好的技术思维、产品思维和项目管理思维。技术思维是基础,评估技术难度、分析技术复杂度、准确把握技术方向,这些都是架构师在设计架构时面临的技术决策。
 
@@ -184,7 +184,7 @@
 
 轻率 & 无意的债务要警惕。谨慎 & 无意的债务要改变。识别债务防患于未然。根据成本收益分析,决定重写(一次性还款)、维持(只还利息)还是重构(分期付款)。
 
-相关阅读:24 | 技术债务:是继续修修补补凑合着用,还是推翻重来? **kirogiyi :** 在研发过程中,产生技术债务的时候,稍微有点技术功底的人,或多或少都会有感觉的。
+相关阅读:24 | 技术债务:是继续修修补补凑合着用,还是推翻重来?**kirogiyi :** 在研发过程中,产生技术债务的时候,稍微有点技术功底的人,或多或少都会有感觉的。
 
 比如:有重复代码的时候,会意识到好像已经写过了;函数命名的时候,会意识到好像有个相似的名称已经命名过;函数行数过多的时候,自己心里会感觉不舒服等等。
 
@@ -200,7 +200,7 @@
 
 在我遇到过的技术债务中,主要由领导决策、产品业务逻辑、技术最初选型、技术更新换代、团队综合素质中的一种或几种导致。对此,我只能说个人能力达到什么层次就应该去解决什么层次的技术债务,不能去推诿和落井下石,在你手中的技术债务就应该当成自己欠下的技术债务来解决,这样才能持续性的做好自己分内和分外的事情,工作起来才能得心应手。
 
-相关阅读:24 | 技术债务:是继续修修补补凑合着用,还是推翻重来? **kirogiyi:** 我觉得高效,意味着自律,自律的好坏是可以通过你散发出来的气息让周围的人感受到的,比如:说话有没有条理,做事拖不拖延等等。
+相关阅读:24 | 技术债务:是继续修修补补凑合着用,还是推翻重来?**kirogiyi:** 我觉得高效,意味着自律,自律的好坏是可以通过你散发出来的气息让周围的人感受到的,比如:说话有没有条理,做事拖不拖延等等。
 
 生活自律,你会发现每一分每一秒都充满了希望和力量,用积极乐观的心态去完成每一件事,知道自己上一步做好什么,下一步才能做好什么。
 
@@ -212,7 +212,7 @@
 
 我的看法是,积极、主动、自律是高效人士的必备素质。
 
-相关阅读: 25 | 有哪些方法可以提高开发效率? **nigel:** 就学习能力而言,“祭海先河,尤务本原之学”,重要的是对基础知识的掌握。就像侯捷先生说的“基础的东西不易变,不易变的可重用”。
+相关阅读: 25 | 有哪些方法可以提高开发效率?**nigel:** 就学习能力而言,“祭海先河,尤务本原之学”,重要的是对基础知识的掌握。就像侯捷先生说的“基础的东西不易变,不易变的可重用”。
 
 相关阅读:27 | 软件工程师的核心竞争力是什么?(上) **_CountingStars:** 之前看到过一个关于 code review 的观点:在让别人 review 你的代码的之前,你要确保你的代码没有基础的问题,比如单元测试要通过,不能有代码风格问题,首先你要确保你的代码是能正常工作并解决需求的。当然这些基本都可以通过自动化来操作,比如提交 PR 的时候,自动化的检查代码风格,运行单元测试。保证邀请别人 review 你代码的时候,不要为这些小事费精力,提高 review 效率和积极性。
 
@@ -225,7 +225,7 @@
 
 然后数据库基本就选 MySQL,够熟悉够成熟。前端的话,web、小程序、ios、android 之类的都统一 MVVM 思想,进行前后端分离开发,这样各个用户端都可以统一 API 提升效率,这个也会从产品角度思考。
 
-如果产品经理就只是需要一个 PC 网站,而且短期也没升级计划,就选择传统的后端渲染 web 页面方案。可能会站在目前项目或经历过的项目经验去思考问题,期待老师回复指正。 **宝玉:**
+如果产品经理就只是需要一个 PC 网站,而且短期也没升级计划,就选择传统的后端渲染 web 页面方案。可能会站在目前项目或经历过的项目经验去思考问题,期待老师回复指正。**宝玉:**
 
 我觉得你的选型思路在项目发展阶段,包括没有很大规模之前都是没有问题的。选最熟悉的、流行的往往也是风险比较低的。包括如果就是一个 PC 站也不做 SPA(单页应用),也没有必要前后端分离。还是看是不是能低成本满足好项目需求和业务发展。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25452\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25452\350\256\262.md"
index f530c1bef..86222522a 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25452\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25452\350\256\262.md"
@@ -24,7 +24,7 @@
 
 ## 一问一答
 
-**No.1**  **hua168** :质量是怎么打分的?算进 KPI 考核吧?直接用代码质量管理软件(如 sonar)实现自动检查可以吧? **宝玉** :很遗憾,都不好量化,软件检查只是辅助,可以作为一个参考。代码质量要看满足需求,是否设计良好,代码简洁逻辑清晰,可维护、可测试、安全高性能;过程质量要看开发过程对软件工程和项目管理知识的应用;功能质量要看客户满意度。 **No.2**  **砍你一刀** :能分享一个比较好的测试用例模板吗? **宝玉** :我建议你试试 testrail,它的测试用例模板非常专业。
+**No.1**  **hua168** :质量是怎么打分的?算进 KPI 考核吧?直接用代码质量管理软件(如 sonar)实现自动检查可以吧?**宝玉** :很遗憾,都不好量化,软件检查只是辅助,可以作为一个参考。代码质量要看满足需求,是否设计良好,代码简洁逻辑清晰,可维护、可测试、安全高性能;过程质量要看开发过程对软件工程和项目管理知识的应用;功能质量要看客户满意度。**No.2**  **砍你一刀** :能分享一个比较好的测试用例模板吗?**宝玉** :我建议你试试 testrail,它的测试用例模板非常专业。
 
 对于测试用例:
 
@@ -40,11 +40,11 @@
 
 - 期望结果:操作完成后结果应该是什么样的。
 
-**No.3**  **kirogiyi** :最近发现一种现象,开发人员面对测试人员的时候,会展现出一种职业选手遇到业余选手的姿态,傲慢、理所当然,我觉得这是一种不正常的心理状态,应该怎么去管理? **宝玉** :这确实是常见的现象,核心还是多一起合作多相互了解吧,让开发人员看到测试的核心价值,就是对测试方案的设计。
+**No.3**  **kirogiyi** :最近发现一种现象,开发人员面对测试人员的时候,会展现出一种职业选手遇到业余选手的姿态,傲慢、理所当然,我觉得这是一种不正常的心理状态,应该怎么去管理?**宝玉** :这确实是常见的现象,核心还是多一起合作多相互了解吧,让开发人员看到测试的核心价值,就是对测试方案的设计。
 
 我对测试人员敬佩的地方不在于他们会写自动化测试,毕竟这个我写起来还更好,而是他们总能从我没想到的角度测试出来 Bug,从而帮助我提升程序质量。
 
-可以安排一些开发人员和测试人员一起合作的事情,比如测试人员提供测试方案测试用例,开发人员按照测试用例去实现自动化测试,让开发人员明白,做好测试其实不是他们想的那么容易。 **No.4**
+可以安排一些开发人员和测试人员一起合作的事情,比如测试人员提供测试方案测试用例,开发人员按照测试用例去实现自动化测试,让开发人员明白,做好测试其实不是他们想的那么容易。**No.4**
 
 和:从需求分析到设计,开发,测试,部署,运维,都是我一个人的工作。这样的情况,如何工作效果会更好一些?
 
@@ -86,7 +86,7 @@
 
 还有就是一个人开发,缺少向其他人合作和学习的机会,可以有意识地创造更多这样的机会,比如内部多和其他部门合作。外面可以参与一些开源项目。
 
-**No.5**  **hua168** :现在不是流行测试驱动开发吗?先写测试代码再写实现代码,那写完再让专门的测试去测? **宝玉** :测试驱动是一种很好的开发实践,但普及率也不算很高。可以看到自动化测试那一篇,测试驱动写的是单元测试,并不能保证不出 Bug,只是说能有效提升代码质量。还有就是开发人员测试自己写代码,很容易遗漏编码时就没考虑到的逻辑。 **No.6** 果然如此:如果每次发布都对所有方法自动化测试,那么:1. 一定会耗费很多时间;2. 数据库产生很多测试历史数据;3. 写测试用例能达到覆盖率高的写代码技巧,如边界测试代码、幂等测试代码如何实现。这种情况怎么办? **宝玉:**
+**No.5**  **hua168** :现在不是流行测试驱动开发吗?先写测试代码再写实现代码,那写完再让专门的测试去测?**宝玉** :测试驱动是一种很好的开发实践,但普及率也不算很高。可以看到自动化测试那一篇,测试驱动写的是单元测试,并不能保证不出 Bug,只是说能有效提升代码质量。还有就是开发人员测试自己写代码,很容易遗漏编码时就没考虑到的逻辑。**No.6** 果然如此:如果每次发布都对所有方法自动化测试,那么:1. 一定会耗费很多时间;2. 数据库产生很多测试历史数据;3. 写测试用例能达到覆盖率高的写代码技巧,如边界测试代码、幂等测试代码如何实现。这种情况怎么办?**宝玉:**
 
 1. 自动化测试确实会耗费很多时间。自动化测试代码通常是金字塔结构:
 
@@ -102,7 +102,7 @@
 
 1. 高覆盖率的关键在于,在写代码时就注意让代码方便地被测试。也不必过于追求 100% 覆盖,70% 以上我觉得就不错了。
 
-**No.7**  **宝宝太喜欢极客时间了** :对测试这块一直很疑惑,测试脚本、测试用例、测试数据这三者如何配合一起通过 CI 进行自动化测试? **宝玉** :是这样的,CI 本质上只是一个像流水线传送带,你的代码提交了,流水线传送带开始工作,你可以在传送带上面添加任务。
+**No.7**  **宝宝太喜欢极客时间了** :对测试这块一直很疑惑,测试脚本、测试用例、测试数据这三者如何配合一起通过 CI 进行自动化测试?**宝玉** :是这样的,CI 本质上只是一个像流水线传送带,你的代码提交了,流水线传送带开始工作,你可以在传送带上面添加任务。
 
 简单来说,你可以想象成 CI 的一个任务启动后,给你一个干净的虚机(实际是运行 Docker Container),然后帮你把当前代码下载下来,帮助你配置好运行环境,然后你就可以在里面安装任何软件、服务和运行任何脚本。
 
@@ -115,21 +115,21 @@
 
 如果上面所有任务都成功了,那么这一次的 CI 任务就成功了,其中一个失败,这一次的 CI 任务就失败了,然后你就要检查什么原因导致的,然后修复再重新执行,保障 CI 任务成功执行。
 
-No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一起看,还是类似 Github 上的合并代码的时候发个 PR,然后另一个人对需要合并的代码进行检查,检查通过后才同意合并请求? **宝玉** :我觉得两种都算 Code Review,只是形式不一样。
+No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一起看,还是类似 Github 上的合并代码的时候发个 PR,然后另一个人对需要合并的代码进行检查,检查通过后才同意合并请求?**宝玉** :我觉得两种都算 Code Review,只是形式不一样。
 
 通常 PR 这种 Code Review 应该是贯穿到日常开发过程中的,每个人都可以去 Review,有 1-2 个人 Review 通过就可以。合并的话不只是 Code Review 通过,还需要自动测试通过。
 
-而大屏幕这种参与人多,成本高,属于偶尔针对特殊故障分析、学习研讨等目的才做的。 **No.9**  **yellowcloud** :我们目前有一个项目是做实时数据采集的,对方将实时数据推送给我们,基本上每天每个时刻都可能有数据推送过来。这样就导致一个问题,我们部署新的版本时,他们的数据还在推送,这样就不可避免地丢失了部署过程中的数据,对方也没有重新推送的机制。请问,这种问题有没有比较好的解决方案,以解决更新版本时数据丢失的问题? **宝玉** :这个问题其实不复杂,你可以将服务分拆,独立出来一个专门接受数据的服务,这个服务极其简单,只做一件事:接收数据,并存储到数据库或消息队列。
+而大屏幕这种参与人多,成本高,属于偶尔针对特殊故障分析、学习研讨等目的才做的。**No.9**  **yellowcloud** :我们目前有一个项目是做实时数据采集的,对方将实时数据推送给我们,基本上每天每个时刻都可能有数据推送过来。这样就导致一个问题,我们部署新的版本时,他们的数据还在推送,这样就不可避免地丢失了部署过程中的数据,对方也没有重新推送的机制。请问,这种问题有没有比较好的解决方案,以解决更新版本时数据丢失的问题?**宝玉** :这个问题其实不复杂,你可以将服务分拆,独立出来一个专门接受数据的服务,这个服务极其简单,只做一件事:接收数据,并存储到数据库或消息队列。
 
-你原有的服务,改从数据库或者消息队列读取即可。更新部署的时候,接受数据的服务就不要轻易更新了,这样就不担心会丢数据了。真要更新,只要和对方协商一下,暂停推送就好了。 **No.10**  **Charles** :如果是瀑布流带点敏捷部分实践的开发方式,总觉得 UI 这个岗位工作量不怎么饱和,一个版本过来特别是小版本迭代,周期可能是两周,UI 可能 1 天就搞定了,其他岗位都差不多要全程跟下来,这个问题出现在哪里? **宝玉** :这个问题有点不好回答,毕竟对你项目情况不够了解。
+你原有的服务,改从数据库或者消息队列读取即可。更新部署的时候,接受数据的服务就不要轻易更新了,这样就不担心会丢数据了。真要更新,只要和对方协商一下,暂停推送就好了。**No.10**  **Charles** :如果是瀑布流带点敏捷部分实践的开发方式,总觉得 UI 这个岗位工作量不怎么饱和,一个版本过来特别是小版本迭代,周期可能是两周,UI 可能 1 天就搞定了,其他岗位都差不多要全程跟下来,这个问题出现在哪里?**宝玉** :这个问题有点不好回答,毕竟对你项目情况不够了解。
 
 我觉得,如果 UI 这个岗位对你的团队来说是必须的,并且 UI 设计师很好地完成了他 / 她的工作,那么就很好,没有任何问题。毕竟有的人效率就是比较高,好过故意磨洋工看起来很忙。
 
-如果 UI 这个岗位是可有可无,那么就可以考虑不设置这岗位,将工作外包出去,或者尽可能用一些标准的 UI,或者让前端工程师兼职 UI 设计工作。 **No.11**  **yellowcloud** :宝玉老师能不能介绍一整套可以简易部署使用的 devOps 的工具,方便小公司快速部署、实现,在实践中感受 devOps 的魅力。 **宝玉** :如果你要部署持续集成环境,可以先试试 Jenkins 或者 Gitlab CI。如果你要部署日志和监控系统,可以试试 ELK,也就是 Elasticsearch、Logstash、Kibana 三者的结合。网上可以找到很多安装使用教程。 **No.12**  **一步** :搭建自动化测试,自动化部署,自动化监控系统,都自动化了,开发都做了,是不是就不需要运维和测试了? **宝玉** :自动化只是把重复的体力活做了。自动化测试的话,还是需要测试人员写测试用例才能有更好的测试效果;自动化部署和监控,也离不开专业运维人员的设计和搭建。
+如果 UI 这个岗位是可有可无,那么就可以考虑不设置这岗位,将工作外包出去,或者尽可能用一些标准的 UI,或者让前端工程师兼职 UI 设计工作。**No.11**  **yellowcloud** :宝玉老师能不能介绍一整套可以简易部署使用的 devOps 的工具,方便小公司快速部署、实现,在实践中感受 devOps 的魅力。**宝玉** :如果你要部署持续集成环境,可以先试试 Jenkins 或者 Gitlab CI。如果你要部署日志和监控系统,可以试试 ELK,也就是 Elasticsearch、Logstash、Kibana 三者的结合。网上可以找到很多安装使用教程。**No.12**  **一步** :搭建自动化测试,自动化部署,自动化监控系统,都自动化了,开发都做了,是不是就不需要运维和测试了?**宝玉** :自动化只是把重复的体力活做了。自动化测试的话,还是需要测试人员写测试用例才能有更好的测试效果;自动化部署和监控,也离不开专业运维人员的设计和搭建。
 
-但是可以预见的是,以后低端的手工测试和运维岗位会被挤压的很厉害。如果你看大厂的招聘岗位,这些低端手工岗位都极少或者根本就没有。 **No.13** 邢爱明:1. 谁来主导线上故障处理的过程?2. 故障排查是不是应该有一个标准的分析过程,让运维、开发、安全各方能更好的协作?3. 便利性和安全性如何平衡? **宝玉:** 1.  谁主导线上故障,我觉得有两个指标要考虑:一个是这个人或者角色要懂技术懂业务,这样出现故障,能对故障进行评级;另一个是要能调动开发和运维去协调处理,这样出现故障能找到合适的人去处理,不然也只能干着急。
+但是可以预见的是,以后低端的手工测试和运维岗位会被挤压的很厉害。如果你看大厂的招聘岗位,这些低端手工岗位都极少或者根本就没有。**No.13** 邢爱明:1. 谁来主导线上故障处理的过程?2. 故障排查是不是应该有一个标准的分析过程,让运维、开发、安全各方能更好的协作?3. 便利性和安全性如何平衡?**宝玉:** 1.  谁主导线上故障,我觉得有两个指标要考虑:一个是这个人或者角色要懂技术懂业务,这样出现故障,能对故障进行评级;另一个是要能调动开发和运维去协调处理,这样出现故障能找到合适的人去处理,不然也只能干着急。
 2\.  故障排查上:如果是操作系统、数据库、网络等非应用程序故障,应该是运维负责;如果是应用服务故障,应该是开发去负责,即使开发最近没有去做发布也应该是开发去查。因为只有开发对于应用程序的结构最清楚,才能找出来问题。排查过程中,运维要给予配合。
-3\.  应该搭建起来像 ELK 这样的日志管理系统(可参考《38 日志管理:如何借助工具快速发现和定位产品问题 ?》),将应用程序日志也放上去,这样正常情况下就不需要去登录服务器了,直接就可以通过日志工具查看到异常信息。另外,一些特殊情况应该允许开发人员登录服务器排查定位。 **No.14**  **Charles** :目前只放了 nginx 日志到日志服务做一些简单的分析,还有其他什么日志是应该放到日志服务里的?有什么比较好的实践吗? **宝玉** :我觉得应用程序的日志也应该考虑放进去,对排查问题很有帮助。应用程序的异常信息、错误堆栈非常有用,必须确保记录下来了。
+3\.  应该搭建起来像 ELK 这样的日志管理系统(可参考《38 日志管理:如何借助工具快速发现和定位产品问题 ?》),将应用程序日志也放上去,这样正常情况下就不需要去登录服务器了,直接就可以通过日志工具查看到异常信息。另外,一些特殊情况应该允许开发人员登录服务器排查定位。**No.14**  **Charles** :目前只放了 nginx 日志到日志服务做一些简单的分析,还有其他什么日志是应该放到日志服务里的?有什么比较好的实践吗?**宝玉** :我觉得应用程序的日志也应该考虑放进去,对排查问题很有帮助。应用程序的异常信息、错误堆栈非常有用,必须确保记录下来了。
 
 举个例子来说,你的一个手机 App,一些特定场景下,某个 API 请求出错,而这个 API 可能背后会连接多个服务或者数据库,这样的场景下,光靠 nginx 日志是不够的,必须要有应用程序的日志配合才好定位。
 
@@ -150,19 +150,19 @@ No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一
 
 因此,个人认为,产品、开发、测试的紧密合作是保障产品质量的必备条件。
 
-相关阅读:31 | 软件测试要为产品质量负责吗? **纯洁的憎恶** :
+相关阅读:31 | 软件测试要为产品质量负责吗?**纯洁的憎恶** :
 
 解铃还须系铃人,要想提高软件质量,就要着眼于整个生产链条,每一个环节都要为提高质量出力,而绝不能仅仅依靠质量监控岗位或部门。相反,很多企业设置了类似的部门或岗位,并把质量、安全的重担压在他们肩上,但又没有赋予足够的权力去介入、影响整个链条,结果可想而知。不谋全局者不足以谋一域啊。
 
 把整体质量按照生产链条或链条上的不同角色,划分为若干子部分。通过有机的把控各个子部分质量,形成合力,达到提高整体质量的目的。
 
-相关阅读:31 | 软件测试要为产品质量负责吗? **纯洁的憎恶** :
+相关阅读:31 | 软件测试要为产品质量负责吗?**纯洁的憎恶** :
 
 看来是否需要专职测试人员,在一定程度上需要视具体业务情境而定。不同的情境会有不同的异常情况和极端情况,需要有针对性的设计出完备的测试用例。而且在 Bug 修复后,也要保证修复本身没有 Bug。
 
 所以测试也是一个系统性的工作,如果取消专职测试人员,不仅对开发业务水平要求更高,还需要项目自身的不确定性低一些。感觉有测试思维的开发人员,更有可能写出健壮的代码。
 
-相关阅读:32 | 软件测试:什么样的公司需要专职测试? **yasuoyuhao** :
+相关阅读:32 | 软件测试:什么样的公司需要专职测试?**yasuoyuhao** :
 
 代码就像程序员的名片,要对写出来的代码负责,最好的负责方式就是写测试代码,让每次代码变动,都不会影响到其他代码的运行,避免所谓的改 A 坏 B,节省迂回的时间浪费。也为 CI/ CD 做好准备,无论目前有没有。
 
@@ -170,7 +170,7 @@ No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一
 
 程序难免有 Bug,透过追踪软件,良好地管控 Bug 数量与修复进度,并且补足测试。
 
-相关阅读:33 | 测试工具:为什么不应该通过 QQ/ 微信 / 邮件报 Bug? **纯洁的憎恶** :
+相关阅读:33 | 测试工具:为什么不应该通过 QQ/ 微信 / 邮件报 Bug?**纯洁的憎恶** :
 
 新手用野路子解决问题,高手用模型解决问题:
 
@@ -196,7 +196,7 @@ No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一
 
 线上故障,这是 ITIL 要解决的问题。我觉得最主要还是从三个方面来看。一是从流程上,对于事件管理、问题管理、变更管理、服务等级管理等,要有明确的流程。二是要有合适的工具,比如 ticket 系统,CMDB,监控工具、日志平台等。三是从人员组织来看,要有一线、二线和三线团队的支持,根据所创建的 ticket 的严重性和紧急性,给予不同 level 的支持。当然这也是目前流行的 devops 要解决的问题。
 
-相关阅读:37 | 遇到线上故障,你和高手的差距在哪里? **alva_xu** :
+相关阅读:37 | 遇到线上故障,你和高手的差距在哪里?**alva_xu** :
 
 我觉得 scrum 方法中提到的两个会,可以作为项目复盘会内容的参考。Sprint 评审会议(Sprint Review Meeting)和 Sprint 回顾会议(Sprint Retrospective Meeting)。Sprint 评审会议在 Sprint 快结束时举行 ,用以检视所交付的产品增量并按需调整产品待办列表,是对工作成果的评审。Sprint 回顾会议是 Scrum 团队检视自身并创建下一个 Sprint 改进计划的机会。是对方法论的回顾和提高。项目复盘会也应该从这两个角度去做总结提高。
 
@@ -220,7 +220,7 @@ No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一
 
 其实即使是小团队,也应该加大对自动化测试对投入,绝对是磨刀不误砍柴工,这样 App 开发完成后,很多测试就可以自动化完成,节约时间和人力。当然没有自动化测试覆盖的话,这也是很好的一种测试方式。
 
-相关阅读:31 | 软件测试要为产品质量负责吗? **毅** :
+相关阅读:31 | 软件测试要为产品质量负责吗?**毅** :
 
 项目负责人为软件质量总责任人。功能,代码,过程都要关注,并不一定要亲力亲为,因为除了质量他还要兼顾范围、时间和成本。提升质量意识最理想状态是组员有质量人人有责的意识与行动,但实际上这很难。如果自下而上做不到,就自上而下用制度强推,有奖有罚。
 
@@ -232,7 +232,7 @@ No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一
 
 比如说在需求上多花点时间精力,把需求确认清楚,这就成本一半了,然后再基于确定的需求做好架构设计再开发,最后开发后做好测试,那么质量就有了基本保障了。
 
-相关阅读:31 | 软件测试要为产品质量负责吗? **邢爱明** :
+相关阅读:31 | 软件测试要为产品质量负责吗?**邢爱明** :
 
 谁来做测试工作,这也是我一直比较疑惑的地方。我现在是甲方,主要做的是企业管理软件,业务逻辑和流程控制都比较复杂,部分系统是需要一些领域的专业知识。
 
@@ -244,7 +244,7 @@ No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一
 
 第二种,需要专职的测试人员,因为上一种方案中,需求分析师大部分做的还是正向测试,即按照自己设计的功能和流程,判断软件交付是否合格。但是对异常场景的测试还是比较少的,会导致软件上线后,在用户实际操作过程和设想的不同的时候,往往会出现一些功能异常,给用户的直接感受就是软件不稳定。所以说希望通过专业的测试人员,多采用一些探索式的测试方法,尽量多地发现软件中存在的缺陷,提升交付质量。
 
-哪种方案更加合理一点? **宝玉** :
+哪种方案更加合理一点?**宝玉** :
 
 我的观点是这种情况下需要专职测试的,业务分析师的重点应该是把需求文档写清楚。
 
@@ -252,9 +252,9 @@ No.8 ****  **一步** :Code Review 是指把代码放到大屏幕上大家一
 
 测试也是类似,应该专业的人来做比较好,可以有更好的测试覆盖。一开始肯定是难一点,但是一段时间业务熟悉后,会极大提升整个团队的测试效率,而你也不需要再为这个问题纠结了。
 
-相关阅读:32 | 软件测试:什么样的公司需要专职测试? **kirogiyi** :
+相关阅读:32 | 软件测试:什么样的公司需要专职测试?**kirogiyi** :
 
-对于 Devops 我只是听说过,并没有具体的去了解过它的使用和应用场景。根据宝玉老师的讲述,Devops 的基础是自动化,那么自动化之外好像更多的是一种概念,可以因环境而产生各种不同的方式和方法,并没有比较明确的定论。感觉就像敏捷开发一样,满足敏捷宣言思想的操作都可以是敏捷开发,最终适合自己或团队的才是最好的。 **宝玉** :
+对于 Devops 我只是听说过,并没有具体的去了解过它的使用和应用场景。根据宝玉老师的讲述,Devops 的基础是自动化,那么自动化之外好像更多的是一种概念,可以因环境而产生各种不同的方式和方法,并没有比较明确的定论。感觉就像敏捷开发一样,满足敏捷宣言思想的操作都可以是敏捷开发,最终适合自己或团队的才是最好的。**宝玉** :
 
 自动化确实没有明确的定论,重要的是得要有应用自动化的意识,让自动化变成你项目开发流程的一部分,从而提升效率、改进质量。
 
diff --git "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25453\350\256\262.md" "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25453\350\256\262.md"
index fd8090ed1..81e5de553 100644
--- "a/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25453\350\256\262.md"
+++ "b/docs/Basic/\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216/\347\254\25453\350\256\262.md"
@@ -22,17 +22,17 @@
 
 ## 一问一答
 
-**No.1**  **hua168** :小团队比较乱的话,最好是规范哪些关键流程?比如我们小团队开发,首先看这个功能有没有开发过,如果是开发过,就直接基于以前开发过的代码改。这就导致运维有问题,有些路径没有替换完,手工输入命令可以运行,用 shell 脚本监控发现程序异常,就重启,结果就报错了,用脚本死活启动不起来。然后发现没有路径及文件,叫开发改,要一拖再拖,都不愿意改。 **宝玉** :流程规范的建立是一个逐步的过程,发现单个的问题,首先解决问题,解决完后就需要思考一下:是不是可以通过流程规范规避类似问题。
+**No.1**  **hua168** :小团队比较乱的话,最好是规范哪些关键流程?比如我们小团队开发,首先看这个功能有没有开发过,如果是开发过,就直接基于以前开发过的代码改。这就导致运维有问题,有些路径没有替换完,手工输入命令可以运行,用 shell 脚本监控发现程序异常,就重启,结果就报错了,用脚本死活启动不起来。然后发现没有路径及文件,叫开发改,要一拖再拖,都不愿意改。**宝玉** :流程规范的建立是一个逐步的过程,发现单个的问题,首先解决问题,解决完后就需要思考一下:是不是可以通过流程规范规避类似问题。
 
 就拿你这个例子来说,可以先把 CI 持续集成环境搭起来,然后在发现这个问题后,就针对这个路径的问题,提一个 Ticket,要求补上这部分的自动化测试代码。这样以后每次提交代码,CI 都会自动运行这个测试,出问题了就能及时发现,不至于到了生产环境再发现。
 
 开发人员任务多可以理解,但是你需要把这些任务通过任务跟踪系统统一管理起来,写一个 Ticket 给他,排上优先级。等其他任务忙完,就该把这个任务给做了。
 
-所以小团队乱,任务跟踪管理、开发规范,这都是需要优先建立的流程规范。 **No.2**  **Joey** :研发过程文档,是否有必要进行统一模版,比如方案设计文档、功能测试报告等。如果不设置模版,大家写的五花八门,别人不好检查;如果设置模版,研发人员又说限制他们的想象力。 **宝玉** :我倒是觉得有模板的文档好写一点,填空就好了。对于文档模板,我没有什么建议,毕竟每个公司情况不一样。我经历过的公司没有强制规定要模板的,但会提供两种模板,一种是风格样式的,字体颜色等都采用公司品牌的风格;一种是基于内容的模板,把大标题小标题都列出来,写的时候填内容就好了。 **文档审查重点是检查内容,而不是格式。**  **No.3**  **Charles** :小团队可能就 10 来个人,每个岗位可能就 1~2 个人,这种情况下做内部分享,希望大家都来参与,那么分享内容不好把控;如果太局限于本岗位知识,其他岗位人员参与度不高,效果也不明显。如果只是本岗位的知识分享,那么就 2、3 个人讨论下就行了,有什么好办法解决这个问题? **宝玉** :可以设定一些学习的课题分享,比如说最近有什么新技术很火,但是大家都不知道具体是什么,也很想了解,可以让一个人去学习研究,然后跟大家一起分享。分享的过程其实以讨论为主,分享的人也不需要太多压力,自己也能学到东西,其他听的人在讨论的过程中也能学到东西,共同学习提高。你可以从中做好主持的作用,最好提前也学习准备一些。 **No.4**  **yellowcloud** :我们目前项目使用的管理工具是 TFS,它好像也自带 CI 和 CD 功能,我想请问一下,它和文中介绍的 Azure DevOps,哪个好用呢? **宝玉** :Azure DevOps 应该是 TFS 的升级版,如果在线托管的话,你应该考虑用 Azure DevOps。 **No.5**  **乐爽** :详细的需求分析是放在迭代内进行的,但此时的需求是一个很小的点,所以不会占据整个迭代太多的时间,是吗?如果在迭代内发现需求方案不合理,放入到下一个迭代,这是否合理呢? **宝玉** :是的,因为一个迭代内的需求不多,所以需求分析相对时间较短。如果一个需求在一个迭代内做不完,可以延到下一个迭代。如果一个需求不合理,那么需要重新讨论,讨论清楚了再决定是放当前迭代还是后续迭代。 **No.6**  **hua168** :小公司复盘,是这个弄好了,那个又变差了,也不想着怎么改进,强制执行大家都很抵触,怎么办? **宝玉** :如果是解决一个问题又导致了新的问题,按下葫芦起了瓢这种情况,需要多在整体思考一下原因,尤其是项目的整体流程和开发计划方面。推广开发流程导致大家反感,觉得时间紧还搞其他事情,解决这个问题,需要两方面入手:
+所以小团队乱,任务跟踪管理、开发规范,这都是需要优先建立的流程规范。**No.2**  **Joey** :研发过程文档,是否有必要进行统一模版,比如方案设计文档、功能测试报告等。如果不设置模版,大家写的五花八门,别人不好检查;如果设置模版,研发人员又说限制他们的想象力。**宝玉** :我倒是觉得有模板的文档好写一点,填空就好了。对于文档模板,我没有什么建议,毕竟每个公司情况不一样。我经历过的公司没有强制规定要模板的,但会提供两种模板,一种是风格样式的,字体颜色等都采用公司品牌的风格;一种是基于内容的模板,把大标题小标题都列出来,写的时候填内容就好了。**文档审查重点是检查内容,而不是格式。**  **No.3**  **Charles** :小团队可能就 10 来个人,每个岗位可能就 1~2 个人,这种情况下做内部分享,希望大家都来参与,那么分享内容不好把控;如果太局限于本岗位知识,其他岗位人员参与度不高,效果也不明显。如果只是本岗位的知识分享,那么就 2、3 个人讨论下就行了,有什么好办法解决这个问题?**宝玉** :可以设定一些学习的课题分享,比如说最近有什么新技术很火,但是大家都不知道具体是什么,也很想了解,可以让一个人去学习研究,然后跟大家一起分享。分享的过程其实以讨论为主,分享的人也不需要太多压力,自己也能学到东西,其他听的人在讨论的过程中也能学到东西,共同学习提高。你可以从中做好主持的作用,最好提前也学习准备一些。**No.4**  **yellowcloud** :我们目前项目使用的管理工具是 TFS,它好像也自带 CI 和 CD 功能,我想请问一下,它和文中介绍的 Azure DevOps,哪个好用呢?**宝玉** :Azure DevOps 应该是 TFS 的升级版,如果在线托管的话,你应该考虑用 Azure DevOps。**No.5**  **乐爽** :详细的需求分析是放在迭代内进行的,但此时的需求是一个很小的点,所以不会占据整个迭代太多的时间,是吗?如果在迭代内发现需求方案不合理,放入到下一个迭代,这是否合理呢?**宝玉** :是的,因为一个迭代内的需求不多,所以需求分析相对时间较短。如果一个需求在一个迭代内做不完,可以延到下一个迭代。如果一个需求不合理,那么需要重新讨论,讨论清楚了再决定是放当前迭代还是后续迭代。**No.6**  **hua168** :小公司复盘,是这个弄好了,那个又变差了,也不想着怎么改进,强制执行大家都很抵触,怎么办?**宝玉** :如果是解决一个问题又导致了新的问题,按下葫芦起了瓢这种情况,需要多在整体思考一下原因,尤其是项目的整体流程和开发计划方面。推广开发流程导致大家反感,觉得时间紧还搞其他事情,解决这个问题,需要两方面入手:
 
 1. 首先要反省项目计划,如果只是加要求而不给相应时间计划,比如说要求写自动化测试,而不留出写自动化测试时间,那当然会抵触。所以相应要制定出更好的项目计划,避免为了砍时间而砍时间,给开发留出时间去设计、去写测试代码,不然就算你制定一个很紧的计划,还是要花很多时间修 Bug,最终花的其实时间差不多。
 
-1. 提升大家的认知,不仅是团队内部,还包括团队外部,你的老板和业务部门,获得他们的支持。让大家知道磨刀不误砍柴工:前期投入时间在开发质量上面,后期会节约大量修改 Bug 的时间。 **No.7**  **浮生** :目前执行过程中发现,如果不是自己负责的功能,团队成员在审查其他人代码的积极性并不高,再加上各自任务都很紧,即使审查也是匆匆过去,有时并未起到应有的效果,请问在流程机制中有方法可以提高审查的效果吗? **宝玉** :很抱歉我暂时没有好的建议。可以尝试的是:
+1. 提升大家的认知,不仅是团队内部,还包括团队外部,你的老板和业务部门,获得他们的支持。让大家知道磨刀不误砍柴工:前期投入时间在开发质量上面,后期会节约大量修改 Bug 的时间。**No.7**  **浮生** :目前执行过程中发现,如果不是自己负责的功能,团队成员在审查其他人代码的积极性并不高,再加上各自任务都很紧,即使审查也是匆匆过去,有时并未起到应有的效果,请问在流程机制中有方法可以提高审查的效果吗?**宝玉** :很抱歉我暂时没有好的建议。可以尝试的是:
 
 1. 首先强制 Review 才能合并是必须的;
 
@@ -42,7 +42,7 @@
 
 1. 鼓励资深的程序员做好带头作用,可以把 Review 代码参与度和 Review 代码质量作为绩效的一部分;
 
-1. 你可以每天检查一遍审查通过的代码,对于明显有问题的,私下找可以找相关人谈一谈。 **No.8**  **胡鹏** :我现在遇到一些情况,需求出来了,估时的时候,通常有两种心理。第一种, 尽量压缩自己的时间,当然领导也会压缩时间, 这时心里想的是要好好表现,把时间压短一点;第二种,尽量多一点充裕时间,当出现问题能有足够的时间来解决,不至于延期。对于估时,取一还是取二还是在一和二之间平衡? **宝玉** :太紧和太松的时间估算都不可取,应该是尽可能准确地选择接近实际情况的时间,并且留有一点富裕应对意外情况。时间太紧了要加班加点还要被质疑能力;时间太松了会影响以后估算时间的真实性。
+1. 你可以每天检查一遍审查通过的代码,对于明显有问题的,私下找可以找相关人谈一谈。**No.8**  **胡鹏** :我现在遇到一些情况,需求出来了,估时的时候,通常有两种心理。第一种, 尽量压缩自己的时间,当然领导也会压缩时间, 这时心里想的是要好好表现,把时间压短一点;第二种,尽量多一点充裕时间,当出现问题能有足够的时间来解决,不至于延期。对于估时,取一还是取二还是在一和二之间平衡?**宝玉** :太紧和太松的时间估算都不可取,应该是尽可能准确地选择接近实际情况的时间,并且留有一点富裕应对意外情况。时间太紧了要加班加点还要被质疑能力;时间太松了会影响以后估算时间的真实性。
 
 准确地估算时间是程序员能力的一种,做好不容易,一些建议供参考:
 
@@ -53,22 +53,22 @@
 1. 计划保持及时更新,当出现延迟或者有延迟风险的时候,或者进度提前,需要及时和项目负责人沟通,作出调整,避免影响整体项目进度。
 1. 留一点余量,应对突发情况。
 
-反过来,如果你是领导,在下属估算时间的时候,也要参考上面的一些建议,让计划尽可能地接近真实情况,而不是下属给一个很紧的时间就按照这个时间执行,最后得加班加点,加班是为了应对突发情况的,而不是正常情况。 **No.9**  **Joey** :1. 如何更好地推广 SonarLint 白盒扫描工具?2. 如何要求各开发团队更好地,有效地做代码走查,而不流于形式?(我们现在使用 Gerrit)3. 如何要求开发人员有效实施单元测试? **宝玉** :这种开发流程问题肯定还是要自上而下推才能推得动。我觉得首先应该先找一两个小项目组试点,摸索出一套适合你们的最佳实践,形成流程规范,比如说基于 Github Flow,把 CI(持续集成)环境搭建起来(如果没有的话),把你说的 SonarLint、自动化测试加入到 CI 流程中。再就是逐步扩大范围,在更多项目组推行最佳实践和流程规范,并且改进流程规范。最后就必须要借助行政手段强制推行了。 **No.10**  **Liber** :我们专栏之前的文章中,以本文注册用户为例,分别写了小、中、大型测试用例,但实际开发过程中,如何权衡对一个场景,是该小、中、大测试都写,还是只写部分? **宝玉** :实际开发中,理论上来说,是一个场景大中小测试都要写的。通常情况,开发写小型测试和中型测试,测试写大型测试,或者开发帮助写大型测试。小型测试:中型测试:大型测试比例大约为 7:2:1。小型测试尽可能多覆盖,不要求 100%,谷歌是 85%。中型测试覆盖大部分用户使用场景,小型测试覆盖主要用户场景。 **No.11**  **OnRoad** :客户需求频繁变更,大领导迫于客户压力全盘答应,导致开发节奏被打乱,除了量化风险上报之外,还有什么好办法? **宝玉** :需要和你的领导私下协商,需要在他的帮助下一起作出一些调整:
+反过来,如果你是领导,在下属估算时间的时候,也要参考上面的一些建议,让计划尽可能地接近真实情况,而不是下属给一个很紧的时间就按照这个时间执行,最后得加班加点,加班是为了应对突发情况的,而不是正常情况。**No.9**  **Joey** :1. 如何更好地推广 SonarLint 白盒扫描工具?2. 如何要求各开发团队更好地,有效地做代码走查,而不流于形式?(我们现在使用 Gerrit)3. 如何要求开发人员有效实施单元测试?**宝玉** :这种开发流程问题肯定还是要自上而下推才能推得动。我觉得首先应该先找一两个小项目组试点,摸索出一套适合你们的最佳实践,形成流程规范,比如说基于 Github Flow,把 CI(持续集成)环境搭建起来(如果没有的话),把你说的 SonarLint、自动化测试加入到 CI 流程中。再就是逐步扩大范围,在更多项目组推行最佳实践和流程规范,并且改进流程规范。最后就必须要借助行政手段强制推行了。**No.10**  **Liber** :我们专栏之前的文章中,以本文注册用户为例,分别写了小、中、大型测试用例,但实际开发过程中,如何权衡对一个场景,是该小、中、大测试都写,还是只写部分?**宝玉** :实际开发中,理论上来说,是一个场景大中小测试都要写的。通常情况,开发写小型测试和中型测试,测试写大型测试,或者开发帮助写大型测试。小型测试:中型测试:大型测试比例大约为 7:2:1。小型测试尽可能多覆盖,不要求 100%,谷歌是 85%。中型测试覆盖大部分用户使用场景,小型测试覆盖主要用户场景。**No.11**  **OnRoad** :客户需求频繁变更,大领导迫于客户压力全盘答应,导致开发节奏被打乱,除了量化风险上报之外,还有什么好办法?**宝玉** :需要和你的领导私下协商,需要在他的帮助下一起作出一些调整:
 
 1. 要设立流程提高客户变更需求的成本,可以需求变更,但不能太过于频繁随意;
 
-1. 缩短开发周期,采用迭代模型或者敏捷开发,2~4 周发布一个版本,每个版本实现当前已经确定的最重要的需求,在一个版本内不接受需求变化,变化的需求放在下一个迭代中实现。 **No.12**  **探索无止境** :对于专栏中提到的“测试验收通过后,预部署分支的代码会部署到生产环境。”我的理解是,部署的分支的代码,上线测试没问题之后,再把这个代码合并回主分支,这样理解对不对? **宝玉** :这里有两种策略:
+1. 缩短开发周期,采用迭代模型或者敏捷开发,2~4 周发布一个版本,每个版本实现当前已经确定的最重要的需求,在一个版本内不接受需求变化,变化的需求放在下一个迭代中实现。**No.12**  **探索无止境** :对于专栏中提到的“测试验收通过后,预部署分支的代码会部署到生产环境。”我的理解是,部署的分支的代码,上线测试没问题之后,再把这个代码合并回主分支,这样理解对不对?**宝玉** :这里有两种策略:
 
 1. 每次线上 Bug,修复后只合并到预部署分支,最后统一把预部署分支合并回主分支。优点是简单,缺点是合并时可能会有很多冲突;
 
 1. 每次线上 Bug,修复后同时合并预部署分支和主分支。优点是以后就不用再合并回去,还有可以及时同步 Bug 修复,缺点是麻烦,每次要 cherry pick。
 
-我们项目中选的是后一种策略,因为能及时同步 Bug 修复到主干,这一点对我们很重要。 **No.13**  **maomaostyle** :在敏捷开发中,如何结合标准的项目管理方法呢?比如 wbs 任务拆解,风险识别,因为这两点相对于项目的整体情况已经应该拿到了足够多的输入,但是在敏捷的背景下需求等细节都是不清晰的。另外比如最小化原型产品更难以结合大而全的项目管理方法了吧? **宝玉** :敏捷开发中,wbs 一样可以帮助分解任务,然后把任务拆分到 Sprint,还可以设置里程碑。风险识别应该和用什么开发模型没太大关系,关键还是识别和确定应对策略。最小化原型法可以是小瀑布开发模型也可以是敏捷开发, **关键在于需求要定义清楚,要小。**  **No.14**  **宝宝太喜欢极客时间了** :方法论、方法、模型这些名词具体怎么理解?敏捷开发属于哪一种?实施敏捷软件架构设计等文档都可以省略吗?如果文档都省略了,那开发人员离职后新接手人员怎么快速熟悉项目呢?公司的知识积累怎么体现? **宝玉** :敏捷宣言说的:“工作的软件 高于 详尽的文档。尽管右项有其价值,我们更重视左项的价值。”没有否认文档的价值,也不代表实施敏捷软件架构设计可以省略文档,只是没有必要写过多繁重的、没有价值的文档。
+我们项目中选的是后一种策略,因为能及时同步 Bug 修复到主干,这一点对我们很重要。**No.13**  **maomaostyle** :在敏捷开发中,如何结合标准的项目管理方法呢?比如 wbs 任务拆解,风险识别,因为这两点相对于项目的整体情况已经应该拿到了足够多的输入,但是在敏捷的背景下需求等细节都是不清晰的。另外比如最小化原型产品更难以结合大而全的项目管理方法了吧?**宝玉** :敏捷开发中,wbs 一样可以帮助分解任务,然后把任务拆分到 Sprint,还可以设置里程碑。风险识别应该和用什么开发模型没太大关系,关键还是识别和确定应对策略。最小化原型法可以是小瀑布开发模型也可以是敏捷开发,**关键在于需求要定义清楚,要小。**  **No.14**  **宝宝太喜欢极客时间了** :方法论、方法、模型这些名词具体怎么理解?敏捷开发属于哪一种?实施敏捷软件架构设计等文档都可以省略吗?如果文档都省略了,那开发人员离职后新接手人员怎么快速熟悉项目呢?公司的知识积累怎么体现?**宝玉** :敏捷宣言说的:“工作的软件 高于 详尽的文档。尽管右项有其价值,我们更重视左项的价值。”没有否认文档的价值,也不代表实施敏捷软件架构设计可以省略文档,只是没有必要写过多繁重的、没有价值的文档。
 
-另一个角度来说,也不要过分夸大文档的作用,离职交接,光文档还不够,还离不开人和人之间的互动,交流;公司的知识积累更多靠的是人、代码、文档、流程规范、文化等多方面因素综合的结果,而不光是文档。 **No.15**  **Tiger** :我们做的项目外包,项目组的人数是固定的,每次都是项目组要离职一个才会再招一个人进来补充,这种情况无法培养技术后备,人员风险怎么把控? **宝玉** :这种确实有点困难,有两种策略你可以考虑:
+另一个角度来说,也不要过分夸大文档的作用,离职交接,光文档还不够,还离不开人和人之间的互动,交流;公司的知识积累更多靠的是人、代码、文档、流程规范、文化等多方面因素综合的结果,而不光是文档。**No.15**  **Tiger** :我们做的项目外包,项目组的人数是固定的,每次都是项目组要离职一个才会再招一个人进来补充,这种情况无法培养技术后备,人员风险怎么把控?**宝玉** :这种确实有点困难,有两种策略你可以考虑:
 
 1. 减少对人的依赖,让人来了跟流水线工人一样可以马上上手。如果你的项目类型比较类似,其实可以考虑将相同部分通过架构简化,通过配置或者定制化适用于不同项目。
-1. 培养现有的人,提升现有人的能力,提升归属感,都不容易做到,但都可以试试,或者你也可以想到更好的办法。 **No.16**  **ailei** :除了《人月神话》《人件》,还有哪些偏管理的软件工程的书? **宝玉** :有几本项目管理的书可以看看:
+1. 培养现有的人,提升现有人的能力,提升归属感,都不容易做到,但都可以试试,或者你也可以想到更好的办法。**No.16**  **ailei** :除了《人月神话》《人件》,还有哪些偏管理的软件工程的书?**宝玉** :有几本项目管理的书可以看看:
 
 《项目管理修炼之道》
 
@@ -82,17 +82,17 @@
 
 其实改 Bug 通常不需要花太多时间,所以一般影响不大。如果偶尔 Bug 修改时间过长,不能如期完成的,需要推迟上线。如果团队不适应这种节奏,那么应该延长 Sprint 周期,例如两周一个 Sprint。
 
-文章的例子只是一个参考,并不是说一定要这样做。 **No.18**  **E** :软件开发的过程和方法之间的关系是什么?
+文章的例子只是一个参考,并不是说一定要这样做。**No.18**  **E** :软件开发的过程和方法之间的关系是什么?
 
 **宝玉** :软件开发过程就是指开发软件时整个过程的开发模式,比如说瀑布模型还是敏捷开发。选择了开发过程,你就需要有具体方法来执行。
 
-比如你选择了瀑布模型,整个软件开发过程就是按照瀑布模型的分阶段来进行,对应的方法就是瀑布模型中的方法,例如需求分析、架构设计;如果你选择了敏捷开发,则整个开发过程就是一种敏捷迭代方式,后面的方法对应的就是敏捷开发的一套方法体系,例如 Scrum、用户故事、持续集成等。 **No.19**  **刘晓林** :关于 Ticket 工期估算我有个疑问。团队中一般都是一两个人负责一个小模块,之所以这样做是为了提高工作效率,避免同一段代码每次迭代都由不同的人去修改,因为大家对自己的小模块很熟悉,所以工作效率很高。但这样带来的问题是,团队成员对其他人负责的模块不熟,所以工期估算只能由模块负责人自己完成,别人很难帮上忙。这种情况怎么解决? **宝玉** :这是个好问题。我的建议是模块要换着做,宁可慢一点,不然的话,不仅仅是其他人不能帮忙不能估算,万一有人离开团队了,会更麻烦的。如果团队不大,做的时候分工都不要太细,都不要太局限前端后端,这样其实对整个团队来讲是最好的,互相能替换。当然,也不要着急,慢慢来,不要一下子改变很大。 **No.20**  **谢禾急文** :我想到一个想法,就是通过用一个工具记录我自己开发过程中遇到的所有 Bug,通过记录、分析、反思这些 Bug,能够有助于提升我的编程能力,有助于避免犯同样的错误。我觉得你上面说的那些工具,能够满足我的需求。如果有一个网站,能够提供 Bug 记录、分享、解答的功能,是不是能够满足某些用户的需求?(好像 stackoverflow 就是这样的工具) **宝玉** :我觉得是有帮助,但这个问题的关键在于分析反思 Bug。自己对自己 Bug 的反思才是价值最大的,其他人看过之后不一定能有那么大的共鸣,因为一个 Bug 都有复杂的业务背景,是很难被记录,缺少上下文也很难理解。StackOverflow 是很有价值的,因为它是从问题切入,而问题是有很多共性的,很容易引起共鸣。 **No.21**  **纯洁的憎恶** :我很早就知道知识体系的重要性,我也比较重视构建知识体系,但并没有什么亲测有效的方法,且对知识体系是个什么样的存在缺乏体感认识。可能还是学得太浅,用的太少? **宝玉** :方法不是最主要的,最多让你学习提升一点速度。关键还是坚持,多练习多实践。
+比如你选择了瀑布模型,整个软件开发过程就是按照瀑布模型的分阶段来进行,对应的方法就是瀑布模型中的方法,例如需求分析、架构设计;如果你选择了敏捷开发,则整个开发过程就是一种敏捷迭代方式,后面的方法对应的就是敏捷开发的一套方法体系,例如 Scrum、用户故事、持续集成等。**No.19**  **刘晓林** :关于 Ticket 工期估算我有个疑问。团队中一般都是一两个人负责一个小模块,之所以这样做是为了提高工作效率,避免同一段代码每次迭代都由不同的人去修改,因为大家对自己的小模块很熟悉,所以工作效率很高。但这样带来的问题是,团队成员对其他人负责的模块不熟,所以工期估算只能由模块负责人自己完成,别人很难帮上忙。这种情况怎么解决?**宝玉** :这是个好问题。我的建议是模块要换着做,宁可慢一点,不然的话,不仅仅是其他人不能帮忙不能估算,万一有人离开团队了,会更麻烦的。如果团队不大,做的时候分工都不要太细,都不要太局限前端后端,这样其实对整个团队来讲是最好的,互相能替换。当然,也不要着急,慢慢来,不要一下子改变很大。**No.20**  **谢禾急文** :我想到一个想法,就是通过用一个工具记录我自己开发过程中遇到的所有 Bug,通过记录、分析、反思这些 Bug,能够有助于提升我的编程能力,有助于避免犯同样的错误。我觉得你上面说的那些工具,能够满足我的需求。如果有一个网站,能够提供 Bug 记录、分享、解答的功能,是不是能够满足某些用户的需求?(好像 stackoverflow 就是这样的工具) **宝玉** :我觉得是有帮助,但这个问题的关键在于分析反思 Bug。自己对自己 Bug 的反思才是价值最大的,其他人看过之后不一定能有那么大的共鸣,因为一个 Bug 都有复杂的业务背景,是很难被记录,缺少上下文也很难理解。StackOverflow 是很有价值的,因为它是从问题切入,而问题是有很多共性的,很容易引起共鸣。**No.21**  **纯洁的憎恶** :我很早就知道知识体系的重要性,我也比较重视构建知识体系,但并没有什么亲测有效的方法,且对知识体系是个什么样的存在缺乏体感认识。可能还是学得太浅,用的太少?**宝玉** :方法不是最主要的,最多让你学习提升一点速度。关键还是坚持,多练习多实践。
 
 从知识转变成技能,一定需要通过反复的刻意的练习,才能形成条件反射,最终掌握。没有任何学习方法能替代练习,最多有催化剂,可以加速练习效果的学习方法。
 
 还有就是对技术的学习,不能太依赖于工作上的输入,工作上如果项目好用户多,那还是很有挑战的,但大多数时候没有那么多挑战,可能就是个增删改查,那么几年的工作经验可能只是简单的重复,不能达到刻意练习的效果。那还是要在工作之外寻找一些练习的途径,比如上次我建议的:自己做一点项目、参与一些开源项目。
 
-要想对知识体系有体感认识,还是建议先在一个领域有深度,有一棵树了才能想像出来森林是什么样子的,不然只能看到一片灌木丛。这过程难免要踩很多的坑,经历很多次的失败和挫折,反复的思考、总结和重试。 **No.22**  **titan** :敏捷开发在一些小公司落地是比较难的,原因我认为主要是人的综合素质达不到,敏捷的一些思想和原则不能落地,比如团队成员人人平等的价值观,在小公司,牛人比较少,大部分都是比较弱的人,你让牛人跟他们强调平等,似乎是不太可能的事情。 **宝玉** :平等和牛人,这其实不矛盾的。就像蜘蛛侠的叔叔说的:能力越大责任越大。牛人担负的责任会更大,贡献多,收入也多。
+要想对知识体系有体感认识,还是建议先在一个领域有深度,有一棵树了才能想像出来森林是什么样子的,不然只能看到一片灌木丛。这过程难免要踩很多的坑,经历很多次的失败和挫折,反复的思考、总结和重试。**No.22**  **titan** :敏捷开发在一些小公司落地是比较难的,原因我认为主要是人的综合素质达不到,敏捷的一些思想和原则不能落地,比如团队成员人人平等的价值观,在小公司,牛人比较少,大部分都是比较弱的人,你让牛人跟他们强调平等,似乎是不太可能的事情。**宝玉** :平等和牛人,这其实不矛盾的。就像蜘蛛侠的叔叔说的:能力越大责任越大。牛人担负的责任会更大,贡献多,收入也多。
 
 一个健康的开发团队,无论大小,都应该是有梯队的,有资深的,有新人,资深的(牛人)负责架构、模块划分、实现核心模块,新手则基于架构实现具体模块。不然单靠个别牛人完成功能也是不现实的。敏捷开发在小公司落地,最根本还是真的懂敏捷,能应用好敏捷的原则和实践,不要追求形式化,不要走捷径。
 
@@ -105,7 +105,7 @@
 
 而且,先通过团队管理方式的转变,培养大家的敏捷文化,然后再切到敏捷开发模式,就会更加顺畅。我觉得,小团队管理,一定要培养自主自治合作分享的文化和能力,通过用轮值 Scrum master 的办法,一点点提高这方面的文化和能力。
 
-相关阅读:40 | 最佳实践:小团队如何应用软件工程? **纯洁的憎恶** :
+相关阅读:40 | 最佳实践:小团队如何应用软件工程?**纯洁的憎恶** :
 
 抛弃妄念,脚踏实地。切忌追求过于宏大的目标、过于新奇的技术,而最终难以落地。做事要有边界和约束,向死而生才有效率。专业短板可以尝试自行补齐,也可以求助他人取长补短。
 
@@ -118,7 +118,7 @@
 
 最惨的是,由于测试人员急着测试,也未能做到详细测试,就上线了。又是各种线上 Bug。因此这种两周一上线,会容易让人死盯着上线日期,给全部人员带来很大的压力,相当于是给自己挖坑和约束了,很不应该的。
 
-我觉得软件工程里,开发阶段是最关键的阶段,得给到合理的时间,不然这个阶段被动了,乱了之后,就会产生一系列的不好级联反应。因此,我觉得应该有开发人员来把控节奏,给出工作量,给出哪些可以优先测试。 **宝玉** :
+我觉得软件工程里,开发阶段是最关键的阶段,得给到合理的时间,不然这个阶段被动了,乱了之后,就会产生一系列的不好级联反应。因此,我觉得应该有开发人员来把控节奏,给出工作量,给出哪些可以优先测试。**宝玉** :
 
 好问题!你说的担忧完全合理,也确实可能会出现这样的情况。
 
@@ -158,9 +158,9 @@
 
 如果这样操作有难度的,那么采用 4 周一个迭代,但是每个迭代功能减少,还是一样可行的。还有每个迭代结束后的上线发布,可以有两种类型,小迭代可以不发布生产环境,只是测试环境,几个小迭代后再发布生产环境。也就是说,方法其实是有的,观念上可以先调整,因为这样的迭代周期肯定是可行的。
 
-相关阅读:40 | 最佳实践:小团队如何应用软件工程? **纯洁的憎恶** :
+相关阅读:40 | 最佳实践:小团队如何应用软件工程?**纯洁的憎恶** :
 
-深深地感受到,软件工程不是为了创造最伟大的软件项目而存在,却是为了保障每一个项目的成本、质量、工期、目标等等可控而存在的。 **果然如此** :
+深深地感受到,软件工程不是为了创造最伟大的软件项目而存在,却是为了保障每一个项目的成本、质量、工期、目标等等可控而存在的。**果然如此** :
 
 软件工程是过程控制的方法论,而产品设计才是保证伟大的产品,两者应该结合。
 
@@ -174,11 +174,11 @@
 
 如果领导层倾向于人性的发挥,那么采用 Google 的开发方式(个人认为适合资金比较雄厚的公司),它能让工程师在舒适的环境中充分发挥所长,并去尝试开拓自己感兴趣的新的技术领域,各自都对自己的领域精雕细琢,质量无形中就得到了一定程度上的保证。
 
-从上面来看,我算是一个激进和冒险的人,更喜欢 Facebook 的开发方式,使我能够在不断的创新和错误中成长。 **宝玉** :你这个角度也很新颖!一个公司的文化和创始人的性格是有很大关系的,这些文化都没有绝对的好坏,都成就了伟大的公司,合适的就是最好的
+从上面来看,我算是一个激进和冒险的人,更喜欢 Facebook 的开发方式,使我能够在不断的创新和错误中成长。**宝玉** :你这个角度也很新颖!一个公司的文化和创始人的性格是有很大关系的,这些文化都没有绝对的好坏,都成就了伟大的公司,合适的就是最好的
 
-相关阅读: 44 | 微软、谷歌、阿里巴巴等大厂是怎样应用软件工程的? **传说中的胖子** :
+相关阅读: 44 | 微软、谷歌、阿里巴巴等大厂是怎样应用软件工程的?**传说中的胖子** :
 
-我以前学习技术,就是看怎么实现,或者说是怎么用;现在学习技术,是学习技术在什么情况下产生的,适合解决什么场景下的问题,需要的资源是什么。多学习一些技术以及使用场景、然后在出现问题的时候可以结合实际情况做多种选择,根据其他因素选择一个比较合适的方案,方案确定了,技术实现就会方便很多。因为在 IT 行业边缘化的三线城市,也不知道这种想法有没有什么遗漏,希望老师帮着补充。 **宝玉** :
+我以前学习技术,就是看怎么实现,或者说是怎么用;现在学习技术,是学习技术在什么情况下产生的,适合解决什么场景下的问题,需要的资源是什么。多学习一些技术以及使用场景、然后在出现问题的时候可以结合实际情况做多种选择,根据其他因素选择一个比较合适的方案,方案确定了,技术实现就会方便很多。因为在 IT 行业边缘化的三线城市,也不知道这种想法有没有什么遗漏,希望老师帮着补充。**宝玉** :
 
 我觉得从思路上是没问题的,我从实践的角度提一点建议:技术只有通过实践才能真正清楚其优缺点和使用场景。建议有些新的流行的技术,哪怕项目中不使用,业余时间也可以自己去试试,这样能给你未来的项目实践有更好的指导。当然也不要走偏,学了一个新技术就要应用到实际项目中,如你所说:学技术的目的是为了帮助你更好的选择,选择了合适的之后才是应用。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25400\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25400\350\256\262.md"
index e53ecc5fe..6c819a0f9 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25400\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25400\350\256\262.md"
@@ -29,16 +29,16 @@
 
 互联网领域有一个非常重要的分析方法,那就是一件事情如果可以成功,无非就是合适的人,在合适的时间,做合适的事情。接下来,我将结合课程设计思路和自己的人生经历来分析, 你为什么要学习我的这门操作系统课。
 
-我们可以先从“事到人”的角度入手。从事情上说,我希望做一门学了就能用的课程, **所以本课程学习目标有两个:一是帮助你顺利通过面试、跳槽涨薪;另一个是帮助你提升应对实际工作场景的能力** ,具体包括以下几点。
+我们可以先从“事到人”的角度入手。从事情上说,我希望做一门学了就能用的课程,**所以本课程学习目标有两个:一是帮助你顺利通过面试、跳槽涨薪;另一个是帮助你提升应对实际工作场景的能力**,具体包括以下几点。
 
 - **提升学习和理解能力** :比如学习 Redis 可以理解到日志文件系统层面;学习 Java/Python/Node 等语言可以理解到语言最底层。
 - **提升应用架构能力** :比如可以将操作系统的微内核架构迁移到自己设计的系统中。
 - **提升系统稳定性架构能力** :比如在多线程设计上更出色,可以帮助同事找到设计漏洞。
 - **提升运维能力** :做到可以方便地管理集群和分析日志。
 
-下面我再来结合我自身背景和互联网行业特点与你具体聊一聊。 **首先,帮你面试涨薪这块我非常有底气,为什么我敢这样说?** 我曾在 3 家互联网大厂任职架构师(技术专家),而且是技术委员会成员;另一方面,我做了 10 年的面试官,每年都会到拉勾网筛选简历,保守估计按每年面试 100 个人来算,我至少已经面试过上千人了;还有一方面,我有很多朋友在大厂做技术 Leader,也有很多学生在大厂工作,因此我有一个精准的面试圈子。
+下面我再来结合我自身背景和互联网行业特点与你具体聊一聊。**首先,帮你面试涨薪这块我非常有底气,为什么我敢这样说?** 我曾在 3 家互联网大厂任职架构师(技术专家),而且是技术委员会成员;另一方面,我做了 10 年的面试官,每年都会到拉勾网筛选简历,保守估计按每年面试 100 个人来算,我至少已经面试过上千人了;还有一方面,我有很多朋友在大厂做技术 Leader,也有很多学生在大厂工作,因此我有一个精准的面试圈子。
 
-我们在面试题、面试技巧方面交流非常多,而且我已经出过几门技术类的在线课程,我的讲课风格也受到了很多学员的认可,他们很喜欢和我交流问题、探讨技术,所以我可以自信地说:我是一个非常懂面试和公司用人标准的老师。 **再者,我有丰富的实际应用场景经历。** 中国互联网系统最主要的设计约束:并发高、数据量大(毕竟中国互联网是以人口红利起家的)。比较巧的是,海量用户的 C 端场景和大数据商业分析场景,我刚好都负责过。而高并发、大数据中的很多知识,又需要从操作系统中获取,加上我本身操作系统方面的知识也比较扎实,所以在实际场景这块我也有丰富的经验。 **接下来再从我的角度来看看“现在要不要学操作系统”,我觉得现在的时机刚刚好** 。
+我们在面试题、面试技巧方面交流非常多,而且我已经出过几门技术类的在线课程,我的讲课风格也受到了很多学员的认可,他们很喜欢和我交流问题、探讨技术,所以我可以自信地说:我是一个非常懂面试和公司用人标准的老师。**再者,我有丰富的实际应用场景经历。** 中国互联网系统最主要的设计约束:并发高、数据量大(毕竟中国互联网是以人口红利起家的)。比较巧的是,海量用户的 C 端场景和大数据商业分析场景,我刚好都负责过。而高并发、大数据中的很多知识,又需要从操作系统中获取,加上我本身操作系统方面的知识也比较扎实,所以在实际场景这块我也有丰富的经验。**接下来再从我的角度来看看“现在要不要学操作系统”,我觉得现在的时机刚刚好** 。
 
 首先,目前是一个在线教育的风口,我结合自身背景以及拉勾网在线招聘求职方向的优势,给你带来一门针对工作场景的就业提升类操作系统课程,符合平台调性。
 
@@ -52,13 +52,13 @@
 
 ### 课程介绍
 
-接下来我们聊聊课程内容, **这门课程对标的是架构师层级的基础能力,看个人的接受程度,学完之后大概会在阿里的 P7 及以上层级** 。
+接下来我们聊聊课程内容,**这门课程对标的是架构师层级的基础能力,看个人的接受程度,学完之后大概会在阿里的 P7 及以上层级** 。
 
 课程共分为 8 个模块,合计 39 个课时。具体每个模块的介绍,我将在下一课时“课前必读”中详细讲解。在这里,我先跟你分享一下课程的整体设计思路。
 
 这门操作系统课程将帮助你系统地解决面试中可能遇到的计算机原理和操作系统类问题,并以大厂面试题作为切入点,引出很多你在实际工作中会遇到的问题和技术难点。同时,每一个模块聚焦操作系统知识的一个方向,每节课的标题就是这个方向最需要掌握的,也是真实出现过的大厂面试题,同时它也代表着一类知识点。而且,我还将结合实战场景帮助你打牢基础知识,向架构师的方向努力。
 
-说到工作场景,我认为有两个非常重要的问题需要解决。 **第 1 个问题是提高大家在实际工作场景中的实战能力。** 除了讲解操作系统的知识结构,还会结合以下 6 个场景深入分析:
+说到工作场景,我认为有两个非常重要的问题需要解决。**第 1 个问题是提高大家在实际工作场景中的实战能力。** 除了讲解操作系统的知识结构,还会结合以下 6 个场景深入分析:
 
 - 架构师必备的高并发、多线程编程技巧;
 - 团队 Leader 如何掌握必备的 Linux 运维技巧;
@@ -100,6 +100,6 @@
 
 中国有超过 1000 万程序员,大部分人的年薪小于 30 万。我观察到一个这样的现象:一方面求职者们抱怨市场竞争激烈,大家争抢一两个岗位;另一方面很多优秀团队的高薪岗位招人难,闲置多个空位,求职者很多但是符合岗位要求的却很少。到底是什么原因造成企业招人难,求职者求职难的情况呢?
 
-其实, **拉开个人薪资和团队整体水平差异的分水岭,根本原因就是计算机基础知识的掌握程度** 。基础好的程序员,学习速度快,愿意花时间去积累知识,提高自身能力,因此涨薪快、跳槽更容易;而基础不好的,学习相对较慢,知识输入少,因此涨薪慢、跳槽难。个人能力的高低决定了收入的水平。
+其实,**拉开个人薪资和团队整体水平差异的分水岭,根本原因就是计算机基础知识的掌握程度** 。基础好的程序员,学习速度快,愿意花时间去积累知识,提高自身能力,因此涨薪快、跳槽更容易;而基础不好的,学习相对较慢,知识输入少,因此涨薪慢、跳槽难。个人能力的高低决定了收入的水平。
 
 这个事情很现实,也很不公平。但是反过来想,为什么基础不好的同学,不把时间精力拿出来去填补自己的知识空缺呢?如果你的操作系统知识还是一盘散沙,那么请你现在就开始行动,跟着我一起重学操作系统,把这块知识捡起来。愿正在看这篇文章的你,能通过自己的努力去到更好的团队,拿更高的薪水,进而得到更广阔的发展空间。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25401\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25401\350\256\262.md"
index 7f835e5f7..ebded2548 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25401\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25401\350\256\262.md"
@@ -1,10 +1,10 @@
 # 01 计算机是什么:“如何把程序写好”这个问题是可计算的吗?
 
-我记得自己在面试中遇到过这样一个问题:“ **可不可以计算一个人程序写得好不好** ?”
+我记得自己在面试中遇到过这样一个问题:“ **可不可以计算一个人程序写得好不好**?”
 
 当时我也没有想明白“ **计算** ”这个词是什么意思。但事后分析来看,“计算”不就是写程序吗?
 
-其实简单理解这个问题就是“ **可不可以用机器来判断人的程序写得好不好** ?”如果从这个角度考虑,我是可以和面试官论述一番的。
+其实简单理解这个问题就是“ **可不可以用机器来判断人的程序写得好不好**?”如果从这个角度考虑,我是可以和面试官论述一番的。
 
 后面我查阅了资料,历史上有一个对计算机领域影响颇深的可计算理论,面试官说的“计算”应该就来源于这里。其实继续深挖还能找出很多涉及计算机本源的有趣的知识,比如图灵机、冯诺依曼模型;再比如说 CPU 的构成、程序如何执行、缓存的分级、总线的作用等。
 
@@ -58,7 +58,7 @@
 
 正是因为世界上存在着大量的这种“公说公有理,婆说婆有理”的问题,才让大家认识到计算不能解决所有问题,所以: **计算机能力也是有边界的。哥德尔的不完备性定理,让大家看到了世界上还有大量不可计算的问题。** #### 图灵机和可计算理论
 
-于是人们意识到了需要一个理论,专门回答这样的问题: **哪些问题可以被计算,哪些不可以被计算** ,这就是可计算性理论,该理论是计算机科学的理论基础之一。
+于是人们意识到了需要一个理论,专门回答这样的问题: **哪些问题可以被计算,哪些不可以被计算**,这就是可计算性理论,该理论是计算机科学的理论基础之一。
 
 1936 年,被誉为人工智能之父的阿兰·图灵提出了图灵机,它是一种不断执行指令的抽象计算机。之所以说抽象,是因为图灵并没有真的造出这台机器,而是把它当成理论去和大家探讨可计算问题。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25402\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25402\350\256\262.md"
index 71f20226f..67e128905 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25402\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25402\350\256\262.md"
@@ -2,7 +2,7 @@
 
 本节课给你讲学习操作系统之前的一个前置知识:程序是如何执行的?
 
-**我们先来看一道常规的面试题:相比 32 位,64 位的优势是什么** ?
+**我们先来看一道常规的面试题:相比 32 位,64 位的优势是什么**?
 
 面试官考察这种类型的问题,主要是想看求职者是否有扎实的计算机基础,同时想知道求职者在工作中是否充满好奇,会主动学习、寻根问底,毕竟 32、64 位是经常出现在程序员视野的词汇,常见的东西都弄明白了,那说明这个人学习能力强。
 
@@ -95,7 +95,7 @@
 
 为什么 CPU 要这样设计呢? 因为一个 byte 最大的表示范围就是 0~255。比如要计算 20000\*50,就超出了byte 最大的表示范围了。因此,CPU 需要支持多个 byte 一起计算。当然,CPU 位数越大,可以计算的数值就越大。但是在现实生活中不一定需要计算这么大的数值。比如说 32 位 CPU 能计算的最大整数是 4294967295,这已经非常大了。
 
-**控制单元和逻辑运算单元** CPU 中有一个控制单元专门负责控制 CPU 工作;还有逻辑运算单元专门负责计算。具体的工作原理我们在指令部分给大家分析。 **寄存器**
+**控制单元和逻辑运算单元** CPU 中有一个控制单元专门负责控制 CPU 工作;还有逻辑运算单元专门负责计算。具体的工作原理我们在指令部分给大家分析。**寄存器**
 
 CPU 要进行计算,比如最简单的加和两个数字时,因为 CPU 离内存太远,所以需要一种离自己近的存储来存储将要被计算的数字。这种存储就是寄存器。寄存器就在 CPU 里,控制单元和逻辑运算单元非常近,因此速度很快。
 
@@ -107,12 +107,12 @@ CPU 要进行计算,比如最简单的加和两个数字时,因为 CPU 离
 
 CPU 和内存以及其他设备之间,也需要通信,因此我们用一种特殊的设备进行控制,就是总线。总线分成 3 种:
 
-- 一种是 **地址总线** ,专门用来指定 CPU 将要操作的内存地址。
-- 还有一种是 **数据总线** ,用来读写内存中的数据。
+- 一种是 **地址总线**,专门用来指定 CPU 将要操作的内存地址。
+- 还有一种是 **数据总线**,用来读写内存中的数据。
 
 当 CPU 需要读写内存的时候,先要通过地址总线来指定内存地址,再通过数据总线来传输数据。
 
-- 最后一种总线叫作 **控制总线** ,用来发送和接收关键信号,比如后面我们会学到的中断信号,还有设备复位、就绪等信号,都是通过控制总线传输。同样的,CPU 需要对这些信号进行响应,这也需要控制总线。
+- 最后一种总线叫作 **控制总线**,用来发送和接收关键信号,比如后面我们会学到的中断信号,还有设备复位、就绪等信号,都是通过控制总线传输。同样的,CPU 需要对这些信号进行响应,这也需要控制总线。
 
 #### 输入、输出设备
 
@@ -150,4 +150,4 @@ CPU 和内存以及其他设备之间,也需要通信,因此我们用一种
 
 ### 总结
 
-关于计算机组成和指令部分,我们就先学到这里。这节课我们通过图灵机和冯诺依曼模型学习了计算机的组成、CPU 的工作原理等。此外,我们还顺带讨论了 32 位和 64 位的区别,现在, **你可以回答 64 位和 32 位比较有哪些优势了吗** ?
+关于计算机组成和指令部分,我们就先学到这里。这节课我们通过图灵机和冯诺依曼模型学习了计算机的组成、CPU 的工作原理等。此外,我们还顺带讨论了 32 位和 64 位的区别,现在,**你可以回答 64 位和 32 位比较有哪些优势了吗**?
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25403\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25403\350\256\262.md"
index 5e6f51b47..9d29cc504 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25403\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25403\350\256\262.md"
@@ -8,7 +8,7 @@
 
 1.首先,CPU 读取 PC 指针指向的指令,将它导入指令寄存器。具体来说,完成读取指令这件事情有 3 个步骤:
 
-**步骤 1** :CPU 的控制单元操作地址总线指定需要访问的内存地址(简单理解,就是把 PC 指针中的值拷贝到地址总线中)。 **步骤 2** :CPU 通知内存设备准备数据(内存设备准备好了,就通过数据总线将数据传送给 CPU)。 **步骤 3** :CPU 收到内存传来的数据后,将这个数据存入指令寄存器。
+**步骤 1** :CPU 的控制单元操作地址总线指定需要访问的内存地址(简单理解,就是把 PC 指针中的值拷贝到地址总线中)。**步骤 2** :CPU 通知内存设备准备数据(内存设备准备好了,就通过数据总线将数据传送给 CPU)。**步骤 3** :CPU 收到内存传来的数据后,将这个数据存入指令寄存器。
 
 完成以上 3 步,CPU 成功读取了 PC 指针指向指令,存入了指令寄存器。
 
@@ -22,7 +22,7 @@
 
 1. 内存虽然是一个随机存取器,但是我们通常不会把指令和数据存在一起,这是为了安全起见。具体的原因我会在模块四进程部分展开讲解,欢迎大家在本课时的留言区讨论起来,我会结合你们留言的内容做后续的课程设计。
 1. 程序指针也是一个寄存器,64 位的 CPU 会提供 64 位的寄存器,这样就可以使用更多内存地址。特别要说明的是,64 位的寄存器可以寻址的范围非常大,但是也会受到地址总线条数的限制。比如和 64 位 CPU 配套工作的地址总线只有 40 条,那么可以寻址的范围就只有 1T,也就是 240。
-1. 从 PC 指针读取指令、到执行、再到下一条指令,构成了一个循环,这个不断循环的过程叫作 **CPU 的指令周期** ,下面我们会详细讲解这个概念。
+1. 从 PC 指针读取指令、到执行、再到下一条指令,构成了一个循环,这个不断循环的过程叫作 **CPU 的指令周期**,下面我们会详细讲解这个概念。
 
 ### 详解 a = 11 + 15 的执行过程
 
@@ -70,7 +70,7 @@
 
 ![12.png](assets/CgqCHl9fMJiAXO1-AABvVvPHepg435.png)
 
-- 最左边的 6 位,叫作 **操作码** ,英文是 OpCode,100011 代表 load 指令;
+- 最左边的 6 位,叫作 **操作码**,英文是 OpCode,100011 代表 load 指令;
 - 中间的 4 位 0000是寄存器的编号,这里代表寄存器 R0;
 - 后面的 22 位代表要读取的地址,也就是 0x100。
 
@@ -142,11 +142,11 @@
 
 ### 总结
 
-接下来我们来做一个总结。这节课我们深入讨论了指令和指令的分类。接下来,我们来看一看在 02 课时中留下的问题: **64 位和 32 位比较有哪些优势** ?
+接下来我们来做一个总结。这节课我们深入讨论了指令和指令的分类。接下来,我们来看一看在 02 课时中留下的问题: **64 位和 32 位比较有哪些优势**?
 
-还是老规矩,请你先自己思考这个问题的答案,写在留言区,然后再来看我接下来的分析。 **【解析】** 其实,这个问题需要分类讨论。
+还是老规矩,请你先自己思考这个问题的答案,写在留言区,然后再来看我接下来的分析。**【解析】** 其实,这个问题需要分类讨论。
 
-1. 如果说的是 64 位宽 CPU,那么有 2 个优势。 **优势 1** :64 位 CPU 可以执行更大数字的运算,这个优势在普通应用上不明显,但是对于数值计算较多的应用就非常明显。 **优势 2** :64 位 CPU 可以寻址更大的内存空间
+1. 如果说的是 64 位宽 CPU,那么有 2 个优势。**优势 1** :64 位 CPU 可以执行更大数字的运算,这个优势在普通应用上不明显,但是对于数值计算较多的应用就非常明显。**优势 2** :64 位 CPU 可以寻址更大的内存空间
 
 1. 如果 32 位/64 位说的是程序,那么说的是指令是 64 位还是 32 位的。32 位指令在 64 位机器上执行,困难不大,可以兼容。 如果是 64 位指令,在 32 位机器上执行就困难了。因为 32 位指令在 64 位机器执行的时候,需要的是一套兼容机制;但是 64 位指令在 32 位机器上执行,32 位的寄存器都存不下指令的参数。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25404\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25404\350\256\262.md"
index 52d5a9c7c..9d4b41116 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25404\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25404\350\256\262.md"
@@ -83,7 +83,7 @@ end:
 
 讲到这里,我要强调几个事情:
 
-1. jump 指令直接操作 PC 指针,但是很多 CPU 会抢先执行下一条指令,因此通常我们在 jump 后面要跟随一条 nop 指令,让 CPU 空转一个周期,避免 jump 下面的指令被执行。 **是不是到了微观世界,和你所认识的程序还不太一样** ?
+1. jump 指令直接操作 PC 指针,但是很多 CPU 会抢先执行下一条指令,因此通常我们在 jump 后面要跟随一条 nop 指令,让 CPU 空转一个周期,避免 jump 下面的指令被执行。**是不是到了微观世界,和你所认识的程序还不太一样**?
 1. 上面我写指令的时候用到了 add/store 这些指令,它们叫作助记符,是帮助你记忆的。整体这段程序,我们就称作汇编程序。
 1. 因为不同的机器助记符也不一样,所以你不用太关注我用的是什么汇编语言,也不用去记忆这些指令。当你拿到指定芯片的时候,直接去查阅芯片的说明书就可以了。
 1. 虽然不同 CPU 的指令不一样,但也是有行业标准的。现在使用比较多的是 RISC(精简指令集)和 CISC(复杂指令集)。比如目前Inte 和 AMD 家族主要使用 CISC 指令集,ARM 和 MIPS 等主要使用RISC 指令集。
@@ -345,7 +345,7 @@ class 有一个特殊的方法叫作构造函数,它会为 class 分配内存
 1. 平时你编程做的事情,用机器指令也能做,所以从计算能力上来说它们是等价的,最终这种计算能力又和图灵机是等价的。如果一个语言的能力和图灵机等价,我们就说这个语言是图灵完备的语言。现在市面上的绝大多数语言都是图灵完备的语言,但也有一些不是,比如 HTML、正则表达式和 SQL 等。
 1. 我们通过汇编语言构造高级程序;通过高级程序构造自己的业务逻辑,这些都是工程能力的一种体现。
 
-那么通过这节课的学习,你现在可以来回答本节关联的面试题目: **一个程序语言如果不支持递归函数的话,该如何实现递归算法?** 老规矩,请你先在脑海里思考问题的答案,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** 思路如下:
+那么通过这节课的学习,你现在可以来回答本节关联的面试题目: **一个程序语言如果不支持递归函数的话,该如何实现递归算法?** 老规矩,请你先在脑海里思考问题的答案,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** 思路如下:
 
 - 我们需要用到一个栈(其实用数组就可以);
 - 我们还需要一个栈指针,支持寄存器的编程语言能够直接用寄存器,而不支持直接用寄存器的编程语言,比如 Java,我们可以用一个变量;
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25405\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25405\350\256\262.md"
index d6bc45eb2..ef7ac10ab 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25405\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25405\350\256\262.md"
@@ -1,6 +1,6 @@
 # 05 存储器分级:L1 Cache 比内存和 SSD 快多少倍?
 
-**近两年我在面试求职者的时候,喜欢问这样一道面试题:SSD、内存和 L1 Cache 相比速度差多少倍** ?
+**近两年我在面试求职者的时候,喜欢问这样一道面试题:SSD、内存和 L1 Cache 相比速度差多少倍**?
 
 其实比起复杂的技术问题,我更喜欢在面试中提问这种像生活常识一样的简单问题。因为我觉得,复杂的问题是由简单的问题组成的,如果你把简单的问题学扎实了,那么复杂问题也是可以自己推导的。
 
@@ -154,7 +154,7 @@ CPU 读取到一个内存地址,我们就增加一个条目。当我们要查
 - 命中率怎么统计?
 - 缓存怎么置换等?
 
-现在我们来说一下课前提出的问题: **SSD、内存和 L1 Cache 相比速度差多少倍** ?
+现在我们来说一下课前提出的问题: **SSD、内存和 L1 Cache 相比速度差多少倍**?
 
 还是老规矩,请你先自己思考这个问题的答案,写在留言区,然后再来看我接下来的分析。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25406\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25406\350\256\262.md"
index a78ca498b..d0a6a95a8 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25406\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25406\350\256\262.md"
@@ -2,7 +2,7 @@
 
 通过模块一的学习,你应该掌握了计算机组成原理的重点知识,到了模块二,我们开始学习 Linux 指令,它是操作系统的前端,学好这部分内容一方面可以帮助你应对工作场景,另一方面可以让你在学习操作系统底层知识前,对 Linux 有一个大概的了解。
 
-**接下来,我们依然通过一道常见的高频面试题,引出今天的主要内容。面试题如下:请你说说** `rm / -rf` **的作用** ?
+**接下来,我们依然通过一道常见的高频面试题,引出今天的主要内容。面试题如下:请你说说** `rm / -rf` **的作用**?
 
 相信 90% 的同学是知道这个指令的。这里先预警一下,你千万不要轻易在服务器上尝试。要想知道这条指令是做什么的,能够帮助我们解决哪些问题,那就请你认真学习今天的内容。在本课时的最后我会公布这道题目的分析过程和答案。
 
@@ -28,7 +28,7 @@ Linux 对文件进行了一个树状的抽象。`/`代表根目录,每一节
 
 #### 路径(path)
 
-像`/usr/bin/rm`称为可执行文件`rm`的路径。路径就是一个文件在文件系统中的地址。如果文件系统是树形结构,那么通常一个文件只有一个地址(路径)。 **目标文件的绝对路径(Absolute path),也叫作完全路径(full path),是从** `/` **开始,接下来每一层都是一级子目录,直到** 定位 **到目标文件为止。**
+像`/usr/bin/rm`称为可执行文件`rm`的路径。路径就是一个文件在文件系统中的地址。如果文件系统是树形结构,那么通常一个文件只有一个地址(路径)。**目标文件的绝对路径(Absolute path),也叫作完全路径(full path),是从** `/` **开始,接下来每一层都是一级子目录,直到** 定位 **到目标文件为止。**
 
 如上图所示的例子中,`/usr/bin/rm`就是一个绝对路径。
 
@@ -161,9 +161,9 @@ Linux 把所有的设备都抽象成了文件,比如说打印机、USB、显
 
 ![Drawing 11.png](assets/CgqCHl9rEK6ANctWAAvN_sMIYLA038.png)
 
-如上图所示,我在`more`查看一个 nginx 日志后,先输入一个`/`,然后输入`192.168`看到的结果。`more`帮我找到了`192.168`所在的位置,然后又帮我定位到了这个位置。整个过程 more 指令只读取我们需要的部分到内存中。 **less** `less`是一个和`more`功能差不多的工具,打开`man`能够看到`less`的介绍上写着自己是`more`的反义词(opposite of more)。这样你可以看出`linux`生态其实也是很自由的一个生态,在这里创造工具也可以按照自己的喜好写文档。`less`支持向上翻页,这个功能`more`是做不到的。所以现在`less`用得更多一些。 **head/tail** `head`和`tail`是一组,它们用来读取一个文件的头部 N 行或者尾部 N 行。比如一个线上的大日志文件,当线上出了 bug,服务暂停的时候,我们就可以用`tail -n 1000`去查看最后的 1000 行日志文件,寻找导致服务异常的原因。
+如上图所示,我在`more`查看一个 nginx 日志后,先输入一个`/`,然后输入`192.168`看到的结果。`more`帮我找到了`192.168`所在的位置,然后又帮我定位到了这个位置。整个过程 more 指令只读取我们需要的部分到内存中。**less** `less`是一个和`more`功能差不多的工具,打开`man`能够看到`less`的介绍上写着自己是`more`的反义词(opposite of more)。这样你可以看出`linux`生态其实也是很自由的一个生态,在这里创造工具也可以按照自己的喜好写文档。`less`支持向上翻页,这个功能`more`是做不到的。所以现在`less`用得更多一些。**head/tail** `head`和`tail`是一组,它们用来读取一个文件的头部 N 行或者尾部 N 行。比如一个线上的大日志文件,当线上出了 bug,服务暂停的时候,我们就可以用`tail -n 1000`去查看最后的 1000 行日志文件,寻找导致服务异常的原因。
 
-另一个比较重要的用法是,如果你想看一个实时的`nginx`日志,可以使用`tail -f 文件名`,这样你会看到用户的请求不断进来。查一下`man`,你会发现`-f`是 follow 的意思,就是文件追加的内容会跟随输出到标准输出流。 **grep**
+另一个比较重要的用法是,如果你想看一个实时的`nginx`日志,可以使用`tail -f 文件名`,这样你会看到用户的请求不断进来。查一下`man`,你会发现`-f`是 follow 的意思,就是文件追加的内容会跟随输出到标准输出流。**grep**
 
 有时候你需要查看一个指定`ip`的nginx日志,或者查看一段时间内的`nginx`日志。如果不想用`less`和`more`进入文件中去查看,就可以用`grep`命令。Linux 的文件命名风格都很短,所以也影响了很多人,比如之前我看到过一个大牛的程序,变量名从来不超过 5 个字母,而且都有意义。
 
@@ -220,7 +220,7 @@ find 指令帮助我们在文件系统中查找文件。 比如我们如果想
 
 在这里,我再强调一个指令,即`man`指令,它是所有指令的手册,所以你一定要多多运用,熟练掌握。另外,一个指令通常有非常多的参数,但都需要用`man`指令去仔细研究。
 
-**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:** `rm / -rf` **的作用是?** 老规矩,请你先在脑海里先思考你的答案,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】**
+**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:** `rm / -rf` **的作用是?** 老规矩,请你先在脑海里先思考你的答案,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】**
 
 - `/`是文件系统根目录;
 - `rm`是删除指令;
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25407\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25407\350\256\262.md"
index 874de735d..b15cb4384 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25407\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25407\350\256\262.md"
@@ -1,6 +1,6 @@
 # 07 进程、重定向和管道指令:xargs 指令的作用是?
 
-在面试中,我们经常会遇到面试官询问 Linux 指令,06 课时中讲到的`rm -rf /`属于比较简单的题目,相当于小学难度。 **这节课给你带来一道初中难度的题目:** `xargs` **指令的作用是什么** ?
+在面试中,我们经常会遇到面试官询问 Linux 指令,06 课时中讲到的`rm -rf /`属于比较简单的题目,相当于小学难度。**这节课给你带来一道初中难度的题目:** `xargs` **指令的作用是什么**?
 
 通常这个指令是和管道一起使用,因此就引出了这节课的主题:管道。为了理解管道,和学习管道相关的内容,还有一些概念需要你理解,比如:进程、标准流和重定向。好的,接下来请和我一起,把这块知识一网打尽!
 
@@ -103,7 +103,7 @@ ls1 > out 2>&1
 
 #### 管道的作用和分类
 
-有了进程和重定向的知识,接下来我们梳理下管道的作用。管道(Pipeline)将一个进程的输出流定向到另一个进程的输入流,就像水管一样,作用就是把这两个文件接起来。如果一个进程输出了一个字符 X,那么另一个进程就会获得 X 这个输入。 **管道和重定向很像,但是管道是一个连接一个进行计算,重定向是将一个文件的内容定向到另一个文件,这二者经常会结合使用** 。
+有了进程和重定向的知识,接下来我们梳理下管道的作用。管道(Pipeline)将一个进程的输出流定向到另一个进程的输入流,就像水管一样,作用就是把这两个文件接起来。如果一个进程输出了一个字符 X,那么另一个进程就会获得 X 这个输入。**管道和重定向很像,但是管道是一个连接一个进行计算,重定向是将一个文件的内容定向到另一个文件,这二者经常会结合使用** 。
 
 Linux 中的管道也是文件,有两种类型的管道:
 
@@ -165,7 +165,7 @@ find ./ | grep Spring | grep -v MyBatis
 
 但是如果你想知道当前目录下有多少个文件,可以用`ls | wc -l`,如下所示:
 
-![Drawing 9.png](assets/Ciqc1F9twSCAN0h-AABgIcsEgKI655.png) **接下来请你思考一个问题:我们如何知道当前** `java` **的项目目录下有多少行代码** ?
+![Drawing 9.png](assets/Ciqc1F9twSCAN0h-AABgIcsEgKI655.png) **接下来请你思考一个问题:我们如何知道当前** `java` **的项目目录下有多少行代码**?
 
 提示一下。你可以使用下面这个指令:
 
@@ -245,7 +245,7 @@ echo "XXX" > pipe1
 
 这节课我们为了学习管道,先简单接触了进程的概念,然后学习了重定向。之后我们学习了匿名管道的应用场景,匿名管道帮助我们把 Linux 指令串联起来形成很强的计算能力。特别是`xargs`指令支持模板化的生成指令,拓展了指令的能力。最后我们还学习了命名管道,命名管道让我们可以真实拿到一个管道文件,让多个程序之间可以方便地进行通信。
 
-**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:xargs 的作用了吗?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** xargs 将标准输入流中的字符串分割成一条条子字符串,然后再按照我们自己想要的方式构建成一条条指令,大大拓展了 Linux 指令的能力。
+**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:xargs 的作用了吗?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** xargs 将标准输入流中的字符串分割成一条条子字符串,然后再按照我们自己想要的方式构建成一条条指令,大大拓展了 Linux 指令的能力。
 
 比如我们可以用来按照某种特定的方式逐个处理一个目录下所有的文件;根据一个 IP 地址列表逐个 ping 这些 IP,收集到每个 IP 地址的延迟等。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25408\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25408\350\256\262.md"
index c73f73d47..568f60c28 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25408\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25408\350\256\262.md"
@@ -1,6 +1,6 @@
 # 08 用户和权限管理指令: 请简述 Linux 权限划分的原则?
 
-**我看到过这样一道面试题:请简述 Linux 权限划分的原则** ?
+**我看到过这样一道面试题:请简述 Linux 权限划分的原则**?
 
 这种类型的面试题也是我比较喜欢的一种题目,因为它考察的不仅是一个具体的指令,还考察了候选人技术层面的认知。
 
@@ -10,13 +10,13 @@
 
 ### 权限抽象
 
-一个完整的权限管理体系,要有合理的抽象。这里就包括对用户、进程、文件、内存、系统调用等抽象。下面我将带你一一了解。 **首先,我们先来说说用户和组** 。Linux 是一个多用户平台,允许多个用户同时登录系统工作。Linux 将用户抽象成了账户,账户可以登录系统,比如通过输入登录名 + 密码的方式登录;也可以通过证书的方式登录。
+一个完整的权限管理体系,要有合理的抽象。这里就包括对用户、进程、文件、内存、系统调用等抽象。下面我将带你一一了解。**首先,我们先来说说用户和组** 。Linux 是一个多用户平台,允许多个用户同时登录系统工作。Linux 将用户抽象成了账户,账户可以登录系统,比如通过输入登录名 + 密码的方式登录;也可以通过证书的方式登录。
 
 但为了方便分配每个用户的权限,Linux 还支持组 **(Group)账户** 。组账户是多个账户的集合,组可以为成员们分配某一类权限。每个用户可以在多个组,这样就可以利用组给用户快速分配权限。
 
 组的概念有点像微信群。一个用户可以在多个群中。比如某个组中分配了 10 个目录的权限,那么新建用户的时候可以将这个用户增加到这个组中,这样新增的用户就不必再去一个个目录分配权限。
 
-而每一个微信群都有一个群主, **Root 账户也叫作超级管理员** ,就相当于微信群主,它对系统有着完全的掌控。一个超级管理员可以使用系统提供的全部能力。
+而每一个微信群都有一个群主,**Root 账户也叫作超级管理员**,就相当于微信群主,它对系统有着完全的掌控。一个超级管理员可以使用系统提供的全部能力。
 
 此外,Linux 还对 **文件** 进行了权限抽象( **注意目录也是一种文件** )。Linux 中一个文件可以设置下面 3 种权限:
 
@@ -36,14 +36,14 @@
 
 因此 Linux 中文件的权限可以用 9 个字符,3 组`rwx`描述:第一组是用户权限,第二组是组权限,第三组是所有用户的权限。然后用`-`代表没有权限。比如`rwxrwxrwx`代表所有维度可以读写执行。`rw--wxr-x`代表用户维度不可以执行,组维度不可以读取,所有用户维度不可以写入。
 
-通常情况下,如果用`ls -l`查看一个文件的权限,会有 10 个字符,这是因为第一个字符代表的是文件类型。我们在 06 课时讲解“几种常见的文件类型”时提到过,有管道文件、目录文件、链接文件等等。`-`代表普通文件、`d`代表目录、`p`代表管道。 **学习了这套机制之后,请你跟着我的节奏一起思考以下 4 个问题** 。
+通常情况下,如果用`ls -l`查看一个文件的权限,会有 10 个字符,这是因为第一个字符代表的是文件类型。我们在 06 课时讲解“几种常见的文件类型”时提到过,有管道文件、目录文件、链接文件等等。`-`代表普通文件、`d`代表目录、`p`代表管道。**学习了这套机制之后,请你跟着我的节奏一起思考以下 4 个问题** 。
 
 1. 文件被创建后,初始的权限如何设置?
 1. 需要全部用户都可以执行的指令,比如`ls`,它们的权限如何分配?
 1. 给一个文本文件分配了可执行权限会怎么样?
 1. 可不可以多个用户都登录`root`,然后只用`root`账户?
 
-你可以把以上 4 个问题作为本课时的小测验,把你的思考或者答案写在留言区,然后再来看我接下来的分析。 **问题一:初始权限问题** 一个文件创建后,文件的所属用户会被设置成创建文件的用户。谁创建谁拥有,这个逻辑很顺理成章。但是文件的组又是如何分配的呢?
+你可以把以上 4 个问题作为本课时的小测验,把你的思考或者答案写在留言区,然后再来看我接下来的分析。**问题一:初始权限问题** 一个文件创建后,文件的所属用户会被设置成创建文件的用户。谁创建谁拥有,这个逻辑很顺理成章。但是文件的组又是如何分配的呢?
 
 这里 Linux 想到了一个很好的办法,就是为每个用户创建一个同名分组。
 
@@ -55,7 +55,7 @@
 rw-rw-r--
 ```
 
-也就是用户、组维度不可以执行,所有用户可读。 **问题二:公共执行文件的权限** 前面提到过可以用`which`指令查看`ls`指令所在的目录,我们发现在`/usr/bin`中。然后用`ls -l`查看`ls`的权限,可以看到下图所示:
+也就是用户、组维度不可以执行,所有用户可读。**问题二:公共执行文件的权限** 前面提到过可以用`which`指令查看`ls`指令所在的目录,我们发现在`/usr/bin`中。然后用`ls -l`查看`ls`的权限,可以看到下图所示:
 
 ![Drawing 2.png](assets/Ciqc1F90SRuAAQCEAADdVOthCFw679.png)
 
@@ -63,13 +63,13 @@ rw-rw-r--
 
 到这里你可能会有一个疑问:如果一个文件设置为不可读,但是可以执行,那么结果会怎样?
 
-答案当然是不可以执行,无法读取文件内容自然不可以执行。 **问题三:执行文件** 在 Linux 中,如~~果~~一个文件可以被执行,则可以直接通过输入文件路径(相对路径或绝对路径)的方式执行。如果想执行一个不可以执行的文件,Linux 则会报错。
+答案当然是不可以执行,无法读取文件内容自然不可以执行。**问题三:执行文件** 在 Linux 中,如~~果~~一个文件可以被执行,则可以直接通过输入文件路径(相对路径或绝对路径)的方式执行。如果想执行一个不可以执行的文件,Linux 则会报错。
 
 当用户输入一个文件名,如果没有指定完整路径,Linux 就会在一部分目录中查找这个文件。你可以通过`echo PATH`看到 Linux 会在哪些目录中查找可执行文件,`PATH`是 Linux 的环境变量,关于环境变量,我将在 “12 | 高级技巧之集群部署中”和你详细讨论。
 
 ![Drawing 3.png](assets/CgqCHl90SSSACa4WAAFIEUypWH4904.png) **问题四:可不可以都 root** 最后一个问题是,可不可以都`root`?
 
-答案当然是不行!这里先给你留个悬念,具体原因我们会在本课时最后来讨论。 **到这里,用户和组相关权限就介绍完了。接下来说说内核和系统调用权限。** 内核是操作系统连接硬件、提供最核心能力的程序。今天我们先简单了解一下,关于内核的详细知识,会在“14 |用户态和内核态:用户态线程和内核态线程有什么区别?”中介绍。
+答案当然是不行!这里先给你留个悬念,具体原因我们会在本课时最后来讨论。**到这里,用户和组相关权限就介绍完了。接下来说说内核和系统调用权限。** 内核是操作系统连接硬件、提供最核心能力的程序。今天我们先简单了解一下,关于内核的详细知识,会在“14 |用户态和内核态:用户态线程和内核态线程有什么区别?”中介绍。
 
 内核提供操作硬件、磁盘、内存分页、进程等最核心的能力,并拥有直接操作全部内存的权限,因此内核不能把自己的全部能力都提供给用户,而且也不能允许用户通过`shell`指令进行系统调用。Linux 下内核把部分进程需要的系统调用以 C 语言 API 的形式提供出来。部分系统调用会有权限检查,比如说设置系统时间的系统调用。
 
@@ -85,7 +85,7 @@ rw-rw-r--
 
 #### 权限划分
 
-此外,权限架构思想还应遵循一个原则,权限划分边界应该足够清晰,尽量做到相互隔离。Linux 提供了用户和分组。当然 Linux 没有强迫你如何划分权限,这是为了应对更多的场景。通常我们服务器上重要的应用,会由不同的账户执行。比如说 Nginx、Web 服务器、数据库不会执行在一个账户下。现在随着容器化技术的发展,我们甚至希望每个应用独享一个虚拟的空间,就好像运行在一个单独的操作系统中一样,让它们互相不用干扰。 **到这里,你可能会问:为什么不用 root 账户执行程序?** 下面我们就来说说 root 的危害。
+此外,权限架构思想还应遵循一个原则,权限划分边界应该足够清晰,尽量做到相互隔离。Linux 提供了用户和分组。当然 Linux 没有强迫你如何划分权限,这是为了应对更多的场景。通常我们服务器上重要的应用,会由不同的账户执行。比如说 Nginx、Web 服务器、数据库不会执行在一个账户下。现在随着容器化技术的发展,我们甚至希望每个应用独享一个虚拟的空间,就好像运行在一个单独的操作系统中一样,让它们互相不用干扰。**到这里,你可能会问:为什么不用 root 账户执行程序?** 下面我们就来说说 root 的危害。
 
 举个例子,你有一个 Mysql 进程执行在 root(最大权限)账户上,如果有黑客攻破了你的 Mysql 服务,获得了在 Mysql 上执行 Sql 的权限,那么,你的整个系统就都暴露在黑客眼前了。这会导致非常严重的后果。
 
@@ -99,7 +99,7 @@ rw-rw-r--
 
 如上图所示,内核在最里面,也就是 Ring 0。 应用在最外面也就是Ring 3。驱动在中间,也就是 Ring 1 和 Ring 2。对于相邻的两个 Ring,内层 Ring 会拥有较高的权限,可以改变外层的 Ring;而外层的 Ring 想要使用内层 Ring 的资源时,会有专门的程序(或者硬件)进行保护。
 
-比如说一个 Ring3 的应用需要使用内核,就需要发送一个系统调用给内核。这个系统调用会由内核进行验证,比如验证用户有没有足够的权限,以及这个行为是否安全等等。 **权限包围(Privilege Bracking)**
+比如说一个 Ring3 的应用需要使用内核,就需要发送一个系统调用给内核。这个系统调用会由内核进行验证,比如验证用户有没有足够的权限,以及这个行为是否安全等等。**权限包围(Privilege Bracking)**
 
 之前我们讨论过,当 Mysql 跑在 root 权限时,如果 Mysql 被攻破,整个机器就被攻破了。因此我们所有应用都不要跑在 root 上。如果所有应用都跑在普通账户下,那么就会有临时提升权限的场景。比如说安装程序可能需要临时拥有管理员权限,将应用装到`/usr/bin`目录下。
 
@@ -220,7 +220,7 @@ chown g.u ./foo
 
 这节课我们学习 Linux 的权限管理的抽象和架构思想。Linux 对用户、组、文件、系统调用等都进行了完善的抽象。之后,我们讨论了最小权限原则。最后我们对用户分组管理和文件权限管理两部分重要的指令进行了系统学习。
 
-那么通过这节课的学习,你现在可以来回答本节关联的面试题目: **请简述 Linux 权限划分的原则?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** Linux 遵循最小权限原则。
+那么通过这节课的学习,你现在可以来回答本节关联的面试题目: **请简述 Linux 权限划分的原则?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** Linux 遵循最小权限原则。
 
 1. 每个用户掌握的权限应该足够小,每个组掌握的权限也足够小。实际生产过程中,最好管理员权限可以拆分,互相牵制防止问题。
 1. 每个应用应当尽可能小的使用权限。最理想的是每个应用单独占用一个容器(比如 Docker),这样就不存在互相影响的问题。即便应用被攻破,也无法攻破 Docker 的保护层。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25409\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25409\350\256\262.md"
index 33f6d8fe0..ee84480a2 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25409\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25409\350\256\262.md"
@@ -1,6 +1,6 @@
 # 09 Linux 中的网络指令:如何查看一个域名有哪些 NS 记录?
 
-**我看到过一道关于 Linux 指令的面试题:如何查看一个域名有哪些 NS 记录** ?
+**我看到过一道关于 Linux 指令的面试题:如何查看一个域名有哪些 NS 记录**?
 
 这类题目是根据一个场景,考察一件具体的事情如何处理。虽然你可以通过查资料找到解决方案,但是,这类问题在面试中还是有必要穿插一下,用于确定求职者技能是否熟练、经验是否丰富。特别是计算机网络相关的指令,平时在远程操作、开发、联调、Debug 线上问题的时候,会经常用到。
 
@@ -70,7 +70,7 @@ Linux 中提供了不少网络相关的指令,因为网络指令比较分散
 
 ![Drawing 5.png](assets/Ciqc1F92j-2AVEYjAAA8xcVMQzc068.png)
 
-你可以看到一共有 615 个 socket 文件,因为有很多 socket 在解决进程间的通信。就是将两个进程一个想象成客户端,一个想象成服务端。并不是真的有 600 多个连接着互联网的请求。 **查看 TCP 连接** 如果想看有哪些 TCP 连接,可以使用`netstat -t`。比如下面我通过`netstat -t`看`tcp`协议的网络情况:
+你可以看到一共有 615 个 socket 文件,因为有很多 socket 在解决进程间的通信。就是将两个进程一个想象成客户端,一个想象成服务端。并不是真的有 600 多个连接着互联网的请求。**查看 TCP 连接** 如果想看有哪些 TCP 连接,可以使用`netstat -t`。比如下面我通过`netstat -t`看`tcp`协议的网络情况:
 
 ![Drawing 6.png](assets/CgqCHl92j_aAbxdlAAEAdzG3a2s636.png)
 
@@ -78,7 +78,7 @@ Linux 中提供了不少网络相关的指令,因为网络指令比较分散
 
 ![Drawing 7.png](assets/CgqCHl92kAaAMuMDAAFWQdSNGfk978.png)
 
-如上图所示,可以看到有一个 TCP 连接了。 **查看端口占用** 还有一种非常常见的情形,我们想知道某个端口是哪个应用在占用。如下图所示:
+如上图所示,可以看到有一个 TCP 连接了。**查看端口占用** 还有一种非常常见的情形,我们想知道某个端口是哪个应用在占用。如下图所示:
 
 ![Drawing 8.png](assets/Ciqc1F92kBKAHr2RAAEnmEOZ8RM010.png)
 
@@ -171,4 +171,4 @@ curl在向`localhost:3000`发送 POST 请求。`-d`后面跟着要发送的数
 - 两个 DNS 查询指令 host 和 dig;
 - 可以发送各种请求包括 HTTPS 的 curl 指令。
 
-**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:如何查看一个域名有哪些 NS 记录了吗?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** host 指令提供了一个`-t`参数指定需要查找的记录类型。我们可以使用`host -t ns {网址}`。另外 dig 也提供了同样的能力。如果你感兴趣,还可以使用`man`对系统进行操作。
+**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:如何查看一个域名有哪些 NS 记录了吗?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** host 指令提供了一个`-t`参数指定需要查找的记录类型。我们可以使用`host -t ns {网址}`。另外 dig 也提供了同样的能力。如果你感兴趣,还可以使用`man`对系统进行操作。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25410\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25410\350\256\262.md"
index b7ce982ac..b709e9408 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25410\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25410\350\256\262.md"
@@ -1,6 +1,6 @@
 # 10 软件的安装: 编译安装和包管理器安装有什么优势和劣势?
 
-**今天给你带来的面试题是:编译安装和包管理器安装有什么优势和劣势** ?为了搞清楚这个问题,就引出了今天的话题,在 Linux 上如何安装程序。
+**今天给你带来的面试题是:编译安装和包管理器安装有什么优势和劣势**?为了搞清楚这个问题,就引出了今天的话题,在 Linux 上如何安装程序。
 
 在 Linux 上安装程序大概有 2 种思路:
 
@@ -46,7 +46,7 @@ Linux 是一个开源生态,因此工具非常多。工具在给用户使用
 
 另一方面,`yum`帮助用户解决了很多依赖,比如用户安装一个软件依赖了 10 个其他的软件,`yum`会把这 11 个软件一次性的装好。
 
-关于`yum`的具体用法,你可以使用man工具进行学习。 **apt**
+关于`yum`的具体用法,你可以使用man工具进行学习。**apt**
 
 接下来我们来重点说说`apt`,然后再一起尝试使用。因为我这次是用`ubuntu`Linux 给你教学,所以我以 apt 为例子,yum 的用法是差不多的,你可以自己 man 一下。
 
@@ -205,7 +205,7 @@ ln -sf /usr/local/nginx/sbin/nginx /usr/local/sbin/nginx
 
 这节课我们学习了在 Linux 上安装软件,简要介绍了`dpkg`和`rpm`,然后介绍了能够解决依赖和帮助用户下载的`yum`和`apt`。重点带你使用了`apt`,在这个过程中看到了强大的包管理机制,今天的`maven`、`npm`、`pip`都继承了这样一个特性。最后我们还尝试了一件高难度的事情,就是编译安装`nginx`。
 
-**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:编译安装和包管理安装有什么优势和劣势了吗?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** 包管理安装很方便,但是有两点劣势。
+**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:编译安装和包管理安装有什么优势和劣势了吗?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** 包管理安装很方便,但是有两点劣势。
 
 第一点是需要提前将包编译好,因此有一个发布的过程,如果某个包没有发布版本,或者在某个平台上找不到对应的发布版本,就需要编译安装。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25413\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25413\350\256\262.md"
index 526d6ffa2..932794bd7 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25413\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25413\350\256\262.md"
@@ -4,7 +4,7 @@ Windows 和 Linux 是当今两款最主流的服务器操作系统产品,都
 
 我觉得,两个操作系统各有千秋。每次学习两个操作系统的技术知识,都让我切实地感受到编程真的是一门艺术,而学习编程就像是在探索艺术。
 
-**今天我们继续从一道面试题目“ Linux 内核和 Windows 内核有什么区别** ?”入手,去了解这两个操作系统内核的设计,帮助你学习操作系统中最核心的一个概念——内核,并希望这些知识可以伴随你日后的每个系统设计。
+**今天我们继续从一道面试题目“ Linux 内核和 Windows 内核有什么区别**?”入手,去了解这两个操作系统内核的设计,帮助你学习操作系统中最核心的一个概念——内核,并希望这些知识可以伴随你日后的每个系统设计。
 
 ### 什么是内核?
 
@@ -27,7 +27,7 @@ Windows 和 Linux 是当今两款最主流的服务器操作系统产品,都
 
 #### 内核是如何工作的?
 
-**为了帮助你理解什么是内核,请你先思考一个问题:进程和内核的关系,是不是像浏览器请求服务端服务** ?你可以先自己思考,然后在留言区写下你此时此刻对这个问题的认知,等学完“模块三”再反过头来回顾这个知识,相信你定会产生新的理解。
+**为了帮助你理解什么是内核,请你先思考一个问题:进程和内核的关系,是不是像浏览器请求服务端服务**?你可以先自己思考,然后在留言区写下你此时此刻对这个问题的认知,等学完“模块三”再反过头来回顾这个知识,相信你定会产生新的理解。
 
 接下来,我们先一起分析一下这个问题。
 
@@ -50,7 +50,7 @@ Linux 操作系统第一版是1991 年林纳斯托·瓦兹(一个芬兰的小
 
 说到 Linux 内核设计,这里有很多有意思的名词。大多数听起来复杂、专业,但是理解起来其实很简单。接下来我们一一讨论。
 
-- **Multitask and SMP(Symmetric multiprocessing)**  **MultiTask 指多任务** ,Linux 是一个多任务的操作系统。多任务就是多个任务可以同时执行,这里的“同时”并不是要求并发,而是在一段时间内可以执行多个任务。当然 Linux 支持并发。 **SMP 指对称多处理** 。其实是说 Linux 下每个处理器的地位是相等的,内存对多个处理器来说是共享的,每个处理器都可以访问完整的内存和硬件资源。 这个特点决定了在 Linux 上不会存在一个特定的处理器处理用户程序或者内核程序,它们可以被分配到任何一个处理器上执行。
+- **Multitask and SMP(Symmetric multiprocessing)**  **MultiTask 指多任务**,Linux 是一个多任务的操作系统。多任务就是多个任务可以同时执行,这里的“同时”并不是要求并发,而是在一段时间内可以执行多个任务。当然 Linux 支持并发。**SMP 指对称多处理** 。其实是说 Linux 下每个处理器的地位是相等的,内存对多个处理器来说是共享的,每个处理器都可以访问完整的内存和硬件资源。 这个特点决定了在 Linux 上不会存在一个特定的处理器处理用户程序或者内核程序,它们可以被分配到任何一个处理器上执行。
 
 - **ELF(Executable and Linkable Format)**
 
@@ -96,7 +96,7 @@ Windows 还有很多独特的能力,比如 Hyper-V 虚拟化技术,有关虚
 
 ### 总结
 
-这一讲我们学习了内核的基础知识,包括内核的作用、整体架构以及 3 种内核类型(宏内核、微内核和混合类型内核)。内核很小(微内核)方便移植,因为体积小、安装快;内核大(宏内核),方便优化性能,毕竟内核更了解计算机中的资源。我们还学习了操作系统对执行文件的抽象,但是没有很深入讨论,内核部分有很多知识是需要在后面的几个模块中体现的,比如进程、文件、内存相关的能力等。 **那么通过这一讲的学习,你现在可以来回答本节关联的面试题目:Linux 内核和 Windows 内核有什么区别?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** Windows 有两个内核,最新的是 NT 内核,目前主流的 Windows 产品都是 NT 内核。NT 内核和 Linux 内核非常相似,没有太大的结构化差异。
+这一讲我们学习了内核的基础知识,包括内核的作用、整体架构以及 3 种内核类型(宏内核、微内核和混合类型内核)。内核很小(微内核)方便移植,因为体积小、安装快;内核大(宏内核),方便优化性能,毕竟内核更了解计算机中的资源。我们还学习了操作系统对执行文件的抽象,但是没有很深入讨论,内核部分有很多知识是需要在后面的几个模块中体现的,比如进程、文件、内存相关的能力等。**那么通过这一讲的学习,你现在可以来回答本节关联的面试题目:Linux 内核和 Windows 内核有什么区别?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** Windows 有两个内核,最新的是 NT 内核,目前主流的 Windows 产品都是 NT 内核。NT 内核和 Linux 内核非常相似,没有太大的结构化差异。
 
 从整体设计上来看,Linux 是宏内核,NT 内核属于混合型内核。和微内核不同,宏内核和混合类型内核从实现上来看是一个完整的程序。只不过混合类型内核内部也抽象出了微内核的概念,从内核内部看混合型内核的架构更像微内核。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25414\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25414\350\256\262.md"
index 0993388e6..004fa3997 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25414\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25414\350\256\262.md"
@@ -1,6 +1,6 @@
 # 14 用户态和内核态:用户态线程和内核态线程有什么区别?
 
-**这节课给你带来了一道非常经典的面试题目:用户态线程和内核态线程有什么区别** ?
+**这节课给你带来了一道非常经典的面试题目:用户态线程和内核态线程有什么区别**?
 
 这是一个组合型的问题,由很多小问题组装而成,比如:
 
@@ -42,7 +42,7 @@ Kernel 运行在超级权限模式(Supervisor Mode)下,所以拥有很高
 
 一个应用程序启动后会在内存中创建一个执行副本,这就是 **进程** 。Linux 的内核是一个 Monolithic Kernel(宏内核),因此可以看作一个进程。也就是开机的时候,磁盘的内核镜像被导入内存作为一个执行副本,成为内核进程。
 
-进程可以分成用户态进程和内核态进程两类。用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。 **那么用户态进程如果要执行程序,是否也要向内核申请呢** ?
+进程可以分成用户态进程和内核态进程两类。用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。**那么用户态进程如果要执行程序,是否也要向内核申请呢**?
 
 程序在现代操作系统中并不是以进程为单位在执行,而是以一种轻量级进程(Light Weighted Process),也称作线程(Thread)的形式执行。
 
@@ -89,7 +89,7 @@ Kernel 运行在超级权限模式(Supervisor Mode)下,所以拥有很高
 
 ### 用户态线程和内核态线程之间的映射关系
 
-线程简单理解,就是要执行一段程序。程序不会自发的执行,需要操作系统进行调度。 **我们思考这样一个问题,如果有一个用户态的进程,它下面有多个线程。如果这个进程想要执行下面的某一个线程,应该如何做呢** ?
+线程简单理解,就是要执行一段程序。程序不会自发的执行,需要操作系统进行调度。**我们思考这样一个问题,如果有一个用户态的进程,它下面有多个线程。如果这个进程想要执行下面的某一个线程,应该如何做呢**?
 
 这时,比较常见的一种方式,就是将需要执行的程序,让一个内核线程去执行。毕竟,内核线程是真正的线程。因为它会分配到 CPU 的执行资源。
 
@@ -97,7 +97,7 @@ Kernel 运行在超级权限模式(Supervisor Mode)下,所以拥有很高
 
 这样操作劣势非常明显,比如无法利用多核优势,每个线程调度分配到的时间较少,而且这种线程在阻塞场景下会直接交出整个进程的执行权限。
 
-由此可见, **用户态线程创建成本低,问题明显,不可以利用多核。内核态线程,创建成本高,可以利用多核,切换速度慢** 。因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。这样,用户态线程和内核态线程之间就构成了下面 4 种可能的关系:
+由此可见,**用户态线程创建成本低,问题明显,不可以利用多核。内核态线程,创建成本高,可以利用多核,切换速度慢** 。因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。这样,用户态线程和内核态线程之间就构成了下面 4 种可能的关系:
 
 #### 多对一(Many to One)
 
@@ -133,7 +133,7 @@ Kernel 运行在超级权限模式(Supervisor Mode)下,所以拥有很高
 
 这节课我们学习了用户态和内核态,然后我们简单学习了进程和线程的基础知识。这部分知识会在“ **模块四:进程和线程** ”中以更细粒度进行详细讲解。等你完成模块四的学习后,可以再返回来看这一节的内容,相信会有更深入的理解。
 
-最后,我们还讨论了用户线程和内核线程的映射关系,这是一种非常经典的设计和思考方式。关于这个场景我们讨论了 1 对 1、1 对多以及多对 1,两层模型 4 种方法。日后你在处理线程池对接;远程 RPC 调用;消息队列时,还会反复用到今天的方法。 **那么通过这节课的学习,你现在是否可以来回答本节关联的面试题目?用户态线程和内核态线程的区别?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** 用户态线程工作在用户空间,内核态线程工作在内核空间。用户态线程调度完全由进程负责,通常就是由进程的主线程负责。相当于进程主线程的延展,使用的是操作系统分配给进程主线程的时间片段。内核线程由内核维护,由操作系统调度。
+最后,我们还讨论了用户线程和内核线程的映射关系,这是一种非常经典的设计和思考方式。关于这个场景我们讨论了 1 对 1、1 对多以及多对 1,两层模型 4 种方法。日后你在处理线程池对接;远程 RPC 调用;消息队列时,还会反复用到今天的方法。**那么通过这节课的学习,你现在是否可以来回答本节关联的面试题目?用户态线程和内核态线程的区别?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** 用户态线程工作在用户空间,内核态线程工作在内核空间。用户态线程调度完全由进程负责,通常就是由进程的主线程负责。相当于进程主线程的延展,使用的是操作系统分配给进程主线程的时间片段。内核线程由内核维护,由操作系统调度。
 
 用户态线程无法跨核心,一个进程的多个用户态线程不能并发,阻塞一个用户态线程会导致进程的主线程阻塞,直接交出执行权限。这些都是用户态线程的劣势。内核线程可以独立执行,操作系统会分配时间片段。因此内核态线程更完整,也称作轻量级进程。内核态线程创建成本高,切换成本高,创建太多还会给调度算法增加压力,因此不会太多。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25415\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25415\350\256\262.md"
index 2faafeb20..6478a915b 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25415\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25415\350\256\262.md"
@@ -2,7 +2,7 @@
 
 你好,发现求知的乐趣,我是林䭽。
 
-**本课时我们依然以一道面试题为引开启今天的学习。请你思考:Java/JS 等语言为什么可以捕获到键盘的输入** ?
+**本课时我们依然以一道面试题为引开启今天的学习。请你思考:Java/JS 等语言为什么可以捕获到键盘的输入**?
 
 其实面试是一个寻找同类的过程,在阿里叫作“闻味道”——用键盘输入是程序员每天必做的事情,如果你对每天发生的事情背后的技术原理保持好奇心和兴趣,并且愿意花时间去探索和学习,这就是技术潜力强的表现。相反,如果你只对马上能为自己创造价值的事情感兴趣,不愿意通过探索和思考的方式,去理解普遍存在的世界,长此以往就会导致知识储备不足。
 
@@ -12,7 +12,7 @@
 
 ### 探索过程:如何设计响应键盘的整个链路?
 
-当你拿到一个问题时,需要冷静下来思考和探索解决方案。你可以查资料、看视频或者咨询专家,但是在这之前,你先要进行一定的思考和梳理,有的问题可以直接找到答案,有的问题却需要继续深挖寻找其背后的理论支撑。 **问题 1:我们的目标是什么?** 我们的目标是在 Java/JS 中实现按键响应程序。这种实现有点像 Switch-Case 语句——根据不同的按键执行不同的程序,比如按下回车键可以换行,按下左右键可以移动光标。 **问题 2:按键怎么抽象?** 键盘上一般不超过 100 个键。因此我们可以考虑用一个 Byte 的数据来描述用户按下了什么键。按键有两个操作,一个是按下、一个是释放,这是两个不同的操作。对于一个 8 位的字节,可以考虑用最高位的 1 来描述按下还是释放的状态,然后后面的 7 位(0~127)描述具体按了哪个键。这样我们只要确定了用户按键/释放的顺序,对我们的系统来说,就不会有歧义。 **问题 3:如何处理按键?使用操作系统处理还是让每个程序自己实现?**
+当你拿到一个问题时,需要冷静下来思考和探索解决方案。你可以查资料、看视频或者咨询专家,但是在这之前,你先要进行一定的思考和梳理,有的问题可以直接找到答案,有的问题却需要继续深挖寻找其背后的理论支撑。**问题 1:我们的目标是什么?** 我们的目标是在 Java/JS 中实现按键响应程序。这种实现有点像 Switch-Case 语句——根据不同的按键执行不同的程序,比如按下回车键可以换行,按下左右键可以移动光标。**问题 2:按键怎么抽象?** 键盘上一般不超过 100 个键。因此我们可以考虑用一个 Byte 的数据来描述用户按下了什么键。按键有两个操作,一个是按下、一个是释放,这是两个不同的操作。对于一个 8 位的字节,可以考虑用最高位的 1 来描述按下还是释放的状态,然后后面的 7 位(0~127)描述具体按了哪个键。这样我们只要确定了用户按键/释放的顺序,对我们的系统来说,就不会有歧义。**问题 3:如何处理按键?使用操作系统处理还是让每个程序自己实现?**
 
 处理按键是一个通用程序,可以考虑由操作系统先进行一部分处理,比如:
 
@@ -28,9 +28,9 @@
 释放 C
 ```
 
-**问题 4:程序用什么模型响应按键?** 当一个 Java 或者 JS 写的应用程序想要响应按键时,应该考虑消息模型。因为如果程序不停地扫描按键,会给整个系统带来很大的负担。比如程序写一个`while`循环去扫描有没有按键,开销会很大。 如果程序在操作系统端注册一个响应按键的函数,每次只有真的触发按键时才执行这个函数,这样就能减少开销了。 **问题 5:处理用户按键,需不需要打断正在执行的程序?** 从用户体验上讲,按键应该是一个高优先级的操作,比如用户按 Ctrl+C 或者 Esc 的时候,可能是因为用户想要打断当前执行的程序。即便是用户只想要输入,也应该尽可能地集中资源给到用户,因为我们不希望用户感觉到延迟。
+**问题 4:程序用什么模型响应按键?** 当一个 Java 或者 JS 写的应用程序想要响应按键时,应该考虑消息模型。因为如果程序不停地扫描按键,会给整个系统带来很大的负担。比如程序写一个`while`循环去扫描有没有按键,开销会很大。 如果程序在操作系统端注册一个响应按键的函数,每次只有真的触发按键时才执行这个函数,这样就能减少开销了。**问题 5:处理用户按键,需不需要打断正在执行的程序?** 从用户体验上讲,按键应该是一个高优先级的操作,比如用户按 Ctrl+C 或者 Esc 的时候,可能是因为用户想要打断当前执行的程序。即便是用户只想要输入,也应该尽可能地集中资源给到用户,因为我们不希望用户感觉到延迟。
 
-如果需要考虑到程序随时会被中断,去响应其他更高优先级的情况,那么从程序执行的底层就应该支持这个行为,而且最好从硬件层面去支持,这样速度最快。 这就引出了本课时的主角——中断。具体如何处理,见下面我们关于中断部分的分析。 **问题 6:操作系统如何知道用户按了哪个键?** 这里有一个和问题 5 类似的问题。操作系统是不断主动触发读取键盘按键,还是每次键盘按键到来的时候都触发一段属于操作系统的程序呢?
+如果需要考虑到程序随时会被中断,去响应其他更高优先级的情况,那么从程序执行的底层就应该支持这个行为,而且最好从硬件层面去支持,这样速度最快。 这就引出了本课时的主角——中断。具体如何处理,见下面我们关于中断部分的分析。**问题 6:操作系统如何知道用户按了哪个键?** 这里有一个和问题 5 类似的问题。操作系统是不断主动触发读取键盘按键,还是每次键盘按键到来的时候都触发一段属于操作系统的程序呢?
 
 显然,后者更节省效率。
 
@@ -54,7 +54,7 @@
 
 讲到这里,我们总结一下,CPU 要做的就是一看到中断,就改变 PC 指针(相当于中断正在执行的程序),而 PC 改变成多少,可以根据不同的类型来判断,比如按键就到 0。操作系统就要向这些具体的位置写入指令,当中断发生时,接管程序的控制权,也就是让 PC 指针指向操作系统处理按键的程序。
 
-上面这个模型和实际情况还有出入,但是我们已经开始逐渐完善了。 **问题 7:主板如何知道键盘被按下?** 经过一层一层地深挖“如何设计响应键盘的整个链路?”这个问题,目前操作系统已经能接管按键,接下来,我们还需要思考主板如何知道有按键,并且通知 CPU。
+上面这个模型和实际情况还有出入,但是我们已经开始逐渐完善了。**问题 7:主板如何知道键盘被按下?** 经过一层一层地深挖“如何设计响应键盘的整个链路?”这个问题,目前操作系统已经能接管按键,接下来,我们还需要思考主板如何知道有按键,并且通知 CPU。
 
 你可以把键盘按键看作按下了某个开关,我们需要一个芯片将按键信息转换成具体按键的值。比如用户按下 A 键,A 键在第几行、第几列,可以看作一个电学信号。接着我们需要芯片把这个电学信号转化为具体的一个数字(一个 Byte)。转化完成后,主板就可以接收到这个数字(按键码),然后将数字写入自己的一个寄存器中,并通知 CPU。
 
@@ -113,4 +113,4 @@ CPU 通常都支持设置一个中断屏蔽位(一个寄存器),设置为
 
 这节课我们通过探索式学习讨论了中断的设计。 通过一个问题,Java/JS 如何响应键盘按键,引出了 7 个问题的思考。通过探索这些问题,我们最终找到 了答案,完成了一次从硬件、内核到应用的完整设计。我想说的是,学习不是最终目的,长远来看我更希望你在学习的过程中得到成长,通过学习技能锻炼自己解决问题的能力。
 
-**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:Java/Js 等语言为什么可以捕获到键盘输入?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** 为了捕获到键盘输入,硬件层面需要把按键抽象成中断,中断 CPU 执行。CPU 根据中断类型找到对应的中断向量。操作系统预置了中断向量,因此发生中断后操作系统接管了程序。操作系统实现了基本解析按键的算法,将按键抽象成键盘事件,并且提供了队列存储多个按键,还提供了监听按键的 API。因此应用程序,比如 Java/Node.js 虚拟机,就可以通过调用操作系统的 API 使用键盘事件。
+**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:Java/Js 等语言为什么可以捕获到键盘输入?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** 为了捕获到键盘输入,硬件层面需要把按键抽象成中断,中断 CPU 执行。CPU 根据中断类型找到对应的中断向量。操作系统预置了中断向量,因此发生中断后操作系统接管了程序。操作系统实现了基本解析按键的算法,将按键抽象成键盘事件,并且提供了队列存储多个按键,还提供了监听按键的 API。因此应用程序,比如 Java/Node.js 虚拟机,就可以通过调用操作系统的 API 使用键盘事件。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25416\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25416\350\256\262.md"
index 0f7699290..9a9a095c1 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25416\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25416\350\256\262.md"
@@ -22,7 +22,7 @@ IBM(International Business Machines Corporation)一开始是卖机器的。
 
 所以 IBM 真正开始做计算机是 1949 年小沃森逐渐掌权后。1954 年,IBM 推出了世界上第一个拥有操作系统的商用计算机——IBM 704,并且在 1956 年时独占了计算机市场的 70% 的份额。
 
-**你可能会问,之前的计算机没有操作系统吗** ?
+**你可能会问,之前的计算机没有操作系统吗**?
 
 我以第一台可编程通用计算机 ENIAC 为例,ENIAC 虽然支持循环、分支判断语句,但是只支持写机器语言。ENIAC 的程序通常需要先写在纸上,然后再由专业的工程师输入到计算机中。 对于 ENIAC 来说执行的是一个个作业,就是每次把输入的程序执行完。
 
@@ -71,7 +71,7 @@ IBM 是一家商业驱动的公司,至今已经 100 多年历史。因为 IBM
 
 ![WiPI95BWeW02HNk8__thumbnail.png](assets/CgqCHl-boDWAWq5VAAFpIdJc_T0867.png)
 
-Unix 早期开放了源代码,可以说是现代操作系统的奠基之作——支持多任务、多用户,还支持分级安全策略。拥有内核、内存管理、文件系统、正则表达式、开发工具、可执行文件格式、命令行工具等等。 **可以说,到今天 Unix 不再代表某种操作系统,而是一套统一的,大家都认可的架构标准** 。
+Unix 早期开放了源代码,可以说是现代操作系统的奠基之作——支持多任务、多用户,还支持分级安全策略。拥有内核、内存管理、文件系统、正则表达式、开发工具、可执行文件格式、命令行工具等等。**可以说,到今天 Unix 不再代表某种操作系统,而是一套统一的,大家都认可的架构标准** 。
 
 因为开源的原因,Unix 的版本非常复杂。具体你可以看下面这张大图。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25417\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25417\350\256\262.md"
index a6d97393b..0268cb80c 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25417\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25417\350\256\262.md"
@@ -110,7 +110,7 @@
 
 用户级线程和内核级线程存在映射关系,因此可以考虑在内核中维护一张内核级线程的表,包括上面说的字段。
 
-如果考虑到这种映射关系,比如 n-m 的多对多映射,可以将线程信息还是存在进程中,每次执行的时候才使用内核级线程。相当于内核中有个线程池,等待用户空间去使用。每次用户级线程把程序计数器等传递过去,执行结束后,内核线程不销毁,等待下一个任务。这里其实有很多灵活的实现, **总体来说,创建进程开销大、成本高;创建线程开销小,成本低** 。
+如果考虑到这种映射关系,比如 n-m 的多对多映射,可以将线程信息还是存在进程中,每次执行的时候才使用内核级线程。相当于内核中有个线程池,等待用户空间去使用。每次用户级线程把程序计数器等传递过去,执行结束后,内核线程不销毁,等待下一个任务。这里其实有很多灵活的实现,**总体来说,创建进程开销大、成本高;创建线程开销小,成本低** 。
 
 #### 隔离方案
 
@@ -176,7 +176,7 @@
 
 本讲我们学习了进程和线程的基本概念。了解了操作系统如何调度进程(线程)和分时算法的基本概念,然后了解进程(线程)的 3 种基本状态。线程也被称作轻量级进程,由操作系统直接调度的,是内核级线程。我们还学习了线程切换保存、恢复状态的过程。
 
-我们发现进程和线程是操作系统为了分配资源设计的两个概念,进程承接存储资源,线程承接计算资源。而进程包含线程,这样就可以做到进程间内存隔离。这是一个非常巧妙的设计,概念清晰,思路明确,你以后做架构的时候可以多参考这样的设计。 如果只有进程,或者只有线程,都不能如此简单的解决我们遇到的问题。 **那么通过这节课的学习,你现在可以来回答本节关联的面试题目:进程的开销比线程大在了哪里?**  **【解析】** Linux 中创建一个进程自然会创建一个线程,也就是主线程。创建进程需要为进程划分出一块完整的内存空间,有大量的初始化操作,比如要把内存分段(堆栈、正文区等)。创建线程则简单得多,只需要确定 PC 指针和寄存器的值,并且给线程分配一个栈用于执行程序,同一个进程的多个线程间可以复用堆栈。因此,创建进程比创建线程慢,而且进程的内存开销更大。
+我们发现进程和线程是操作系统为了分配资源设计的两个概念,进程承接存储资源,线程承接计算资源。而进程包含线程,这样就可以做到进程间内存隔离。这是一个非常巧妙的设计,概念清晰,思路明确,你以后做架构的时候可以多参考这样的设计。 如果只有进程,或者只有线程,都不能如此简单的解决我们遇到的问题。**那么通过这节课的学习,你现在可以来回答本节关联的面试题目:进程的开销比线程大在了哪里?**  **【解析】** Linux 中创建一个进程自然会创建一个线程,也就是主线程。创建进程需要为进程划分出一块完整的内存空间,有大量的初始化操作,比如要把内存分段(堆栈、正文区等)。创建线程则简单得多,只需要确定 PC 指针和寄存器的值,并且给线程分配一个栈用于执行程序,同一个进程的多个线程间可以复用堆栈。因此,创建进程比创建线程慢,而且进程的内存开销更大。
 
 ### 思考题 **最后我再给你出一道思考题。考虑下面的程序:**
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25418\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25418\350\256\262.md"
index eea2a9a02..5440d0d13 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25418\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25418\350\256\262.md"
@@ -160,7 +160,7 @@ enter(){
 }
 ```
 
-这段代码不断在 CPU 中执行指令,直到锁被其他线程释放。这种情况线程不会主动释放资源,我们称为 **自旋锁** 。自旋锁的优点就是不会主动发生 Context Switch,也就是线程切换,因为线程切换比较消耗时间。 **自旋锁** 缺点也非常明显,比较消耗 CPU 资源。如果自旋锁一直拿不到锁,会一直执行。
+这段代码不断在 CPU 中执行指令,直到锁被其他线程释放。这种情况线程不会主动释放资源,我们称为 **自旋锁** 。自旋锁的优点就是不会主动发生 Context Switch,也就是线程切换,因为线程切换比较消耗时间。**自旋锁** 缺点也非常明显,比较消耗 CPU 资源。如果自旋锁一直拿不到锁,会一直执行。
 
 #### wait 操作
 
@@ -219,7 +219,7 @@ leave(){
 }
 ```
 
-上面的代码具有一定的欺骗性,没有考虑到 **竞争条件** ,执行的时候会出问题,可能会有超过2个线程同时进入临界区。
+上面的代码具有一定的欺骗性,没有考虑到 **竞争条件**,执行的时候会出问题,可能会有超过2个线程同时进入临界区。
 
 下面优化一下,作为一个考虑了竞争条件的版本:
 
@@ -312,7 +312,7 @@ leave(&lock2)
 
 上面程序线程 1 获得了`lock1`,线程 2 获得了`lock2`。接下来线程 1 尝试获得`lock2`,线程 2 尝试获得`lock1`,于是两个线程都陷入了等待。这个等待永远都不会结束,我们称之为 **死锁** 。
 
-关于死锁如何解决,我们会在“ **21 | 哲学家就餐问题:什么情况下会触发饥饿和死锁** ?”讨论。这里我先讲一种最简单的解决方案,你可以尝试让两个线程对锁的操作顺序相同,这样就可以避免死锁问题。
+关于死锁如何解决,我们会在“ **21 | 哲学家就餐问题:什么情况下会触发饥饿和死锁**?”讨论。这里我先讲一种最简单的解决方案,你可以尝试让两个线程对锁的操作顺序相同,这样就可以避免死锁问题。
 
 ### 分布式环境的锁
 
@@ -334,6 +334,6 @@ leave(&lock2)
 
 ### 总结
 
-**那么通过这节课的学习,你现在可以尝试来回答本讲关联的面试题目:如何控制同一时间只有 2 个线程运行?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。 **【解析】** 同时控制两个线程进入临界区,一种方式可以考虑用信号量(semaphore)。
+**那么通过这节课的学习,你现在可以尝试来回答本讲关联的面试题目:如何控制同一时间只有 2 个线程运行?** 老规矩,请你先在脑海里构思下给面试官的表述,并把你的思考写在留言区,然后再来看我接下来的分析。**【解析】** 同时控制两个线程进入临界区,一种方式可以考虑用信号量(semaphore)。
 
 另一种方式是考虑生产者、消费者模型。想要进入临界区的线程先在一个等待队列中等待,然后由消费者每次消费两个。这种实现方式,类似于实现一个线程池,所以也可以考虑实现一个 ThreadPool 类,然后再实现一个调度器类,最后实现一个每次选择两个线程执行的调度算法。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25419\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25419\350\256\262.md"
index 4ad4657d4..0f9ad402a 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25419\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25419\350\256\262.md"
@@ -1,6 +1,6 @@
 # 19 乐观锁、区块链:除了上锁还有哪些并发控制方法?
 
-**这一讲我带来的面试题是:除了上锁还有哪些并发控制方法** ?
+**这一讲我带来的面试题是:除了上锁还有哪些并发控制方法**?
 
 上面这道面试题是在“有哪些并发控制方法?”这个问题的基础上加了一个限制条件。
 
@@ -76,7 +76,7 @@ account=bob, iphone=100
 
 如图,这个结构也叫作区块链。每个 Block 下面可以存一些数据,每个 Block 知道上一个节点是谁。每个 Block 有上一个节点的摘要签名。也就是说,如果 Block 10 是 Block 11 的上一个节点,那么 Block 11 会知道 Block 10 的存在,且用 Block 11 中 Block 10 的摘要签名,可以证明 Block 10 的数据没有被篡改过。
 
-区块链构成了一个基于历史版本的事实链,前一个版本是后一个版本的历史。Alice 的钱和苹果店的 iPhone 数量,包括全世界所有人的钱,都在这些 Block 里。 **购买转账的过程** 下面请你思考,Alice 购买了 iPhone,需要提交两条新数据到上面的区块链。
+区块链构成了一个基于历史版本的事实链,前一个版本是后一个版本的历史。Alice 的钱和苹果店的 iPhone 数量,包括全世界所有人的钱,都在这些 Block 里。**购买转账的过程** 下面请你思考,Alice 购买了 iPhone,需要提交两条新数据到上面的区块链。
 
 ```plaintext
 from=A, to=B, price=10000, signature=alice的签名
@@ -138,4 +138,4 @@ Alice 需要新增一个末端的节点,比如她在末端节点上将自己
 
 在这一讲,我们主要学习了一些比锁更加有趣的处理方式, 其实还有很多方式,你可以去思考。并发问题也不仅仅是要解决并发问题,并发还伴随着一致性、可用性、欺诈及吞吐量等。一名优秀的架构师是需要储备多个维度的知识,所以还是我常常跟你强调的,知识在于积累,绝非朝夕之功。
 
-另外,我想告诉你的是,其实大厂并不是只招收处理过并发场景的工程师。作为一名资深面试官,我愿意给任何人机会,前提是你的方案打动了我。而设计方案的能力,是可以学习的。你要多思考,多查资料,多整理总结,这样久而久之,就有公司愿意让你做架构了。 **那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:除了上锁还有哪些并发控制方法?**  **【解析】** 这个问题比较发散,这一讲我们介绍了基于乐观锁的版本控制,还介绍了区块链技术。另外还有一个名词,并不属于操作系统课程范畴,我也简单给你介绍下。处理并发还可以考虑 Lock-Free 数据结构。比如 Lock-Free 队列,是基于 cas 指令实现的,允许多个线程使用这个队列。再比如 ThreadLocal,让每个线程访问不同的资源,旨在用空间换时间,也是避免锁的一种方案。
+另外,我想告诉你的是,其实大厂并不是只招收处理过并发场景的工程师。作为一名资深面试官,我愿意给任何人机会,前提是你的方案打动了我。而设计方案的能力,是可以学习的。你要多思考,多查资料,多整理总结,这样久而久之,就有公司愿意让你做架构了。**那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:除了上锁还有哪些并发控制方法?**  **【解析】** 这个问题比较发散,这一讲我们介绍了基于乐观锁的版本控制,还介绍了区块链技术。另外还有一个名词,并不属于操作系统课程范畴,我也简单给你介绍下。处理并发还可以考虑 Lock-Free 数据结构。比如 Lock-Free 队列,是基于 cas 指令实现的,允许多个线程使用这个队列。再比如 ThreadLocal,让每个线程访问不同的资源,旨在用空间换时间,也是避免锁的一种方案。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25420\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25420\350\256\262.md"
index fb6e4a01c..ae9af0b08 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25420\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25420\350\256\262.md"
@@ -1,14 +1,14 @@
 # 20 线程的调度:线程调度都有哪些方法?
 
-**这一讲我带来的面试题目是:线程调度都有哪些方法** ?
+**这一讲我带来的面试题目是:线程调度都有哪些方法**?
 
-所谓 **调度** ,是一个制定计划的过程,放在线程调度背景下,就是操作系统如何决定未来执行哪些线程?
+所谓 **调度**,是一个制定计划的过程,放在线程调度背景下,就是操作系统如何决定未来执行哪些线程?
 
 这类型的题目考察的并不是一个死的概念,面试官会通过你的回答考量你对知识进行加工和理解的能力。这有点类似于设计技术方案,要对知识进行系统化、结构化地思考和分类。就这道题目而言,可以抓两条主线,第一条是形形色色调度场景怎么来的?第二条是每个调度算法是如何工作的?
 
 ### 先到先服务
 
-早期的操作系统是一个个处理作业(Job),比如很多保险业务,每处理一个称为一个作业(Job)。处理作业最容易想到的就是 **先到先服务(First Come First Service,FCFS)** ,也就是先到的作业先被计算,后到的作业,排队进行。
+早期的操作系统是一个个处理作业(Job),比如很多保险业务,每处理一个称为一个作业(Job)。处理作业最容易想到的就是 **先到先服务(First Come First Service,FCFS)**,也就是先到的作业先被计算,后到的作业,排队进行。
 
 这里需要用到一个叫作队列的数据结构,具有 **先入先出(First In First Out,FIFO)性质** 。先进入队列的作业,先处理,因此从 **公平性** 来说,这个算法非常朴素。另外,一个作业完全完成才会进入下一个作业,作业之间不会发生切换,从 **吞吐量** 上说,是最优的——因为没有额外开销。
 
@@ -20,7 +20,7 @@
 
 ![Lark20201113-173325.png](assets/Ciqc1F-uUwyAXKj6AABwvcEuVH0735.png)
 
-这样就会优先考虑第一个到来预估时间为 3 分钟的任务。 我们还可以从另外一个角度来审视短作业优先的优势,就是平均等待时间。 **平均等待时间 = 总等待时间/任务数** 上面例子中,如果按照 3,3,10 的顺序处理,平均等待时间是:(0 + 3 + 6) / 3 = 3 分钟。 如果按照 10,3,3 的顺序来处理,就是( 0+10+13 )/ 3 = 7.66 分钟。
+这样就会优先考虑第一个到来预估时间为 3 分钟的任务。 我们还可以从另外一个角度来审视短作业优先的优势,就是平均等待时间。**平均等待时间 = 总等待时间/任务数** 上面例子中,如果按照 3,3,10 的顺序处理,平均等待时间是:(0 + 3 + 6) / 3 = 3 分钟。 如果按照 10,3,3 的顺序来处理,就是( 0+10+13 )/ 3 = 7.66 分钟。
 
 平均等待时间和用户满意度是成反比的,等待时间越长,用户越不满意,因此 **在大多数情况下,应该优先处理用时少的,从而降低平均等待时长** 。
 
@@ -86,7 +86,7 @@
 
 ### 总结
 
-**那么通过这一讲的学习,你现在可以尝试来回答本节关联的面试题目:线程调度都有哪些方法** ? **【解析】** 回答这个问题你要把握主线,千万不要教科书般的回答:任务调度分成抢占和非抢占的,抢占的可以轮流执行,也可以用优先级队列执行;非抢占可以先到先服务,也可以最短任务优先。
+**那么通过这一讲的学习,你现在可以尝试来回答本节关联的面试题目:线程调度都有哪些方法**?**【解析】** 回答这个问题你要把握主线,千万不要教科书般的回答:任务调度分成抢占和非抢占的,抢占的可以轮流执行,也可以用优先级队列执行;非抢占可以先到先服务,也可以最短任务优先。
 
 上面这种回答可以用来过普通的程序员岗位,但是面试官其实更希望听到你的见解,这是初中级开发人员与高级开发人员之间的差异。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25421\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25421\350\256\262.md"
index 48fe83d36..fa3cb944e 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25421\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25421\350\256\262.md"
@@ -1,10 +1,10 @@
 # 21 哲学家就餐问题:什么情况下会触发饥饿和死锁?
 
-**这一讲给你带来的面试题目是:什么情况下会触发饥饿和死锁** ?
+**这一讲给你带来的面试题目是:什么情况下会触发饥饿和死锁**?
 
 读题可知,这道题目在提问“场景”,从表面来看,解题思路是列举几个例子。但是在回答这类面试题前你一定要想一想面试官在考察什么,往往在题目中看到“ **什么情况下** ”时,其实考察的是你总结和概括信息的能力。
 
-关于上面这道题目,如果你只回答一个场景,而没有输出概括性的总结内容,就很容易被面试官认为对知识理解不到位,因而挂掉面试。另外, **提问死锁和饥饿还有一个更深层的意思,就是考察你在实战中对并发控制算法的理解,是否具备设计并发算法来解决死锁问题并且兼顾性能(并发量)的思维和能力** 。
+关于上面这道题目,如果你只回答一个场景,而没有输出概括性的总结内容,就很容易被面试官认为对知识理解不到位,因而挂掉面试。另外,**提问死锁和饥饿还有一个更深层的意思,就是考察你在实战中对并发控制算法的理解,是否具备设计并发算法来解决死锁问题并且兼顾性能(并发量)的思维和能力** 。
 
 要学习这部分知识有一个非常不错的模型,就是哲学家就餐问题。1965 年,计算机科学家 Dijkstra 为了帮助学生更好地学习并发编程设计的一道练习题,后来逐渐成为大家广泛讨论的问题。
 
@@ -398,4 +398,4 @@ public class DiningPhilosophers {
 
 ### 总结
 
-**那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:什么情况下会触发饥饿和死锁** ? **【解析】** 线程需要资源没有拿到,无法进行下一步,就是饥饿。死锁(Deadlock)和活锁(Livelock)都是饥饿的一种形式。 非抢占的系统中,互斥的资源获取,形成循环依赖就会产生死锁。死锁发生后,如果利用抢占解决,导致资源频繁被转让,有一定概率触发活锁。死锁、活锁,都可以通过设计并发控制算法解决,比如哲学家就餐问题。
+**那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:什么情况下会触发饥饿和死锁**?**【解析】** 线程需要资源没有拿到,无法进行下一步,就是饥饿。死锁(Deadlock)和活锁(Livelock)都是饥饿的一种形式。 非抢占的系统中,互斥的资源获取,形成循环依赖就会产生死锁。死锁发生后,如果利用抢占解决,导致资源频繁被转让,有一定概率触发活锁。死锁、活锁,都可以通过设计并发控制算法解决,比如哲学家就餐问题。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25422\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25422\350\256\262.md"
index d228a2c0a..44de98777 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25422\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25422\350\256\262.md"
@@ -1,10 +1,10 @@
 # 22 进程间通信: 进程间通信都有哪些方法?
 
-**这节课带给你的面试题目是:进程间通信都有哪些方法** ?
+**这节课带给你的面试题目是:进程间通信都有哪些方法**?
 
 在上一讲中,我们提到过,凡是面试官问“ **什么情况下** ”的时候,面试官实际想听的是你经过理解,整理得到的认知。回答应该是概括的、简要的。而不是真的去列举每一种 case。
 
-另外, **面试官考察进程间通信,有一个非常重要的意义——进程间通信是架构复杂系统的基石** 。复杂系统往往是分成各种子系统、子模块、微服务等等,按照 Unix 的设计哲学,系统的每个部分应该是稳定、独立、简单有效,而且强大的。系统本身各个模块就像人的器官,可以协同工作。而这个协同的枢纽,就是我们今天的主题——进程间通信。
+另外,**面试官考察进程间通信,有一个非常重要的意义——进程间通信是架构复杂系统的基石** 。复杂系统往往是分成各种子系统、子模块、微服务等等,按照 Unix 的设计哲学,系统的每个部分应该是稳定、独立、简单有效,而且强大的。系统本身各个模块就像人的器官,可以协同工作。而这个协同的枢纽,就是我们今天的主题——进程间通信。
 
 ### 什么是进程间通信?
 
@@ -12,7 +12,7 @@
 
 ### 管道
 
-之前我们在“ **07 | 进程、重定向和管道指令:xargs 指令的作用是** ?”中讲解过管道和命名管道。 管道提供了一种非常重要的能力,就是组织计算。进程不用知道有管道存在,因此管道的设计是非侵入的。程序员可以先着重在程序本身的设计,只需要预留响应管道的接口,就可以利用管道的能力。比如用`shell`执行MySQL语句,可能会这样:
+之前我们在“ **07 | 进程、重定向和管道指令:xargs 指令的作用是**?”中讲解过管道和命名管道。 管道提供了一种非常重要的能力,就是组织计算。进程不用知道有管道存在,因此管道的设计是非侵入的。程序员可以先着重在程序本身的设计,只需要预留响应管道的接口,就可以利用管道的能力。比如用`shell`执行MySQL语句,可能会这样:
 
 ```plaintext
 进程1 | 进程2 | 进程3 | mysql -u... -p | 爬虫进程
@@ -70,7 +70,7 @@ RPC 调用过程有很多约定, 比如函数参数格式、返回结果格式
 
 上面这些问题比较棘手,因此在实战中通常的做法是使用框架。比如 Thrift 框架(Facebook 开源)、Dubbo 框架(阿里开源)、grpc(Google 开源)。这些 RPC 框架通常支持多种语言,这需要一个接口定义语言支持在多个语言间定义接口(IDL)。
 
-RPC 调用的方式比较适合微服务环境的开发,当然 RPC 通常需要专业团队的框架以支持高并发、低延迟的场景。不过,硬要说 RPC 有额外转化数据的开销(主要是序列化),也没错,但这不是 RPC 的主要缺点。 **RPC 真正的缺陷是增加了系统间的耦合** 。 **当系统主动调用另一个系统的方法时** , **就意味着在增加两个系统的耦合** 。 **长期增加 RPC 调用** , **会让系统的边界逐渐腐** 化。这才是使用 RPC 时真正需要注意的东西。
+RPC 调用的方式比较适合微服务环境的开发,当然 RPC 通常需要专业团队的框架以支持高并发、低延迟的场景。不过,硬要说 RPC 有额外转化数据的开销(主要是序列化),也没错,但这不是 RPC 的主要缺点。**RPC 真正的缺陷是增加了系统间的耦合** 。**当系统主动调用另一个系统的方法时**,**就意味着在增加两个系统的耦合** 。**长期增加 RPC 调用**,**会让系统的边界逐渐腐** 化。这才是使用 RPC 时真正需要注意的东西。
 
 ### 消息队列
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25423\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25423\350\256\262.md"
index cab8e2be2..1f84774f3 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25423\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25423\350\256\262.md"
@@ -14,11 +14,11 @@ fork()
 
 print("Hello World\\n")
 
-请问这个程序执行后, 输出结果 Hello World 会被打印几次? **【解析】** 这道题目考察大家对 fork 能力的理解。
+请问这个程序执行后, 输出结果 Hello World 会被打印几次?**【解析】** 这道题目考察大家对 fork 能力的理解。
 
 fork 的含义是复制一份当前进程的全部状态。第 1 个 fork 执行 1 次产生 1 个额外的进程。 第 2 个 fork,执行 2 次,产生 2 个额外的进程。第 3 个 fork 执行 4 次,产生 4 个额外的进程。所以执行 print 的进程一共是 8 个。
 
-#### 18 | 锁、信号量和分布式锁:如何控制同一时间只有 2 个线程运行? **【问题】如果考虑到 CPU 缓存的存在,会对上面我们讨论的算法有什么影响** ? **【解析】** 这是一道需要大家查一些资料的题目。这里涉及一个叫作内存一致性模型的概念。具体就是说,在同一时刻,多线程之间,对内存中某个地址的数据认知是否一致(简单理解,就是多个线程读取同一个内存地址能不能读到一致的值)
+#### 18 | 锁、信号量和分布式锁:如何控制同一时间只有 2 个线程运行?**【问题】如果考虑到 CPU 缓存的存在,会对上面我们讨论的算法有什么影响**?**【解析】** 这是一道需要大家查一些资料的题目。这里涉及一个叫作内存一致性模型的概念。具体就是说,在同一时刻,多线程之间,对内存中某个地址的数据认知是否一致(简单理解,就是多个线程读取同一个内存地址能不能读到一致的值)
 
 对某个地址,和任意时刻,如果所有线程读取值,得到的结果都一样,是一种强一致性,我们称为线性一致性(Sequencial Consistency),含义就是所有线程对这个地址中数据的历史达成了一致,历史没有分差,有一条大家都能认可的主线,因此称为线性一致。 如果只有部分时刻所有线程的理解是一致的,那么称为弱一致性(Weak Consistency)。
 
@@ -51,7 +51,7 @@ volatile int lock = 0;
 
 就可以避免从读取不到对lock的写入问题。
 
-#### 19 | 乐观锁、区块链:除了上锁还有哪些并发控制方法? **【问题】举例各 2 个悲观锁和乐观锁的应用场景** ? **【解析】** 乐观锁、悲观锁都能够实现避免竞争条件,实现数据的一致性。 比如减少库存的操作,无论是乐观锁、还是悲观锁都能保证最后库存算对(一致性)。 但是对于并发减库存的各方来说,体验是不一样的。悲观锁要求各方排队等待。 乐观锁,希望各方先各自进步。所以进步耗时较长,合并耗时较短的应用,比较适合乐观锁。 比如协同创作(写文章、视频编辑、写程序等),协同编辑(比如共同点餐、修改购物车、共同编辑商品、分布式配置管理等),非常适合乐观锁,因为这些操作需要较长的时间进步(比如写文章要思考、配置管理可能会连续修改多个配置)。乐观锁可以让多个协同方不急于合并自己的版本,可以先 focus 在进步上
+#### 19 | 乐观锁、区块链:除了上锁还有哪些并发控制方法?**【问题】举例各 2 个悲观锁和乐观锁的应用场景**?**【解析】** 乐观锁、悲观锁都能够实现避免竞争条件,实现数据的一致性。 比如减少库存的操作,无论是乐观锁、还是悲观锁都能保证最后库存算对(一致性)。 但是对于并发减库存的各方来说,体验是不一样的。悲观锁要求各方排队等待。 乐观锁,希望各方先各自进步。所以进步耗时较长,合并耗时较短的应用,比较适合乐观锁。 比如协同创作(写文章、视频编辑、写程序等),协同编辑(比如共同点餐、修改购物车、共同编辑商品、分布式配置管理等),非常适合乐观锁,因为这些操作需要较长的时间进步(比如写文章要思考、配置管理可能会连续修改多个配置)。乐观锁可以让多个协同方不急于合并自己的版本,可以先 focus 在进步上
 
 相反,悲观锁适用在进步耗时较短的场景,比如锁库存刚好是进步(一次库存计算)耗时少的场景。这种场景使用乐观锁,不但没有足够的收益,同时还会导致各个等待方(线程、客户端等)频繁读取库存——而且还会面临缓存一致性的问题(类比内存一致性问题)。这种进步耗时短,频繁同步的场景,可以考虑用悲观锁。类似的还有银行的交易,订单修改状态等。
 
@@ -59,7 +59,7 @@ volatile int lock = 0;
 
 综上:有一个误区就是悲观锁对冲突持有悲观态度,所以性能低;乐观锁,对冲突持有乐观态度,鼓励线程进步,因此性能高。 这个不能一概而论,要看具体的场景。最后补充一下,悲观锁性能最高的一种实现就是阻塞队列,你可以参考 Java 的 7 种继承于 BlockingQueue 阻塞队列类型。
 
-#### 20 | 线程的调度:线程调度都有哪些方法? **【问题】用你最熟悉的语言模拟分级队列调度的模型** ? **【解析】** 我用 Java 实现了一个简单的 yield 框架。 没有到协程的级别,但是也初具规模。考虑到协程实现需要更复杂一些,所以我用 PriorityQueue 来放高优任务;然后我用 LinkedList 来作为放普通任务的队列。Java 语言中的`add`和`remove`方法刚好构成了入队和出队操作
+#### 20 | 线程的调度:线程调度都有哪些方法?**【问题】用你最熟悉的语言模拟分级队列调度的模型**?**【解析】** 我用 Java 实现了一个简单的 yield 框架。 没有到协程的级别,但是也初具规模。考虑到协程实现需要更复杂一些,所以我用 PriorityQueue 来放高优任务;然后我用 LinkedList 来作为放普通任务的队列。Java 语言中的`add`和`remove`方法刚好构成了入队和出队操作
 
 ```plaintext
 private PriorityQueue urgents;
@@ -296,9 +296,9 @@ _Process finished with exit code 0_
 
 #### 21 | 哲学家就餐问题:什么情况下会触发饥饿和死锁?
 
-**【问题】如果哲学家就餐问题拿起叉子、放下叉子,只需要微小的时间,主要时间开销集中在 think 需要计算资源(CPU 资源)上,那么使用什么模型比较合适** ? **【解析】** 哲学家就餐问题最多允许两组哲学家就餐,如果开销集中在计算上,那么只要同时有两组哲学家可以进入临界区即可。不考虑 I/O 成本,问题就很简化了,也失去了讨论的意义。比如简单要求哲学家们同时拿起左右手的叉子的做法就可以达到 2 组哲学家同时进餐。
+**【问题】如果哲学家就餐问题拿起叉子、放下叉子,只需要微小的时间,主要时间开销集中在 think 需要计算资源(CPU 资源)上,那么使用什么模型比较合适**?**【解析】** 哲学家就餐问题最多允许两组哲学家就餐,如果开销集中在计算上,那么只要同时有两组哲学家可以进入临界区即可。不考虑 I/O 成本,问题就很简化了,也失去了讨论的意义。比如简单要求哲学家们同时拿起左右手的叉子的做法就可以达到 2 组哲学家同时进餐。
 
-#### 22 | 进程间通信: 进程间通信都有哪些方法? **【问题】还有哪些我没有讲到的进程间通信方法** ? **【解析】** 我看到有同学提到了 Android 系统的 OpenBinder 机制——允许不同进程的线程间调用(类似 RPC)。底层是 Linux 的文件系统和内核对 Binder 的驱动
+#### 22 | 进程间通信: 进程间通信都有哪些方法?**【问题】还有哪些我没有讲到的进程间通信方法**?**【解析】** 我看到有同学提到了 Android 系统的 OpenBinder 机制——允许不同进程的线程间调用(类似 RPC)。底层是 Linux 的文件系统和内核对 Binder 的驱动
 
 我还有没讲到的进程间的通信方法,比如说:
 
@@ -314,7 +314,7 @@ kill -s USR1 9999
 
 #### 23 | 分析服务的特性:我的服务应该开多少个进程、多少个线程?
 
-**【问题】如果磁盘坏了,通常会是怎样的情况** ? **【解析】** 磁盘如果彻底坏了,服务器可能执行程序报错,无法写入,甚至死机。这些情况非常容易发现。而比较不容易观察的是坏道,坏道是磁盘上某个小区域数据无法读写了。有可能是硬损坏,就是物理损坏了,相当于永久损坏。也有可能是软损坏,比如数据写乱了。导致磁盘坏道的原因很多,比如电压不稳、灰尘、磁盘质量等问题。
+**【问题】如果磁盘坏了,通常会是怎样的情况**?**【解析】** 磁盘如果彻底坏了,服务器可能执行程序报错,无法写入,甚至死机。这些情况非常容易发现。而比较不容易观察的是坏道,坏道是磁盘上某个小区域数据无法读写了。有可能是硬损坏,就是物理损坏了,相当于永久损坏。也有可能是软损坏,比如数据写乱了。导致磁盘坏道的原因很多,比如电压不稳、灰尘、磁盘质量等问题。
 
 磁盘损坏之前,往往还伴随性能整体的下降;坏道也会导致读写错误。所以在出现问题前,通常是可以在监控系统中观察到服务器性能指标变化的。比如 CPU 使用量上升,I/O Wait 增多,相同并发量下响应速度变慢等。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25424\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25424\350\256\262.md"
index 66f59baa0..122e1f631 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25424\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25424\350\256\262.md"
@@ -18,7 +18,7 @@
 
 ### 为什么内存不够用?
 
-要理解一个技术,就必须理解它为何而存在。总体来说, **虚拟化技术是为了解决内存不够用的问题** ,那么内存为何不够用呢?
+要理解一个技术,就必须理解它为何而存在。总体来说,**虚拟化技术是为了解决内存不够用的问题**,那么内存为何不够用呢?
 
 主要是因为程序越来越复杂。比如说我现在给你录音的机器上就有 200 个进程,目前内存的消耗是 21G,我的内存是 64G 的,但是多开一些程序还是会被占满。 另外,如果一个程序需要使用大的内存,比如 1T,是不是应该报错?如果报错,那么程序就会不好写,程序员必须小心翼翼地处理内存的使用,避免超过允许的内存使用阈值。以上提到的这些都是需要解决的问题,也是虚拟化技术存在的价值和意义。
 
@@ -127,4 +127,4 @@ MMU 根据 1 级编号找到 1 级页表条目,1 级页表条目中记录了
 
 ### 总结
 
-**那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:一个程序最多能使用多少内存** ? **【解析】** 目前我们主要都是在用 64bit 的机器。因为 264 数字过于大,即便是虚拟内存都不需要这么大的空间。因此通常操作系统会允许进程使用非常大,但是不到 264 的地址空间。通常是几十到几百 EB(1EB = 106TB = 109GB)。
+**那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:一个程序最多能使用多少内存**?**【解析】** 目前我们主要都是在用 64bit 的机器。因为 264 数字过于大,即便是虚拟内存都不需要这么大的空间。因此通常操作系统会允许进程使用非常大,但是不到 264 的地址空间。通常是几十到几百 EB(1EB = 106TB = 109GB)。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25425\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25425\350\256\262.md"
index 7ca68758c..6cec3ae76 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25425\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25425\350\256\262.md"
@@ -1,6 +1,6 @@
 # 25 内存管理单元: 什么情况下使用大内存分页?
 
-今天我们的学习目标是:了解如何通过内存,提升你的程序性能。 **这一讲我带来了一道和内存优化相关的面试题:什么情况下使用大内存分页** ?
+今天我们的学习目标是:了解如何通过内存,提升你的程序性能。**这一讲我带来了一道和内存优化相关的面试题:什么情况下使用大内存分页**?
 
 这道题目属于一个实用技巧,可以作为你积累高并发处理技能的一个小小的组成部分。要理解和解决这个问题,我们还需要在上一讲的基础上,继续挖掘虚拟内存和内存管理单元更底层的工作原理,以及了解转置检测缓冲区(TLB)的作用。
 
@@ -46,7 +46,7 @@ Frame Number
 
 一种是 **软失效** (Soft Miss),这种情况 Frame 还在内存中,只不过 TLB 缓存中没有。那么这个时候需要刷新 TLB 缓存。如果 TLB 缓存已经满了,就需要选择一个已经存在的缓存条目进行覆盖。具体选择哪个条目进行覆盖,我们称为缓存置换(缓存不够用了,需要置换)。缓存置换时,通常希望高频使用的数据保留,低频使用的数据被替换。比如常用的 LRU(Least Recently Used)算法就是基于这种考虑,每次置换最早使用的条目。
 
-另一种情况是 **硬失效(Hard Miss)** ,这种情况下对应的 Frame 没有在内存中,需要从磁盘加载。这种情况非常麻烦,首先操作系统要触发一个缺页中断(原有需要读取内存的线程被休眠),然后中断响应程序开始从磁盘读取对应的 Frame 到内存中,读取完成后,再次触发中断通知更新 TLB,并且唤醒被休眠的线程去排队。 **注意,线程不可能从休眠态不排队就进入执行态,因此 Hard Miss 是相对耗时的** 。 **无论是软失效、还是硬失效,都会带来性能损失,这是我们不希望看到的。因此缓存的设计,就非常重要了** 。
+另一种情况是 **硬失效(Hard Miss)**,这种情况下对应的 Frame 没有在内存中,需要从磁盘加载。这种情况非常麻烦,首先操作系统要触发一个缺页中断(原有需要读取内存的线程被休眠),然后中断响应程序开始从磁盘读取对应的 Frame 到内存中,读取完成后,再次触发中断通知更新 TLB,并且唤醒被休眠的线程去排队。**注意,线程不可能从休眠态不排队就进入执行态,因此 Hard Miss 是相对耗时的** 。**无论是软失效、还是硬失效,都会带来性能损失,这是我们不希望看到的。因此缓存的设计,就非常重要了** 。
 
 #### TLB 缓存的设计
 
@@ -60,7 +60,7 @@ Frame Number
 
 **方案一:全相联映射(Fully Associative Mapping)** 如果 TLB 用全相联映射实现,那么一个 Frame,可能在任何缓存行中。虽然名词有点复杂,但是通常新人设计缓存时,会本能地想到全相联。因为在给定的空间下,最容易想到的就是把缓存数据都放进一个数组里。
 
-对于 TLB 而言,如果是全相联映射,给定一个具体的 Page Number,想要查找 Frame,需要遍历整个缓存。当然作为硬件实现的缓存,如果缓存条目少的情况下,可以并行查找所有行。这种行为在软件设计中是不存在的,软件设计通常需要循环遍历才能查找行,但是利用硬件电路可以实现这种并行查找到过程。可是如果条目过多,比如几百个上千个,硬件查询速度也会下降。所以,全相联映射,有着明显性能上的缺陷。我们不考虑采用。 **方案二:直接映射(Direct Mapping)** 对于水平更高一些的同学,马上会想到直接映射。直接映射类似一种哈希函数的形式,给定一个内存地址,可以通过类似于哈希函数计算的形式,去计算它在哪一个缓存条目。假设我们有 64 个条目,那么可以考虑这个计算方法: **缓存行号 = Page Number % 64** 。
+对于 TLB 而言,如果是全相联映射,给定一个具体的 Page Number,想要查找 Frame,需要遍历整个缓存。当然作为硬件实现的缓存,如果缓存条目少的情况下,可以并行查找所有行。这种行为在软件设计中是不存在的,软件设计通常需要循环遍历才能查找行,但是利用硬件电路可以实现这种并行查找到过程。可是如果条目过多,比如几百个上千个,硬件查询速度也会下降。所以,全相联映射,有着明显性能上的缺陷。我们不考虑采用。**方案二:直接映射(Direct Mapping)** 对于水平更高一些的同学,马上会想到直接映射。直接映射类似一种哈希函数的形式,给定一个内存地址,可以通过类似于哈希函数计算的形式,去计算它在哪一个缓存条目。假设我们有 64 个条目,那么可以考虑这个计算方法: **缓存行号 = Page Number % 64** 。
 
 当然在这个方法中,假如实际的虚拟地址空间大小是 1G,页面大小是 4K,那么一共有 1G/4K = 262144 个页,平均每 262144/64 = 4096 个页共享一个条目。这样的共享行为是很正常的,本身缓存大小就不可能太大,之前我们讲过,性能越高的存储离 CPU 越近,成本越高,空间越小。
 
@@ -68,7 +68,7 @@ Frame Number
 
 一种最简单的思考就是能不能基于直接映射实现 LRU 缓存。仔细思考,其实是不可能实现的。因为当我们想要置换缓存的时候(新条目进来,需要寻找一个旧条目出去),会发现每次都只有唯一的选择,因为对于一个确定的虚拟地址,它所在的条目也是确定的。这导致直接映射不支持各种缓存置换算法,因此 TLB Miss 肯定会更高。
 
-综上,我们既要解决直接映射的缓存命中率问题,又希望解决全相联映射的性能问题。而核心就是需要能够实现类似 LRU 的算法,让高频使用的缓存留下来——最基本的要求,就是一个被缓存的值,必须可以存在于多个位置——于是人们就发明了 n 路组相联映射。 **方案三:n 路组相联映射(n-way Set-Associative Mapping)** 组相联映射有点像哈希表的开放寻址法,但是又有些差异。组相联映射允许一个虚拟页号(Page Number)映射到固定数量的 n 个位置。举个例子,比如现在有 64 个条目,要查找地址 100 的位置,可以先用一个固定的方法计算,比如 100%64 = 36。这样计算出应该去条目 36 获取 Frame 数据。但是取出条目 36 看到条目 36 的 Page Number 不是 100,这个时候就顺延一个位置,去查找 37,38,39……如果是 4 路组相联,那么就只看 36,37,38,39,如果是8 路组相联,就只看 36-43 位置。
+综上,我们既要解决直接映射的缓存命中率问题,又希望解决全相联映射的性能问题。而核心就是需要能够实现类似 LRU 的算法,让高频使用的缓存留下来——最基本的要求,就是一个被缓存的值,必须可以存在于多个位置——于是人们就发明了 n 路组相联映射。**方案三:n 路组相联映射(n-way Set-Associative Mapping)** 组相联映射有点像哈希表的开放寻址法,但是又有些差异。组相联映射允许一个虚拟页号(Page Number)映射到固定数量的 n 个位置。举个例子,比如现在有 64 个条目,要查找地址 100 的位置,可以先用一个固定的方法计算,比如 100%64 = 36。这样计算出应该去条目 36 获取 Frame 数据。但是取出条目 36 看到条目 36 的 Page Number 不是 100,这个时候就顺延一个位置,去查找 37,38,39……如果是 4 路组相联,那么就只看 36,37,38,39,如果是8 路组相联,就只看 36-43 位置。
 
 这样的方式,一个 Page Number 可以在 n 个位置出现,这样就解决了 LRU 算法的问题。每次新地址需要置换进来的时候,可以从 n 个位置中选择更新时间最早的条目置换出去。至于具体 n 设置为多少,需要实战的检验。而且缓存是一个模糊、基于概率的方案,本身对 n 的要求不是很大。比如:i7 CPU 的 L1 TLB 采用 4-way 64 条目的设计;L2 TLB 采用 8-way 1024 条目的设计。Intel 选择了这样的设计,背后有大量的数据支撑。这也是缓存设计的一个要点,在做缓存设计的时候,你一定要收集数据实际验证。
 
@@ -98,4 +98,4 @@ sudo sysctl -w vm.nr_hugepages=2048
 
 注意:我的 Java 应用使用的分页数 = Total-Free+Rsvd = 2048-2032+180 = 196。Total 就是总共的分页数,Free 代表空闲的(包含 Rsvd,Reserved 预留的)。因此是上面的计算关系。
 
-### 总结 **那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:什么情况下使用大内存分页** ? **【解析】** 通常应用对内存需求较大时,可以考虑开启大内存分页。比如一个搜索引擎,需要大量在内存中的索引。有时候应用对内存的需求是隐性的。比如有的机器用来抗高并发访问,虽然平时对内存使用不高,但是当高并发到来时,应用对内存的需求自然就上去了。虽然每个并发请求需要的内存都不大, 但是总量上去了,需求总量也会随之提高高。这种情况下,你也可以考虑开启大内存分页
+### 总结 **那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:什么情况下使用大内存分页**?**【解析】** 通常应用对内存需求较大时,可以考虑开启大内存分页。比如一个搜索引擎,需要大量在内存中的索引。有时候应用对内存的需求是隐性的。比如有的机器用来抗高并发访问,虽然平时对内存使用不高,但是当高并发到来时,应用对内存的需求自然就上去了。虽然每个并发请求需要的内存都不大, 但是总量上去了,需求总量也会随之提高高。这种情况下,你也可以考虑开启大内存分页
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25426\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25426\350\256\262.md"
index 10b4020a2..7062ec119 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25426\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25426\350\256\262.md"
@@ -1,8 +1,8 @@
 # 26 缓存置换算法: LRU 用什么数据结构实现更合理?
 
-**这一讲给你带来的面试题目是:LRU 用什么数据结构实现更合理** ?
+**这一讲给你带来的面试题目是:LRU 用什么数据结构实现更合理**?
 
-LRU(最近最少使用),是一种缓存置换算法。缓存是用来存储常用的数据,加速常用数据访问的数据结构。有软件实现,比如数据库的缓存;也有硬件实现,比如我们上一讲学的 TLB。 **缓存设计中有一个重要的环节:当缓存满了,新的缓存条目要写入时,哪个旧条目被置换出去呢** ?
+LRU(最近最少使用),是一种缓存置换算法。缓存是用来存储常用的数据,加速常用数据访问的数据结构。有软件实现,比如数据库的缓存;也有硬件实现,比如我们上一讲学的 TLB。**缓存设计中有一个重要的环节:当缓存满了,新的缓存条目要写入时,哪个旧条目被置换出去呢**?
 
 这就需要用到缓存置换算法(Cache Replacement Algorithm)。缓存置换应用场景非常广,比如发生缺页中断后,操作系统需要将磁盘的页导入内存,那么已经在内存中的页就需要置换出去。CDN 服务器为了提高访问速度,需要决定哪些 Web 资源在内存中,哪些在磁盘上。CPU 缓存每次写入一个条目,也就相当于一个旧的条目被覆盖。数据库要决定哪些数据在内存中,应用开发要决定哪些数据在 Redis 中,而空间是有限的,这些都关联着缓存的置换。
 
@@ -76,17 +76,17 @@ LRU 的一种常见实现是链表,如下图所示:
 
 通常 LRU 缓存还要提供查询能力,这里我们可以考虑用类似 Java 中 LinkedHashMap 的数据结构,同时具备双向链表和根据 Key 查找值的能力。
 
-以上是常见的实现方案,但是这种方案在缓存访问量非常大的情况下,需要同时维护一个链表和一个哈希表,因此开销较高。 **举一个高性能场景的例子,比如页面置换算法。** 如果你需要维护一个很大的链表来存储所有页,然后经常要删除大量的页面(置换缓存),并把大量的页面移动到链表头部。这对于页面置换这种高性能场景来说,是不可以接受的。 **另外一个需要 LRU 高性能的场景是 CPU 的缓存** ,CPU 的多路组相联设计,比如 8-way 设计,需要在 8 个地址中快速找到最久未使用的数据,不可能再去内存中建立一个链表来实现。
+以上是常见的实现方案,但是这种方案在缓存访问量非常大的情况下,需要同时维护一个链表和一个哈希表,因此开销较高。**举一个高性能场景的例子,比如页面置换算法。** 如果你需要维护一个很大的链表来存储所有页,然后经常要删除大量的页面(置换缓存),并把大量的页面移动到链表头部。这对于页面置换这种高性能场景来说,是不可以接受的。**另外一个需要 LRU 高性能的场景是 CPU 的缓存**,CPU 的多路组相联设计,比如 8-way 设计,需要在 8 个地址中快速找到最久未使用的数据,不可能再去内存中建立一个链表来实现。
 
 正因为有这么多困难,才需要不断地优化迭代,让缓存设计成为一门艺术。接下来我选取了内存置换算法中数学模拟 LRU 的算法,分享给你。
 
-#### 如何描述最近使用次数? **设计 LRU 缓存第一个困难是描述最近使用次数** 。 因为“最近”是一个模糊概念,没有具体指出是多长时间?按照 CPU 周期计算还是按照时间计算?还是用其他模糊的概念替代?
+#### 如何描述最近使用次数?**设计 LRU 缓存第一个困难是描述最近使用次数** 。 因为“最近”是一个模糊概念,没有具体指出是多长时间?按照 CPU 周期计算还是按照时间计算?还是用其他模糊的概念替代?
 
 比如说页面置换算法。在实际的设计中,可以考虑把页表的读位利用起来。做一个定时器,每隔一定的 ms 数,就把读位累加到一个计数器中。相当于在每个页表条目上再增加一个累计值。
 
 例如:现在某个页表条目的累计值是 0, 接下来在多次计数中看到的读位是:1,0,0,1,1,那么累计值就会变成 3。这代表在某段时间内(5 个计数器 Tick 中)有 3 次访问操作。
 
-通过这种方法,就解决了描述使用次数的问题。如果单纯基于使用次数最少判断置换,我们称为最少使用(Least Frequently Used,,LFU)算法。 **LFU 的劣势在于它不会忘记数据,累计值不会减少** 。比如如果有内存数据过去常常被用到,但是现在已经有很长一段时间没有被用到了,在这种情况下它并不会置换出去。那么我们该如何描述“最近”呢?
+通过这种方法,就解决了描述使用次数的问题。如果单纯基于使用次数最少判断置换,我们称为最少使用(Least Frequently Used,,LFU)算法。**LFU 的劣势在于它不会忘记数据,累计值不会减少** 。比如如果有内存数据过去常常被用到,但是现在已经有很长一段时间没有被用到了,在这种情况下它并不会置换出去。那么我们该如何描述“最近”呢?
 
 有一个很不错的策略就是利用一个叫作“老化”(Aging)的算法。比起传统的累加计数的方式,Aging 算法的累加不太一样。
 
@@ -106,13 +106,13 @@ LRU 的一种常见实现是链表,如下图所示:
 
 而计算 Aging(累计值)的过程,可以由硬件实现,这样就最大程度提升了性能。
 
-相比写入操作,查询是耗时相对较少的。这是因为有 CPU 缓存的存在,我们通常不用直接去内存中查找数据,而是在缓存中进行。对于发生缺页中断的情况,并不需要追求绝对的精确,可以在部分页中找到一个相对累计值较小的页面进行置换。不过即便是模拟的 LRU 算法,也不是硬件直接支持的,总有一部分需要软件实现,因此还是有较多的时间开销。 **是否采用 LRU,一方面要看你所在场景的性能要求,有没有足够的优化措施(比如硬件提速);另一方面,就要看最终的结果是否能够达到期望的命中率和期望的使用延迟了** 。
+相比写入操作,查询是耗时相对较少的。这是因为有 CPU 缓存的存在,我们通常不用直接去内存中查找数据,而是在缓存中进行。对于发生缺页中断的情况,并不需要追求绝对的精确,可以在部分页中找到一个相对累计值较小的页面进行置换。不过即便是模拟的 LRU 算法,也不是硬件直接支持的,总有一部分需要软件实现,因此还是有较多的时间开销。**是否采用 LRU,一方面要看你所在场景的性能要求,有没有足够的优化措施(比如硬件提速);另一方面,就要看最终的结果是否能够达到期望的命中率和期望的使用延迟了** 。
 
 ### 总结
 
 本讲我们讨论的频次较高、频次较低,是基于历史的。 历史在未来并不一定重演。比如读取一个大型文件,无论如何操作都很难建立一个有效的缓存。甚至有的时候,最近使用频次最低的数据被缓存,使用频次最高的数据被置换,效率会更高。比如说有的数据库设计同时支持 LRU 缓存和 MRU( Most Recently Used)缓存。MRU 是 LRU 的对立面,这看似茅盾,但其实是为了解决不同情况下的需求。
 
-这并不是说缓存设计无迹可寻,而是经过思考和预判,还得以事实的命中率去衡量缓存置换算法是否合理。 **那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:LRU 用什么数据结构实现更合理** ? **【解析】** 最原始的方式是用数组,数组的每一项中有数据最近的使用频次。数据的使用频次可以用计时器计算。每次置换的时候查询整个数组实现。
+这并不是说缓存设计无迹可寻,而是经过思考和预判,还得以事实的命中率去衡量缓存置换算法是否合理。**那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:LRU 用什么数据结构实现更合理**?**【解析】** 最原始的方式是用数组,数组的每一项中有数据最近的使用频次。数据的使用频次可以用计时器计算。每次置换的时候查询整个数组实现。
 
 另一种更好的做法是利用双向链表实现。将使用到的数据移动到链表头部,每次置换时从链表尾部拿走数据。链表头部是最近使用的,链表尾部是最近没有被使用到的数据。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25427\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25427\350\256\262.md"
index 9677129d1..2656fd523 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25427\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25427\350\256\262.md"
@@ -2,7 +2,7 @@
 
 内存泄漏一直是很多大型系统故障的根源,也是一个面试热点。那么在编程语言层面已经提供了内存回收机制,为什么还会产生内存泄漏呢?
 
-这是因为应用的内存管理一直处于一个和应用程序执行并发的状态,如果应用程序申请内存的速度,超过内存回收的速度,内存就会被用满。当内存用满,操作系统就开始需要频繁地切换页面,进行频繁地磁盘读写。 **所以我们观察到的系统性能下降,往往是一种突然的崩溃,因为一旦内存被占满,系统性能就开始雪崩式下降** 。
+这是因为应用的内存管理一直处于一个和应用程序执行并发的状态,如果应用程序申请内存的速度,超过内存回收的速度,内存就会被用满。当内存用满,操作系统就开始需要频繁地切换页面,进行频繁地磁盘读写。**所以我们观察到的系统性能下降,往往是一种突然的崩溃,因为一旦内存被占满,系统性能就开始雪崩式下降** 。
 
 特别是有时候程序员不懂内存回收的原理,错误地使用内存回收器,导致部分对象没有被回收。而在高并发场景下,每次并发都产生一点不能回收的内存,不用太长时间内存就满了,这就是泄漏通常的成因。
 
@@ -49,13 +49,13 @@ S = 1 / (1 - 0.9) = 10
 
 上面表达式代表着有 90% 的任务可以并行,只有 10% 的任务不能够并行。假设我们拥有无限多的 CPU 去分担 90% 可以并行的任务,其实就相当于并行的任务可以在非常短的时间内完成。但是还有 10% 的任务不能并行,因此理论极限是 1/0.1=10 倍。
 
-通常我们设计 GC,都希望它能够支持并行处理任务。因为 GC 本身也有着繁重的工作量,需要扫描所有的对象,对内存进行标记清除和整理等。 **经过上述分析,那么我们在设计算法的时候是不是应该尽量做到高并发呢?** 很可惜并不是这样。如果算法支持的并发度非常高,那么和单线程算法相比,它也会带来更多的其他开销。比如任务拆分的开销、解决同步问题的开销,还有就是空间开销,GC 领域空间开销通常称为 FootPrint。理想情况下当然是核越多越好,但是如果考虑计算本身的成本,就需要找到折中的方案。
+通常我们设计 GC,都希望它能够支持并行处理任务。因为 GC 本身也有着繁重的工作量,需要扫描所有的对象,对内存进行标记清除和整理等。**经过上述分析,那么我们在设计算法的时候是不是应该尽量做到高并发呢?** 很可惜并不是这样。如果算法支持的并发度非常高,那么和单线程算法相比,它也会带来更多的其他开销。比如任务拆分的开销、解决同步问题的开销,还有就是空间开销,GC 领域空间开销通常称为 FootPrint。理想情况下当然是核越多越好,但是如果考虑计算本身的成本,就需要找到折中的方案。
 
 还有一个问题是,GC 往往不能拥有太长的暂停时间(Pause Time),因为 GC 和应用是并发的执行。如果 GC 导致应用暂停(Stop The World,STL)太久,那么对有的应用来说是灾难性的。 比如说你用鼠标的时候,如果突然卡了你会很抓狂。如果一个应用提供给百万级的用户用,假设这个应用帮每个用户每天节省了 1s 的等待时间,那么按照乔布斯的说法每天就为用户节省了 11 天的时间,每年是 11 年——5 年就相当于拯救了一条生命。
 
 如果暂停时间只允许很短,那么 GC 和应用的交替就需要非常频繁。这对 GC 算法要求就会上升,因为每次用户程序执行后,会产生新的变化,甚至会对已有的 GC 结果产生影响。后面我们在讨论标记-清除算法的时候,你会感受到这种情况。
 
-所以说,吞吐量高,不代表暂停时间少,也不代表空间使用(FootPrint)小。 同样的,使用空间小的 GC 算法,吞吐量反而也会下降。 **正因为三者之间存在类似相同成本代价下不可兼得的关系,往往编程语言会提供参数让你选择根据自己的应用特性决定 GC 行为** 。
+所以说,吞吐量高,不代表暂停时间少,也不代表空间使用(FootPrint)小。 同样的,使用空间小的 GC 算法,吞吐量反而也会下降。**正因为三者之间存在类似相同成本代价下不可兼得的关系,往往编程语言会提供参数让你选择根据自己的应用特性决定 GC 行为** 。
 
 ### 引用计数算法(Reference Counter)
 
@@ -77,7 +77,7 @@ S = 1 / (1 - 0.9) = 10
 
 ![图片4.png](assets/CgqCHl_TUSuAPeDgAACSOYD8YQE974.png)
 
-综上, **引用计数法出错概率大** ,比如我们编程时会有对象的循环引用;另一方面, **引用计数法容错能力差** ,一旦计算错了,就会导致内存永久无法被回收,因此我们需要更好的方式。
+综上,**引用计数法出错概率大**,比如我们编程时会有对象的循环引用;另一方面,**引用计数法容错能力差**,一旦计算错了,就会导致内存永久无法被回收,因此我们需要更好的方式。
 
 ### Root Tracing 算法
 
@@ -139,7 +139,7 @@ for root in rootSet {
 }
 ```
 
-以上程序执行结束后,所有和 Root Object 连通的对象都已经被染成了黑色。然后我们遍历整个 heapSet 找到白色的对象进行回收,这一步开始是 **清除(Sweep)阶段** ,以上是 **标记(Mark)阶段** 。
+以上程序执行结束后,所有和 Root Object 连通的对象都已经被染成了黑色。然后我们遍历整个 heapSet 找到白色的对象进行回收,这一步开始是 **清除(Sweep)阶段**,以上是 **标记(Mark)阶段** 。
 
 ```plaintext
 for obj in heapSet {
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25428\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25428\350\256\262.md"
index 6ad9212c7..a8232778c 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25428\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25428\350\256\262.md"
@@ -6,7 +6,7 @@
 
 #### 24 | 虚拟内存 :一个程序最多能使用多少内存?
 
-**【问题】可不可以利用哈希表直接将页编号映射到 Frame 编号** ?
+**【问题】可不可以利用哈希表直接将页编号映射到 Frame 编号**?
 
 【 **解析** 】按照普通页表的设计,如果页大小是 4K,1G 空间内存需要 262144 个页表条目,如果每个条目用 4 个字节来存储,就需要 1M 的空间。那么创建 1T 的虚拟内存,就需要 1G 的空间。这意味着操作系统需要在启动时,就把这块需要的内存空间预留出来。
 
@@ -20,7 +20,7 @@
 
 当然节省空间也是有代价的,这会直接导致性能下降,因为比起传统页表我们可以直接通过页的编号知道页表条目,基于 HashTable 的做法需要先进行一次 Hash 函数的计算,然后再遍历一次链表。 最后,HashTable 的时间复杂度可以看作 O(k),k 为 HashTable 表中总共的 \ 数量除以哈希表的条目数。当 k 较小的时候 HashTable 的时间复杂度趋向于 O(1)。
 
-#### 25 | 内存管理单元:什么情况下使用大内存分页? **【问题】Java 和 Go 默认需不需要开启大内存分页?** 【 **解析** 】在回答什么情况下使用前,我们先说说这两个语言对大内存分页的支持
+#### 25 | 内存管理单元:什么情况下使用大内存分页?**【问题】Java 和 Go 默认需不需要开启大内存分页?** 【 **解析** 】在回答什么情况下使用前,我们先说说这两个语言对大内存分页的支持
 
 当然,两门语言能够使用大内存分页的前提条件,是通过“ **25 讲”** 中演示的方式,开启了操作系统的大内存分页。满足这个条件后,我们再来说说两门语言还需要做哪些配置。
 
@@ -43,22 +43,22 @@ madvise(mymemory, size, MADV_HUGEPAGE);
 
 **Java 语言** JVM 是一个虚拟机,应用了Just-In-Time 在虚拟指令执行的过程中,将虚拟指令转换为机器码执行。 JVM 自己有一套完整的动态内存管理方案,而且提供了很多内存管理工具可选。在使用 JVM 时,虽然 Java 提供了 UnSafe 类帮助我们执行底层操作,但是通常情况下我们不会使用UnSafe 类。一方面 UnSafe 类功能不全,另一方面看名字就知道它过于危险。
 
-Java 语言在“ **25 讲”中** 提到过有一个虚拟机参数:XX:+UseLargePages,开启这个参数,JVM 会开始尝试使用大内存分页。 **那么到底该不该用大内存分页** ?
+Java 语言在“ **25 讲”中** 提到过有一个虚拟机参数:XX:+UseLargePages,开启这个参数,JVM 会开始尝试使用大内存分页。**那么到底该不该用大内存分页**?
 
 首先可以分析下你应用的特性,看看有没有大内存分页的需求。通常 OS 是 4K,思考下你有没有需要反复用到大内存分页的场景。
 
 另外你可以使用`perf`指令衡量你系统的一些性能指标,其中就包括`iTLB-load-miss`可以用来衡量 TLB Miss。 如果发现自己系统的 TLB Miss 较高,那么可以深入分析是否需要开启大内存分页。
 
-#### 26 | 缓存置换算法: LRU 用什么数据结构实现更合理? **【问题】在 TLB 多路组相联缓存设计中(比如 8-way),如何实现 LRU 缓存** ?
+#### 26 | 缓存置换算法: LRU 用什么数据结构实现更合理?**【问题】在 TLB 多路组相联缓存设计中(比如 8-way),如何实现 LRU 缓存**?
 
-【 **解析** 】TLB 是 CPU 的一个“零件”,在 TLB 的设计当中不可能再去内存中创建数据结构。因此在 8 路组相联缓存设计中,我们每次只需要从 8 个缓存条目中选择 Least Recently Used 缓存。 **增加累计值** 先说一种方法, 比如用硬件同时比较 8 个缓存中记录的缓存使用次数。这种方案需要做到 2 点:
+【 **解析** 】TLB 是 CPU 的一个“零件”,在 TLB 的设计当中不可能再去内存中创建数据结构。因此在 8 路组相联缓存设计中,我们每次只需要从 8 个缓存条目中选择 Least Recently Used 缓存。**增加累计值** 先说一种方法, 比如用硬件同时比较 8 个缓存中记录的缓存使用次数。这种方案需要做到 2 点:
 
 1. 缓存条目中需要额外的空间记录条目的使用次数(累计位)。类似我们在页表设计中讨论的基于计时器的读位操作——每过一段时间就自动将读位累计到一个累计位上。
 1. 硬件能够实现一个快速查询最小值的算法。
 
-第 1 种方法会产生额外的空间开销,还需要定时器配合,成本较高。 注意缓存是很贵的,对于缓存空间利用自然能省则省。而第 2 种方法也需要额外的硬件设计。那么,有没有更好的方案呢? **1bit 模拟 LRU** 一个更好的方案就是模拟 LRU,我们可以考虑继续采用上面的方式,但是每个缓存条目只拿出一个 LRU 位(bit)来描述缓存近期有没有被使用过。 缓存置换时只是查找 LRU 位等于 0 的条目置换。
+第 1 种方法会产生额外的空间开销,还需要定时器配合,成本较高。 注意缓存是很贵的,对于缓存空间利用自然能省则省。而第 2 种方法也需要额外的硬件设计。那么,有没有更好的方案呢?**1bit 模拟 LRU** 一个更好的方案就是模拟 LRU,我们可以考虑继续采用上面的方式,但是每个缓存条目只拿出一个 LRU 位(bit)来描述缓存近期有没有被使用过。 缓存置换时只是查找 LRU 位等于 0 的条目置换。
 
-还有一个基于这种设计更好的方案,可以考虑在所有 LRU 位都被置 1 的时候,清除 8 个条目中的 LRU 位(置零),这样可以节省一个计时器。 相当于发生内存操作,LRU 位置 1;8 个位置都被使用,LRU 都置 0。 **搜索树模拟 LRU** 最后我再介绍一个巧妙的方法——用搜索树模拟 LRU。
+还有一个基于这种设计更好的方案,可以考虑在所有 LRU 位都被置 1 的时候,清除 8 个条目中的 LRU 位(置零),这样可以节省一个计时器。 相当于发生内存操作,LRU 位置 1;8 个位置都被使用,LRU 都置 0。**搜索树模拟 LRU** 最后我再介绍一个巧妙的方法——用搜索树模拟 LRU。
 
 对于一个 8 路组相联缓存,这个方法需要 8-1 = 7bit 去构造一个树。如下图所示:
 
@@ -90,13 +90,13 @@ Java 语言在“ **25 讲”中** 提到过有一个虚拟机参数:XX:+UseLa
 
 ![8.png](assets/CgpVE1_cbnmAMnbJAACm2EGytKM521.png)
 
-这个结果并没有改变下一个更新的位置,但是翻转了指向 c 的路径。 如果要读取`x`,那么这个时候就会覆盖橘黄色的位置。 **因此,本质上这种树状的方式,其实是在构造一种先入先出的顺序。任何一个节点箭头指向的子节点,应该被先淘汰(最早被使用)** 。
+这个结果并没有改变下一个更新的位置,但是翻转了指向 c 的路径。 如果要读取`x`,那么这个时候就会覆盖橘黄色的位置。**因此,本质上这种树状的方式,其实是在构造一种先入先出的顺序。任何一个节点箭头指向的子节点,应该被先淘汰(最早被使用)** 。
 
 这是一个我个人觉得非常天才的设计,因为如果在这个地方构造一个队列,然后每次都把命中的元素的当前位置移动到队列尾部。就至少需要构造一个链表,而链表的每个节点都至少要有当前的值和 next 指针,这就需要创建复杂的数据结构。在内存中创建复杂的数据结构轻而易举,但是在 CPU 中就非常困难。 所以这种基于 bit-tree,就轻松地解决了这个问题。当然,这是一个模拟 LRU 的情况,你还是可以构造出违反 LRU 缓存的顺序。
 
 #### 27 | 内存回收上篇:如何解决内存的循环引用问题?
 
-#### 28 | 内存回收下篇:三色标记-清除算法是怎么回事? **【问题】如果内存太大了,无论是标记还是清除速度都很慢,执行一次完整的 GC 速度下降该如何处理** ?
+#### 28 | 内存回收下篇:三色标记-清除算法是怎么回事?**【问题】如果内存太大了,无论是标记还是清除速度都很慢,执行一次完整的 GC 速度下降该如何处理**?
 
 【 **解析** 】当应用申请到的内存很大的时候,如果其中内部对象太多。只简单划分几个生代,每个生代占用的内存都很大,这个时候使用 GC 性能就会很糟糕。
 
@@ -108,6 +108,6 @@ Java 语言在“ **25 讲”中** 提到过有一个虚拟机参数:XX:+UseLa
 
 ### 总结
 
-这个模块我们学习了内存管理。 **通过内存管理的学习,我希望你开始理解虚拟化的价值,内存管理部分的虚拟化,是一种应对资源稀缺、增加资源流动性的手段** (听起来那么像银行印的货币)。
+这个模块我们学习了内存管理。**通过内存管理的学习,我希望你开始理解虚拟化的价值,内存管理部分的虚拟化,是一种应对资源稀缺、增加资源流动性的手段** (听起来那么像银行印的货币)。
 
-既然内存资源可以虚拟化,那么计算资源可以虚拟化吗?用户发生大量的请求时,响应用户请求的处理程序可以虚拟化吗?当消息太大的情况下,一个队列可以虚拟化吗?当浏览的页面很大时,用户看到的可视区域可以虚拟化吗?——我觉得这些问题都是值得大家深思的,如果你对这几个问题有什么想法,也欢迎写在留言区,大家一起交流。 **另外,缓存设计部分的重点在于算法的掌握** 。因为你可以从这些算法中获得很多处理实际问题的思路,服务端同学会反思 MySQL/Redis 的使用,前端同学会反思浏览器缓存、Native 缓存、CDN 的使用。很多时候,工具还会给你提供参数,那么你应该用哪种缓存置换算法,你的目的是什么?我们只学习了如何收集和操作系统相关的性能指标,但当你面对应用的时候,还会碰到更多的指标,这个时候就需要你在实战中继续进步和分析了。 **这个模块还有一个重要的课题,就是内存回收,这块的重点在于理解内存回收器,你需要关注:暂停时间、足迹和吞吐量、实时性,还需要知道如何针对自己的业务场景,分析这几个指标的要求,学会选择不同的 GC 算法,配置不同的 GC 参数** 。
+既然内存资源可以虚拟化,那么计算资源可以虚拟化吗?用户发生大量的请求时,响应用户请求的处理程序可以虚拟化吗?当消息太大的情况下,一个队列可以虚拟化吗?当浏览的页面很大时,用户看到的可视区域可以虚拟化吗?——我觉得这些问题都是值得大家深思的,如果你对这几个问题有什么想法,也欢迎写在留言区,大家一起交流。**另外,缓存设计部分的重点在于算法的掌握** 。因为你可以从这些算法中获得很多处理实际问题的思路,服务端同学会反思 MySQL/Redis 的使用,前端同学会反思浏览器缓存、Native 缓存、CDN 的使用。很多时候,工具还会给你提供参数,那么你应该用哪种缓存置换算法,你的目的是什么?我们只学习了如何收集和操作系统相关的性能指标,但当你面对应用的时候,还会碰到更多的指标,这个时候就需要你在实战中继续进步和分析了。**这个模块还有一个重要的课题,就是内存回收,这块的重点在于理解内存回收器,你需要关注:暂停时间、足迹和吞吐量、实时性,还需要知道如何针对自己的业务场景,分析这几个指标的要求,学会选择不同的 GC 算法,配置不同的 GC 参数** 。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25429\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25429\350\256\262.md"
index d5b0eb9e7..3fc88012a 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25429\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25429\350\256\262.md"
@@ -11,7 +11,7 @@
 
 ![Lark20201223-163616.png](assets/Cip5yF_jAd-APzhvAADyJAEGLTc170.png)
 
-当你访问一个目录或者文件,虽然用的是 Linux 标准的文件 API 对文件进行操作,但实际操作的可能是磁盘、内存、网络或者数据库等。 **因此,Linux 上不同的目录可能是不同的磁盘,不同的文件可能是不同的设备** 。
+当你访问一个目录或者文件,虽然用的是 Linux 标准的文件 API 对文件进行操作,但实际操作的可能是磁盘、内存、网络或者数据库等。**因此,Linux 上不同的目录可能是不同的磁盘,不同的文件可能是不同的设备** 。
 
 ### 分区结构
 
@@ -59,9 +59,9 @@ mount /dev/sda6 /abc
 
 ![Lark20201223-163621.png](assets/Ciqc1F_jAhGADnWLAAFf1qd349k816.png)
 
-**最顶层的目录称作根目录,** 用`/`表示。`/`目录下用户可以再创建目录,但是有一些目录随着系统创建就已经存在,接下来我会和你一起讨论下它们的用途。 **/bin(二进制** )包含了许多所有用户都可以访问的可执行文件,如 ls, cp, cd 等。这里的大多数程序都是二进制格式的,因此称作`bin`目录。`bin`是一个命名习惯,比如说`nginx`中的可执行文件会在 Nginx 安装目录的 bin 文件夹下面。 **/dev(设备文件)** 通常挂载在`devtmpfs`文件系统上,里面存放的是设备文件节点。通常直接和内存进行映射,而不是存在物理磁盘上。
+**最顶层的目录称作根目录,** 用`/`表示。`/`目录下用户可以再创建目录,但是有一些目录随着系统创建就已经存在,接下来我会和你一起讨论下它们的用途。**/bin(二进制** )包含了许多所有用户都可以访问的可执行文件,如 ls, cp, cd 等。这里的大多数程序都是二进制格式的,因此称作`bin`目录。`bin`是一个命名习惯,比如说`nginx`中的可执行文件会在 Nginx 安装目录的 bin 文件夹下面。**/dev(设备文件)** 通常挂载在`devtmpfs`文件系统上,里面存放的是设备文件节点。通常直接和内存进行映射,而不是存在物理磁盘上。
 
-值得一提的是其中有几个有趣的文件,它们是虚拟设备。 **/dev/null** 是可以用来销毁任何输出的虚拟设备。你可以用`>`重定向符号将任何输出流重定向到`/dev/null`来忽略输出的结果。 **/dev/zero** 是一个产生数字 0 的虚拟设备。无论你对它进行多少次读取,都会读到 0。 **/dev/ramdom** 是一个产生随机数的虚拟设备。读取这个文件中数据,你会得到一个随机数。你不停地读取这个文件,就会得到一个随机数的序列。 **/etc(配置文件),** `/etc`名字的含义是`and so on……`,也就是“等等及其他”,Linux 用它来保管程序的配置。比如说`mysql`通常会在`/etc/mysql`下创建配置。再比如说`/etc/passwd`是系统的用户配置,存储了用户信息。 **/proc(进程和内核文件)** 存储了执行中进程和内核的信息。比如你可以通过`/proc/1122`目录找到和进程`1122`关联的全部信息。还可以在`/proc/cpuinfo`下找到和 CPU 相关的全部信息。 **/sbin(系统二进制)** 和`/bin`类似,通常是系统启动必需的指令,也可以包括管理员才会使用的指令。 **/tmp(临时文件)** 用于存放应用的临时文件,通常用的是`tmpfs`文件系统。因为`tmpfs`是一个内存文件系统,系统重启的时候清除`/tmp`文件,所以这个目录不能放应用和重要的数据。 **/var (Variable data file,,可变数据文件)** 用于存储运行时的数据,比如日志通常会存放在`/var/log`目录下面。再比如应用的缓存文件、用户的登录行为等,都可以放到`/var`目录下,`/var`下的文件会长期保存。 **/boot(启动)** 目录下存放了 Linux 的内核文件和启动镜像,通常这个目录会写入磁盘最头部的分区,启动的时候需要加载目录内的文件。 **/opt(Optional Software,可选软件)** 通常会把第三方软件安装到这个目录。以后你安装软件的时候,可以考虑在这个目录下创建。 **/root(root 用户家目录)** 为了防止误操作,Linux 设计中 root 用户的家目录没有设计在`/home/root`下,而是放到了`/root`目录。 **/home(家目录)** 用于存放用户的个人数据,比如用户`lagou`的个人数据会存放到`/home/lagou`下面。并且通常在用户登录,或者执行`cd`指令后,都会在家目录下工作。 用户通常会对自己的家目录拥有管理权限,而无法访问其他用户的家目录。 **/media(媒体)** 自动挂载的设备通常会出现在`/media`目录下。比如你插入 U 盘,通常较新版本的 Linux 都会帮你自动完成挂载,也就是在`/media`下创建一个目录代表 U 盘。 **/mnt(Mount,挂载)** 我们习惯把手动挂载的设备放到这个目录。比如你插入 U 盘后,如果 Linux 没有帮你完成自动挂载,可以用`mount`命令手动将 U 盘内容挂载到`/mnt`目录下。 **/svr(Service Data,,服务数据)** 通常用来存放服务数据,比如说你开发的网站资源文件(脚本、网页等)。不过现在很多团队的习惯发生了变化, 有的团队会把网站相关的资源放到`/www`目录下,也有的团队会放到`/data`下。总之,在存放资源的角度,还是比较灵活的。 **/usr(Unix System Resource)** 包含系统需要的资源文件,通常应用程序会把后来安装的可执行文件也放到这个目录下,比如说
+值得一提的是其中有几个有趣的文件,它们是虚拟设备。**/dev/null** 是可以用来销毁任何输出的虚拟设备。你可以用`>`重定向符号将任何输出流重定向到`/dev/null`来忽略输出的结果。**/dev/zero** 是一个产生数字 0 的虚拟设备。无论你对它进行多少次读取,都会读到 0。**/dev/ramdom** 是一个产生随机数的虚拟设备。读取这个文件中数据,你会得到一个随机数。你不停地读取这个文件,就会得到一个随机数的序列。**/etc(配置文件),** `/etc`名字的含义是`and so on……`,也就是“等等及其他”,Linux 用它来保管程序的配置。比如说`mysql`通常会在`/etc/mysql`下创建配置。再比如说`/etc/passwd`是系统的用户配置,存储了用户信息。**/proc(进程和内核文件)** 存储了执行中进程和内核的信息。比如你可以通过`/proc/1122`目录找到和进程`1122`关联的全部信息。还可以在`/proc/cpuinfo`下找到和 CPU 相关的全部信息。**/sbin(系统二进制)** 和`/bin`类似,通常是系统启动必需的指令,也可以包括管理员才会使用的指令。**/tmp(临时文件)** 用于存放应用的临时文件,通常用的是`tmpfs`文件系统。因为`tmpfs`是一个内存文件系统,系统重启的时候清除`/tmp`文件,所以这个目录不能放应用和重要的数据。**/var (Variable data file,,可变数据文件)** 用于存储运行时的数据,比如日志通常会存放在`/var/log`目录下面。再比如应用的缓存文件、用户的登录行为等,都可以放到`/var`目录下,`/var`下的文件会长期保存。**/boot(启动)** 目录下存放了 Linux 的内核文件和启动镜像,通常这个目录会写入磁盘最头部的分区,启动的时候需要加载目录内的文件。**/opt(Optional Software,可选软件)** 通常会把第三方软件安装到这个目录。以后你安装软件的时候,可以考虑在这个目录下创建。**/root(root 用户家目录)** 为了防止误操作,Linux 设计中 root 用户的家目录没有设计在`/home/root`下,而是放到了`/root`目录。**/home(家目录)** 用于存放用户的个人数据,比如用户`lagou`的个人数据会存放到`/home/lagou`下面。并且通常在用户登录,或者执行`cd`指令后,都会在家目录下工作。 用户通常会对自己的家目录拥有管理权限,而无法访问其他用户的家目录。**/media(媒体)** 自动挂载的设备通常会出现在`/media`目录下。比如你插入 U 盘,通常较新版本的 Linux 都会帮你自动完成挂载,也就是在`/media`下创建一个目录代表 U 盘。**/mnt(Mount,挂载)** 我们习惯把手动挂载的设备放到这个目录。比如你插入 U 盘后,如果 Linux 没有帮你完成自动挂载,可以用`mount`命令手动将 U 盘内容挂载到`/mnt`目录下。**/svr(Service Data,,服务数据)** 通常用来存放服务数据,比如说你开发的网站资源文件(脚本、网页等)。不过现在很多团队的习惯发生了变化, 有的团队会把网站相关的资源放到`/www`目录下,也有的团队会放到`/data`下。总之,在存放资源的角度,还是比较灵活的。**/usr(Unix System Resource)** 包含系统需要的资源文件,通常应用程序会把后来安装的可执行文件也放到这个目录下,比如说
 
 - `vim`编辑器的可执行文件通常会在`/usr/bin`目录下,区别于`ls`会在`/bin`目录下
 - `/usr/sbin`中会包含有通常系统管理员才会使用的指令。
@@ -75,6 +75,6 @@ mount /dev/sda6 /abc
 
 今天我们讲到的这些规范是整个世界通用的,如果每个人都能遵循规范的原则,工作起来就会有很好的默契。登录一台`linux`服务器,你可以通过目录结构快速熟悉。你可以查阅`/etc`下的配置,看看`/opt`下装了什么软件,这就是规范的好处。
 
-**那么通过这节课的学习,你现在可以尝试来回答本节标题中的试题目:Linux下各个目录有什么作用了吗** ?
+**那么通过这节课的学习,你现在可以尝试来回答本节标题中的试题目:Linux下各个目录有什么作用了吗**?
 
 【 **解析** 】通常面试官会挑选其中一部分对你进行抽查,如果你快要面试了,再 Review 一下本讲的内容吧。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25430\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25430\350\256\262.md"
index 63a1064ad..5f0753239 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25430\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25430\350\256\262.md"
@@ -1,16 +1,16 @@
 # 30 文件系统的底层实现:FAT、NTFS 和 Ext3 有什么区别?
 
-**这一讲给你带来的面试题是: FAT、NTFS 和 Ext3 文件系统有什么区别** ?
+**这一讲给你带来的面试题是: FAT、NTFS 和 Ext3 文件系统有什么区别**?
 
-10 年前 FAT 文件系统还是常见的格式,而现在 Windows 上主要是 NTFS,Linux 上主要是 Ext3、Ext4 文件系统。关于这块知识,一般资料只会从支持的磁盘大小、数据保护、文件名等各种维度帮你比较,但是最本质的内容却被一笔带过。 **它们最大的区别是文件系统的实现不同,具体怎么不同** ? **文件系统又有哪些实现** ?这一讲,我将带你一起来探索和学习这部分知识。
+10 年前 FAT 文件系统还是常见的格式,而现在 Windows 上主要是 NTFS,Linux 上主要是 Ext3、Ext4 文件系统。关于这块知识,一般资料只会从支持的磁盘大小、数据保护、文件名等各种维度帮你比较,但是最本质的内容却被一笔带过。**它们最大的区别是文件系统的实现不同,具体怎么不同**?**文件系统又有哪些实现**?这一讲,我将带你一起来探索和学习这部分知识。
 
 ### 硬盘分块
 
-在了解文件系统实现之前,我们先来了解下操作系统如何使用硬盘。 **使用硬盘和使用内存有一个很大的区别,内存可以支持到字节级别的随机存取,而这种情况在硬盘中通常是不支持的** 。过去的机械硬盘内部是一个柱状结构,有扇区、柱面等。读取硬盘数据要转动物理的磁头,每转动一次磁头时间开销都很大,因此一次只读取一两个字节的数据,非常不划算。
+在了解文件系统实现之前,我们先来了解下操作系统如何使用硬盘。**使用硬盘和使用内存有一个很大的区别,内存可以支持到字节级别的随机存取,而这种情况在硬盘中通常是不支持的** 。过去的机械硬盘内部是一个柱状结构,有扇区、柱面等。读取硬盘数据要转动物理的磁头,每转动一次磁头时间开销都很大,因此一次只读取一两个字节的数据,非常不划算。
 
 随着 SSD 的出现,机械硬盘开始逐渐消失(还没有完全结束),现在的固态硬盘内部是类似内存的随机存取结构。但是硬盘的读写速度还是远远不及内存。而连续读多个字节的速度,还远不如一次读一个硬盘块的速度。
 
-因此, **为了提高性能,通常会将物理存储(硬盘)划分成一个个小块** ,比如每个 4KB。这样做也可以让硬盘的使用看起来非常整齐,方便分配和回收空间。况且,数据从磁盘到内存,需要通过电子设备,比如 DMA、总线等,如果一个字节一个字节读取,速度较慢的硬盘就太耗费时间了。过去的机械硬盘的速度可以比内存慢百万倍,现在的固态硬盘,也会慢几十到几百倍。即便是最新的 NvMe 接口的硬盘,和内存相比速度仍然有很大的差距。因此,一次读/写一个块(Block)才是可行的方案。
+因此,**为了提高性能,通常会将物理存储(硬盘)划分成一个个小块**,比如每个 4KB。这样做也可以让硬盘的使用看起来非常整齐,方便分配和回收空间。况且,数据从磁盘到内存,需要通过电子设备,比如 DMA、总线等,如果一个字节一个字节读取,速度较慢的硬盘就太耗费时间了。过去的机械硬盘的速度可以比内存慢百万倍,现在的固态硬盘,也会慢几十到几百倍。即便是最新的 NvMe 接口的硬盘,和内存相比速度仍然有很大的差距。因此,一次读/写一个块(Block)才是可行的方案。
 
 ![Lark20201225-174103.png](assets/Cip5yF_ls_aAEer_AADHBXF7EHw534.png)
 
@@ -30,7 +30,7 @@
 
 ![Lark20201225-174106.png](assets/CgpVE1_ltAKAZe8tAACczq1tAiY181.png)
 
-**一个文件,最基本的就是要描述文件在硬盘中到底对应了哪些块。FAT 表通过一种类似链表的结构描述了文件对应的块** 。上图中:文件 1 从位置 5 开始,这就代表文件 1 在硬盘上的第 1 个块的序号是 5 的块 。然后位置 5 的值是 2,代表文件 1 的下一个块的是序号 2 的块。顺着这条链路,我们可以找到 5 → 2 → 9 → 14 → 15 → -1。-1 代表结束,所以文件 1 的块是:5,2,9,14,15。同理,文件 2 的块是 3,8,12。 **FAT 通过一个链表结构解决了文件和物理块映射的问题,算法简单实用,因此得到过广泛的应用,到今天的 Windows/Linux/MacOS 都还支持 FAT 格式的文件系统** 。FAT 的缺点就是非常占用内存,比如 1T 的硬盘,如果块的大小是 1K,那么就需要 1G 个 FAT 条目。通常一个 FAT 条目还会存一些其他信息,需要 2~3 个字节,这就又要占用 2-3G 的内存空间才能用 FAT 管理 1T 的硬盘空间。显然这样做是非常浪费的,问题就出在了 FAT 表需要全部维护在内存当中。
+**一个文件,最基本的就是要描述文件在硬盘中到底对应了哪些块。FAT 表通过一种类似链表的结构描述了文件对应的块** 。上图中:文件 1 从位置 5 开始,这就代表文件 1 在硬盘上的第 1 个块的序号是 5 的块 。然后位置 5 的值是 2,代表文件 1 的下一个块的是序号 2 的块。顺着这条链路,我们可以找到 5 → 2 → 9 → 14 → 15 → -1。-1 代表结束,所以文件 1 的块是:5,2,9,14,15。同理,文件 2 的块是 3,8,12。**FAT 通过一个链表结构解决了文件和物理块映射的问题,算法简单实用,因此得到过广泛的应用,到今天的 Windows/Linux/MacOS 都还支持 FAT 格式的文件系统** 。FAT 的缺点就是非常占用内存,比如 1T 的硬盘,如果块的大小是 1K,那么就需要 1G 个 FAT 条目。通常一个 FAT 条目还会存一些其他信息,需要 2~3 个字节,这就又要占用 2-3G 的内存空间才能用 FAT 管理 1T 的硬盘空间。显然这样做是非常浪费的,问题就出在了 FAT 表需要全部维护在内存当中。
 
 #### 索引节点(inode)
 
@@ -75,7 +75,7 @@ ln -s a b # 将b设置为a的软链接(b是a的快捷方式)
 1. 修改内存中的数据
 1. 计算要写入第几个块
 1. 查询 inode 找到真实块的序号
-1. 将这个块的数据完整的写入一次磁盘 **你可以思考一个问题,如果频繁读写磁盘,上面这个模型会有什么问题** ?可以把你的思考和想法写在留言区,我们在本讲后面会详细讨论。
+1. 将这个块的数据完整的写入一次磁盘 **你可以思考一个问题,如果频繁读写磁盘,上面这个模型会有什么问题**?可以把你的思考和想法写在留言区,我们在本讲后面会详细讨论。
 
 ### 解决性能和故障:日志文件系统 **在传统的文件系统实现中,inode 解决了 FAT 容量限制问题,但是随着 CPU、内存、传输线路的速度越来越快,对磁盘读写性能的要求也越来越高** 。传统的设计,每次写入操作都需要进行一次持久化,所谓“持久化”就是将数据写入到磁盘,这种设计会成为整个应用的瓶颈。因为磁盘速度较慢,内存和 CPU 缓存的速度非常快,如果 CPU 进行高速计算并且频繁写入磁盘,那么就会有大量线程阻塞在等待磁盘 I/O 上。磁盘的瓶颈通常在写入上,因为通常读取数据的时候,会从缓存中读取,不存在太大的瓶颈
 
@@ -99,7 +99,7 @@ A=3
 
 ![Lark20201225-174123.png](assets/Cip5yF_ltD-ANGHYAAD43z0foHQ229.png)
 
-上图这种设计可以让写入变得非常快速,多数时间都是写内存,最后写一次磁盘。 **而上图这样的设计成不成立,核心在能不能解决容灾问题** 。
+上图这种设计可以让写入变得非常快速,多数时间都是写内存,最后写一次磁盘。**而上图这样的设计成不成立,核心在能不能解决容灾问题** 。
 
 你可以思考一下这个问题—— **丢失一批日志和丢失一批数据的差别大不大** 。其实它们之间最大的差别在于,如果丢失一批日志,只不过丢失了近期的变更;但如果丢失一批数据,那么就可能造成永久伤害。
 
@@ -121,6 +121,6 @@ A=3
 
 现在我们很多分布式系统的设计也是基于日志,比如 MySQL 同步数据用 binlog,Redis 的 AOF,著名的分布式一致性算法 Paxos ,因此 Zookeeper 内部也在通过实现日志的一致性来实现分布式一致性。
 
-**那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:FAT、NTFS 和 Ext3 有什么区别** ?
+**那么通过这节课的学习,你现在可以尝试来回答本节关联的面试题目:FAT、NTFS 和 Ext3 有什么区别**?
 
-【 **解析** 】FAT 通过内存中一个类似链表的结构,实现对文件的管理。 **NTFS 和 Ext3 是日志文件系统,它们和 FAT 最大的区别在于写入到磁盘中的是日志,而不是数据** 。日志文件系统会先把日志写入到内存中一个高速缓冲区,定期写入到磁盘。日志写入是追加式的,不用考虑数据的覆盖。一段时间内的日志内容,会形成还原点。这种设计大大提高了性能,当然也会有一定的数据冗余。
+【 **解析** 】FAT 通过内存中一个类似链表的结构,实现对文件的管理。**NTFS 和 Ext3 是日志文件系统,它们和 FAT 最大的区别在于写入到磁盘中的是日志,而不是数据** 。日志文件系统会先把日志写入到内存中一个高速缓冲区,定期写入到磁盘。日志写入是追加式的,不用考虑数据的覆盖。一段时间内的日志内容,会形成还原点。这种设计大大提高了性能,当然也会有一定的数据冗余。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25432\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25432\350\256\262.md"
index 2284bf882..38af06789 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25432\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25432\350\256\262.md"
@@ -6,7 +6,7 @@
 
 #### 29 | Linux下各个目录有什么作用?
 
-【 **问题** 】 **socket 文件都存在哪里** ?
+【 **问题** 】 **socket 文件都存在哪里**?
 
 【 **解析** 】socket 没有实体文件,只有 inode,所以 socket 是没有名字的文件。
 
@@ -18,7 +18,7 @@
 
 ![操作系统 (assets/Ciqc1F_1k1iAfL9JAAUoAKqNqrU408.png).png](assets/Ciqc1F_1k1iAfL9JAAUoAKqNqrU408.png)
 
-#### 30 | 文件系统的底层实现:FAT、NTFS 和 Ext3 有什么区别? **【问题】思考日志文件系统的数据冗余如何处理** ?
+#### 30 | 文件系统的底层实现:FAT、NTFS 和 Ext3 有什么区别?**【问题】思考日志文件系统的数据冗余如何处理**?
 
 **【解析】** 日志系统产生冗余几乎是必然发生的。 只要发生了修改、删除,肯定就会有数据冗余。日志系统通常不会主动压缩,但是日志文件系统通常会对磁盘碎片进行整理,这种机制和内存的管理非常相似。
 
@@ -28,15 +28,15 @@
 
 #### 31 | 数据库文件系统实例:MySQL 中 B 树和 B+ 树有什么区别?
 
-【 **问题** 】 **按照应该尽量减少磁盘读写操作的原则,是不是哈希表的索引更有优势** ?
+【 **问题** 】 **按照应该尽量减少磁盘读写操作的原则,是不是哈希表的索引更有优势**?
 
-【 **解析** 】哈希表是一种稀疏的离散结构,通常使用键查找值。给定一个键,哈希表会通过数学计算的方式找到值的内存地址。因此,从这个角度去分析,哈希表的查询速度非常快。单独查找某一个数据速度超过了 B+ 树(比如根据姓名查找用户)。因此,包括 MySQL 在内的很多数据库,在支持 B+ 树索引的同时,也支持哈希表索引。 **这两种索引最大的区别是:B+ 树是对范围的划分,其中的数据还保持着连续性;而哈希表是一种离散的查询结构,数据已经分散到不同的空间中去了** 。所以当数据要进行 **范围查找** 时,比如查找某个区间内的订单,或者进行聚合运算,这个时候哈希表的性能就非常低了。
+【 **解析** 】哈希表是一种稀疏的离散结构,通常使用键查找值。给定一个键,哈希表会通过数学计算的方式找到值的内存地址。因此,从这个角度去分析,哈希表的查询速度非常快。单独查找某一个数据速度超过了 B+ 树(比如根据姓名查找用户)。因此,包括 MySQL 在内的很多数据库,在支持 B+ 树索引的同时,也支持哈希表索引。**这两种索引最大的区别是:B+ 树是对范围的划分,其中的数据还保持着连续性;而哈希表是一种离散的查询结构,数据已经分散到不同的空间中去了** 。所以当数据要进行 **范围查找** 时,比如查找某个区间内的订单,或者进行聚合运算,这个时候哈希表的性能就非常低了。
 
 哈希表有一个设计约束,如果我们用了 m 个桶(Bucket,比如链表)去存储哈希表中的数据,再假设总共需要存储 N 个数据。那么平均查询次数 k = N/m。为了让 k 不会太大,当数据增长到一定规模时,哈希表需要增加桶的数目,这个时候就需要重新计算所有节点的哈希值(重新分配所有节点属于哪个桶)。
 
 综上,对于大部分的操作 B+ 树都有较好的性能,比如说 >,\<, =,BETWEEN,LIKE 等,哈希表只能用于等于的情况。
 
-#### 32 | HDFS 介绍:分布式文件系统是怎么回事? **【问题】Master 节点如果宕机了,影响有多大,如何恢复** ?
+#### 32 | HDFS 介绍:分布式文件系统是怎么回事?**【问题】Master 节点如果宕机了,影响有多大,如何恢复**?
 
 【 **解析** 】在早期的设计中,Master 是一个单点(Single Point),如果发生故障,系统就会停止运转,这就是所谓的单点故障(Single Point of Failure)。由此带来的后果会非常严重。发生故障后,虽然我们可以设置第二节点不断备份还原点,通过还原点加快系统恢复的速度,但是在数据的恢复期间,整个系统是不可用的。
 
@@ -52,7 +52,7 @@
 
 而且,为了保证日志数据不丢失,它们应该存储至少 3 份。即使其中一份数据发生损坏,也可以通过对比半数以上的节点(2 个)恢复数据。因此,这里需要设计专门的日志节点(Journal Node)存储日志。至少需要 3 个日志节点,而且必须是奇数。活动节点将自己的日志发送给日志节点,待命节点则从日志节点中读取日志,同步自己的状态。
 
-我们再来回顾一下这个高可用的设计。 **为了保证可用性,我们增加了备用节点待命,随时替代活动节点** 。为了达成这个目标。有 3 类数据需要同步。
+我们再来回顾一下这个高可用的设计。**为了保证可用性,我们增加了备用节点待命,随时替代活动节点** 。为了达成这个目标。有 3 类数据需要同步。
 
 - **数据节点同步给主节点的日志** 。这类数据由数据节点同时同步给活动、待命节点。
 - **活动节点同步给待命节点的操作记录** 。这类数据由活动节点同步给日志节点,再由日志节点同步给待命节点。日志又至少有 3 态机器的集群保管,每个上放一个日志节点。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25433\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25433\350\256\262.md"
index 1081d0b0d..e4bdcfccc 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25433\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25433\350\256\262.md"
@@ -6,13 +6,13 @@
 - 《操作系统》讲述的是如何去理解和架构应用程序。
 - 《计算机网络》讲述的是如何去理解今天的互联网。
 
-本模块讲解的计网知识,以科普为主,我会用通俗的比喻、简单明了的语言,帮你在短时间内构建起网络的基本概念。 **如果要深入学习计算机网络的原理、算法,可以关注我即将在拉勾教育推出的《计算机网络》专栏** 。
+本模块讲解的计网知识,以科普为主,我会用通俗的比喻、简单明了的语言,帮你在短时间内构建起网络的基本概念。**如果要深入学习计算机网络的原理、算法,可以关注我即将在拉勾教育推出的《计算机网络》专栏** 。
 
 现在来看,“计算机网络”也许是一个过时的词汇,它讲的是怎么用计算实现通信。今天我们已经发展到了一个互联网、物联网的时代,社交网络、云的时代,再来看网络,意义已经发生转变。但这里面还是有很多经典的知识依旧在传承。比如说 TCP/IP 协议,问世后就逐渐成为占有统治地位的通信协议。虽然后面诞生出了许许多多的协议,但是我们仍然习惯性地把整个互联网的架构称为 TCP/IP 协议群,也叫作互联网协议群(Internet Protocol Suit)。
 
 ### 协议的分层
 
-对于多数的 **应用** 和 **用户** 而言,使用互联网的一个基本要求就是数据可以无损地到达。用户通过应用进行网络通信,应用启动之后就变成了进程。因此, **所有网络通信的本质目标就是进程间通信** 。世界上有很多进程需要通信,我们要找到一种通用的,每个进程都能认可和接受的通信方式,这就是 **协议** 。
+对于多数的 **应用** 和 **用户** 而言,使用互联网的一个基本要求就是数据可以无损地到达。用户通过应用进行网络通信,应用启动之后就变成了进程。因此,**所有网络通信的本质目标就是进程间通信** 。世界上有很多进程需要通信,我们要找到一种通用的,每个进程都能认可和接受的通信方式,这就是 **协议** 。
 
 #### 应用层
 
@@ -34,7 +34,7 @@
 
 #### 网络层 **接下来你要思考的问题是:传输层到底负不负责将数据从一个设备传输到另一个设备** (主机到主机,Host To Host)。仔细思考这个过程,你会发现如果这样设计,传输层就会违反简单、高效、专注的设计原则
 
-我们从一个主机到另一个主机传输数据的网络环境是非常复杂的。中间会通过各种各样的线路,有形形色色的交叉路口——有各式各样的路径和节点需要选择。 **核心的设计原则是,我们不希望一层协议处理太多的问题。传输层作为应用间数据传输的媒介,服务好应用即可** 。对应用层而言,传输层帮助实现应用到应用的通信。而实际的传输功能交给传输层的下一层,也就是 **网络层(Internet Layer)** 会更好一些。
+我们从一个主机到另一个主机传输数据的网络环境是非常复杂的。中间会通过各种各样的线路,有形形色色的交叉路口——有各式各样的路径和节点需要选择。**核心的设计原则是,我们不希望一层协议处理太多的问题。传输层作为应用间数据传输的媒介,服务好应用即可** 。对应用层而言,传输层帮助实现应用到应用的通信。而实际的传输功能交给传输层的下一层,也就是 **网络层(Internet Layer)** 会更好一些。
 
 ![Lark20210108-173928.png](assets/CgpVE1_4KKaAeyVoAABvyFiqSu8542.png)
 
@@ -46,7 +46,7 @@ IP 协议里也有这个问题,类似行政区域划分,IP 协议中具体
 
 除了 **寻址** ( **Addressing** ),IP 协议还有一个非常重要的能力就是路由。在实际传输过程当中,数据并不是从主机直接就传输到了主机。而是会经过网关、基站、防火墙、路由器、交换机、代理服务器等众多的设备。而网络的路径,也称作链路,和现实生活中道路非常相似,会有岔路口、转盘、高速路、立交桥等。
 
-因此,当封包到达一个节点,需要通过算法决定下一步走哪条路径。我们在现实生活中经常会碰到多条路径都可以到达同一个目的地的情况,在网络中也是如此。总结一下。 **寻址告诉我们去往下一个目的地该朝哪个方向走,路由则是根据下一个目的地选择路径。寻址更像在导航,路由更像在操作方向盘** 。
+因此,当封包到达一个节点,需要通过算法决定下一步走哪条路径。我们在现实生活中经常会碰到多条路径都可以到达同一个目的地的情况,在网络中也是如此。总结一下。**寻址告诉我们去往下一个目的地该朝哪个方向走,路由则是根据下一个目的地选择路径。寻址更像在导航,路由更像在操作方向盘** 。
 
 #### 数据链路层(Data Link Layer)
 
@@ -78,13 +78,13 @@ IP 协议里也有这个问题,类似行政区域划分,IP 协议中具体
 
 网络层没有连接这个概念。你可以把网络层理解成是一个巨大的物流公司。不断从传输层接收数据,然后进行打包,每一个包是一个 IP 封包。然后这个物流公司,负责 IP 封包的收发。所以,是很多很多的传输层在共用底下同一个网络层,这就是网络层的多路复用。
 
-总结一下。 **应用层的多路复用,如多个请求使用同一个信道并行的传输,实际上是传输层提供的多路复用能力** 。 **传输层的多路复用,比如多个 TCP 连接复用一条线路,实际上是网络层在提供多路复用能力** 。你可以把网络层想象成一个不断收发包裹的机器,在网络层中并没有连接这个概念,所以网络层天然就是支持多路复用的。
+总结一下。**应用层的多路复用,如多个请求使用同一个信道并行的传输,实际上是传输层提供的多路复用能力** 。**传输层的多路复用,比如多个 TCP 连接复用一条线路,实际上是网络层在提供多路复用能力** 。你可以把网络层想象成一个不断收发包裹的机器,在网络层中并没有连接这个概念,所以网络层天然就是支持多路复用的。
 
 #### 多路复用的意义
 
 在工作当中,我们经常会使用到多路复用的能力。多路复用让多个信号(例如:请求/返回等)共用一个信道(例如:一个 TCP 连接),那么在这个信道上,信息密度就会增加。在密度增加的同时,通过并行发送信号的方式,可以减少阻塞。比如说应用层的 HTTP 协议,浏览器打开的时候就会往服务器发送很多个请求,多个请求混合在一起,复用相同连接,数据紧密且互相隔离(不互相阻塞)。同理,服务之间的远程调用、消息队列,这些也经常需要多路复用。
 
-### 总结 **那么通过这一讲的学习,你现在可以尝试来回答本节关联的面试题目:多路复用是怎么回事** ?
+### 总结 **那么通过这一讲的学习,你现在可以尝试来回答本节关联的面试题目:多路复用是怎么回事**?
 
 【 **解析** 】多路复用让多个信号(例如:请求/返回等)共用一个信道(例如:一个 TCP 连接)。它有两个明显的优势。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25434\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25434\350\256\262.md"
index 41076ed1b..d0fca0dd2 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25434\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25434\350\256\262.md"
@@ -18,9 +18,9 @@ TCP 和 UDP 是目前使用最广泛的两个传输层协议,同时也是面
 
 #### 校验和(Checksum)
 
-首先我们来说说 **校验和** 。 **这是一种非常常见的可靠性检查手段** 。
+首先我们来说说 **校验和** 。**这是一种非常常见的可靠性检查手段** 。
 
-尽管 UDP 不支持可靠性,但是像校验和(Checksum)这一类最基本的数据校验,它还是支持的。 **不支持可靠性,并不意味着完全放弃可靠性。TCP 和 UDP 都支持最基本的校验和算法** 。
+尽管 UDP 不支持可靠性,但是像校验和(Checksum)这一类最基本的数据校验,它还是支持的。**不支持可靠性,并不意味着完全放弃可靠性。TCP 和 UDP 都支持最基本的校验和算法** 。
 
 下面我为你举例 **一种最简单的校验和算法:纵向冗余检查** 。伪代码如下:
 
@@ -35,7 +35,7 @@ for(byte x in bytes) {
 
 ![Lark20210113-153833.png](assets/CgpVE1_-o5GADgRkAABcTgxXiyw544.png)
 
-当要传输数据的时候,数据会被分片,我们把每个分片看作一个字节数组。然后在分片中,预留几个字节去存储校验和。校验和随着数据分片一起传输到目的地,目的地会用同样的算法再次计算校验和。如果二者校验和不一致,代表中途数据发生了损坏。 **对于 TCP 和 UDP,都实现了校验和算法,但二者的区别是,TCP 如果发现校验核对不上,也就是数据损坏,会主动丢失这个封包并且重发。而 UDP 什么都不会处理,UDP 把处理的权利交给使用它的程序员** 。
+当要传输数据的时候,数据会被分片,我们把每个分片看作一个字节数组。然后在分片中,预留几个字节去存储校验和。校验和随着数据分片一起传输到目的地,目的地会用同样的算法再次计算校验和。如果二者校验和不一致,代表中途数据发生了损坏。**对于 TCP 和 UDP,都实现了校验和算法,但二者的区别是,TCP 如果发现校验核对不上,也就是数据损坏,会主动丢失这个封包并且重发。而 UDP 什么都不会处理,UDP 把处理的权利交给使用它的程序员** 。
 
 #### 请求/应答/连接模型
 
@@ -45,7 +45,7 @@ for(byte x in bytes) {
 
 在 TCP 协议当中,任何一方向另一方发送信息,另一方都需要给予一个应答。如果发送方在一定的时间内没有获得应答,发送方就会认为自己的信息没有到达目的地,中途发生了损坏或者丢失等,因此发送方会选择重发这条消息。
 
-这样一个模式也造成了 TCP 协议的三次握手和四次挥手,下面我们一起来具体分析一下。 **1. TCP 的三次握手** 在 TCP 协议当中。我们假设 Alice 和 Bob 是两个通信进程。当 Alice 想要和 Bob 建立连接的时候,Alice 需要发送一个请求建立连接的消息给 Bob。这种请求建立连接的消息在 TCP 协议中称为 **同步** ( **Synchronization, SYN** )。而 Bob 收到 SYN,必须马上给 Alice 一个响应。这个响应在 TCP 协议当中称为 **响应** ( **Acknowledgement,ACK** )。请你务必记住这两个单词。不仅是 TCP 在用,其他协议也会复用这样的概念,来描述相同的事情。
+这样一个模式也造成了 TCP 协议的三次握手和四次挥手,下面我们一起来具体分析一下。**1. TCP 的三次握手** 在 TCP 协议当中。我们假设 Alice 和 Bob 是两个通信进程。当 Alice 想要和 Bob 建立连接的时候,Alice 需要发送一个请求建立连接的消息给 Bob。这种请求建立连接的消息在 TCP 协议中称为 **同步** ( **Synchronization, SYN** )。而 Bob 收到 SYN,必须马上给 Alice 一个响应。这个响应在 TCP 协议当中称为 **响应** ( **Acknowledgement,ACK** )。请你务必记住这两个单词。不仅是 TCP 在用,其他协议也会复用这样的概念,来描述相同的事情。
 
 当 Alice 给 Bob SYN,Bob 给 Alice ACK,这个时候,对 Alice 而言,连接就建立成功了。但是 TCP 是一个双工协议。所谓双工协议,代表数据可以双向传送。虽然对 Alice 而言,连接建立成功了。但是对 Bob 而言,连接还没有建立。为什么这么说呢?你可以这样思考,如果这个时候,Bob 马上给 Alice 发送信息,信息可能先于 Bob 的 ACK 到达 Alice,但这个时候 Alice 还不知道连接建立成功。 所以解决的办法就是 Bob 再给 Alice 发一次 SYN ,Alice 再给 Bob 一个 ACK。以上就是 TCP 的三次握手内容。
 
@@ -64,15 +64,15 @@ for(byte x in bytes) {
 
 ![Lark20210113-153824.png](assets/Ciqc1F_-o7uASykoAAD1Eo7HnP4749.png)
 
-**3. 连接** 连接是一个虚拟概念,连接的目的是让连接的双方达成默契,倾尽资源,给对方最快的响应。经历了三次握手,Alice 和 Bob 之间就建立了连接。 **连接也是一个很好的编程模型。当连接不稳定的时候,可以中断连接后再重新连接。这种模式极大地增加了两个应用之间的数据传输的可靠性** 。
+**3. 连接** 连接是一个虚拟概念,连接的目的是让连接的双方达成默契,倾尽资源,给对方最快的响应。经历了三次握手,Alice 和 Bob 之间就建立了连接。**连接也是一个很好的编程模型。当连接不稳定的时候,可以中断连接后再重新连接。这种模式极大地增加了两个应用之间的数据传输的可靠性** 。
 
 以上就是 TCP 中存在的,而 UDP 中没有的机制,你可以仔细琢磨琢磨。
 
 #### 封包排序 **可靠性有一个最基本的要求是数据有序发出、无序传输,并且有序组合。TCP 协议保证了这种可靠性,UDP 则没有保证**
 
-在传输之前,数据被拆分成分块。在 TCP 中叫作一个 **TCP Segment** 。在 UDP 中叫作一个 **UDP Datagram** 。Datagram 单词的含义是数据传输的最小单位。在到达目的地之后,尽管所有的数据分块可能是乱序到达的,但为了保证可靠性,乱序到达的数据又需要被重新排序,恢复到原有数据的顺序。 **在这个过程当中,TCP 利用了滑动窗口、快速重传等算法,保证了数据的顺序。而 UDP,仅仅是为每个 Datagram 标注了序号,并没有帮助应用程序进行数据的排序** , **这也是 TCP 和 UDP 在保证可靠性上一个非常重要的区别。** ### 使用场景
+在传输之前,数据被拆分成分块。在 TCP 中叫作一个 **TCP Segment** 。在 UDP 中叫作一个 **UDP Datagram** 。Datagram 单词的含义是数据传输的最小单位。在到达目的地之后,尽管所有的数据分块可能是乱序到达的,但为了保证可靠性,乱序到达的数据又需要被重新排序,恢复到原有数据的顺序。**在这个过程当中,TCP 利用了滑动窗口、快速重传等算法,保证了数据的顺序。而 UDP,仅仅是为每个 Datagram 标注了序号,并没有帮助应用程序进行数据的排序**,**这也是 TCP 和 UDP 在保证可靠性上一个非常重要的区别。** ### 使用场景
 
-上面的内容中,我们比较了 TCP 和 UDP 在可靠性上的区别,接下来我们看看两个协议的使用场景。 **我们先来看一道面试题:如果客户端和服务器之间的单程平均延迟是 30 毫秒,那么客户端 Ping 服务端需要多少毫秒** ?
+上面的内容中,我们比较了 TCP 和 UDP 在可靠性上的区别,接下来我们看看两个协议的使用场景。**我们先来看一道面试题:如果客户端和服务器之间的单程平均延迟是 30 毫秒,那么客户端 Ping 服务端需要多少毫秒**?
 
 【 **分析** 】这个问题最核心的点是需要思考 Ping 服务应该由 TCP 实现还是 UDP 实现?请你思考:Ping 需不需要保持连接呢?答案是不需要,Ping 服务器的时候把数据发送过去即可,并不需要特地建立一个连接。
 
@@ -80,16 +80,16 @@ for(byte x in bytes) {
 
 所以这道面试题应该是 Round Trip 最快需要在 60 毫秒左右。一个来回的时间,我们也通常称为 Round Trip 时间。
 
-通过分析上面的例子,我想告诉你,TCP 和 UDP 的使用场景是不同的。 **TCP 适用于需要可靠性,需要连接的场景** 。UDP 因为足够简单,只对数据进行简单加工处理,就调用底层的网络层(IP 协议)传输数据去了。 **因此 UDP 更适合对可靠性要求不高的场景** 。
+通过分析上面的例子,我想告诉你,TCP 和 UDP 的使用场景是不同的。**TCP 适用于需要可靠性,需要连接的场景** 。UDP 因为足够简单,只对数据进行简单加工处理,就调用底层的网络层(IP 协议)传输数据去了。**因此 UDP 更适合对可靠性要求不高的场景** 。
 
-另外很多需要定制化的场景,非常需要 UDP。以 HTTP 协议为例,在早期的 HTTP 协议的设计当中就选择了 TCP 协议。因为在 HTTP 的设计当中,请求和返回都是需要可靠性的。但是随着 HTTP 协议的发展,到了 HTTP 3.0 的时候,就开始基于 UDP 进行传输。这是因为,在 HTTP 3.0 协议当中,在 UDP 之上有另一个QUIC 协议在负责可靠性。UDP 足够简单,在其上构建自己的协议就很方便。 **你可以再思考一个问题:文件上传应该用 TCP 还是 UDP 呢** ?乍一看肯定是 TCP 协议,因为文件上传当然需要可靠性,防止数据损坏。但是如果你愿意在 UDP 上去实现一套专门上传文件的可靠性协议,性能是可以超越 TCP 协议的。因为你只需要解决文件上传一种需求,不用像 TCP 协议那样解决通用需求。
+另外很多需要定制化的场景,非常需要 UDP。以 HTTP 协议为例,在早期的 HTTP 协议的设计当中就选择了 TCP 协议。因为在 HTTP 的设计当中,请求和返回都是需要可靠性的。但是随着 HTTP 协议的发展,到了 HTTP 3.0 的时候,就开始基于 UDP 进行传输。这是因为,在 HTTP 3.0 协议当中,在 UDP 之上有另一个QUIC 协议在负责可靠性。UDP 足够简单,在其上构建自己的协议就很方便。**你可以再思考一个问题:文件上传应该用 TCP 还是 UDP 呢**?乍一看肯定是 TCP 协议,因为文件上传当然需要可靠性,防止数据损坏。但是如果你愿意在 UDP 上去实现一套专门上传文件的可靠性协议,性能是可以超越 TCP 协议的。因为你只需要解决文件上传一种需求,不用像 TCP 协议那样解决通用需求。
 
-所以时至今日,到底什么情况应该用 TCP,什么情况用 UDP?这个问题边界的确在模糊化。 **总体来说,需要可靠性,且不希望花太多心思在网络协议的研发上,就使用 TCP 协议** 。
+所以时至今日,到底什么情况应该用 TCP,什么情况用 UDP?这个问题边界的确在模糊化。**总体来说,需要可靠性,且不希望花太多心思在网络协议的研发上,就使用 TCP 协议** 。
 
 ### 总结
 
 最后我们再来总结一下,大而全的协议用起来舒服,比如 TCP;灵活的协议方便定制和扩展,比如 UDP。二者不分伯仲,各有千秋。
 
-这一讲我们深入比较了 TCP 和 UDP 的可靠性及它们的使用场景。关于原理部分,比如具体 TCP 的滑动窗口算法、数据的切割算法、数据重传算法;TCP、UDP 的封包内部究竟有哪些字段,格式如何等。如果你感兴趣,可以来学习我将在拉勾教育推出的《 **计算机网络** 》专栏。 **那么通过这一讲的学习,你现在可以尝试来回答本讲关联的面试题目:UDP 比 TCP 快在哪里** ?
+这一讲我们深入比较了 TCP 和 UDP 的可靠性及它们的使用场景。关于原理部分,比如具体 TCP 的滑动窗口算法、数据的切割算法、数据重传算法;TCP、UDP 的封包内部究竟有哪些字段,格式如何等。如果你感兴趣,可以来学习我将在拉勾教育推出的《 **计算机网络** 》专栏。**那么通过这一讲的学习,你现在可以尝试来回答本讲关联的面试题目:UDP 比 TCP 快在哪里**?
 
 【 **解析** 】使用 UDP 传输数据,不用建立连接,数据直接丢过去即可。至于接收方,有没有在监听?会不会接收?那就是接收方的事情了。UDP 甚至不考虑数据的可靠性。至于发送双方会不会基于 UDP 再去定制研发可靠性协议,那就是开发者的事情了。所以 UDP 快在哪里?UDP 快在它足够简单。因为足够简单,所以 UDP 对计算性能、对网络占用都是比 TCP 少的。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25435\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25435\350\256\262.md"
index f483f75b2..70818d84e 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25435\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25435\350\256\262.md"
@@ -2,13 +2,13 @@
 
 我们总是想方设法地提升系统的性能。操作系统层面不能给予处理业务逻辑太多帮助,但对于 I/O 性能,操作系统可以通过底层的优化,帮助应用做到极致。
 
-这一讲我将和你一起讨论 I/O 模型。为了引发你更多的思考,我将同步/异步、阻塞/非阻塞等概念滞后讲解。 **我们先回到一个最基本的问题:如果有一台服务器,需要响应大量的请求,操作系统如何去架构以适应这样高并发的诉求** 。
+这一讲我将和你一起讨论 I/O 模型。为了引发你更多的思考,我将同步/异步、阻塞/非阻塞等概念滞后讲解。**我们先回到一个最基本的问题:如果有一台服务器,需要响应大量的请求,操作系统如何去架构以适应这样高并发的诉求** 。
 
 说到架构,就离不开操作系统提供给应用程序的系统调用。我们今天要介绍的 select/poll/epoll 刚好是操作系统提供给应用的三类处理 I/O 的系统调用。这三类系统调用有非常强的代表性,这一讲我会围绕它们,以及处理并发和 I/O 多路复用,为你讲解操作系统的 I/O 模型。
 
 ### 从网卡到操作系统
 
-为了弄清楚高并发网络场景是如何处理的,我们先来看一个最基本的内容: **当数据到达网卡之后,操作系统会做哪些事情** ?
+为了弄清楚高并发网络场景是如何处理的,我们先来看一个最基本的内容: **当数据到达网卡之后,操作系统会做哪些事情**?
 
 网络数据到达网卡之后,首先需要把数据拷贝到内存。拷贝到内存的工作往往不需要消耗 CPU 资源,而是通过 DMA 模块直接进行内存映射。之所以这样做,是因为网卡没有大量的内存空间,只能做简单的缓冲,所以必须赶紧将它们保存下来。
 
@@ -16,7 +16,7 @@ Linux 中用一个双向链表作为缓冲区,你可以观察下图中的 Buff
 
 ![1111.png](assets/Cip5yGABb8uAECMGAAERrnFoSrI090.png)!
 
-如果高并发的请求量级实在太大,有可能把 Buffer 占满,此时,操作系统就会拒绝服务。网络上有一种著名的攻击叫作 **拒绝服务攻击** ,就是利用的这个原理。 **操作系统拒绝服务,实际上是一种保护策略。通过拒绝服务,避免系统内部应用因为并发量太大而雪崩** 。
+如果高并发的请求量级实在太大,有可能把 Buffer 占满,此时,操作系统就会拒绝服务。网络上有一种著名的攻击叫作 **拒绝服务攻击**,就是利用的这个原理。**操作系统拒绝服务,实际上是一种保护策略。通过拒绝服务,避免系统内部应用因为并发量太大而雪崩** 。
 
 如上图所示,传入网卡的数据被我称为 Frames。一个 Frame 是数据链路层的传输单位(或封包)。现代的网卡通常使用 DMA 技术,将 Frame 写入缓冲区(Buffer),然后在触发 CPU 中断交给操作系统处理。操作系统从缓冲区中不断取出 Frame,通过协进栈(具体的协议)进行还原。
 
@@ -32,7 +32,7 @@ Linux 中用一个双向链表作为缓冲区,你可以观察下图中的 Buff
 
 ![Lark20210115-150702.png](assets/Ciqc1GABP3OAHezqAABndlGAu9c457.png)
 
-如上图所示,Socket 连接了应用和协议,如果应用层的程序想要传输数据,就创建一个 Socket。应用向 Socket 中写入数据,相当于将数据发送给了另一个应用。应用从 Socket 中读取数据,相当于接收另一个应用发送的数据。而具体的操作就是由 Socket 进行封装。具体来说, **对于 UNIX 系的操作系统,是利用 Socket 文件系统,Socket 是一种特殊的文件——每个都是一个双向的管道。一端是应用,一端是缓冲** 区。
+如上图所示,Socket 连接了应用和协议,如果应用层的程序想要传输数据,就创建一个 Socket。应用向 Socket 中写入数据,相当于将数据发送给了另一个应用。应用从 Socket 中读取数据,相当于接收另一个应用发送的数据。而具体的操作就是由 Socket 进行封装。具体来说,**对于 UNIX 系的操作系统,是利用 Socket 文件系统,Socket 是一种特殊的文件——每个都是一个双向的管道。一端是应用,一端是缓冲** 区。
 
 那么作为一个服务端的应用,如何知道有哪些 Socket 呢?也就是,哪些客户端连接过来了呢?这是就需要一种特殊类型的 Socket,也就是服务端 Socket 文件。
 
@@ -48,11 +48,11 @@ Linux 中用一个双向链表作为缓冲区,你可以观察下图中的 Buff
 
 ### I/O 多路复用
 
-在上面的讨论当中,进程拿到了它关注的所有 Socket,也称作关注的集合(Intersting Set)。如下图所示,这种过程相当于进程从所有的 Socket 中,筛选出了自己关注的一个子集,但是这时还有一个问题没有解决: **进程如何监听关注集合的状态变化,比如说在有数据进来,如何通知到这个进程** ?
+在上面的讨论当中,进程拿到了它关注的所有 Socket,也称作关注的集合(Intersting Set)。如下图所示,这种过程相当于进程从所有的 Socket 中,筛选出了自己关注的一个子集,但是这时还有一个问题没有解决: **进程如何监听关注集合的状态变化,比如说在有数据进来,如何通知到这个进程**?
 
 ![Lark20210115-150708.png](assets/Ciqc1GABP4OAdKBcAACAbVkbI0g191.png)
 
-其实更准确地说,一个线程需要处理所有关注的 Socket 产生的变化,或者说消息。实际上一个线程要处理很多个文件的 I/O。 **所有关注的 Socket 状态发生了变化,都由一个线程去处理,构成了 I/O 的多路复用问题** 。如下图所示:
+其实更准确地说,一个线程需要处理所有关注的 Socket 产生的变化,或者说消息。实际上一个线程要处理很多个文件的 I/O。**所有关注的 Socket 状态发生了变化,都由一个线程去处理,构成了 I/O 的多路复用问题** 。如下图所示:
 
 ![Lark20210115-150711.png](assets/Ciqc1GABP4uAW8-dAAB_SubmZ4Q301.png)
 
@@ -66,9 +66,9 @@ Linux 中用一个双向链表作为缓冲区,你可以观察下图中的 Buff
 
 ![Lark20210115-150654.png](assets/Ciqc1GABP5KAVSWVAAFSurtl2bU931.png)
 
-**一个 Socket 文件,可以由多个进程使用;而一个进程,也可以使用多个 Socket 文件** 。进程和 Socket 之间是多对多的关系。 **另一方面,一个 Socket 也会有不同的事件类型** 。因此操作系统很难判断,将哪样的事件给哪个进程。
+**一个 Socket 文件,可以由多个进程使用;而一个进程,也可以使用多个 Socket 文件** 。进程和 Socket 之间是多对多的关系。**另一方面,一个 Socket 也会有不同的事件类型** 。因此操作系统很难判断,将哪样的事件给哪个进程。
 
-这样 **在进程内部就需要一个数据结构来描述自己会关注哪些 Socket 文件的哪些事件(读、写、异常等** )。通常有两种考虑方向, **一种是利用线性结构** ,比如说数组、链表等,这类结构的查询需要遍历。每次内核产生一种消息,就遍历这个线性结构。看看这个消息是不是进程关注的? **另一种是索引结构** ,内核发生了消息可以通过索引结构马上知道这个消息进程关不关注。
+这样 **在进程内部就需要一个数据结构来描述自己会关注哪些 Socket 文件的哪些事件(读、写、异常等** )。通常有两种考虑方向,**一种是利用线性结构**,比如说数组、链表等,这类结构的查询需要遍历。每次内核产生一种消息,就遍历这个线性结构。看看这个消息是不是进程关注的?**另一种是索引结构**,内核发生了消息可以通过索引结构马上知道这个消息进程关不关注。
 
 #### select()
 
@@ -295,7 +295,7 @@ while (1)
 ```sql
 #### poll()
 
-从写程序的角度来看,select 并不是一个很好的编程模型。一个好的编程模型应该直达本质,当网络请求发生状态变化的时候,核心是会发生事件。 **一个好的编程模型应该是直接抽象成消息:用户不需要用 select 来设置自己的集合,而是可以通过系统的 API 直接拿到对应的消息,从而处理对应的文件描述符** 。
+从写程序的角度来看,select 并不是一个很好的编程模型。一个好的编程模型应该直达本质,当网络请求发生状态变化的时候,核心是会发生事件。**一个好的编程模型应该是直接抽象成消息:用户不需要用 select 来设置自己的集合,而是可以通过系统的 API 直接拿到对应的消息,从而处理对应的文件描述符** 。
 
 比如下面这段伪代码就是一个更好的编程模型,具体的分析如下:
 
@@ -338,13 +338,13 @@ poll 虽然优化了编程模型,但是从性能角度分析,它和 select 
 
 #### epoll
 
-为了解决上述问题, **epoll 通过更好的方案实现了从操作系统订阅消息。epoll 将进程关注的文件描述符存入一棵二叉搜索树,通常是红黑树的实现** 。在这棵红黑树当中,Key 是 Socket 的编号,值是这个 Socket 关注的消息。因此,当内核发生了一个事件:比如 Socket 编号 1000 可以读取。这个时候,可以马上从红黑树中找到进程是否关注这个事件。
+为了解决上述问题,**epoll 通过更好的方案实现了从操作系统订阅消息。epoll 将进程关注的文件描述符存入一棵二叉搜索树,通常是红黑树的实现** 。在这棵红黑树当中,Key 是 Socket 的编号,值是这个 Socket 关注的消息。因此,当内核发生了一个事件:比如 Socket 编号 1000 可以读取。这个时候,可以马上从红黑树中找到进程是否关注这个事件。
 
-**另外当有关注的事件发生时,epoll 会先放到一个队列当中。当用户调用** `epoll_wait`时候,就会从队列中返回一个消息。epoll 函数本身是一个构造函数,只用来创建红黑树和队列结构。`epoll_wait`调用后,如果队列中没有消息,也可以马上返回。因此`epoll`是一个非阻塞模型。 **总结一下,select/poll 是阻塞模型,epoll 是非阻塞模型** 。 **当然,并不是说非阻塞模型性能就更好。在多数情况下,epoll 性能更好是因为内部有红黑树的实现** 。
+**另外当有关注的事件发生时,epoll 会先放到一个队列当中。当用户调用** `epoll_wait`时候,就会从队列中返回一个消息。epoll 函数本身是一个构造函数,只用来创建红黑树和队列结构。`epoll_wait`调用后,如果队列中没有消息,也可以马上返回。因此`epoll`是一个非阻塞模型。**总结一下,select/poll 是阻塞模型,epoll 是非阻塞模型** 。**当然,并不是说非阻塞模型性能就更好。在多数情况下,epoll 性能更好是因为内部有红黑树的实现** 。
 
 最后我再贴一段用 epoll 实现的 Socket 服务给你做参考,这段程序的作者将这段代码放到了 Public Domain,你以后看到公有领域的代码可以放心地使用。
 
-下面这段程序跟之前 select 的原理一致,对于每一个新的客户端连接,都使用 accept 拿到这个连接的文件描述符,并且创建一个客户端的 Socket。然后通过`epoll_ctl`将客户端的文件描述符和关注的消息类型放入 epoll 的红黑树。操作系统每次监测到一个新的消息产生,就会通过红黑树对比这个消息是不是进程关注的(当然这段代码你看不到,因为它在内核程序中)。 **非阻塞模型的核心价值,并不是性能更好。当真的高并发来临的时候,所有的 CPU 资源,所有的网络资源可能都会被用完。这个时候无论是阻塞还是非阻塞,结果都不会相差太大** 。(前提是程序没有写错)。
+下面这段程序跟之前 select 的原理一致,对于每一个新的客户端连接,都使用 accept 拿到这个连接的文件描述符,并且创建一个客户端的 Socket。然后通过`epoll_ctl`将客户端的文件描述符和关注的消息类型放入 epoll 的红黑树。操作系统每次监测到一个新的消息产生,就会通过红黑树对比这个消息是不是进程关注的(当然这段代码你看不到,因为它在内核程序中)。**非阻塞模型的核心价值,并不是性能更好。当真的高并发来临的时候,所有的 CPU 资源,所有的网络资源可能都会被用完。这个时候无论是阻塞还是非阻塞,结果都不会相差太大** 。(前提是程序没有写错)。
 
 `epoll`有 2 个最大的优势:
 
@@ -795,15 +795,15 @@ return 0;
 ```plaintext
 ### 重新思考:I/O 模型
 
-在上面的模型当中,select/poll 是阻塞(Blocking)模型,epoll 是非阻塞(Non-Blocking)模型。 **阻塞和非阻塞强调的是线程的状态** ,所以阻塞就是触发了线程的阻塞状态,线程阻塞了就停止执行,并且切换到其他线程去执行,直到触发中断再回来。
+在上面的模型当中,select/poll 是阻塞(Blocking)模型,epoll 是非阻塞(Non-Blocking)模型。**阻塞和非阻塞强调的是线程的状态**,所以阻塞就是触发了线程的阻塞状态,线程阻塞了就停止执行,并且切换到其他线程去执行,直到触发中断再回来。
 
 还有一组概念是同步(Synchrounous)和异步(Asynchrounous),select/poll/epoll 三者都是同步调用。
 
 **同步强调的是顺序,** 所谓同步调用,就是可以确定程序执行的顺序的调用。比如说执行一个调用,知道调用返回之前下一行代码不会执行。这种顺序是确定的情况,就是同步。
 
-而异步调用则恰恰相反, **异步调用不明确执行顺序** 。比如说一个回调函数,不知道何时会回来。异步调用会加大程序员的负担,因为我们习惯顺序地思考程序。因此,我们还会发明像协程的 yield 、迭代器等将异步程序转为同步程序。
+而异步调用则恰恰相反,**异步调用不明确执行顺序** 。比如说一个回调函数,不知道何时会回来。异步调用会加大程序员的负担,因为我们习惯顺序地思考程序。因此,我们还会发明像协程的 yield 、迭代器等将异步程序转为同步程序。
 
-由此可见, **非阻塞不一定是异步,阻塞也未必就是同步** 。比如一个带有回调函数的方法,阻塞了线程 100 毫秒,又提供了回调函数,那这个方法是异步阻塞。例如下面的伪代码:
+由此可见,**非阻塞不一定是异步,阻塞也未必就是同步** 。比如一个带有回调函数的方法,阻塞了线程 100 毫秒,又提供了回调函数,那这个方法是异步阻塞。例如下面的伪代码:
 ```
 
 asleep(100ms, () -> {
@@ -817,9 +817,9 @@ asleep(100ms, () -> {
 
 总结下,操作系统给大家提供各种各样的 API,是希望满足各种各样程序架构的诉求。但总体诉求其实是一致的:希望程序员写的单机代码,能够在多线程甚至分布式的环境下执行。这样你就不需要再去学习复杂的并发控制算法。从这个角度去看,非阻塞加上同步的编程模型确实省去了我们编程过程当中的很多思考。
 
-但可惜的是,至少在今天这个时代, **多线程、并发编程依然是程序员们的必修课** 。因此你在思考 I/O 模型的时候,还是需要结合自己的业务特性及系统自身的架构特点,进行选择。 **I/O 模型并不是选择效率,而是选择编程的手段** 。试想一个所有资源都跑满了的服务器,并不会因为是异步或者非阻塞模型就获得更高的吞吐量。
+但可惜的是,至少在今天这个时代,**多线程、并发编程依然是程序员们的必修课** 。因此你在思考 I/O 模型的时候,还是需要结合自己的业务特性及系统自身的架构特点,进行选择。**I/O 模型并不是选择效率,而是选择编程的手段** 。试想一个所有资源都跑满了的服务器,并不会因为是异步或者非阻塞模型就获得更高的吞吐量。
 
-**那么通过以上的学习,你现在可以尝试来回答本讲关联的面试题目:select/poll/epoll 有什么区别** ?
+**那么通过以上的学习,你现在可以尝试来回答本讲关联的面试题目:select/poll/epoll 有什么区别**?
 
 【 **解析** 】这三者都是处理 I/O 多路复用的编程手段。select/poll 模型是一种阻塞模型,epoll 是非阻塞模型。select/poll 内部使用线性结构存储进程关注的 Socket 集合,因此每次内核要判断某个消息是否发送给 select/poll 需要遍历进程关注的 Socket 集合。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25436\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25436\350\256\262.md"
index 1c905c1a3..fee424e8e 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25436\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25436\350\256\262.md"
@@ -24,7 +24,7 @@
 
 采用摘要算法,从理论上来说就杜绝了篡改合同的内容的做法。但在现实当中,公司也有可能出现内鬼。我们不能假定所有公司内部员工的行为就是安全的。因此可以考虑将合同和摘要分开存储,并且设置不同的权限。这样就确保在机构内部,没有任何一名员工同时拥有合同和摘要的权限。但是即便如此,依然留下了巨大的安全隐患。比如两名员工串通一气,或者员工利用安全漏洞,和外部的不法分子进行非法交易。
 
-那么现在请你思考这个问题: **如何确保公司内部的员工不会篡改合同呢** ?当然从理论上来说是做不到的。没有哪个系统能够杜绝内部人员接触敏感信息,除非敏感信息本身就不存在。因此,可以考虑将原文存到合同双方的手中,第三方机构中只存摘要。但是这又产生了一个新的问题,会不会有第三方机构的员工和某个用户串通一气修改合同呢?
+那么现在请你思考这个问题: **如何确保公司内部的员工不会篡改合同呢**?当然从理论上来说是做不到的。没有哪个系统能够杜绝内部人员接触敏感信息,除非敏感信息本身就不存在。因此,可以考虑将原文存到合同双方的手中,第三方机构中只存摘要。但是这又产生了一个新的问题,会不会有第三方机构的员工和某个用户串通一气修改合同呢?
 
 至此,事情似乎陷入了僵局。由第三方平台保存合同,背后同样有很大的风险。而由用户自己保存合同,就是签约双方交换合同原文及摘要。但是这样的形式中,摘要本身是没有公信力的,无法证明合同和摘要确实是对方给的。
 
@@ -40,13 +40,13 @@
 
 Bob 如果想证明合同是 Alice 的,就要用 Alice 的公钥,将签名解密得到摘要 X。然后,Bob 计算原文的 SHA 摘要 Y。Bob 对比 X 和 Y,如果 X = Y 则说明数据没有被篡改过。
 
-在这样的一个过程当中,Bob 不能篡改 Alice 合同。因为篡改合同不但要改原文还要改摘要,而摘要被加密了,如果要重新计算摘要,就必须提供 Alice 的私钥。所谓私钥,就是 Alice 独有的密码。所谓公钥,就是 Alice 公布给他人使用的密码。 **公钥加密的数据,只有私钥才可以解密。私钥加密的数据,只有公钥才可以解密** 。这样的加密方法我们称为 **非对称加密** ,基于非对称加密算法建立的安全体系,也被称作 **公私钥体系** 。用这样的方法,签约双方都不可以篡改合同。
+在这样的一个过程当中,Bob 不能篡改 Alice 合同。因为篡改合同不但要改原文还要改摘要,而摘要被加密了,如果要重新计算摘要,就必须提供 Alice 的私钥。所谓私钥,就是 Alice 独有的密码。所谓公钥,就是 Alice 公布给他人使用的密码。**公钥加密的数据,只有私钥才可以解密。私钥加密的数据,只有公钥才可以解密** 。这样的加密方法我们称为 **非对称加密**,基于非对称加密算法建立的安全体系,也被称作 **公私钥体系** 。用这样的方法,签约双方都不可以篡改合同。
 
 ### 证书
 
 但是在上面描述的过程当中,仍然存在着一个非常明显的信任风险。这个风险在于,Alice 虽然不能篡改合同,但是可以否认给过 Bob 的公钥和合同。这样,尽管合同双方都不可以篡改合同本身,但是双方可以否认签约行为本身。
 
-如果要解决这个问题,那么 Alice 提供的公钥,必须有足够的信誉。这就需要引入第三方机构和证书机制。 **证书为公钥提供方提供公正机制** 。证书之所以拥有信用,是因为证书的签发方拥有信用。假设 Alice 想让 Bob 承认自己的公钥。Alice 不能把公钥直接给 Bob,而是要提供第三方公证机构签发的、含有自己公钥的证书。如果 Bob 也信任这个第三方公证机构,信任关系和签约就成立。当然,法律也得承认,不然没法打官司。
+如果要解决这个问题,那么 Alice 提供的公钥,必须有足够的信誉。这就需要引入第三方机构和证书机制。**证书为公钥提供方提供公正机制** 。证书之所以拥有信用,是因为证书的签发方拥有信用。假设 Alice 想让 Bob 承认自己的公钥。Alice 不能把公钥直接给 Bob,而是要提供第三方公证机构签发的、含有自己公钥的证书。如果 Bob 也信任这个第三方公证机构,信任关系和签约就成立。当然,法律也得承认,不然没法打官司。
 
 ![Lark20210120-162728.png](assets/CgpVE2AH6j6ASBKvAADJu5B4-Bc773.png)
 
@@ -82,11 +82,11 @@ Bob 如果想证明合同是 Alice 的,就要用 Alice 的公钥,将签名
 
 ### 总结
 
-总结一下,在信任的基础上才能产生合作。有了合作才能让整个互联网的世界有序运转,信任是整个互联网世界的基石。 **在互联网中解决信任问题不仅需要数学和算法,还需要一个信任链条** 。有人提供信用,比如证书机构;有人消费信用,比如网络服务的提供者。
+总结一下,在信任的基础上才能产生合作。有了合作才能让整个互联网的世界有序运转,信任是整个互联网世界的基石。**在互联网中解决信任问题不仅需要数学和算法,还需要一个信任链条** 。有人提供信用,比如证书机构;有人消费信用,比如网络服务的提供者。
 
 这一讲我试图带你理解“ **如何构造一个拥有信誉的互联网世界** ”,但是还有很多的细节,比如说有哪些加密解密算法?HTTPS 协议具体的工作原理、架构等。这些更具体的内容,我会在拉勾教育即将推出的《 **计算机网络** 》专栏中和你继续深入讨论。
 
-**那么通过这一讲的学习,你现在可以尝试来回答本节关联的面试题目:什么是中间人攻击** ?
+**那么通过这一讲的学习,你现在可以尝试来回答本节关联的面试题目:什么是中间人攻击**?
 
 【 **解析** 】中间人攻击中,一方面,黑客利用不法手段,让客户端相信自己是服务提供方。另一方面,黑客伪装成客户端和服务器交互。这样黑客就介入了客户端和服务之间的连接,并从中获取信息,从而获利。在上述过程当中,黑客必须攻破信任链的体系,比如直接潜入对方机房现场暴力破解、诱骗对方员工在工作电脑中安装非法的证书等。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25437\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25437\350\256\262.md"
index a728d2206..be3124e06 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25437\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25437\350\256\262.md"
@@ -14,11 +14,11 @@
 
 顾名思义,虚拟是相对于现实而言。虚拟化(Virutualization)通常是指构造真实的虚拟版本。不严谨地说,用软件模拟计算机,就是虚拟机;用数字模拟价值,就是货币;用存储空间模拟物理存储,就是虚拟磁盘。
 
-VMware 和 Docker 是目前虚拟化技术中最具代表性的两种设计。 **VMware 为应用提供虚拟的计算机** ( **虚拟机** ); **Docker 为应用提供虚拟的空间,被称作容器** ( **Containe** r),关于空间的含义,我们会在下文中详细讨论。 **VMware** 在 1998 年诞生,通过 Hypervisor 的设计彻底改变了虚拟化技术。2005 年,VMware 不断壮大,在全球雇用了 1000 名员工, **成为世界上最大的云基础架构提供商** 。 **Docker** 则是 2013 年发布的一个社区产品,后来逐渐在程序员群体中流行了起来。大量程序员开始习惯使用 Docker,所以各大公司才决定使用它。在“ **38 讲** ”中我们要介绍的 Kubernates(K8s)容器编排系统,一开始也是将 Docker 作为主要容器。虽然业内不时有传出二者即将分道扬镳的消息,但是目前(2021 年)K8s 下的容器主要还是 Docker。
+VMware 和 Docker 是目前虚拟化技术中最具代表性的两种设计。**VMware 为应用提供虚拟的计算机** ( **虚拟机** ); **Docker 为应用提供虚拟的空间,被称作容器** ( **Containe** r),关于空间的含义,我们会在下文中详细讨论。**VMware** 在 1998 年诞生,通过 Hypervisor 的设计彻底改变了虚拟化技术。2005 年,VMware 不断壮大,在全球雇用了 1000 名员工,**成为世界上最大的云基础架构提供商** 。**Docker** 则是 2013 年发布的一个社区产品,后来逐渐在程序员群体中流行了起来。大量程序员开始习惯使用 Docker,所以各大公司才决定使用它。在“ **38 讲** ”中我们要介绍的 Kubernates(K8s)容器编排系统,一开始也是将 Docker 作为主要容器。虽然业内不时有传出二者即将分道扬镳的消息,但是目前(2021 年)K8s 下的容器主要还是 Docker。
 
 ### 虚拟机的设计
 
-接下来我们说说虚拟机设计。要虚拟一台计算机,要满足三个条件: **隔离、仿真、高效** 。 **隔离(Isolation),** 很好理解, **指的是一台实体机上的所有的虚拟机实例不能互相影响** 。这也是早期设计虚拟机的一大动力,比如可以在一台实体机器上同时安装 Linux、Unix、Windows、MacOS 四种操作系统,那么一台实体机器就可以执行四种操作系统上的程序,这就节省了采购机器的开销。 **仿真(Simulation)指的是用起来像一台真的机器那样,包括开机、关机,以及各种各样的硬件设备** 。在虚拟机上执行的操作系统认为自己就是在实体机上执行。仿真主要的贡献是 **让进程可以无缝的迁移,** 也就是让虚拟机中执行的进程,真实地感受到和在实体机上执行是一样的——这样程序从虚拟机到虚拟机、实体机到虚拟机的应用迁移,就不需要修改源代码。
+接下来我们说说虚拟机设计。要虚拟一台计算机,要满足三个条件: **隔离、仿真、高效** 。**隔离(Isolation),** 很好理解,**指的是一台实体机上的所有的虚拟机实例不能互相影响** 。这也是早期设计虚拟机的一大动力,比如可以在一台实体机器上同时安装 Linux、Unix、Windows、MacOS 四种操作系统,那么一台实体机器就可以执行四种操作系统上的程序,这就节省了采购机器的开销。**仿真(Simulation)指的是用起来像一台真的机器那样,包括开机、关机,以及各种各样的硬件设备** 。在虚拟机上执行的操作系统认为自己就是在实体机上执行。仿真主要的贡献是 **让进程可以无缝的迁移,** 也就是让虚拟机中执行的进程,真实地感受到和在实体机上执行是一样的——这样程序从虚拟机到虚拟机、实体机到虚拟机的应用迁移,就不需要修改源代码。
 
 **高效(Efficient)的目标是减少虚拟机对 CPU、对硬件资源的占用** 。通常在虚拟机上执行指令需要额外负担10~15% 的执行成本,这个开销是相对较低的。因为应用通常很少将 CPU 真的用满,在容器中执行 CPU 指令开销会更低更接近在本地执行程序的速度。
 
@@ -30,11 +30,11 @@ VMware 和 Docker 是目前虚拟化技术中最具代表性的两种设计。 *
 
 #### 二进制翻译
 
-通常硬件的设计假定是由单操作系统管理的。如果多个操作系统要共享这些设备,就需要通过 Hypervisor。当操作系统需要执行程序的时候,程序的指令就通过 Hypervisor 执行。早期的虚拟机设计当中,Hypervisor 不断翻译来自虚拟机的程序指令,将它们翻译成可以适配在目标硬件上执行的指令。这样的设计,我们称为二进制翻译。 **二进制翻译的弱点在于性能,所有指令都需要翻译** 。相当于在执行所有指令的时候,都会产生额外的开销。当然可以用动态翻译技术进行弥补,比如说预读指令进行翻译,但是依然会产生较大的性能消耗。
+通常硬件的设计假定是由单操作系统管理的。如果多个操作系统要共享这些设备,就需要通过 Hypervisor。当操作系统需要执行程序的时候,程序的指令就通过 Hypervisor 执行。早期的虚拟机设计当中,Hypervisor 不断翻译来自虚拟机的程序指令,将它们翻译成可以适配在目标硬件上执行的指令。这样的设计,我们称为二进制翻译。**二进制翻译的弱点在于性能,所有指令都需要翻译** 。相当于在执行所有指令的时候,都会产生额外的开销。当然可以用动态翻译技术进行弥补,比如说预读指令进行翻译,但是依然会产生较大的性能消耗。
 
 #### 世界切换和虚拟化支持
 
-另一种方式就是当虚拟机上的应用需要执行程序的时候,进行一次世界切换(World Switch)。 **所谓世界切换就是交接系统的控制权,比如虚拟机上的操作系统,进入内核接管中断,成为实际的机器的控制者** 。在这样的条件下,虚拟机上程序的执行就变成了本地程序的执行。相对来说,这种切换行为相较于二进制翻译,成本是更低的。
+另一种方式就是当虚拟机上的应用需要执行程序的时候,进行一次世界切换(World Switch)。**所谓世界切换就是交接系统的控制权,比如虚拟机上的操作系统,进入内核接管中断,成为实际的机器的控制者** 。在这样的条件下,虚拟机上程序的执行就变成了本地程序的执行。相对来说,这种切换行为相较于二进制翻译,成本是更低的。
 
 为了实现世界切换,虚拟机上的操作系统需要使用硬件设备,比如内存管理单元(MMR)、TLB、DMA 等。这些设备都需要支持虚拟机上操作系统的使用,比如说 TLB 需要区分是虚拟机还是实体机程序。虽然可以用软件模拟出这些设备给虚拟机使用,但是如果能让虚拟机使用真实的设备,性能会更好。现在的 CPU 通常都支持虚拟化技术,比如 Intel 的 VT-X 和 AMD 的 AMD-V(也称作 Secure Virtual Machine)。如果你对硬件虚拟化技术非常感兴趣,可以阅读[这篇文档](https://www.mimuw.edu.pl/~vincent/lecture6/sources/amd-pacifica-specification.pdf)。
 
@@ -44,7 +44,7 @@ Type-1 虚拟机本身是一个操作系统,所以需要用户预装。为了
 
 ![Lark20210127-174145.png](assets/Ciqc1GARNYSAKM46AADCxGGyD4s927.png)
 
-在第二种设计当中,虚拟机本身也作为一个进程。它和操作系统中执行的其他进程并没有太大的区别。但是 **为了提升性能,有一部分 Hypervisor 程序会作为内核中的驱动执行** 。 **当虚拟机操作系统(Guest OS)执行程序的时候,会通过 Hypervisor 实现世界切换** 。因此,虽然和 Type-1 虚拟机有一定的区别,但是从本质上来看差距不大,同样是需要二进制翻译技术和虚拟化技术。
+在第二种设计当中,虚拟机本身也作为一个进程。它和操作系统中执行的其他进程并没有太大的区别。但是 **为了提升性能,有一部分 Hypervisor 程序会作为内核中的驱动执行** 。**当虚拟机操作系统(Guest OS)执行程序的时候,会通过 Hypervisor 实现世界切换** 。因此,虽然和 Type-1 虚拟机有一定的区别,但是从本质上来看差距不大,同样是需要二进制翻译技术和虚拟化技术。
 
 #### Hyper-V
 
@@ -56,7 +56,7 @@ Type-1 虚拟机本身是一个操作系统,所以需要用户预装。为了
 
 ### 容器(Container) **虚拟机虚拟的是计算机,容器虚拟的是执行环境** 。每个容器都是一套独立的执行环境,如下图所示,容器直接被管理在操作系统之内,并不需要一个虚拟机监控程序
 
-![Lark20210127-174137.png](assets/Ciqc1GARNZOAM0V8AAExEgSEXPg097.png) **和虚拟机有一个最大的区别就是:容器是直接跑在操作系统之上的,容器内部是应用,应用执行起来就是进程** 。这个进程和操作系统上的其他进程也没有本质区别,但这个架构设计没有了虚拟机监控系统。当然,容器有一个更轻量级的管理程序,用户可以从网络上下载镜像,启动起来就是容器。容器中预装了一些程序,比如说一个 Python 开发环境中,还会预装 Web 服务器和数据库。因为没有了虚拟机管理程序在中间的开销,因而性能会更高。而且因为不需要安装操作系统,因此容器安装速度更快,可以达到 ms 级别。 **容器依赖操作系统的能力直接实现,比如:**
+![Lark20210127-174137.png](assets/Ciqc1GARNZOAM0V8AAExEgSEXPg097.png) **和虚拟机有一个最大的区别就是:容器是直接跑在操作系统之上的,容器内部是应用,应用执行起来就是进程** 。这个进程和操作系统上的其他进程也没有本质区别,但这个架构设计没有了虚拟机监控系统。当然,容器有一个更轻量级的管理程序,用户可以从网络上下载镜像,启动起来就是容器。容器中预装了一些程序,比如说一个 Python 开发环境中,还会预装 Web 服务器和数据库。因为没有了虚拟机管理程序在中间的开销,因而性能会更高。而且因为不需要安装操作系统,因此容器安装速度更快,可以达到 ms 级别。**容器依赖操作系统的能力直接实现,比如:**
 
 - **Linux 的 Cgroups(Linux Control Groups)能力,可以用来限制某组进程使用的 CPU 资源和内存资源,控制进程的资源能使用;** - 另外 **Linux 的 Namespace 能力,可以设置每个容器能看到能够使用的目录和文件** 。
 
@@ -68,4 +68,4 @@ Type-1 虚拟机本身是一个操作系统,所以需要用户预装。为了
 
 容器虽然达到了虚拟机同样的隔离性,创建、销毁、维护成本都更低,但是从安全性考虑,还是要优先选用虚拟机执行操作系统。基础设施是一件大事,比如操作系统会发生故障、任何应用都有可能不安全,甚至容器管理程序本身也可能出现问题。因此,现在更多的情况是 Docker 被安装到了虚拟机上。
 
-**那么通过这一讲的学习,你现在可以尝试来回答本讲关联的面试题目:VMware 和 Docker 的区别** ? **【解析】** VMware 提供虚拟机,Docker 提供容器。 虚拟机是一台完整的计算机,因此需要安装操作系统。虚拟机中的程序执行在虚拟机的操作系统上,为了让多个操作系统可以高效率地同时执行,虚拟机非常依赖底层的硬件架构提供的虚拟化能力。容器则是利用操作系统的能力直接实现隔离,容器中的程序可以以进程的身份直接执行。
+**那么通过这一讲的学习,你现在可以尝试来回答本讲关联的面试题目:VMware 和 Docker 的区别**?**【解析】** VMware 提供虚拟机,Docker 提供容器。 虚拟机是一台完整的计算机,因此需要安装操作系统。虚拟机中的程序执行在虚拟机的操作系统上,为了让多个操作系统可以高效率地同时执行,虚拟机非常依赖底层的硬件架构提供的虚拟化能力。容器则是利用操作系统的能力直接实现隔离,容器中的程序可以以进程的身份直接执行。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25438\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25438\350\256\262.md"
index 6edc9f075..dc9881950 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25438\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25438\350\256\262.md"
@@ -81,7 +81,7 @@ Docker Swarm 是 Docker 团队基于 Docker 生态打造的容器编排引擎。
 
 在这样的设计当中,容器最好是 **无状态** 的,所以容器中最好不要用来运行 MySQL 这样的数据库。对于 MySQL 数据库,并不是多个实例都可以通过负载均衡来使用。有的实例只可以读,有的实例只可以写,中间还有 Binlog 同步。因此,虽然 K8s 提供了状态管理组件,但是使用起来可能不如虚拟机划算。
 
-也是因为这种原因,我们现在倾向于进行无状态服务的开发。所有的状态都是存储在远程,应用本身并没有状态。当然, **在开发测试环境,用容器来管理数据库是一个非常好的方案** 。这样可以帮助我们快速搭建、切换开发测试环境,并且可以做到一人一环境,互不影响,也可以做到开发环境、测试环境和线上环境统一。
+也是因为这种原因,我们现在倾向于进行无状态服务的开发。所有的状态都是存储在远程,应用本身并没有状态。当然,**在开发测试环境,用容器来管理数据库是一个非常好的方案** 。这样可以帮助我们快速搭建、切换开发测试环境,并且可以做到一人一环境,互不影响,也可以做到开发环境、测试环境和线上环境统一。
 
 ### 总结
 
@@ -89,7 +89,7 @@ Docker Swarm 是 Docker 团队基于 Docker 生态打造的容器编排引擎。
 
 至于到底选择哪个?你可以根据自己的业务场景综合考虑。
 
-另外,一些大厂通常还会有自己的一套容器编排引擎。这些架构未必用了开源领域的产品,也许会让程序员感受到非常痛苦。因为即便是一家强大的商业公司,在研发产品的时候还是很难做到像社区产品这样认真和专注。所以我希望,当你以后成为一名优秀的架构师,如果不想让公司的技术栈被社区淘汰,就要不断地进行技术升级。 **那么通过这一讲的学习,你现在可以尝试来回答本讲关联的面试题目:如何利用 K8s 和 Docker Swarm 管理微服务** ?
+另外,一些大厂通常还会有自己的一套容器编排引擎。这些架构未必用了开源领域的产品,也许会让程序员感受到非常痛苦。因为即便是一家强大的商业公司,在研发产品的时候还是很难做到像社区产品这样认真和专注。所以我希望,当你以后成为一名优秀的架构师,如果不想让公司的技术栈被社区淘汰,就要不断地进行技术升级。**那么通过这一讲的学习,你现在可以尝试来回答本讲关联的面试题目:如何利用 K8s 和 Docker Swarm 管理微服务**?
 
 【 **解析** 】这两个容器编排引擎都可以用来管理微服务。K8s 和 Docker Swarm 在使用微服务的时候有许多共性的步骤。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25439\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25439\350\256\262.md"
index 1d7816c02..39fb489e0 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25439\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25439\350\256\262.md"
@@ -1,6 +1,6 @@
 # 39 Linux 架构优秀在哪里
 
-我们在面试的时候经常会和面试官聊架构,多数同学可能会认为架构是一个玄学问题,讨论的是“玄而又玄”的知识——如同道德经般的开头“玄之又玄、众妙之门”。 **其实架构领域也有通用的语言,有自己独有的词汇** 。虽然架构师经常为了系统架构争得面红耳赤,但是即使发生争吵,大家也会遵守架构思想准则。
+我们在面试的时候经常会和面试官聊架构,多数同学可能会认为架构是一个玄学问题,讨论的是“玄而又玄”的知识——如同道德经般的开头“玄之又玄、众妙之门”。**其实架构领域也有通用的语言,有自己独有的词汇** 。虽然架构师经常为了系统架构争得面红耳赤,但是即使发生争吵,大家也会遵守架构思想准则。
 
 这些优秀的架构思想和准则,很大一部分来自早期的黑客们对程序语言编译器实现的探索、对操作系统实现方案的探索,以及对计算机网络应用发展的思考,并且一直沿用至今。比如现在的面向对象编程、函数式编程、子系统的拆分和组织,以及分层架构设计,依然沿用了早期的架构思路。
 
@@ -16,19 +16,19 @@
 
 与其所有的程序工具模块都由自己维护,不如将这项权利分发给需要的人,让更多的人参与进来。让更多的小团队去贡献代码,这样才可以把更多的工具体验做到极致。
 
-这个思想在面向对象以及函数式编程的设计中,同样存在。比如在面向对象中,我们会尽量使用组合去替代继承。 **因为继承是一种 Mono 的设计,一旦发生继承关系,就意味着父类和子类之间的强耦合** 。 **而组合是一种更轻量级的复用** 。对于函数式编程,我们有 Monad 设计(单子),本质上是让事物(对象)和处理事物(计算)的函数之间可以进行组合,这样就可以最小粒度的复用函数。
+这个思想在面向对象以及函数式编程的设计中,同样存在。比如在面向对象中,我们会尽量使用组合去替代继承。**因为继承是一种 Mono 的设计,一旦发生继承关系,就意味着父类和子类之间的强耦合** 。**而组合是一种更轻量级的复用** 。对于函数式编程,我们有 Monad 设计(单子),本质上是让事物(对象)和处理事物(计算)的函数之间可以进行组合,这样就可以最小粒度的复用函数。
 
 同理,Unix 系操作系统用管道组合进程,也是在最小粒度的复用程序。
 
 ### 管道设计(Pipeline)
 
-提到最小粒度的复用程序,就必然要提到管道(Pipeline)。Douglas McIlroy 在 Unix 的哲学中提到: **一个应用的输出,应该是另一个应用的输入** 。 **这句话,其实道出了计算的本质** 。
+提到最小粒度的复用程序,就必然要提到管道(Pipeline)。Douglas McIlroy 在 Unix 的哲学中提到: **一个应用的输出,应该是另一个应用的输入** 。**这句话,其实道出了计算的本质** 。
 
 计算其实就是将一个计算过程的输出给另一个计算过程作为输入。在构造流计算、管道运算、Monad 类型、泛型容器体系时——很大程度上,我们希望计算过程间拥有一定的相似性,比如泛型类型的统一。这样才可以把一个过程的输出给到另一个过程的输入。
 
 ### 重构和丢弃
 
-在 Unix 设计当中有一个非常有趣的哲学。 **就是希望每个应用都只做一件事情,并且把这件事情做到极致。如果当一个应用变得过于复杂的时候,就去重构这个应用,或者重新写一个应用。而不是在原有的应用上增加功能。** 上述逻辑和商业策略是否有相悖的地方?
+在 Unix 设计当中有一个非常有趣的哲学。**就是希望每个应用都只做一件事情,并且把这件事情做到极致。如果当一个应用变得过于复杂的时候,就去重构这个应用,或者重新写一个应用。而不是在原有的应用上增加功能。** 上述逻辑和商业策略是否有相悖的地方?
 
 关于这个问题,我觉得需要你自己进行思考,我不能给你答案,但欢迎把你的想法和答案写在留言区,我们一起交流。
 
@@ -38,15 +38,15 @@
 
 还有,以我多年从事大型系统开发的经验来看,我宁愿重新做一些微服务,也不愿意去重构巨大的、复杂的系统。换句话说,我更乐意将新功能做到新系统里面,而不是在一个巨大的系统上不断地迭代和改进。这样不仅节省开发成本,还可以把事情做得更好。从这个角度看,我们进入微服务时代,是一个不可逆的过程。
 
-另外多说一句,如果一定要在原有系统上增加功能,也应该多重构。 **重构和重写原有的系统有很多的好处** ,希望你不要有 **畏难情绪** 。优秀的团队,总是处在一个代码不断迭代的过程。一方面是因为业务在高速发展,旧代码往往承接不了新需求;另一方面,是因为程序员本身也在不断地追求更好的架构思路。
+另外多说一句,如果一定要在原有系统上增加功能,也应该多重构。**重构和重写原有的系统有很多的好处**,希望你不要有 **畏难情绪** 。优秀的团队,总是处在一个代码不断迭代的过程。一方面是因为业务在高速发展,旧代码往往承接不了新需求;另一方面,是因为程序员本身也在不断地追求更好的架构思路。
 
-而重构旧代码,还经常可以看到业务逻辑中出问题的地方,看到潜在的隐患和风险,同时让程序员更加熟悉系统和业务逻辑。而且程序的复杂度,并不是随着需求量线性增长的。 **当需求量超过一定的临界值,复杂度增长会变快,类似一条指数曲线** 。 **因此,控制复杂度也是软件工程的一个核心问题。**
+而重构旧代码,还经常可以看到业务逻辑中出问题的地方,看到潜在的隐患和风险,同时让程序员更加熟悉系统和业务逻辑。而且程序的复杂度,并不是随着需求量线性增长的。**当需求量超过一定的临界值,复杂度增长会变快,类似一条指数曲线** 。**因此,控制复杂度也是软件工程的一个核心问题。**
 
 ### 写复杂的程序就是写错了
 
-我们经常听到优秀的架构师说, **程序写复杂了,就是写错了。** 在 Unix 哲学中,也提出过这样的说法: **写一个程序的时候,先用几周时间去构造一个简单的版本,如果发现复杂了,就重写它** 。
+我们经常听到优秀的架构师说,**程序写复杂了,就是写错了。** 在 Unix 哲学中,也提出过这样的说法: **写一个程序的时候,先用几周时间去构造一个简单的版本,如果发现复杂了,就重写它** 。
 
-确实实际情景也是如此。我们在写程序的时候,如果一开始没有用对工具、没有分对层、没有选对算法和数据结构、没有用对设计模式,那么写程序的时候,就很容易陷入大量的调试,还会出现很多 Bug。 **优秀的程序往往是思考的过程很长,调试的时间很短,能够迅速地在短时间内完成测试和上线。**
+确实实际情景也是如此。我们在写程序的时候,如果一开始没有用对工具、没有分对层、没有选对算法和数据结构、没有用对设计模式,那么写程序的时候,就很容易陷入大量的调试,还会出现很多 Bug。**优秀的程序往往是思考的过程很长,调试的时间很短,能够迅速地在短时间内完成测试和上线。**
 
 所以当你发现一段代码,或者一段业务逻辑很消耗时间的时候,可能是你的思维方式出错了。想一想是不是少了必要的工具的封装,或者遗漏了什么中间环节。当然,也有可能是你的架构设计有问题,这就需要重新做架构了。
 
@@ -66,7 +66,7 @@
 
 再给你讲一个我身边的故事:我刚刚工作的时候,我的老板自己写了一个小程序,去判断 HR 发过来简历是否符合他的用人条件。所以他每天可以看完几百份简历,并筛选出面试人选。而那些没有利用工具的技术 Leader,每天都在埋怨简历太多看不过来。
 
-这些故事告诉我们, **作为程序员,不仅仅需要完成工作,还要重视中间过程的工具缔造** 。
+这些故事告诉我们,**作为程序员,不仅仅需要完成工作,还要重视中间过程的工具缔造** 。
 
 ### 其他优秀的原则
 
@@ -78,11 +78,11 @@
 
 这也是我们在架构程序的时候经常会出错的地方。我们习惯性地选择用脑海中记忆的时间复杂度最低的算法,但是却忽略了 **时间复杂度只是一种增长关系,一个算法在某个场景中到底可不可行,是要以实际执行时收集数据为准的** 。
 
-再比如: **数据主导规则。当你的数据结构设计得足够好,那么你的计算方法就会深刻地反映出你系统的逻辑。这也叫作自证明代码。编程的核心是构造好的数据结构,而不是算法。** 尽管我们在学习的时候,算法和数据结构是一起学的。但是在大牛们看来, **数据结构的抽象可以深刻反映系统的本质** 。比如抽象出文件描述符反应文件、抽象出页表反应内存、抽象出 Socket 反应连接——这些数据结构才是设计系统最核心的东西。
+再比如: **数据主导规则。当你的数据结构设计得足够好,那么你的计算方法就会深刻地反映出你系统的逻辑。这也叫作自证明代码。编程的核心是构造好的数据结构,而不是算法。** 尽管我们在学习的时候,算法和数据结构是一起学的。但是在大牛们看来,**数据结构的抽象可以深刻反映系统的本质** 。比如抽象出文件描述符反应文件、抽象出页表反应内存、抽象出 Socket 反应连接——这些数据结构才是设计系统最核心的东西。
 
 ### 总结
 
-最后,再和你分享一句 Unix 的设计者Ken Thompson 的经典语录: **搞不定就用蛮力** 。这是打破所有规则的规则。 **在我们开发的过程当中,首先要把事情搞定!只有把事情搞定,才有我们上面谈到的这一大堆哲学产生价值的可能性** 。事情没有搞定,一切都尘归尘土归土,毫无意义。
+最后,再和你分享一句 Unix 的设计者Ken Thompson 的经典语录: **搞不定就用蛮力** 。这是打破所有规则的规则。**在我们开发的过程当中,首先要把事情搞定!只有把事情搞定,才有我们上面谈到的这一大堆哲学产生价值的可能性** 。事情没有搞定,一切都尘归尘土归土,毫无意义。
 
 今天所讲的这些哲学,可以作为你平时和架构师们沟通的语言。架构有自己领域的语言,比如设计模式、编程范式、数据结构,等等。还有许多像 Unix 哲学这样——经过历史积淀,充满着人文气息的行业标准和规范。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25440\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25440\350\256\262.md"
index a7207ddc3..94907d78f 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25440\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25440\350\256\262.md"
@@ -16,7 +16,7 @@
 
 (注:需要一个账号并且登录)
 
-#### 38 | 容器编排技术:如何利用 K8s 和 Docker Swarm 管理微服务? **【问题** 】 **为什么会有多个容器共用一个 Pod 的需求** ?
+#### 38 | 容器编排技术:如何利用 K8s 和 Docker Swarm 管理微服务?**【问题** 】 **为什么会有多个容器共用一个 Pod 的需求**?
 
 【 **解析** 】Pod 内部的容器共用一个网络空间,可以通过 localhost 进行通信。另外多个容器,还可以共享一个存储空间。
 
@@ -26,7 +26,7 @@
 
 以上这种设计模式,我们称为 **边车模式** (Sidecar),边车模式将数个容器放入一个分组内(例如 K8s 的 Pod),让它们可以分配到相同的节点上。这样它们彼此间可以共用磁盘、网络等。
 
-在边车模式中,有一类容器,被称为 **Ambassador Container** ,翻译过来是使节容器。对于一个主容器(Main Container)上的服务,可以通过 Ambassador Container 来连接外部服务。如下图所示:
+在边车模式中,有一类容器,被称为 **Ambassador Container**,翻译过来是使节容器。对于一个主容器(Main Container)上的服务,可以通过 Ambassador Container 来连接外部服务。如下图所示:
 
 ![图片1.png](assets/CioPOWAjdxiATdfKAADv_hHJszc514.png)
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25441\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25441\350\256\262.md"
index 78966d763..fa9dd892a 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25441\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\223\215\344\275\234\347\263\273\347\273\237/\347\254\25441\350\256\262.md"
@@ -12,7 +12,7 @@
 
 有了信仰,自然而然,人就会 **选择** 。比如我林䭽的信仰是“ **知识改变命运** ”,而获取知识需要渠道和时间。
 
-**拓展渠道** 就要虚心地请教拥有知识的人,不能吝啬请客吃饭的钱,过节要给技术大牛筹备礼物,花钱买书不能心疼。为了 **节省时间** ,就需要租下公司边上很贵的房子,去节省上下班的时间。哪怕我工资将将过万的时候,我也愿意花 5000 块钱去租公司旁边的房子。
+**拓展渠道** 就要虚心地请教拥有知识的人,不能吝啬请客吃饭的钱,过节要给技术大牛筹备礼物,花钱买书不能心疼。为了 **节省时间**,就需要租下公司边上很贵的房子,去节省上下班的时间。哪怕我工资将将过万的时候,我也愿意花 5000 块钱去租公司旁边的房子。
 
 那么做这些事情,是对还是错呢?——我永远都无法去证明这些答案的对错,甚至我们得不到答案。不过有了相信的东西是美好的,因为你选择的时候不需要焦虑和犹豫。有了相信的东西,不去做,一定会后悔。这不是我给你的建议,我不太喜欢给人以人生大道理和建议,我觉得每个人思考的方式是不同的。我只能告诉你,我在这样思考问题。其实你也可以在留言区和我交流你的想法,和大家一起交流。
 
@@ -28,4 +28,4 @@
 
 我现在所说的并不是一次心灵的鸡汤,不是告诉你“爱拼就会赢”这种无法证明的道理。我是把一个工作了 11 年的资深程序员的感受告诉给你: **当为了自己所相信的东西去努力的时候,人的快乐和幸福指数会高一些** 。
 
-以上就是我,对程序员职业发展的一点见解。最后还是感谢你来学习我的专栏,我会继续努力。 **将更多、更难的知识,以简单、有趣的形式带入你的视野,帮助你成长** 。如果你感兴趣,今后可以和我一起学习。
+以上就是我,对程序员职业发展的一点见解。最后还是感谢你来学习我的专栏,我会继续努力。**将更多、更难的知识,以简单、有趣的形式带入你的视野,帮助你成长** 。如果你感兴趣,今后可以和我一起学习。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25400\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25400\350\256\262.md"
index c5eb5e5ae..df14253e1 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25400\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25400\350\256\262.md"
@@ -10,21 +10,21 @@
 
 但是数据结构和算法的学习并不轻松,你往往会经历以下痛苦:
 
-- 从原理到应用,所 **涵盖的知识非常多** ,大而全的通盘学习往往不现实,即使付出大量时间和精力坚持下来,但学得快,忘得也快;
-- 为了应试或求职, **大量刷题,却刷不会,即使看着答案也不理解,而且刷题效率低下** ,耐着性子学一天,也不过刷完了两道题,同类型问题稍加变动就又束手无策;
+- 从原理到应用,所 **涵盖的知识非常多**,大而全的通盘学习往往不现实,即使付出大量时间和精力坚持下来,但学得快,忘得也快;
+- 为了应试或求职,**大量刷题,却刷不会,即使看着答案也不理解,而且刷题效率低下**,耐着性子学一天,也不过刷完了两道题,同类型问题稍加变动就又束手无策;
 - 看了大量图书和学习资料,掌握了理论知识,但一 **遇到实际问题仍然无从下手** 。
 
 **其实学习和实践数据结构与算法,是有方法的。** 这正是我和拉勾教育合作设计这个课程的初衷。我希望帮助你摆脱盲目刷题与漫无目的地学习方式,更加高效地掌握数据结构与算法知识,真正掌握程序开发、代码优化的方法论,完成从掌握理论知识到解决实际问题的转变。
 
 ### 你为什么需要重学数据结构与算法?
 
-很多软件工程师都有进大厂的诉求,获得高薪 Offer,或者体验大厂的优质文化。但互联网的红利期早已过去,竞争也越来越激烈,“僧多粥少”的情况直接提高了面试“门槛”,诞生了“优秀工程师”的概念。 **优秀的软件工程师必须具备过硬的代码开发能力,而这就体现在你对数据结构、算法思维、代码效率优化等知识的储备上,并直接反应在你工作中解决实际问题的好坏上。** 比如,你要去开发某个复杂系统,如何才能围绕系统的复杂性去选择最合适的解决方案呢?一方面是对所用算法的选型,另一方面是对所用数据结构的选型,这都要求你对数据结构与算法有充分的理解和掌握。
+很多软件工程师都有进大厂的诉求,获得高薪 Offer,或者体验大厂的优质文化。但互联网的红利期早已过去,竞争也越来越激烈,“僧多粥少”的情况直接提高了面试“门槛”,诞生了“优秀工程师”的概念。**优秀的软件工程师必须具备过硬的代码开发能力,而这就体现在你对数据结构、算法思维、代码效率优化等知识的储备上,并直接反应在你工作中解决实际问题的好坏上。** 比如,你要去开发某个复杂系统,如何才能围绕系统的复杂性去选择最合适的解决方案呢?一方面是对所用算法的选型,另一方面是对所用数据结构的选型,这都要求你对数据结构与算法有充分的理解和掌握。
 
-但是 996、007 的互联网快节奏下,开发者普遍专注当下工作本身,并不追求极致的性能,一直在追语言,学框架,而忽视了数据结构与算法的学习和落地训练,基础知识储备不足,很难顺利做出最优的技术选择,从而导致开发的系统性能、稳定性都存在很多缺陷。 **此外,面试中都要重点考察数据结构与算法知识,这是不争的事实。** 一是因为代码能力不容易评估,而数据结构和算法的掌握情况相对可衡量;二是可以衡量工程师的基本功,以及逻辑思考能力。
+但是 996、007 的互联网快节奏下,开发者普遍专注当下工作本身,并不追求极致的性能,一直在追语言,学框架,而忽视了数据结构与算法的学习和落地训练,基础知识储备不足,很难顺利做出最优的技术选择,从而导致开发的系统性能、稳定性都存在很多缺陷。**此外,面试中都要重点考察数据结构与算法知识,这是不争的事实。** 一是因为代码能力不容易评估,而数据结构和算法的掌握情况相对可衡量;二是可以衡量工程师的基本功,以及逻辑思考能力。
 
 我曾经有个海外名校毕业的应届生同事,他的计算机领域基础知识,尤其是数据结构和算法、机器学习、深度学习等基本功特别扎实,在面对陌生问题时往往能更快速地锁定问题,并根据已有知识去寻找解决方法。短短几个月后,他就从刚入职的小白转变为某些项目的负责人。所以说,基本功扎实的人潜力会非常大,取得业绩结果只不过是时间问题。
 
-而据我所知,为了快速掌握数据结构与算法知识,或者提高代码能力,绝大多数的学生或候选人一定会通过公开的题库去刷题,却常常被那些千变万化的代码题搞得晕头转向、不明所以,浪费了大量时间和精力,得不偿失。 **这并不是说刷题本身有错,而是应该掌握正确的方式方法。而且刷题只是形式,更重要的是掌握算法思维和原理,并用以解决实际的编码问题。**
+而据我所知,为了快速掌握数据结构与算法知识,或者提高代码能力,绝大多数的学生或候选人一定会通过公开的题库去刷题,却常常被那些千变万化的代码题搞得晕头转向、不明所以,浪费了大量时间和精力,得不偿失。**这并不是说刷题本身有错,而是应该掌握正确的方式方法。而且刷题只是形式,更重要的是掌握算法思维和原理,并用以解决实际的编码问题。**
 
 我经常说,真题实际上是刨除了特定场景和业务问题后,对于我们实际解决问题的方法的提炼。考核真题和刷题不是目标,还是要最终回归到能力培养上来。这也是这门课中,我要核心传达给你的内容。
 
@@ -35,7 +35,7 @@
 #### 课程特色
 
 - **重视方法论** 。我没有单纯去讲数据结构与算法,而是从程序优化的通用方法论讲起,以此为引子,让你更深刻地理解数据结构和算法思维在程序优化中的作用。
-- **内容精简、重点突出** 。市面上的课程,都会主打“大而全”,唯恐某些知识点没有讲到。殊不知, **高频使用的数据结构就那么几个,其他往往是这些基础知识点的不同组合与变形** ,把这些牢牢掌握后,就已经足够解决你绝大多数的实际问题了。
+- **内容精简、重点突出** 。市面上的课程,都会主打“大而全”,唯恐某些知识点没有讲到。殊不知,**高频使用的数据结构就那么几个,其他往往是这些基础知识点的不同组合与变形**,把这些牢牢掌握后,就已经足够解决你绝大多数的实际问题了。
 - **学习收获快** 。内容精简,且重视方法论的建设,可以快速建立程序优化的思想,并牢牢掌握知识体系中最核心、最根本的内容。我希望你快速抓住重点,迅速“所学即所得”地将知识运用到工作中去。
 
 #### 课程设置
@@ -45,16 +45,16 @@
 - **第一部分:方法论,也就是把“烂”代码优化为高效率代码的方法和路径,是这门课关于代码开发与优化方法框架的总纲** 。代码的目标,除了完成任务,还要求把某项任务高效率地完成。
 - **第二部分,在方法论的指引下,带你补充必备的数据结构基础知识** 。复杂度的降低,要求对数据有更好的组织方式,这正是数据结构需要解决的问题。为了合理选择数据结构,我们需要全面分析任务对数据处理和计算的基本操作,再根据不同数据结构在这些基本操作中的优劣特点去灵活选用合适的数据结构。
 - **第三部分,在方法论的指引下,带你掌握必备的算法思维,也就是用算法思考问题的逻辑和程序设计的重要思想** 。在一些实际问题的解决中,需要运用一些巧妙的方法,它们不会改变数据的组织方式,但可以通过巧妙的计算方式降低代码复杂度。常见的方法,如递归、二分法、排序算法、动态规划等,会在这一部分介绍。
-- **第四部分,面试真题详解,带你用前面的知识体系,去真正地解决问题** 。前三部分的知识合在一起,就是解决实际问题的工具包。 **面试题并非单纯考核人才的工具,更是实际业务问题高度提炼后的缩影,它能反映一个开发者的知识储备和问题解决能力** 。这一部分将深入剖析高频真题的解题方法和思路。
+- **第四部分,面试真题详解,带你用前面的知识体系,去真正地解决问题** 。前三部分的知识合在一起,就是解决实际问题的工具包。**面试题并非单纯考核人才的工具,更是实际业务问题高度提炼后的缩影,它能反映一个开发者的知识储备和问题解决能力** 。这一部分将深入剖析高频真题的解题方法和思路。
 - **第五部分,面试现场,给你一些求职时的切实建议** 。很多工程师有个共性问题,那就是明明有能力,却说不出来,表现得就像是个初学者一样。这部分,我通过补充面试经验,包括现场手写代码、问题分析、面试官注重的软素质等内容,来帮你解决这个问题。
 
 ![数据结构.png](assets/CgqCHl8_QyeAX6RGAAIqe25t_U8556.png)
 
 如果你是以下用户,那么本课程一定适合你:
 
-- **参加校招的应届生** ,应届生需要具备充足的知识储备,这也是面试官核心考察的内容之一。
-- **需要补学数据结构与算法的程序员** ,以便在工作中更好地支撑和优化业务逻辑(比如搭建在线系统的 Java 后端同学,需要不断提高和优化系统性能),以及有意转行 Python 算法或人工智能等方向的程序员。
-- **基本功薄弱的软件工程师求职者** ,尤其是常年挂在面试手写代码的求职者。
+- **参加校招的应届生**,应届生需要具备充足的知识储备,这也是面试官核心考察的内容之一。
+- **需要补学数据结构与算法的程序员**,以便在工作中更好地支撑和优化业务逻辑(比如搭建在线系统的 Java 后端同学,需要不断提高和优化系统性能),以及有意转行 Python 算法或人工智能等方向的程序员。
+- **基本功薄弱的软件工程师求职者**,尤其是常年挂在面试手写代码的求职者。
 
 ### 讲师寄语
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25401\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25401\350\256\262.md"
index 948c3ee4c..52235ff2b 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25401\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25401\350\256\262.md"
@@ -19,26 +19,26 @@
 
 ![1.gif](assets/CgqCHl7CRGiAe-NpAR0S70dSC2M990.gif)
 
-那提到降低复杂度,我们首先需要知道怎么衡量复杂度。而在实际衡量时,我们通常会围绕以下2 个维度进行。 **首先,这段代码消耗的资源是什么** 。一般而言,代码执行过程中会消耗计算时间和计算空间,那需要衡量的就是时间复杂度和空间复杂度。
+那提到降低复杂度,我们首先需要知道怎么衡量复杂度。而在实际衡量时,我们通常会围绕以下2 个维度进行。**首先,这段代码消耗的资源是什么** 。一般而言,代码执行过程中会消耗计算时间和计算空间,那需要衡量的就是时间复杂度和空间复杂度。
 
 我举一个实际生活中的例子。某个十字路口没有建立立交桥时,所有车辆通过红绿灯分批次行驶通过。当大量汽车同时过路口的时候,就会分别消耗大家的时间。但建了立交桥之后,所有车辆都可以同时通过了,因为立交桥的存在,等于是消耗了空间资源,来换取了时间资源。
 
 ![2.gif](assets/CgqCHl7CRMaAO_oEAJfz6fjfMNQ403.gif) **其次,这段代码对于资源的消耗是多少** 。我们不会关注这段代码对于资源消耗的绝对量,因为不管是时间还是空间,它们的消耗程度都与输入的数据量高度相关,输入数据少时消耗自然就少。为了更客观地衡量消耗程度,我们通常会关注时间或者空间消耗量与输入数据量之间的关系。
 
-好,现在我们已经了解了衡量复杂度的两个纬度,那应该如何去计算复杂度呢? **复杂度是一个关于输入数据量 n 的函数** 。假设你的代码复杂度是 f(n),那么就用个大写字母 O 和括号,把 f(n) 括起来就可以了,即 O(f(n))。例如,O(n) 表示的是,复杂度与计算实例的个数 n 线性相关;O(logn) 表示的是,复杂度与计算实例的个数 n 对数相关。
+好,现在我们已经了解了衡量复杂度的两个纬度,那应该如何去计算复杂度呢?**复杂度是一个关于输入数据量 n 的函数** 。假设你的代码复杂度是 f(n),那么就用个大写字母 O 和括号,把 f(n) 括起来就可以了,即 O(f(n))。例如,O(n) 表示的是,复杂度与计算实例的个数 n 线性相关;O(logn) 表示的是,复杂度与计算实例的个数 n 对数相关。
 
 通常,复杂度的计算方法遵循以下几个原则:
 
-- 首先, **复杂度与具体的常系数无关** ,例如 O(n) 和 O(2n) 表示的是同样的复杂度。我们详细分析下,O(2n) 等于 O(n+n),也等于 O(n) + O(n)。也就是说,一段 O(n) 复杂度的代码只是先后执行两遍 O(n),其复杂度是一致的。
-- 其次, **多项式级的复杂度相加的时候,选择高者作为结果** ,例如 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。具体分析一下就是,O(n²)+O(n) = O(n²+n)。随着 n 越来越大,二阶多项式的变化率是要比一阶多项式更大的。因此,只需要通过更大变化率的二阶多项式来表征复杂度就可以了。
+- 首先,**复杂度与具体的常系数无关**,例如 O(n) 和 O(2n) 表示的是同样的复杂度。我们详细分析下,O(2n) 等于 O(n+n),也等于 O(n) + O(n)。也就是说,一段 O(n) 复杂度的代码只是先后执行两遍 O(n),其复杂度是一致的。
+- 其次,**多项式级的复杂度相加的时候,选择高者作为结果**,例如 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。具体分析一下就是,O(n²)+O(n) = O(n²+n)。随着 n 越来越大,二阶多项式的变化率是要比一阶多项式更大的。因此,只需要通过更大变化率的二阶多项式来表征复杂度就可以了。
 
-值得一提的是, **O(1) 也是表示一个特殊复杂度** ,含义为某个任务通过有限可数的资源即可完成。此处有限可数的具体意义是, **与输入数据量 n 无关** 。
+值得一提的是,**O(1) 也是表示一个特殊复杂度**,含义为某个任务通过有限可数的资源即可完成。此处有限可数的具体意义是,**与输入数据量 n 无关** 。
 
 例如,你的代码处理 10 条数据需要消耗 5 个单位的时间资源,3 个单位的空间资源。处理 1000 条数据,还是只需要消耗 5 个单位的时间资源,3 个单位的空间资源。那么就能发现资源消耗与输入数据量无关,就是 O(1) 的复杂度。
 
 为了方便你理解不同计算方法对复杂度的影响,我们来看一个代码任务:对于输入的数组,输出与之逆序的数组。例如,输入 a=\[1,2,3,4,5\],输出 \[5,4,3,2,1\]。
 
-先看 **方法一** ,建立并初始化数组 b,得到一个与输入数组等长的全零数组。通过一个 for 循环,从左到右将 a 数组的元素,从右到左地赋值到 b 数组中,最后输出数组 b 得到结果。
+先看 **方法一**,建立并初始化数组 b,得到一个与输入数组等长的全零数组。通过一个 for 循环,从左到右将 a 数组的元素,从右到左地赋值到 b 数组中,最后输出数组 b 得到结果。
 
 ![3.gif](assets/Ciqc1F7CRP6ARwDTAGHL-opG6Bk835.gif)
 
@@ -62,7 +62,7 @@ public void s1_1() {
 
 空间方面主要体现在计算过程中,对于存储资源的消耗情况。上面这段代码中,我们定义了一个新的数组 b,它与输入数组 a 的长度相等。因此,空间复杂度就是 O(n)。
 
-**接着我们看一下第二种编码方法** ,它定义了缓存变量 tmp,接着通过一个 for 循环,从 0 遍历到a 数组长度的一半(即 len(a)/2)。每次遍历执行的是什么内容?就是交换首尾对应的元素。最后打印数组 a,得到结果。
+**接着我们看一下第二种编码方法**,它定义了缓存变量 tmp,接着通过一个 for 循环,从 0 遍历到a 数组长度的一半(即 len(a)/2)。每次遍历执行的是什么内容?就是交换首尾对应的元素。最后打印数组 a,得到结果。
 
 ![4.gif](assets/Ciqc1F7CR22AIbSuABc0Rwl-t3w666.gif)
 
@@ -85,13 +85,13 @@ public void s1_2() {
 
 空间方面,我们定义了一个 tmp 变量,它与数组长度无关。也就是说,输入是 5 个元素的数组,需要一个 tmp 变量;输入是 50 个元素的数组,依然只需要一个 tmp 变量。因此,空间复杂度与输入数组长度无关,即 O(1)。
 
-可见, **对于同一个问题,采用不同的编码方法,对时间和空间的消耗是有可能不一样的** 。因此,工程师在写代码的时候,一方面要完成任务目标;另一方面,也需要考虑时间复杂度和空间复杂度,以求用尽可能少的时间损耗和尽可能少的空间损耗去完成任务。
+可见,**对于同一个问题,采用不同的编码方法,对时间和空间的消耗是有可能不一样的** 。因此,工程师在写代码的时候,一方面要完成任务目标;另一方面,也需要考虑时间复杂度和空间复杂度,以求用尽可能少的时间损耗和尽可能少的空间损耗去完成任务。
 
 #### 时间复杂度与代码结构的关系
 
 好了,通过前面的内容,相信你已经对时间复杂度和空间复杂度有了很好的理解。从本质来看,时间复杂度与代码的结构有着非常紧密的关系;而空间复杂度与数据结构的设计有关,关于这一点我们会在下一讲进行详细阐述。接下来我先来系统地讲一下时间复杂度和代码结构的关系。
 
-代码的 **时间复杂度,与代码的结构有非常强的关系** ,我们一起来看一些具体的例子。
+代码的 **时间复杂度,与代码的结构有非常强的关系**,我们一起来看一些具体的例子。
 
 例 1,定义了一个数组 a = \[1, 4, 3\],查找数组 a 中的最大值,代码如下:
 
@@ -166,9 +166,9 @@ OK,今天的内容到这儿就结束了。相信你对复杂度的概念有了
 
 复杂度通常包括时间复杂度和空间复杂度。在具体计算复杂度时需要注意以下几点。
 
-1. **它与具体的常系数无关** ,O(n) 和 O(2n) 表示的是同样的复杂度。
-   2. **复杂度相加的时候,选择高者作为结果** ,也就是说 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。
-   3. **O(1) 也是表示一个特殊复杂度** ,即任务与算例个数 n 无关。
+1. **它与具体的常系数无关**,O(n) 和 O(2n) 表示的是同样的复杂度。
+   2. **复杂度相加的时候,选择高者作为结果**,也就是说 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。
+   3. **O(1) 也是表示一个特殊复杂度**,即任务与算例个数 n 无关。
 
 复杂度细分为时间复杂度和空间复杂度,其中时间复杂度与 **代码的结构设计** 高度相关;空间复杂度与代码中 **数据结构的选择** 高度相关。会计算一段代码的时间复杂度和空间复杂度,是工程师的基本功。这项技能你在实际工作中一定会用到,甚至在参加互联网公司面试的时候,也是面试中的必考内容。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25402\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25402\350\256\262.md"
index 7cc424ccb..78ed3d844 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25402\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25402\350\256\262.md"
@@ -12,13 +12,13 @@
 
 一段代码会消耗计算时间、资源空间,从而产生时间复杂度和空间复杂度,那么你是否尝试过将时间复杂度和空间复杂进行下对比呢?其实对比过后,你就会发现一个重要的现象。
 
-**假设一段代码经过优化后,虽然降低了时间复杂度,但依然需要消耗非常高的空间复杂度。** 例如,对于固定数据量的输入,这段代码需要消耗几十 G 的内存空间,很显然普通计算机根本无法完成这样的计算。如果一定要解决的话,一个最简单粗暴的办法就是,购买大量的高性能计算机,来弥补空间性能的不足。 **反过来,假设一段代码经过优化后,依然需要消耗非常高的时间复杂度。** 例如,对于固定数据量的输入,这段代码需要消耗 1 年的时间去完成计算。如果在跑程序的 1 年时间内,出现了断电、断网或者程序抛出异常等预期范围之外的问题,那很可能造成 1 年时间浪费的惨重后果。很显然,用 1 年的时间去跑一段代码,对开发者和运维者而言都是极不友好的。
+**假设一段代码经过优化后,虽然降低了时间复杂度,但依然需要消耗非常高的空间复杂度。** 例如,对于固定数据量的输入,这段代码需要消耗几十 G 的内存空间,很显然普通计算机根本无法完成这样的计算。如果一定要解决的话,一个最简单粗暴的办法就是,购买大量的高性能计算机,来弥补空间性能的不足。**反过来,假设一段代码经过优化后,依然需要消耗非常高的时间复杂度。** 例如,对于固定数据量的输入,这段代码需要消耗 1 年的时间去完成计算。如果在跑程序的 1 年时间内,出现了断电、断网或者程序抛出异常等预期范围之外的问题,那很可能造成 1 年时间浪费的惨重后果。很显然,用 1 年的时间去跑一段代码,对开发者和运维者而言都是极不友好的。
 
 这告诉我们一个什么样的现实问题呢?代码效率的瓶颈可能发生在时间或者空间两个方面。如果是缺少计算空间,花钱买服务器就可以了。这是个花钱就能解决的问题。相反,如果是缺少计算时间,只能投入宝贵的人生去跑程序。即使你有再多的钱、再多的服务器,也是毫无用处。相比于空间复杂度,时间复杂度的降低就显得更加重要了。因此,你会发现这样的结论:空间是廉价的,而时间是昂贵的。
 
 #### 数据结构连接时空
 
-假定在不限制时间、也不限制空间的情况下,你可以完成某个任务的代码的开发。这就是通常我们所说的 **暴力解法** ,更是程序优化的起点。
+假定在不限制时间、也不限制空间的情况下,你可以完成某个任务的代码的开发。这就是通常我们所说的 **暴力解法**,更是程序优化的起点。
 
 例如,如果要在 100 以内的正整数中,找到同时满足以下两个条件的最小数字:
 
@@ -31,7 +31,7 @@
 
 为了降低复杂度,一个直观的思路是:梳理程序,看其流程中是否有无效的计算或者无效的存储。
 
-我们需要从时间复杂度和空间复杂度两个维度来考虑。常用的 **降低时间复杂度** 的方法有 **递归、二分法、排序算法、动态规划** 等,这些知识我们都会在后续课程中逐一学习,这里我先不讲。而降低空间复杂度的方法,就要围绕 **数据结构** 做文章了。 **降低空间复杂度的核心思路就是,能用低复杂度的数据结构能解决问题,就千万不要用高复杂度的数据结构。** 经过了前面剔除无效计算和存储的处理之后,如果程序在时间和空间等方面的性能依然还有瓶颈,又该怎么办呢?前面我们提到过,空间是廉价的,最不济也是可以通过购买更高性能的计算机进行解决的。然而时间是昂贵的,如果无法降低时间复杂度,那系统的效率就永远无法得到提高。
+我们需要从时间复杂度和空间复杂度两个维度来考虑。常用的 **降低时间复杂度** 的方法有 **递归、二分法、排序算法、动态规划** 等,这些知识我们都会在后续课程中逐一学习,这里我先不讲。而降低空间复杂度的方法,就要围绕 **数据结构** 做文章了。**降低空间复杂度的核心思路就是,能用低复杂度的数据结构能解决问题,就千万不要用高复杂度的数据结构。** 经过了前面剔除无效计算和存储的处理之后,如果程序在时间和空间等方面的性能依然还有瓶颈,又该怎么办呢?前面我们提到过,空间是廉价的,最不济也是可以通过购买更高性能的计算机进行解决的。然而时间是昂贵的,如果无法降低时间复杂度,那系统的效率就永远无法得到提高。
 
 这时候,开发者们想到这样的一个解决思路。如果可以通过某种方式,把时间复杂度转移到空间复杂度的话,就可以把无价的东西变成有价了。这种时空转移的思想,在实际生活中也会经常遇到。
 
@@ -148,7 +148,7 @@ public void s2_4() {
 
 我们来计算下这种方法的时空复杂度。代码结构上,有两个 for 循环。不过,这两个循环不是嵌套关系,而是顺序执行关系。其中,第一个循环实现了数组转字典的过程,也就是 O(n) 的复杂度。第二个循环再次遍历字典找到出现次数最多的那个元素,也是一个 O(n) 的时间复杂度。
 
-因此,总体的时间复杂度为 O(n) + O(n),就是 O(2n), **根据复杂度与具体的常系数无关的原则** ,也就是O(n) 的复杂度。空间方面,由于定义了 k-v 字典,其字典元素的个数取决于输入数组元素的个数。因此,空间复杂度增加为 O(n)。
+因此,总体的时间复杂度为 O(n) + O(n),就是 O(2n),**根据复杂度与具体的常系数无关的原则**,也就是O(n) 的复杂度。空间方面,由于定义了 k-v 字典,其字典元素的个数取决于输入数组元素的个数。因此,空间复杂度增加为 O(n)。
 
 这段代码的开发,就是借鉴了方法论中的步骤三,通过采用更复杂、高效的数据结构,完成了时空转移,提高了空间复杂度,让时间复杂度再次降低。
 
@@ -158,9 +158,9 @@ public void s2_4() {
 
 其实,无论什么难题,降低复杂度的方法就是这三个步骤。只要你能深入理解这里的核心思想,就能把问题迎刃而解。
 
-- 第一步, **暴力解法** 。在没有任何时间、空间约束下,完成代码任务的开发。
-- 第二步, **无效操作处理** 。将代码中的无效计算、无效存储剔除,降低时间或空间复杂度。
-- 第三步, **时空转换** 。设计合理数据结构,完成时间复杂度向空间复杂度的转移。
+- 第一步,**暴力解法** 。在没有任何时间、空间约束下,完成代码任务的开发。
+- 第二步,**无效操作处理** 。将代码中的无效计算、无效存储剔除,降低时间或空间复杂度。
+- 第三步,**时空转换** 。设计合理数据结构,完成时间复杂度向空间复杂度的转移。
 
 既然说这是这门专栏的总纲,那么很显然后续的学习都是在这个总纲体系的框架中。第一步的暴力解法没有太多的套路,只要围绕你面临的问题出发,大胆发挥想象去尝试解决即可。第二步的无效操作处理中,你需要学会并掌握递归、二分法、排序算法、动态规划等常用的算法思维。第三步的时空转换,你需要对数据的操作进行细分,全面掌握常见数据结构的基础知识。再围绕问题,有针对性的设计数据结构、采用合理的算法思维,去不断完成时空转移,降低时间复杂度。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25403\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25403\350\256\262.md"
index 9d01325dc..3dd107586 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25403\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25403\350\256\262.md"
@@ -34,7 +34,7 @@
 
 ### 数据处理的基本操作
 
-不管是数组还是字典,都需要额外开辟空间,对数据进行存储。而且数据存储的数量,与输入的数据量一致。因此,消耗的空间复杂度相同,都是 O(n)。由前面的分析可见,同样采用复杂的数据结构,消耗了 O(n) 的空间复杂度,其对时间复杂度降低的贡献有可能不一样。因此,我们必须要 **设计合理的数据结构** ,以达到降低时间损耗的目的。
+不管是数组还是字典,都需要额外开辟空间,对数据进行存储。而且数据存储的数量,与输入的数据量一致。因此,消耗的空间复杂度相同,都是 O(n)。由前面的分析可见,同样采用复杂的数据结构,消耗了 O(n) 的空间复杂度,其对时间复杂度降低的贡献有可能不一样。因此,我们必须要 **设计合理的数据结构**,以达到降低时间损耗的目的。
 
 而设计合理的数据结构,又要从问题本身出发,我们可以采用这样的思考顺序:
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25410\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25410\350\256\262.md"
index 8de0b4b4f..19bfbdb33 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25410\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25410\350\256\262.md"
@@ -123,7 +123,7 @@ address (张四) = ASCII (Z) + ASCII (S) = 90 + 83 = 173;
 
 下面我们来讲解两个案例,帮助你进一步理解哈希表的操作过程。
 
-**例 1** ,将关键字序列 {7, 8, 30, 11, 18, 9, 14} 存储到哈希表中。哈希函数为: H (key) = (key * 3) % 7,处理冲突采用线性探测法。
+**例 1**,将关键字序列 {7, 8, 30, 11, 18, 9, 14} 存储到哈希表中。哈希函数为: H (key) = (key * 3) % 7,处理冲突采用线性探测法。
 
 接下来,我们分析一下建立哈希表和查找关键字的细节过程。
 
@@ -143,16 +143,16 @@ H (9) = 6
 
 H (14) = 0
 
-**按关键字序列顺序依次向哈希表中填入,发生冲突后按照“线性探测”探测到第一个空位置填入。** ![3.gif](assets/CgqCHl7p9vKAZm8sABD1_Vye6xM491.gif) **最终的插入结果如下表所示:** ![Lark20200710-172310.png](assets/CgqCHl8IM5KADjjgAABYugcwKiI662.png) **接着,有了这个表之后,我们再来看一下查找的流程:**
+**按关键字序列顺序依次向哈希表中填入,发生冲突后按照“线性探测”探测到第一个空位置填入。**![3.gif](assets/CgqCHl7p9vKAZm8sABD1_Vye6xM491.gif) **最终的插入结果如下表所示:**![Lark20200710-172310.png](assets/CgqCHl8IM5KADjjgAABYugcwKiI662.png) **接着,有了这个表之后,我们再来看一下查找的流程:**
 
 - 查找 7。输入 7,计算得到 H (7) = 0,根据哈希表,在 0 的位置,得到结果为 7,跟待匹配的关键字一样,则完成查找。
 - 查找 18。输入 18,计算得到 H (18) = 5,根据哈希表,在 5 的位置,得到结果为 11,跟待匹配的关键字不一样(11 不等于 18)。因此,往后挪移一位,在 6 的位置,得到结果为 30,跟待匹配的关键字不一样(11 不等于 30)。因此,继续往后挪移一位,在 7 的位置,得到结果为 18,跟待匹配的关键字一样,完成查找。
 
-**例 2** ,假设有一个在线系统,可以实时接收用户提交的字符串型关键字,并实时返回给用户累积至今这个关键字被提交的次数。
+**例 2**,假设有一个在线系统,可以实时接收用户提交的字符串型关键字,并实时返回给用户累积至今这个关键字被提交的次数。
 
-例如,用户输入"abc",系统返回 1。用户再输入"jk",系统返回 1。用户再输入"xyz",系统返回 1。用户再输入"abc",系统返回 2。用户再输入"abc",系统返回 3。 **一种解决方法是** ,用一个数组保存用户提交过的所有关键字。当接收到一个新的关键字后,插入到数组中,并且统计这个关键字出现的次数。
+例如,用户输入"abc",系统返回 1。用户再输入"jk",系统返回 1。用户再输入"xyz",系统返回 1。用户再输入"abc",系统返回 2。用户再输入"abc",系统返回 3。**一种解决方法是**,用一个数组保存用户提交过的所有关键字。当接收到一个新的关键字后,插入到数组中,并且统计这个关键字出现的次数。
 
-根据数组的知识可以计算出,插入到最后的动作,时间复杂度是 O(1)。但统计出现次数必须要全部数据遍历一遍,时间复杂度是 O(n)。随着数据越来越多,这个在线系统的处理时间将会越来越长。显然,这不是一个好的方法。 **如果采用哈希表** ,则可以利用哈希表新增、查找的常数级时间复杂度,在 O(1) 时间复杂度内完成响应。预先定义好哈希表后(可以采用 Map \< String, Integer > d = new HashMap \<> (); )对于关键字(用变量 key_str 保存),判断 d 中是否存在 key_str 的记录。
+根据数组的知识可以计算出,插入到最后的动作,时间复杂度是 O(1)。但统计出现次数必须要全部数据遍历一遍,时间复杂度是 O(n)。随着数据越来越多,这个在线系统的处理时间将会越来越长。显然,这不是一个好的方法。**如果采用哈希表**,则可以利用哈希表新增、查找的常数级时间复杂度,在 O(1) 时间复杂度内完成响应。预先定义好哈希表后(可以采用 Map \< String, Integer > d = new HashMap \<> (); )对于关键字(用变量 key_str 保存),判断 d 中是否存在 key_str 的记录。
 
 - 如果存在,则把它对应的value(用来记录出现的频次)加 1;
 - 如果不存在,则把它添加到 d 中,对应的 value 赋值为 1。最后,打印处 key_str 对应的 value,即累积出现的频次。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25411\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25411\350\256\262.md"
index fba862d7b..57d9beb76 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25411\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25411\350\256\262.md"
@@ -15,9 +15,9 @@
 1. 递归问题必须可以分解为若干个规模较小、与原问题形式相同的子问题。并且这些子问题可以用完全相同的解题思路来解决;
 1. 递归问题的演化过程是一个对原问题从大到小进行拆解的过程,并且会有一个明确的终点(临界点)。一旦原问题到达了这个临界点,就不用再往更小的问题上拆解了。最后,从这个临界点开始,把小问题的答案按照原路返回,原问题便得以解决。
 
-简而言之, **递归的基本思想就是把规模大的问题转化为规模小的相同的子问题来解决。** 在函数实现时,因为大问题和小问题是一样的问题,因此大问题的解决方法和小问题的解决方法也是同一个方法。这就产生了函数调用它自身的情况,这也正是递归的定义所在。
+简而言之,**递归的基本思想就是把规模大的问题转化为规模小的相同的子问题来解决。** 在函数实现时,因为大问题和小问题是一样的问题,因此大问题的解决方法和小问题的解决方法也是同一个方法。这就产生了函数调用它自身的情况,这也正是递归的定义所在。
 
-格外重要的是, **这个解决问题的函数必须有明确的结束条件,否则就会导致无限递归的情况** 。总结起来, **递归的实现包含了两个部分,一个是递归主体,另一个是终止条件** 。
+格外重要的是,**这个解决问题的函数必须有明确的结束条件,否则就会导致无限递归的情况** 。总结起来,**递归的实现包含了两个部分,一个是递归主体,另一个是终止条件** 。
 
 ### 递归的算法思想
 
@@ -65,7 +65,7 @@ public static void inOrderTraverse(Node node) {
 }
 ```
 
-以上就是递归的算法思想。我们总结一下, **写出递归代码的关键在于,写出递推公式和找出终止条件。** 也就是说我们需要:首先找到将大问题分解成小问题的规律,并基于此写出递推公式;然后找出终止条件,就是当找到最简单的问题时,如何写出答案;最终将递推公式和终止条件翻译成实际代码。
+以上就是递归的算法思想。我们总结一下,**写出递归代码的关键在于,写出递推公式和找出终止条件。** 也就是说我们需要:首先找到将大问题分解成小问题的规律,并基于此写出递推公式;然后找出终止条件,就是当找到最简单的问题时,如何写出答案;最终将递推公式和终止条件翻译成实际代码。
 
 ### 递归的案例
 
@@ -85,7 +85,7 @@ public static void inOrderTraverse(Node node) {
 
 1. 把从小到大的 n-1 个盘子,从 x 移动到 y;
 1. 接着把最大的一个盘子,从 x 移动到 z;
-1. 再把从小到大的 n-1 个盘子,从 y 移动到 z。 **首先,我们来判断它是否满足递归的第一个条件。** 其中,第 1 和第 3 个问题就是汉诺塔问题。这样我们就完成了一次把大问题缩小为完全一样的小规模问题。我们已经定义好了递归体,也就是满足来递归的第一个条件。如下图所示:
+1. 再把从小到大的 n-1 个盘子,从 y 移动到 z。**首先,我们来判断它是否满足递归的第一个条件。** 其中,第 1 和第 3 个问题就是汉诺塔问题。这样我们就完成了一次把大问题缩小为完全一样的小规模问题。我们已经定义好了递归体,也就是满足来递归的第一个条件。如下图所示:
 
 ![5.gif](assets/Ciqc1F7wjAuAJ7yrAAzAObiXQfs227.gif) **接下来我们来看判断它是否满足终止条件** 。随着递归体不断缩小范围,汉诺塔问题由原来“移动从小到大的 n 个盘子”,缩小为“移动从小到大的 n-1 个盘子”,直到缩小为“移动从小到大的 1 个盘子”。移动从小到大的 1 个盘子,就是移动最小的那个盘子。根据规则可以发现,最小的盘子是可以自由移动的。因此,递归的第二个条件,终止条件,也是满足的。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25412\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25412\350\256\262.md"
index 65ef36df5..f867bfe08 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25412\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25412\350\256\262.md"
@@ -24,7 +24,7 @@
 
 因此,要想通过上述方法直接解决一个规模较大的问题,其实是相当困难的。
 
-基于此, **分治法的核心思想就是分而治之** 。具体来说,它先将一个难以直接解决的大问题,分割成一些可以直接解决的小问题。如果分割后的问题仍然无法直接解决,那么就继续递归地分割,直到每个小问题都可解。
+基于此,**分治法的核心思想就是分而治之** 。具体来说,它先将一个难以直接解决的大问题,分割成一些可以直接解决的小问题。如果分割后的问题仍然无法直接解决,那么就继续递归地分割,直到每个小问题都可解。
 
 通常而言,这些子问题具备互相独立、形式相同的特点。这样,我们就可以采用同一种解法,递归地去解决这些子问题。最后,再将每个子问题的解合并,就得到了原问题的解。
 
@@ -32,22 +32,22 @@
 
 关于分治法,很多同学都有这样一个误区。那就是,当你的计算机性能还不错的时候,采用分治法相对于全局遍历一遍没有什么差别。
 
-例如下面这个问题, **在 1000 个有序数字构成的数组 a 中,判断某个数字 c 是否出现过。**  **第一种方法,全局遍历。** 复杂度 O(n)。采用 for 循环,对 1000 个数字全部判断一遍。 **第二种方法,采用二分查找。** 复杂度 O(logn)。递归地判断 c 与 a 的中位数的大小关系,并不断缩小范围。
+例如下面这个问题,**在 1000 个有序数字构成的数组 a 中,判断某个数字 c 是否出现过。**  **第一种方法,全局遍历。** 复杂度 O(n)。采用 for 循环,对 1000 个数字全部判断一遍。**第二种方法,采用二分查找。** 复杂度 O(logn)。递归地判断 c 与 a 的中位数的大小关系,并不断缩小范围。
 
 这两种方法,对时间的消耗几乎一样。那分治法的价值又是什么呢?
 
 其实,在小数据规模上,分治法没有什么特殊价值。无非就是让代码显得更牛一些。只有在大数据集上,分治法的价值才能显现出来。
 
-下面我们通过一个经典的案例带你感受分治法的价值。 **假如有一张厚度为 1 毫米且足够柔软的纸,问将它对折多少次之后,厚度能达到地球到月球的距离?** 这个问题看起来很异想天开。根据百度百科,地月平均距离是 384,403.9 千米,大约 39 万千米。粗看怎么也需要对折 1 万次吧?但实际上,根据计算,我们只需要对折 39 次就够了。计算的过程是 239 = 549,755,813,888 = 55 万千米 > 39 万千米。那么,这个例子意味着什么呢?
+下面我们通过一个经典的案例带你感受分治法的价值。**假如有一张厚度为 1 毫米且足够柔软的纸,问将它对折多少次之后,厚度能达到地球到月球的距离?** 这个问题看起来很异想天开。根据百度百科,地月平均距离是 384,403.9 千米,大约 39 万千米。粗看怎么也需要对折 1 万次吧?但实际上,根据计算,我们只需要对折 39 次就够了。计算的过程是 239 = 549,755,813,888 = 55 万千米 > 39 万千米。那么,这个例子意味着什么呢?
 
-我们回到前面讲到的在数组 a 中查找数字 c 的例子,如果数组 a 的大小拓展到 549,755,813,888 这个量级上,使用第二种的二分查找方法,仅仅需要 39 次判断,就能找到最终结果。相比暴力搜索的方法,性能优势高的不是一星半点!这也证明了, **复杂度为 O(logn) 相比复杂度为 O(n) 的算法,在大数据集合中性能有着爆发式的提高。** ### 分治法的使用方法
+我们回到前面讲到的在数组 a 中查找数字 c 的例子,如果数组 a 的大小拓展到 549,755,813,888 这个量级上,使用第二种的二分查找方法,仅仅需要 39 次判断,就能找到最终结果。相比暴力搜索的方法,性能优势高的不是一星半点!这也证明了,**复杂度为 O(logn) 相比复杂度为 O(n) 的算法,在大数据集合中性能有着爆发式的提高。** ### 分治法的使用方法
 
-前面我们讲到分治法的核心思想是“分而治之”,当你需要采用分治法时, **一般原问题都需要具备以下几个特征:** 1. **难度在降低** ,即原问题的解决难度,随着数据的规模的缩小而降低。这个特征绝大多数问题都是满足的。
-2. **问题可分** ,原问题可以分解为若干个规模较小的同类型问题。这是应用分治法的前提。
-3. **解可合并** ,利用所有子问题的解,可合并出原问题的解。这个特征很关键,能否利用分治法完全取决于这个特征。
-4. **相互独立** ,各个子问题之间相互独立,某个子问题的求解不会影响到另一个子问题。如果子问题之间不独立,则分治法需要重复地解决公共的子问题,造成效率低下的结果。
+前面我们讲到分治法的核心思想是“分而治之”,当你需要采用分治法时,**一般原问题都需要具备以下几个特征:** 1. **难度在降低**,即原问题的解决难度,随着数据的规模的缩小而降低。这个特征绝大多数问题都是满足的。
+2. **问题可分**,原问题可以分解为若干个规模较小的同类型问题。这是应用分治法的前提。
+3. **解可合并**,利用所有子问题的解,可合并出原问题的解。这个特征很关键,能否利用分治法完全取决于这个特征。
+4. **相互独立**,各个子问题之间相互独立,某个子问题的求解不会影响到另一个子问题。如果子问题之间不独立,则分治法需要重复地解决公共的子问题,造成效率低下的结果。
 
-根据前面我们对分治法的分析,你一定能迅速联想到递归。分治法需要递归地分解问题,再去解决问题。因此, **分治法在每轮递归上,都包含了分解问题、解决问题和合并结果这 3 个步骤。** 为了让大家对分治法有更清晰地了解,我们以二分查找为例,看一下分治法如何使用。关于分治法在排序中的使用,我们会在第 11 课时中讲到。查找问题指的是,在一个有序的数列中,判断某个待查找的数字是否出现过。二分查找,则是利用分治法去解决查找问题。通常二分查找需要一个前提,那就是输入的数列是有序的。 **二分查找的思路比较简单,步骤如下** :
+根据前面我们对分治法的分析,你一定能迅速联想到递归。分治法需要递归地分解问题,再去解决问题。因此,**分治法在每轮递归上,都包含了分解问题、解决问题和合并结果这 3 个步骤。** 为了让大家对分治法有更清晰地了解,我们以二分查找为例,看一下分治法如何使用。关于分治法在排序中的使用,我们会在第 11 课时中讲到。查找问题指的是,在一个有序的数列中,判断某个待查找的数字是否出现过。二分查找,则是利用分治法去解决查找问题。通常二分查找需要一个前提,那就是输入的数列是有序的。**二分查找的思路比较简单,步骤如下** :
 
 1. 选择一个标志 i 将集合 L 分为二个子集合,一般可以使用中位数;
 1. 判断标志 L(i) 是否能与要查找的值 des 相等,相等则直接返回结果;
@@ -59,7 +59,7 @@
 
 ### 分治法的案例
 
-下面我们一起来看一个例子。 **在数组 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } 中,查找 8 是否出现过。** 首先判断 8 和中位数 5 的大小关系。因为 8 更大,所以在更小的范围 6, 7, 8, 9, 10 中继续查找。此时更小的范围的中位数是 8。由于 8 等于中位数 8,所以查找到并打印查找到的 8 对应在数组中的 index 值。如下图所示。
+下面我们一起来看一个例子。**在数组 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } 中,查找 8 是否出现过。** 首先判断 8 和中位数 5 的大小关系。因为 8 更大,所以在更小的范围 6, 7, 8, 9, 10 中继续查找。此时更小的范围的中位数是 8。由于 8 等于中位数 8,所以查找到并打印查找到的 8 对应在数组中的 index 值。如下图所示。
 
 ![Lark20200624-163712.gif](assets/Ciqc1F7zEOSAElX7ABXXgmxI808203.gif)
 
@@ -135,4 +135,4 @@ public static void main(String[] args) {
 
 ### 总结
 
-分治法经常会用在海量数据处理中。这也是它显著区别于遍历查找方法的优势。 **在面对陌生问题时,需要注意原问题的数据是否有序,预期的时间复杂度是否带有 logn 项,是否可以通过小问题的答案合并出原问题的答案。如果这些先决条件都满足,你就应该第一时间想到分治法。** 
\ No newline at end of file
+分治法经常会用在海量数据处理中。这也是它显著区别于遍历查找方法的优势。**在面对陌生问题时,需要注意原问题的数据是否有序,预期的时间复杂度是否带有 logn 项,是否可以通过小问题的答案合并出原问题的答案。如果这些先决条件都满足,你就应该第一时间想到分治法。** 
\ No newline at end of file
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25413\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25413\350\256\262.md"
index 91f87cf11..d536e2203 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25413\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25413\350\256\262.md"
@@ -8,11 +8,11 @@
 
 衡量一个排序算法的优劣,我们主要会从以下 3 个角度进行分析:
 
-1. **时间复杂度** ,具体包括,最好时间复杂度、最坏时间复杂度以及平均时间复杂度。
+1. **时间复杂度**,具体包括,最好时间复杂度、最坏时间复杂度以及平均时间复杂度。
 
-2. **空间复杂度** ,如果空间复杂度为 1,也叫作原地排序。
+2. **空间复杂度**,如果空间复杂度为 1,也叫作原地排序。
 
-3. **稳定性** ,排序的稳定性是指相等的数据对象,在排序之后,顺序是否能保证不变。
+3. **稳定性**,排序的稳定性是指相等的数据对象,在排序之后,顺序是否能保证不变。
 
 ### 常见的排序算法及其思想
 
@@ -24,9 +24,9 @@
 
 ![动画1.gif](assets/CgqCHl75xgeAF_xkABrEk0C0heo355.gif)
 
-2、 **冒泡排序的性能**  **冒泡排序最好时间复杂度是 O(n)** ,也就是当输入数组刚好是顺序的时候,只需要挨个比较一遍就行了,不需要做交换操作,所以时间复杂度为 O(n)。 **冒泡排序最坏时间复杂度会比较惨,是 O(n*n)** 。也就是说当数组刚好是完全逆序的时候,每轮排序都需要挨个比较 n 次,并且重复 n 次,所以时间复杂度为 O(n\*n)。
+2、 **冒泡排序的性能**  **冒泡排序最好时间复杂度是 O(n)**,也就是当输入数组刚好是顺序的时候,只需要挨个比较一遍就行了,不需要做交换操作,所以时间复杂度为 O(n)。**冒泡排序最坏时间复杂度会比较惨,是 O(n*n)** 。也就是说当数组刚好是完全逆序的时候,每轮排序都需要挨个比较 n 次,并且重复 n 次,所以时间复杂度为 O(n\*n)。
 
-很显然, **当输入数组杂乱无章时,它的平均时间复杂度也是 O(n*n)** 。 **冒泡排序不需要额外的空间,所以空间复杂度是 O(1)。冒泡排序过程中,当元素相同时不做交换,所以冒泡排序是稳定的排序算法** 。代码如下:
+很显然,**当输入数组杂乱无章时,它的平均时间复杂度也是 O(n*n)** 。**冒泡排序不需要额外的空间,所以空间复杂度是 O(1)。冒泡排序过程中,当元素相同时不做交换,所以冒泡排序是稳定的排序算法** 。代码如下:
 
 ```java
 public static void main(String[] args) {
@@ -51,13 +51,13 @@ public static void main(String[] args) {
 
 ![动画2.gif](assets/CgqCHl75xmqAXrQnAB7zyryidSU192.gif)
 
-2、 **插入排序的性能**  **插入排序最好时间复杂度是 O(n)** ,即当数组刚好是完全顺序时,每次只用比较一次就能找到正确的位置。这个过程重复 n 次,就可以清空未排序区间。 **插入排序最坏时间复杂度则需要 O(n*n)** 。即当数组刚好是完全逆序时,每次都要比较 n 次才能找到正确位置。这个过程重复 n 次,就可以清空未排序区间,所以最坏时间复杂度为 O(n\*n)。
+2、 **插入排序的性能**  **插入排序最好时间复杂度是 O(n)**,即当数组刚好是完全顺序时,每次只用比较一次就能找到正确的位置。这个过程重复 n 次,就可以清空未排序区间。**插入排序最坏时间复杂度则需要 O(n*n)** 。即当数组刚好是完全逆序时,每次都要比较 n 次才能找到正确位置。这个过程重复 n 次,就可以清空未排序区间,所以最坏时间复杂度为 O(n\*n)。
 
 **插入排序的平均时间复杂度是 O(n*n)** 。这是因为往数组中插入一个元素的平均时间复杂度为 O(n),而插入排序可以理解为重复 n 次的数组插入操作,所以平均时间复杂度为 O(n\*n)。
 
 **插入排序不需要开辟额外的空间,所以空间复杂度是 O(1)** 。
 
-根据上面的例子可以发现, **插入排序是稳定的排序算法** 。代码如下:
+根据上面的例子可以发现,**插入排序是稳定的排序算法** 。代码如下:
 
 ```java
 public static void main(String[] args) {
@@ -138,7 +138,7 @@ public static void customDoubleMerge(int[] a, int[] tmp, int left, int mid, int
 
 2、 **归并排序的性能**  **对于归并排序,它采用了二分的迭代方式,复杂度是 logn** 。
 
-每次的迭代,需要对两个有序数组进行合并,这样的动作在 O(n) 的时间复杂度下就可以完成。因此, **归并排序的复杂度就是二者的乘积 O(nlogn)。** 同时, **它的执行频次与输入序列无关,因此,归并排序最好、最坏、平均时间复杂度都是 O(nlogn)** 。
+每次的迭代,需要对两个有序数组进行合并,这样的动作在 O(n) 的时间复杂度下就可以完成。因此,**归并排序的复杂度就是二者的乘积 O(nlogn)。** 同时,**它的执行频次与输入序列无关,因此,归并排序最好、最坏、平均时间复杂度都是 O(nlogn)** 。
 
 **空间复杂度方面,由于每次合并的操作都需要开辟基于数组的临时内存空间,所以空间复杂度为 O(n)** 。归并排序合并的时候,相同元素的前后顺序不变,所以 **归并是稳定的排序算法** 。
 
@@ -187,17 +187,17 @@ public void customQuickSort(int[] arr, int low, int high) {
 }
 ```
 
-2、 **快速排序法的性能**  **在快排的最好时间的复杂度下** ,如果每次选取分区点时,都能选中中位数,把数组等分成两个,那么 **此时的时间复杂度和归并一样,都是 O(n*logn)** 。 **而在最坏的时间复杂度下** ,也就是如果每次分区都选中了最小值或最大值,得到不均等的两组。那么就需要 n 次的分区操作,每次分区平均扫描 n / 2 个元素, **此时时间复杂度就退化为 O(n*n) 了** 。 **快速排序法在大部分情况下,统计上是很难选到极端情况的。因此它平均的时间复杂度是 O(n*logn)** 。 **快速排序法的空间方面,使用了交换法,因此空间复杂度为 O(1)** 。
+2、 **快速排序法的性能**  **在快排的最好时间的复杂度下**,如果每次选取分区点时,都能选中中位数,把数组等分成两个,那么 **此时的时间复杂度和归并一样,都是 O(n*logn)** 。**而在最坏的时间复杂度下**,也就是如果每次分区都选中了最小值或最大值,得到不均等的两组。那么就需要 n 次的分区操作,每次分区平均扫描 n / 2 个元素,**此时时间复杂度就退化为 O(n*n) 了** 。**快速排序法在大部分情况下,统计上是很难选到极端情况的。因此它平均的时间复杂度是 O(n*logn)** 。**快速排序法的空间方面,使用了交换法,因此空间复杂度为 O(1)** 。
 
 很显然,快速排序的分区过程涉及交换操作,所以 **快排是不稳定的排序算法** 。
 
 ### 排序算法的性能分析
 
-我们先思考一下排序算法性能的下限,也就是最差的情况。在前面的课程中,我们写过求数组最大值的代码,它的时间复杂度是 O(n)。对于 n 个元素的数组,只要重复执行 n 次最大值的查找就能完成排序。因此 **排序最暴力的方法,时间复杂度是 O(n*n)。这恰如冒泡排序和插入排序** 。 **当我们利用算法思维去解决问题时,就会想到尝试分治法。此时,利用归并排序就能让时间复杂度降低到 O(nlogn)** 。然而, **归并排序需要额外开辟临时空间。一方面是为了保证稳定性,另一方面则是在归并时,由于在数组中插入元素导致了数据挪移的问题。**  **为了规避因此而带来的时间损耗,此时我们采用快速排序** 。通过交换操作,可以解决插入元素导致的数据挪移问题,而且降低了不必要的空间开销。但是由于其动态二分的交换数据,导致了由此得出的排序结果并不稳定。
+我们先思考一下排序算法性能的下限,也就是最差的情况。在前面的课程中,我们写过求数组最大值的代码,它的时间复杂度是 O(n)。对于 n 个元素的数组,只要重复执行 n 次最大值的查找就能完成排序。因此 **排序最暴力的方法,时间复杂度是 O(n*n)。这恰如冒泡排序和插入排序** 。**当我们利用算法思维去解决问题时,就会想到尝试分治法。此时,利用归并排序就能让时间复杂度降低到 O(nlogn)** 。然而,**归并排序需要额外开辟临时空间。一方面是为了保证稳定性,另一方面则是在归并时,由于在数组中插入元素导致了数据挪移的问题。**  **为了规避因此而带来的时间损耗,此时我们采用快速排序** 。通过交换操作,可以解决插入元素导致的数据挪移问题,而且降低了不必要的空间开销。但是由于其动态二分的交换数据,导致了由此得出的排序结果并不稳定。
 
 ### 总结
 
-本课时我们讲了4 种常见的排序算法,包括冒泡排序、插入排序、归并排序以及快速排序。这些经典算法没有绝对的好和坏,它们各有利弊。在工作过程中,需要你根据实际问题的情况来选择最优的排序算法。 **如果对数据规模比较小的数据进行排序,可以选择时间复杂度为 O(n*n) 的排序算法** 。因为当数据规模小的时候,时间复杂度 O(nlogn) 和 O(n\*n) 的区别很小,它们之间仅仅相差几十毫秒,因此对实际的性能影响并不大。
+本课时我们讲了4 种常见的排序算法,包括冒泡排序、插入排序、归并排序以及快速排序。这些经典算法没有绝对的好和坏,它们各有利弊。在工作过程中,需要你根据实际问题的情况来选择最优的排序算法。**如果对数据规模比较小的数据进行排序,可以选择时间复杂度为 O(n*n) 的排序算法** 。因为当数据规模小的时候,时间复杂度 O(nlogn) 和 O(n\*n) 的区别很小,它们之间仅仅相差几十毫秒,因此对实际的性能影响并不大。
 
 **但对数据规模比较大的数据进行排序,就需要选择时间复杂度为 O(nlogn) 的排序算法了** 。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25414\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25414\350\256\262.md"
index 69ff020b9..5875d0a9a 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25414\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25414\350\256\262.md"
@@ -2,7 +2,7 @@
 
 在前面课时中,我们学习了分治法的思想,并以二分查找为例介绍了分治的实现逻辑。
 
-我们提到过, **分治法的使用必须满足 4 个条件:** 1.  问题的解决难度与数据规模有关;
+我们提到过,**分治法的使用必须满足 4 个条件:** 1.  问题的解决难度与数据规模有关;
 2\.  原问题可被分解;
 3\.  子问题的解可以合并为原问题的解;
 4\.  所有的子问题相互独立。
@@ -11,13 +11,13 @@
 
 ### 什么是动态规划 **从数学的视角来看,动态规划是一种运筹学方法,是在多轮决策过程中的最优方法**
 
-那么,什么是多轮决策呢?其实多轮决策的每一轮都可以看作是一个子问题。 **从分治法的视角来看,每个子问题必须相互独立。但在多轮决策中,这个假设显然不成立。这也是动态规划方法产生的原因之一** 。
+那么,什么是多轮决策呢?其实多轮决策的每一轮都可以看作是一个子问题。**从分治法的视角来看,每个子问题必须相互独立。但在多轮决策中,这个假设显然不成立。这也是动态规划方法产生的原因之一** 。
 
 动态规划是候选人参加面试的噩梦,也是面试过程中的难点。虽然动态规划很难,但在实际的工作中,使用频率并不高,不是所有的岗位都会用到动态规划。
 
 #### 最短路径问题
 
-接下来。 **我们来看一个非常典型的例子,最短路径问题** 。如下图所示:
+接下来。**我们来看一个非常典型的例子,最短路径问题** 。如下图所示:
 
 ![image](assets/Ciqc1F78bdmAGdktAADnlpYQrHk607.png)
 
@@ -25,28 +25,28 @@
 
 不难发现,我们需要求解的路线是由 A 到 G,这就意味着 A 要先到 B,再到 C,再到 D,再到 E,再到 F。每一轮都需要做不同的决策,而每次的决策又依赖上一轮决策的结果。
 
-例如,做 D2 -> E 的决策时,D2 -> E2 的距离为 1,最短。但这轮的决策,基于的假设是从 D2 出发,这就意味着前面一轮的决策结果是 D2。由此可见,相邻两轮的决策结果并不是独立的。 **动态规划还有一个重要概念叫作状态** 。在这个例子中,状态是个变量,而且受决策动作的影响。例如,第一轮决策的状态是 S1,可选的值是 A,第二轮决策的状态是 S2,可选的值就是 B1 和 B2。以此类推。
+例如,做 D2 -> E 的决策时,D2 -> E2 的距离为 1,最短。但这轮的决策,基于的假设是从 D2 出发,这就意味着前面一轮的决策结果是 D2。由此可见,相邻两轮的决策结果并不是独立的。**动态规划还有一个重要概念叫作状态** 。在这个例子中,状态是个变量,而且受决策动作的影响。例如,第一轮决策的状态是 S1,可选的值是 A,第二轮决策的状态是 S2,可选的值就是 B1 和 B2。以此类推。
 
 ### 动态规划的基本方法
 
-动态规划问题之所以难,是因为动态规划的解题方法并没有那么标准化,它需要你因题而异,仔细分析问题并寻找解决方案。 **虽然动态规划问题没有标准化的解题方法,但它有一些宏观层面通用的方法论** :
+动态规划问题之所以难,是因为动态规划的解题方法并没有那么标准化,它需要你因题而异,仔细分析问题并寻找解决方案。**虽然动态规划问题没有标准化的解题方法,但它有一些宏观层面通用的方法论** :
 
 > 下面的 k 表示多轮决策的第 k 轮
 
-1. **分阶段** ,将原问题划分成几个子问题。一个子问题就是多轮决策的一个阶段,它们可以是不满足独立性的。
-2. **找状态** ,选择合适的状态变量 Sk。它需要具备描述多轮决策过程的演变,更像是决策可能的结果。
+1. **分阶段**,将原问题划分成几个子问题。一个子问题就是多轮决策的一个阶段,它们可以是不满足独立性的。
+2. **找状态**,选择合适的状态变量 Sk。它需要具备描述多轮决策过程的演变,更像是决策可能的结果。
 3. **做决** 策,确定决策变量 uk。每一轮的决策就是每一轮可能的决策动作,例如 D2 的可能的决策动作是 D2 -> E2 和 D2 -> E3。
-4. **状态转移方程** 。这个步骤是动态规划最重要的核心, **即 sk+1= uk(sk) 。** 5. **定目标** 。写出代表多轮决策目标的指标函数 Vk,n。
+4. **状态转移方程** 。这个步骤是动态规划最重要的核心,**即 sk+1= uk(sk) 。** 5. **定目标** 。写出代表多轮决策目标的指标函数 Vk,n。
 6. **寻找终止条件** 。
 
 了解了方法论、状态、多轮决策之后,我们再补充一些动态规划的基本概念。
 
-- **策略** ,每轮的动作是决策,多轮决策合在一起常常被称为策略。
-- **策略集合** ,由于每轮的决策动作都是一个变量,这就导致合在一起的策略也是一个变量。我们通常会称所有可能的策略为策略集合。因此,动态规划的目标,也可以说是从策略集合中,找到最优的那个策略。 **一般而言,具有如下几个特征的问题,可以采用动态规划求解** :
+- **策略**,每轮的动作是决策,多轮决策合在一起常常被称为策略。
+- **策略集合**,由于每轮的决策动作都是一个变量,这就导致合在一起的策略也是一个变量。我们通常会称所有可能的策略为策略集合。因此,动态规划的目标,也可以说是从策略集合中,找到最优的那个策略。**一般而言,具有如下几个特征的问题,可以采用动态规划求解** :
 
 1. **最优子结构** 。它的含义是,原问题的最优解所包括的子问题的解也是最优的。例如,某个策略使得 A 到 G 是最优的。假设它途径了 Fi,那么它从 A 到 Fi 也一定是最优的。
 2. **无后效性** 。某阶段的决策,无法影响先前的状态。可以理解为今天的动作改变不了历史。
-3. **有重叠子问题** 。也就是,子问题之间不独立。 **这个性质是动态规划区别于分治法的条件** 。如果原问题不满足这个特征,也是可以用动态规划求解的,无非就是杀鸡用了宰牛刀。
+3. **有重叠子问题** 。也就是,子问题之间不独立。**这个性质是动态规划区别于分治法的条件** 。如果原问题不满足这个特征,也是可以用动态规划求解的,无非就是杀鸡用了宰牛刀。
 
 ### 动态规划的案例
 
@@ -83,7 +83,7 @@
 
 #### 计算过程详解
 
-好了,为了让大家清晰地看到结果,我们给出详细的计算过程。为了书写简单, **我们把函数 Vk,7(s1=A, s7=G) 精简为 V7(G),含义为经过了 6 轮决策后,状态到达 G 后所使用的距离** 。我们把图片复制到这里一份,方便大家不用上下切换。
+好了,为了让大家清晰地看到结果,我们给出详细的计算过程。为了书写简单,**我们把函数 Vk,7(s1=A, s7=G) 精简为 V7(G),含义为经过了 6 轮决策后,状态到达 G 后所使用的距离** 。我们把图片复制到这里一份,方便大家不用上下切换。
 
 ![image](assets/CgqCHl78bpKAF2FWAADnlpYQrHk836.png)
 
@@ -101,7 +101,7 @@
 
 ![imag](assets/CgqCHl78bzKAQTrCAABoEJ4y5UM123.png)
 
-因此, **最终输出路径为 A -> B1 -> C2 -> D1 -> E2 -> F2 -> G,最短距离为 18** 。
+因此,**最终输出路径为 A -> B1 -> C2 -> D1 -> E2 -> F2 -> G,最短距离为 18** 。
 
 #### 代码实现过程
 
@@ -147,7 +147,7 @@ public class testpath {
 }
 ```
 
-#### 代码解读 **下面我们对这段代码进行解读** : **代码的 27 行是主函数** ,在代码中定义了二维数组 m,对应于输入的距离图。m 是 15 x 16 维的,我们忽略了最后一行的全 0(即使输入也不会影响结果)。 **然后调用函数 minPath1** 。 **在第 2 到第 4 行** ,它的内部又调用了 process1(matrix, matrix\[0\].length-1)。在这里,matrix\[0\].length-1 的值是 15,表示的含义是 matrix 数组的第 16 列(G)是目的地。 **接着进入 process1 函数中** 。我们知道在动态规划的过程中,是从后往前不断地推进结果,这就是状态转移的过程。 **对应代码中的 13-24 行:**
+#### 代码解读 **下面我们对这段代码进行解读** : **代码的 27 行是主函数**,在代码中定义了二维数组 m,对应于输入的距离图。m 是 15 x 16 维的,我们忽略了最后一行的全 0(即使输入也不会影响结果)。**然后调用函数 minPath1** 。**在第 2 到第 4 行**,它的内部又调用了 process1(matrix, matrix\[0\].length-1)。在这里,matrix\[0\].length-1 的值是 15,表示的含义是 matrix 数组的第 16 列(G)是目的地。**接着进入 process1 函数中** 。我们知道在动态规划的过程中,是从后往前不断地推进结果,这就是状态转移的过程。**对应代码中的 13-24 行:**
 
 - 第 15 行开始循环,j 变量是纵向的循环变量。
 - 第 16 行判断 matrix\[j\]\[i\] 与 0 的关系,含义为,只有值不为 0 才说明两个结点之间存在通路。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25415\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25415\350\256\262.md"
index ffe2b3564..ff35df76c 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25415\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25415\350\256\262.md"
@@ -43,7 +43,7 @@ public void s1_3() {
 
 前面的例子只是一个简单的热身。在实际工作中,我们遇到的问题通常会更复杂多变。那么。面对这些问题是否有一些通用的解决方法呢?答案是有的。
 
-**面对一个未知问题时,你可以从复杂度入手** 。尝试去分析这个问题的时间复杂度上限是多少,也就是复杂度再高能高到哪里。这就是不计任何时间、空间损耗,采用暴力求解的方法去解题。然后分析这个问题的时间复杂度下限是多少,也就是时间复杂度再低能低到哪里。这就是你写代码的目标。 **接着,尝试去定位问题** 。在分析出这两个问题之后,就需要去设计合理的数据结构和运用合适的算法思维,从暴力求解的方法去逼近写代码的目标了。 在这里需要先定位问题,这个问题的类型就决定了采用哪种算法思维。 **最后,需要对数据操作进行分析** 。例如:在这个问题中,需要对数据进行哪些操作(增删查),数据之间是否需要保证顺序或逆序?当分析出这些操作的步骤、频次之后,就可以根据不同数据结构的特性,去合理选择你所应该使用的那几种数据结构了。
+**面对一个未知问题时,你可以从复杂度入手** 。尝试去分析这个问题的时间复杂度上限是多少,也就是复杂度再高能高到哪里。这就是不计任何时间、空间损耗,采用暴力求解的方法去解题。然后分析这个问题的时间复杂度下限是多少,也就是时间复杂度再低能低到哪里。这就是你写代码的目标。**接着,尝试去定位问题** 。在分析出这两个问题之后,就需要去设计合理的数据结构和运用合适的算法思维,从暴力求解的方法去逼近写代码的目标了。 在这里需要先定位问题,这个问题的类型就决定了采用哪种算法思维。**最后,需要对数据操作进行分析** 。例如:在这个问题中,需要对数据进行哪些操作(增删查),数据之间是否需要保证顺序或逆序?当分析出这些操作的步骤、频次之后,就可以根据不同数据结构的特性,去合理选择你所应该使用的那几种数据结构了。
 
 经过以上分析,我们对方法论进行提练,宏观上的步骤总结为以下 4 步:
 
@@ -56,7 +56,7 @@ public void s1_3() {
 
 ### 案例
 
-梳理完方法论之后,我们回过头来再看一下以前的例子,看看采用方法论是如何分析题目并找到答案的。 **例 1** ,在一个数组 a = \[1, 3, 4, 3, 4, 1, 3\] 中,找到出现次数最多的那个数字。如果并列存在多个,随机输出一个。 **我们先来分析一下复杂度。假设我们采用最暴力的方法。利用双层循环的方式计算** :
+梳理完方法论之后,我们回过头来再看一下以前的例子,看看采用方法论是如何分析题目并找到答案的。**例 1**,在一个数组 a = \[1, 3, 4, 3, 4, 1, 3\] 中,找到出现次数最多的那个数字。如果并列存在多个,随机输出一个。**我们先来分析一下复杂度。假设我们采用最暴力的方法。利用双层循环的方式计算** :
 
 - 第一层循环,我们对数组中的每个元素进行遍历;
 - 第二层循环,对于每个元素计算出现的次数,并且通过当前元素次数 time_tmp 和全局最大次数变量 time_max 的大小关系,持续保存出现次数最多的那个元素及其出现次数。
@@ -103,9 +103,9 @@ public void s2_4() {
 }
 ```
 
-这个问题,我们在前面的课时中曾给出了答案。答案并不是最重要的,重要的是它背后的解题思路。这个思路可以运用在很多我们没有遇到过的复杂问题中。例如下面的问题。 **例 2** ,这个问题是力扣的经典问题,two sums。给定一个整数数组 arr 和一个目标值 target,请你在该数组中找出加和等于目标值的两个整数,并返回它们在原数组中的下标。
+这个问题,我们在前面的课时中曾给出了答案。答案并不是最重要的,重要的是它背后的解题思路。这个思路可以运用在很多我们没有遇到过的复杂问题中。例如下面的问题。**例 2**,这个问题是力扣的经典问题,two sums。给定一个整数数组 arr 和一个目标值 target,请你在该数组中找出加和等于目标值的两个整数,并返回它们在原数组中的下标。
 
-你可以假设,原数组中没有重复元素,而且有且只有一组答案。但是,数组中的元素只能使用一次。例如,arr = \[1, 2, 3, 4, 5, 6\],target = 4。因为,arr\[0\] + arr\[2\] = 1 + 3 = 4 = target,则输出 0,2。 **首先,我们来分析一下复杂度** 。假设我们采用最暴力的方法,利用双层循环的方式计算,步骤如下:
+你可以假设,原数组中没有重复元素,而且有且只有一组答案。但是,数组中的元素只能使用一次。例如,arr = \[1, 2, 3, 4, 5, 6\],target = 4。因为,arr\[0\] + arr\[2\] = 1 + 3 = 4 = target,则输出 0,2。**首先,我们来分析一下复杂度** 。假设我们采用最暴力的方法,利用双层循环的方式计算,步骤如下:
 
 - 第一层循环,我们对数组中的每个元素进行遍历;
 - 第二层循环,对于第一层的元素与 target 的差值进行查找。
@@ -118,7 +118,7 @@ public void s2_4() {
 
 在暴力的方法中,第二层循环的目的是查找 target - arr\[i\] 是否出现在数组中。很自然地就会联想到可能要使用哈希表。同时,这个例子中对于数据处理的顺序并不关心,栈或者队列使用的可能性也会很低。因此,不妨试试如何用哈希表去降低复杂度。
 
-既然是要查找 target - arr\[i\] 是否出现过,因此哈希表的 key 自然就是 target - arr\[i\]。而 value 如何设计呢?这就要看一下结果了,最终要输出的是查找到的 arr\[i\] 和 target - arr\[i\] 在数组中的索引,因此 value 存放的必然是 index 的索引值。 **基于上面的分析,我们就能找到解决方案,分析如下** :
+既然是要查找 target - arr\[i\] 是否出现过,因此哈希表的 key 自然就是 target - arr\[i\]。而 value 如何设计呢?这就要看一下结果了,最终要输出的是查找到的 arr\[i\] 和 target - arr\[i\] 在数组中的索引,因此 value 存放的必然是 index 的索引值。**基于上面的分析,我们就能找到解决方案,分析如下** :
 
 1. 预期的时间复杂度是 O(n),这就意味着编码采用一层的 for 循环,对原数组进行遍历。
 1. 数据结构需要额外设计哈希表,其中 key 是 target - arr\[i\],value 是 index。这样可以支持 O(1) 时间复杂度的查找动作。
@@ -143,7 +143,7 @@ private static int[] twoSum(int[] arr, int target) {
 
 在这段代码中我们采用了两个 for 循环,时间复杂度就是 O(n) + O(n) = O(n)。额外使用了 map,空间复杂度也是 O(n)。第一个 for 循环,把数组转为字典,存放的是“数值 -index”的键值对。第二个 for 循环,在字典中依次判断,target - arr\[i\] 是否出现过。如果它出现过,且不是它自己,则打印 target - arr\[i\] 和 arr\[i\] 的索引。
 
-### 总结 **在开发前,一定要对问题的复杂度进行分析,做好技术选型。这就是定位问题的过程** 。只有把这个过程做好,才能更好地解决问题。 **通过本课时的学习,常用的分析问题的方法有以下 4 种**
+### 总结 **在开发前,一定要对问题的复杂度进行分析,做好技术选型。这就是定位问题的过程** 。只有把这个过程做好,才能更好地解决问题。**通过本课时的学习,常用的分析问题的方法有以下 4 种**
 
 1. **复杂度分析** 。估算问题中复杂度的上限和下限。
 2. **定位问题** 。根据问题类型,确定采用何种算法思维。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25416\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25416\350\256\262.md"
index 2778b011a..a8c318187 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25416\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25416\350\256\262.md"
@@ -13,7 +13,7 @@
 
 #### 例题 1:斐波那契数列
 
-斐波那契数列是:0,1,1,2,3,5,8,13,21,34,55,89,144……。你会发现,这个数列中元素的性质是,某个数等于它前面两个数的和;也就是 a\[n+2\] = a\[n+1\] + a\[n\]。至于起始两个元素,则分别为 0 和 1。 **在这个数列中的数字,就被称为斐波那契数** 。 **【题目】写一个函数,输入 x,输出斐波那契数列中第 x 位的元素** 。例如,输入 4,输出 2;输入 9,输出 21。要求:需要用递归的方式来实现。 **【解析】** 在本课时开头,我们复习了解决代码问题的方法论,下面我们按照解题步骤进行详细分析。
+斐波那契数列是:0,1,1,2,3,5,8,13,21,34,55,89,144……。你会发现,这个数列中元素的性质是,某个数等于它前面两个数的和;也就是 a\[n+2\] = a\[n+1\] + a\[n\]。至于起始两个元素,则分别为 0 和 1。**在这个数列中的数字,就被称为斐波那契数** 。**【题目】写一个函数,输入 x,输出斐波那契数列中第 x 位的元素** 。例如,输入 4,输出 2;输入 9,输出 21。要求:需要用递归的方式来实现。**【解析】** 在本课时开头,我们复习了解决代码问题的方法论,下面我们按照解题步骤进行详细分析。
 
 - 首先我们还是先做好复杂度的分析
 
@@ -49,7 +49,7 @@ private static int fun(int n) {
 }
 ```
 
-**下面,我们来对代码进行解读** 。 **主函数中** ,第 1 行到第 4 行,定义输入变量 x,并调用 fun(x) 去计算第 x 位的斐波那契数列元素。 **在 fun() 函数内部** ,采用了递归去完成计算。递归分为递归体和终止条件:
+**下面,我们来对代码进行解读** 。**主函数中**,第 1 行到第 4 行,定义输入变量 x,并调用 fun(x) 去计算第 x 位的斐波那契数列元素。**在 fun() 函数内部**,采用了递归去完成计算。递归分为递归体和终止条件:
 
 - 递归体是第 13 行。即当输入变量 n 比 2 大的时候,递归地调用 fun() 函数,并传入 n-1 和 n-2,即 return fun(n - 1) + fun(n - 2);
 - 终止条件则是在第 7 行到第 12 行,分别定义了当 n 为 1 或 2 的时候,直接返回 0 或 1。
@@ -58,7 +58,7 @@ private static int fun(int n) {
 
 **【题目】给定一个经过任意位数的旋转后的排序数组,判断某个数是否在里面** 。
 
-例如,对于一个给定数组 {4, 5, 6, 7, 0, 1, 2},它是将一个有序数组的前三位旋转地放在了数组末尾。假设输入的 target 等于 0,则输出答案是 4,即 0 所在的位置下标是 4。如果输入 3,则返回 -1。 **【解析】** 这道题目依旧是按照解决代码问题的方法论的步骤进行分析。
+例如,对于一个给定数组 {4, 5, 6, 7, 0, 1, 2},它是将一个有序数组的前三位旋转地放在了数组末尾。假设输入的 target 等于 0,则输出答案是 4,即 0 所在的位置下标是 4。如果输入 3,则返回 -1。**【解析】** 这道题目依旧是按照解决代码问题的方法论的步骤进行分析。
 
 - 先做复杂度分析
 
@@ -74,7 +74,7 @@ private static int fun(int n) {
 
 在利用二分查找时,更多的是判断,基本没有数据的增删操作,因此不需要太多地定义复杂的数据结构。
 
-分析到这里,解决方案已经非常明朗了,就是采用二分查找的方法,在 O(logn) 的时间复杂度下去解决这个问题。二分查找可以通过递归来实现。 **而每次递归的关键点在于,根据切分的点(最中间的那个数字),确定是向左走还是向右走。这也是这个例题中唯一的难点了** 。
+分析到这里,解决方案已经非常明朗了,就是采用二分查找的方法,在 O(logn) 的时间复杂度下去解决这个问题。二分查找可以通过递归来实现。**而每次递归的关键点在于,根据切分的点(最中间的那个数字),确定是向左走还是向右走。这也是这个例题中唯一的难点了** 。
 
 试想一下,在一个旋转后的有序数组中,利用中间元素作为切分点得到的两个子数组有什么样的性质。经过枚举不难发现,这两个子数组中,一定存在一个数组是有序的。也可能出现一个极端情况,二者都是有序的。如下图所示:
 
@@ -124,7 +124,7 @@ private static int bs(int[] arr, int target, int begin, int end) {
 }
 ```
 
-**我们对代码进行解读:**  **主函数中,第 2 到 4 行。定义数组和 target,并且执行二分查找** 。二分查找包括两部分,其一是二分策略,其二是终止条件。 **二分策略在代码的 16~33 行:**
+**我们对代码进行解读:**  **主函数中,第 2 到 4 行。定义数组和 target,并且执行二分查找** 。二分查找包括两部分,其一是二分策略,其二是终止条件。**二分策略在代码的 16~33 行:**
 
 - 16 行计算分裂点的索引值。17 到 19 行,进行目标值与分裂点的判断。
   - 如果相等,则查找到结果并返回;
@@ -148,7 +148,7 @@ private static int bs(int[] arr, int target, int begin, int end) {
 
 **【题目】输入两个字符串,用动态规划的方法,求解出最大公共子串** 。
 
-例如,输入 a = "13452439", b = "123456"。由于字符串"345"同时在 a 和 b 中出现,且是同时出现在 a 和 b 中的最长的子串。因此输出"345"。 **【解析】这里已经定义了问题,就是寻找最大公共子串。同时也定义了方法,就是要用动态规划的方法** 。那么我们也不需要做太多的分析,只要依赖动态规划的步骤完成就可以了。
+例如,输入 a = "13452439", b = "123456"。由于字符串"345"同时在 a 和 b 中出现,且是同时出现在 a 和 b 中的最长的子串。因此输出"345"。**【解析】这里已经定义了问题,就是寻找最大公共子串。同时也定义了方法,就是要用动态规划的方法** 。那么我们也不需要做太多的分析,只要依赖动态规划的步骤完成就可以了。
 
 首先,我们回顾一下先前学过的最短路径问题。在最短路径问题中,我们是定义了起点和终点后,再去寻找二者之间的最短路径。
 
@@ -156,7 +156,7 @@ private static int bs(int[] arr, int target, int begin, int end) {
 
 如果要基于已有的知识来探索陌生问题,那就需要根据每个可能的公共子串起点,去寻找与之对应的最远终点。这样就能得到全部的子串。随后再从中找到最大的那个子串。
 
-别忘了, **动态规划的基本方法是:分阶段、找状态、做决策、状态转移方程、定目标、寻找终止条件** 。下面我们来具体分析一下动态规划的步骤:
+别忘了,**动态规划的基本方法是:分阶段、找状态、做决策、状态转移方程、定目标、寻找终止条件** 。下面我们来具体分析一下动态规划的步骤:
 
 - 对于一个可能的起点,它后面的每个字符都是一个阶段。
 - 状态就是当前寻找到的相匹配的字符。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25417\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25417\350\256\262.md"
index c94058559..c151ee92b 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25417\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25417\350\256\262.md"
@@ -11,7 +11,7 @@
 
 ### 数据结构训练题
 
-#### 例题 1:反转字符串中的单词 **【题目】** 给定一个字符串,逐个翻转字符串中的每个单词。例如,输入:"This is a good example",输出:"example good a is This"。如果有多余的空格需要删除。 **【解析】** 在本课时开头,我们复习了解决代码问题的方法论,下面我们按照解题步骤进行详细分析。 **首先分析一下复杂度** 。这里的动作可以分为拆模块和做翻转两部分。在采用比较暴力的方法时,拆模块使用一个 for 循环,做翻转也使用一个 for 循环。这样双重循环的嵌套,就是 O(n²) 的复杂度。 **接下来定位问题** 。我们可以看到它对数据的顺序非常敏感,敏感点一是每个单词需要保证顺序;敏感点二是所有单词放在一起的顺序需要调整为逆序。我们曾学过的关于数据顺序敏感的结构有队列和栈,也许这些结构可以适用在这个问题中。此处需要逆序,栈是有非常大的可能性被使用到的。 **然后我们进行数据操作分析** 。如果要使用栈的话,从结果出发,就需要按照顺序,把 This、is、a、good、example 分别入栈。要想把它们正确地入栈,就需要根据空格来拆分原始字符串。 **因此,经过分析后,这个例子的解法为:用空格把句子分割成单词。如果发现了多余的连续空格,需要做一些删除的额外处理。一边得到单词,一边把单词放入栈中。直到最后,再把单词从栈中倒出来,形成结果字符串**
+#### 例题 1:反转字符串中的单词 **【题目】** 给定一个字符串,逐个翻转字符串中的每个单词。例如,输入:"This is a good example",输出:"example good a is This"。如果有多余的空格需要删除。**【解析】** 在本课时开头,我们复习了解决代码问题的方法论,下面我们按照解题步骤进行详细分析。**首先分析一下复杂度** 。这里的动作可以分为拆模块和做翻转两部分。在采用比较暴力的方法时,拆模块使用一个 for 循环,做翻转也使用一个 for 循环。这样双重循环的嵌套,就是 O(n²) 的复杂度。**接下来定位问题** 。我们可以看到它对数据的顺序非常敏感,敏感点一是每个单词需要保证顺序;敏感点二是所有单词放在一起的顺序需要调整为逆序。我们曾学过的关于数据顺序敏感的结构有队列和栈,也许这些结构可以适用在这个问题中。此处需要逆序,栈是有非常大的可能性被使用到的。**然后我们进行数据操作分析** 。如果要使用栈的话,从结果出发,就需要按照顺序,把 This、is、a、good、example 分别入栈。要想把它们正确地入栈,就需要根据空格来拆分原始字符串。**因此,经过分析后,这个例子的解法为:用空格把句子分割成单词。如果发现了多余的连续空格,需要做一些删除的额外处理。一边得到单词,一边把单词放入栈中。直到最后,再把单词从栈中倒出来,形成结果字符串**
 
 ![1.gif](assets/Ciqc1F8MP8yAS72oABGrGx_blwA588.gif) **最后,我们按照上面的思路进行编码开发** 。代码如下:
 
@@ -65,7 +65,7 @@ private static String reverseWords(String s) {
 
 ![3.png](assets/CgqCHl8MP_WAERuIAACStyOKMQk754.png)
 
-则打印 16、13、20、10、15、22、21、26。格外需要注意的是,这并不是前序遍历。 **【解析】** 如果你一直在学习这门课的话,一定对这道题目似曾相识。它是我们在 09 课时中留下的练习题。同时它也是高频面试题。仔细分析下这个问题,不难发现它是一个关于树的遍历问题。理论上是可以在 O(n) 时间复杂度下完成访问的。
+则打印 16、13、20、10、15、22、21、26。格外需要注意的是,这并不是前序遍历。**【解析】** 如果你一直在学习这门课的话,一定对这道题目似曾相识。它是我们在 09 课时中留下的练习题。同时它也是高频面试题。仔细分析下这个问题,不难发现它是一个关于树的遍历问题。理论上是可以在 O(n) 时间复杂度下完成访问的。
 
 以往我们学过的遍历方式有前序、中序和后序遍历,它们的实现方法都是通过递归。以前序遍历为例,递归可以理解为,先解决根结点,再解决左子树一边的问题,最后解决右子树的问题。这很像是在用深度优先的原则去遍历一棵树。
 
@@ -81,7 +81,7 @@ private static String reverseWords(String s) {
 
 分析到这里,你应该能找到一些感觉了吧。一个结果序列对顺序敏感,而且没有逆序的操作,满足这些特点的数据结构只有队列。所以我们猜测这个问题的解决方案,极有可能要用到队列。
 
-队列只有入队列和出队列的操作。如果输出结果就是出队列的顺序,那这个顺序必然也是入队列的顺序,原因就在于队列的出入原则是先进先出。而入队列的原则是,上层父节点先进,左孩子再进,右孩子最后进。 **因此,这道题目的解决方案就是,根结点入队列,随后循环执行结点出队列并打印结果,左孩子入队列,右孩子入队列。直到队列为空** 。如下图所示:
+队列只有入队列和出队列的操作。如果输出结果就是出队列的顺序,那这个顺序必然也是入队列的顺序,原因就在于队列的出入原则是先进先出。而入队列的原则是,上层父节点先进,左孩子再进,右孩子最后进。**因此,这道题目的解决方案就是,根结点入队列,随后循环执行结点出队列并打印结果,左孩子入队列,右孩子入队列。直到队列为空** 。如下图所示:
 
 ![2.gif](assets/CgqCHl8MQA2AWELaAA_8m3_f-_Q592.gif)
 
@@ -125,11 +125,11 @@ queue.offer(current.rightChild);
 
 输入 10,服务端收到 0、1、2、10、20,返回 2。
 
-输入 22,服务端收到 0、1、2、10、20、22,返回 2。 **【解析】** 这道题目依旧是按照解决代码问题的方法论的步骤进行分析。 **先看一下复杂度** 。显然,这里的问题定位就是个查找问题。对于累积的客户端输入,查找其中位数。中位数的定义是,一组数字按照从小到大排列后,位于中间位置的那个数字。
+输入 22,服务端收到 0、1、2、10、20、22,返回 2。**【解析】** 这道题目依旧是按照解决代码问题的方法论的步骤进行分析。**先看一下复杂度** 。显然,这里的问题定位就是个查找问题。对于累积的客户端输入,查找其中位数。中位数的定义是,一组数字按照从小到大排列后,位于中间位置的那个数字。
 
-根据这个定义,最简单粗暴的做法,就是对服务端收到的数据进行排序得到有序数组,再通过 index 直接取出数组的中位数。排序选择快排的时间复杂度是 O(nlogn)。 **接下来分析一下这个查找问题** 。该问题有一个非常重要的特点,我们注意到,上一轮已经得到了有序的数组,那么这一轮该如何巧妙利用呢?
+根据这个定义,最简单粗暴的做法,就是对服务端收到的数据进行排序得到有序数组,再通过 index 直接取出数组的中位数。排序选择快排的时间复杂度是 O(nlogn)。**接下来分析一下这个查找问题** 。该问题有一个非常重要的特点,我们注意到,上一轮已经得到了有序的数组,那么这一轮该如何巧妙利用呢?
 
-举个例子,如果采用全排序的方法,那么在第 n 次收到用户输入时,则需要对 n 个数字进行排序并输出中位数,此时服务端已经保存了这 n 个数字的有序数组了。而在第 n+1 次收到用户输入时,是不需要对 n+1 个数字整体排序的,仅仅通过插入这个数字到一个有序数组中就可以完成排序。显然,利用这个性质后,时间复杂度可以降低到 O(n)。 **接着,我们从数据的操作层面来看,是否仍然有优化的空间** 。对于这个问题,其目标是输出中位数。只要你能在 n 个数字中,找到比 x 小的 n/2 个数字和比 x 大的 n/2 个数字,那么 x 就是最终需要返回的结果。
+举个例子,如果采用全排序的方法,那么在第 n 次收到用户输入时,则需要对 n 个数字进行排序并输出中位数,此时服务端已经保存了这 n 个数字的有序数组了。而在第 n+1 次收到用户输入时,是不需要对 n+1 个数字整体排序的,仅仅通过插入这个数字到一个有序数组中就可以完成排序。显然,利用这个性质后,时间复杂度可以降低到 O(n)。**接着,我们从数据的操作层面来看,是否仍然有优化的空间** 。对于这个问题,其目标是输出中位数。只要你能在 n 个数字中,找到比 x 小的 n/2 个数字和比 x 大的 n/2 个数字,那么 x 就是最终需要返回的结果。
 
 基于这个思想,可以动态的维护一个最小的 n/2 个数字的集合,和一个最大的 n/2 个数字的集合。如果数字是奇数个,就我们就在左边最小的 n/2 个数字集合中多存一个元素。
 
@@ -137,11 +137,11 @@ queue.offer(current.rightChild);
 
 具体而言,当前的中位数是 2,额外增加一个数字之后,新的中位数只可能发生在 1、2、10 和新增的一个数字之间。不管中位数发生在哪里,都可以通过一些 if-else 语句进行查找,那么时间复杂度就是 O(1)。
 
-虽然这种方法对于查找中位数的时间复杂度降低到了 O(1),但是它还需要有一些后续的处理,这主要是辅助下一次的请求。 **例如,当前用两个数据结构分别维护着 0、1、2 和 10、20,那么新增了 22 之后,这两个数据结构如何更新。这就是原问题最核心的瓶颈了** 。
+虽然这种方法对于查找中位数的时间复杂度降低到了 O(1),但是它还需要有一些后续的处理,这主要是辅助下一次的请求。**例如,当前用两个数据结构分别维护着 0、1、2 和 10、20,那么新增了 22 之后,这两个数据结构如何更新。这就是原问题最核心的瓶颈了** 。
 
 从结果来看,如果新增的数字比较小,那么就添加到左边的数据结构,并且把其中最大的 2 新增到右边,以保证二者数量相同。如果新增的数字比较大,那么就放到右边的数据结构,以保证二者数量相同。在这里,可能需要的数据操作包括,查找、中间位置的新增、最后位置的删除。
 
-顺着这个思路继续分析,有序环境中的查找可以采用二分查找,时间复杂度是 O(logn)。最后位置的删除,并不牵涉到数据的挪移,时间复杂度是 O(1)。中间位置的新增就麻烦了,它需要对数据进行挪移,时间复杂度是 O(n)。如果要降低它的复杂度就需要用一些其他手段了。 **在这个问题中,有一个非常重要的信息,那就是题目只要中位数,而中位数左边和右边是否有序不重要。于是,我们需要用到这样的数据结构,大顶堆和小顶堆** 。
+顺着这个思路继续分析,有序环境中的查找可以采用二分查找,时间复杂度是 O(logn)。最后位置的删除,并不牵涉到数据的挪移,时间复杂度是 O(1)。中间位置的新增就麻烦了,它需要对数据进行挪移,时间复杂度是 O(n)。如果要降低它的复杂度就需要用一些其他手段了。**在这个问题中,有一个非常重要的信息,那就是题目只要中位数,而中位数左边和右边是否有序不重要。于是,我们需要用到这样的数据结构,大顶堆和小顶堆** 。
 
 *   大顶堆是一棵完全二叉树,它的性质是,父结点的数值比子结点的数值大;
 *   小顶堆的性质与此相反,父结点的数值比子结点的数值小。
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25418\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25418\350\256\262.md"
index fdd119279..fb8e4bcf6 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25418\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25418\350\256\262.md"
@@ -19,7 +19,7 @@
 
 要求:空间复杂度为 O(1),即不要使用额外的数组空间。
 
-例如,给定数组 nums = \[1,1,2\],函数应该返回新的长度 2,并且原数组 nums 的前两个元素被修改为 1, 2。又如,给定 nums = \[0,0,1,1,1,2,2,3,3,4\],函数应该返回新的长度 5,并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。 **【解析】** 这个题目比较简单,应该是送分题。不过,面试过程中的送分题也是送命题。这是因为,如果送分题没有拿下,就会显得非常说不过去。 **我们先来看一下复杂度** 。这里并没有限定时间复杂度,仅仅是要求了空间上不能定义新的数组。 **然后我们来定位问题** 。显然这是一个数据去重的问题。 **按照解题步骤,接下来我们需要做数据操作分析。** 在一个去重问题中,每次遍历的新的数据,都需要与已有的不重复数据进行对比。这时候,就需要查找了。整体来看,遍历嵌套查找,就是 O(n²) 的复杂度。如果要降低时间复杂度,那么可以在查找上入手,比如使用哈希表。不过很可惜,使用了哈希表之后,空间复杂度就是 O(n)。幸运的是,原数组是有序的,这就可以让查找的动作非常简单了。
+例如,给定数组 nums = \[1,1,2\],函数应该返回新的长度 2,并且原数组 nums 的前两个元素被修改为 1, 2。又如,给定 nums = \[0,0,1,1,1,2,2,3,3,4\],函数应该返回新的长度 5,并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。**【解析】** 这个题目比较简单,应该是送分题。不过,面试过程中的送分题也是送命题。这是因为,如果送分题没有拿下,就会显得非常说不过去。**我们先来看一下复杂度** 。这里并没有限定时间复杂度,仅仅是要求了空间上不能定义新的数组。**然后我们来定位问题** 。显然这是一个数据去重的问题。**按照解题步骤,接下来我们需要做数据操作分析。** 在一个去重问题中,每次遍历的新的数据,都需要与已有的不重复数据进行对比。这时候,就需要查找了。整体来看,遍历嵌套查找,就是 O(n²) 的复杂度。如果要降低时间复杂度,那么可以在查找上入手,比如使用哈希表。不过很可惜,使用了哈希表之后,空间复杂度就是 O(n)。幸运的是,原数组是有序的,这就可以让查找的动作非常简单了。
 
 因此,解决方案上就是,一次循环嵌套查找完成。查找不可使用哈希表,但由于数组有序,时间复杂度是 O(1)。因此整体的时间复杂度就是 O(n)。
 
@@ -60,15 +60,15 @@ public static void main(String[] args) {
 
 nums2 = \[2, 4, 8, 12\]
 
-输出 5。 **【解析】** 这个题目是我个人非常喜欢的,原因是,它所有的解法和思路,都隐含在了题目的描述中。如果你具备很强的分析和解决问题的能力,那么一定可以找到最优解法。 **我们先看一下复杂度的分析** 。这里的 nums1 和 nums2 都是有序的,这让我们第一时间就想到了归并排序。方法很简单,我们把两个数组合并,就得到了合在一起后的有序数组。这个动作的时间复杂度是 O(m+n)。接着,我们从数组中就可以直接取出中位数了。很可惜,这并不满足题目的时间复杂度 O(log(m + n)) 的要求。 **接着,我们来看一下这个问题的定位** 。题目中有一个关键字,那就是“找出”。很显然,我们要找的目标就藏在 nums1 或 nums2 中。这明显就是一个查找问题。而在查找问题中,我们学过的知识是分治法下的二分查找。
+输出 5。**【解析】** 这个题目是我个人非常喜欢的,原因是,它所有的解法和思路,都隐含在了题目的描述中。如果你具备很强的分析和解决问题的能力,那么一定可以找到最优解法。**我们先看一下复杂度的分析** 。这里的 nums1 和 nums2 都是有序的,这让我们第一时间就想到了归并排序。方法很简单,我们把两个数组合并,就得到了合在一起后的有序数组。这个动作的时间复杂度是 O(m+n)。接着,我们从数组中就可以直接取出中位数了。很可惜,这并不满足题目的时间复杂度 O(log(m + n)) 的要求。**接着,我们来看一下这个问题的定位** 。题目中有一个关键字,那就是“找出”。很显然,我们要找的目标就藏在 nums1 或 nums2 中。这明显就是一个查找问题。而在查找问题中,我们学过的知识是分治法下的二分查找。
 
-回想一下,二分查找适用的重要条件就是,原数组有序。恰好,在这个问题中 nums1 和 nums2 分别都是有序的。而且二分查找的时间复杂度是 O(logn),这和题目中给出的时间复杂度 O(log(m + n)) 的要求也是不谋而合。因此,经过分析,我们可以大胆猜测,此题极有可能要用到二分查找。 **我们再来看一下数据结构方面** 。如果要用二分查找,就需要用到若干个指针,去约束查找范围。除此以外,并不需要去定义复杂的数据结构。也就是说,空间复杂度是 O(1) 。
+回想一下,二分查找适用的重要条件就是,原数组有序。恰好,在这个问题中 nums1 和 nums2 分别都是有序的。而且二分查找的时间复杂度是 O(logn),这和题目中给出的时间复杂度 O(log(m + n)) 的要求也是不谋而合。因此,经过分析,我们可以大胆猜测,此题极有可能要用到二分查找。**我们再来看一下数据结构方面** 。如果要用二分查找,就需要用到若干个指针,去约束查找范围。除此以外,并不需要去定义复杂的数据结构。也就是说,空间复杂度是 O(1) 。
 
 好了,接下来,我们就来看一下二分查找如何能解决这个问题。二分查找需要一个分裂点,去把原来的大问题,拆分成两个部分,并在其中一部分继续执行二分查找。既然是查找中位数,我们不妨先试试以中位数作为切分点,看看会产生什么结果。如下图所示:
 
 ![2.png](assets/Ciqc1F8O4BWAJgOUAABMJW6Ihfk508.png)
 
-经过切分后,两个数组分别被拆分为 3 个部分,合在一起是 6 个部分。二分查找的思路是,需要从这 6 个部分中,剔除掉一些,让查找的范围缩小。那么,我们来思考一个问题,在这 6 个部分中,目标中位数一定不会发生在哪几个部分呢? **中位数有一个重要的特质,那就是比中位数小的数字个数,和比中位数大的数字个数,是相等的** 。围绕这个性质来看,中位数就一定不会发生在 C 和 D 的区间。
+经过切分后,两个数组分别被拆分为 3 个部分,合在一起是 6 个部分。二分查找的思路是,需要从这 6 个部分中,剔除掉一些,让查找的范围缩小。那么,我们来思考一个问题,在这 6 个部分中,目标中位数一定不会发生在哪几个部分呢?**中位数有一个重要的特质,那就是比中位数小的数字个数,和比中位数大的数字个数,是相等的** 。围绕这个性质来看,中位数就一定不会发生在 C 和 D 的区间。
 
 如果中位数在 C 部分,那么在 nums1 中,比中位数小的数字就会更多一些。因为 4 < 5(nums2 的中位数小于 nums1 的中位数),所以在 nums2 中,比中位数小的数字也会更多一些(最不济也就是一样多)。因此,整体来看,中位数不可能在 C 部分。同理,中位数也不会发生在 D 部分。
 
@@ -80,7 +80,7 @@ nums2 = \[2, 4, 8, 12\]
 
 应该剔除 C 部分和 D 部分。但 D 部分更少,因此剔除 D 和 C 中的 9。
 
-二分查找还需要考虑终止条件。对于这个题目,终止条件必然是某个数组小到无法继续二分的时候。这是因为,每次二分剔除掉的是更少的那个部分。因此,在终止条件中,查找范围应该是一个大数组和一个只有 1~2 个元素的小数组。这样就需要根据大数组的奇偶性和小数组的数量,拆开 4 个可能性: **可能性一** :nums1 奇数个,nums2 只有 1 个元素。例如,nums1 = \[a, b, **c** , d, e\],nums2 = \[m\]。此时,有以下 3 种可能性:
+二分查找还需要考虑终止条件。对于这个题目,终止条件必然是某个数组小到无法继续二分的时候。这是因为,每次二分剔除掉的是更少的那个部分。因此,在终止条件中,查找范围应该是一个大数组和一个只有 1~2 个元素的小数组。这样就需要根据大数组的奇偶性和小数组的数量,拆开 4 个可能性: **可能性一** :nums1 奇数个,nums2 只有 1 个元素。例如,nums1 = \[a, b,**c**, d, e\],nums2 = \[m\]。此时,有以下 3 种可能性:
 
 1.  如果 m < b,则结果为 b;
 2.  如果 b < m < c,则结果为 m;
@@ -88,7 +88,7 @@ nums2 = \[2, 4, 8, 12\]
 
 这 3 个情况,可以利用 "A?B:C" 合并为一个表达式,即 m < b ? b : (m < c ? m : c)。
 
-**可能性二** :nums1 偶数个,nums2 只有 1 个元素。例如,nums1 = \[a, b, **c** , d, e, f\],nums2 = \[m\]。此时,有以下 3 种可能性:
+**可能性二** :nums1 偶数个,nums2 只有 1 个元素。例如,nums1 = \[a, b,**c**, d, e, f\],nums2 = \[m\]。此时,有以下 3 种可能性:
 
 1.  如果 m < c,则结果为 c;
 2.  如果 c < m < d,则结果为 m;
@@ -96,7 +96,7 @@ nums2 = \[2, 4, 8, 12\]
 
 这 3 个情况,可以利用"A?B:C"合并为一个表达式,即 m < c ? c : (m < d? m : d)。
 
-**可能性三** :nums1 奇数个,nums2 有 2 个元素。例如,nums1 = \[a, b, **c** , d, e\],nums2 = \[ **m** ,n\]。此时,有以下 6 种可能性:
+**可能性三** :nums1 奇数个,nums2 有 2 个元素。例如,nums1 = \[a, b,**c**, d, e\],nums2 = \[ **m**,n\]。此时,有以下 6 种可能性:
 
 1.  如果 n < b,则结果为 b;
 2.  如果 b < n < c,则结果为 n;
@@ -109,7 +109,7 @@ nums2 = \[2, 4, 8, 12\]
 
 ![image](assets/Ciqc1F8Odd-AdDghAAAd2xZYP1g802.png)
 
-**可能性四** :nums1 偶数个,nums2 有 2 个元素。例如,nums1 = \[a, b, **c** , d, e, f\],nums2 = \[ **m** ,n\]。此时,有以下 6 种可能性:
+**可能性四** :nums1 偶数个,nums2 有 2 个元素。例如,nums1 = \[a, b,**c**, d, e, f\],nums2 = \[ **m**,n\]。此时,有以下 6 种可能性:
 
 1.  如果 n < b,则结果为 b;
 2.  如果 b < n < c,则结果为 n;
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25419\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25419\350\256\262.md"
index 501f8bb35..1cd34df9f 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25419\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25419\350\256\262.md"
@@ -9,9 +9,9 @@
 
 ### 大厂真题实战演练
 
-#### 例题 1:判断数组中所有的数字是否只出现一次 **【题目】** 判断数组中所有的数字是否只出现一次。给定一个个数字 arr,判断数组 arr 中是否所有的数字都只出现过一次。约束时间复杂度为 O(n)。例如,arr = {1, 2, 3},输出 YES。又如,arr = {1, 2, 1},输出 NO。 **【解析】** 这个题目相当于一道开胃菜,也是一道送分题。我们还是严格围绕解题方法论,去拆解这个问题。 **我们先来看一下复杂度** 。判断是否所有数字都只出现一次,很显然我们需要对每个数字进行遍历,因此时间复杂度为 O(n)。而每次的遍历,都要判断当前元素在先前已经扫描过的区间内是否出现过。由于此时并没有额外信息(例如数组有序)输入,因此,还需要 O(n) 的时间进行判断。综合起来看就是 O(n²) 的时间复杂度。这显然与题目的要求不符合。 **然后我们来定位问题** 。根据题目来看,你可以理解这是一个数据去重的问题。但是由于我们并没有学过太多解决这类问题的算法思维,因此我们不妨再从数据操作的视角看一下。 **按照解题步骤,接下来我们需要做数据操作分析。** 每轮迭代需要去判断当前元素在先前已经扫描过的区间内是否出现过,这就是一个查找的动作。也就是说,每次迭代需要对数据进行数值特征方面的查找。这个题目只需要判断是否有重复,并不需要新增、删除的动作
+#### 例题 1:判断数组中所有的数字是否只出现一次 **【题目】** 判断数组中所有的数字是否只出现一次。给定一个个数字 arr,判断数组 arr 中是否所有的数字都只出现过一次。约束时间复杂度为 O(n)。例如,arr = {1, 2, 3},输出 YES。又如,arr = {1, 2, 1},输出 NO。**【解析】** 这个题目相当于一道开胃菜,也是一道送分题。我们还是严格围绕解题方法论,去拆解这个问题。**我们先来看一下复杂度** 。判断是否所有数字都只出现一次,很显然我们需要对每个数字进行遍历,因此时间复杂度为 O(n)。而每次的遍历,都要判断当前元素在先前已经扫描过的区间内是否出现过。由于此时并没有额外信息(例如数组有序)输入,因此,还需要 O(n) 的时间进行判断。综合起来看就是 O(n²) 的时间复杂度。这显然与题目的要求不符合。**然后我们来定位问题** 。根据题目来看,你可以理解这是一个数据去重的问题。但是由于我们并没有学过太多解决这类问题的算法思维,因此我们不妨再从数据操作的视角看一下。**按照解题步骤,接下来我们需要做数据操作分析。** 每轮迭代需要去判断当前元素在先前已经扫描过的区间内是否出现过,这就是一个查找的动作。也就是说,每次迭代需要对数据进行数值特征方面的查找。这个题目只需要判断是否有重复,并不需要新增、删除的动作
 
-在优化数值特性的查找时,我们应该立马想到哈希表。因为它能在 O(1) 的时间内完成查找动作。这样,整体的时间复杂度就可以被降低为 O(n) 了。与此同时,空间复杂度也提高到了 O(n)。 **根据上面的思路进行编码开发** ,具体代码如下:
+在优化数值特性的查找时,我们应该立马想到哈希表。因为它能在 O(1) 的时间内完成查找动作。这样,整体的时间复杂度就可以被降低为 O(n) 了。与此同时,空间复杂度也提高到了 O(n)。**根据上面的思路进行编码开发**,具体代码如下:
 
 ```java
 public static void main(String[] args) {
@@ -43,13 +43,13 @@ public static boolean isUniquel(int[] arr) {
 
 #### 例题 2:找出数组中出现次数超过数组长度一半的元素 **【题目】** 假设在一个数组中,有一个数字出现的次数超过数组长度的一半,现在要求你找出这个数字。
 
-你可以假设一定存在这个出现次数超过数组长度的一半的数字,即不用考虑输入不合法的情况。要求时间复杂度是 O(n),空间复杂度是 O(1)。例如,输入 a = {1,2,1,1,2,4,1,5,1},输出 1。 **【解析】先来看一下时间复杂度的分析** 。一个直观想法是,一边扫描一边记录每个元素出现的次数,并利用 k-v 结构的哈希表存储。例如,一次扫描后,得到元素-次数(1-5,2-2,4-1,5-1)的字典。接着再在这个字典里去找到次数最多的元素。这样的时间复杂度和空间复杂度都是 O(n)。不过可惜,这并不满足题目的要求。 **接着,我们需要定位问题。** 从问题出发,这并不是某个特定类型的问题。而且既然空间复杂度限定是 O(1),也就意味着不允许使用任何复杂的数据结构。也就是说,数据结构的优化不可以用,算法思维的优化也不可以用。
+你可以假设一定存在这个出现次数超过数组长度的一半的数字,即不用考虑输入不合法的情况。要求时间复杂度是 O(n),空间复杂度是 O(1)。例如,输入 a = {1,2,1,1,2,4,1,5,1},输出 1。**【解析】先来看一下时间复杂度的分析** 。一个直观想法是,一边扫描一边记录每个元素出现的次数,并利用 k-v 结构的哈希表存储。例如,一次扫描后,得到元素-次数(1-5,2-2,4-1,5-1)的字典。接着再在这个字典里去找到次数最多的元素。这样的时间复杂度和空间复杂度都是 O(n)。不过可惜,这并不满足题目的要求。**接着,我们需要定位问题。** 从问题出发,这并不是某个特定类型的问题。而且既然空间复杂度限定是 O(1),也就意味着不允许使用任何复杂的数据结构。也就是说,数据结构的优化不可以用,算法思维的优化也不可以用。
 
 面对这类问题,我们只能从问题出发,看还有哪些信息我们没有使用上。题目中有一个重要的信息是,这个出现超过半数的数字一定存在。回想我们上边的解法,它可以找到出现次数最多的数字,但没有使用到“必然超过半数”这个重要的信息。
 
 分析到这里,我们不妨想一下这个场景。假设现在三国交战,其中 A 国的兵力比 B 国和 C 国的总和还多。那么人们就常常会说,哪怕是 A 国士兵“一个碰一个”地和另外两国打消耗战,都能取得最后的胜利。
 
-说到这里,不知道你有没有一些发现。“一个碰一个”的思想,那就是如果相等则加 1,如果不等则减 1。这样,只需要记录一个当前的缓存元素变量和一个次数统计变量就可以了。 **根据上面的思路进行编码开发** ,具体代码为:
+说到这里,不知道你有没有一些发现。“一个碰一个”的思想,那就是如果相等则加 1,如果不等则减 1。这样,只需要记录一个当前的缓存元素变量和一个次数统计变量就可以了。**根据上面的思路进行编码开发**,具体代码为:
 
 ```java
 public static void main(String\[\] args) {
@@ -108,7 +108,7 @@ System.out.println(result);
 
 ![2.png](assets/Ciqc1F8VUi2AFvluAAAd3YHGcpM960.png) **【解析】** 题目要求使用动态规划的方法,这是我们解题的一个难点,也正是因为这一点限制才让这道题目区别于常见的题目。
 
-对于 O2O 领域的公司,尤其对于经常要遇到有限资源下,去最优化某个目标的岗位时,动态规划应该是高频考察的内容。我们依然是围绕动态规划的解题方法,从寻找最优子结构的视角去解决问题。 **千万别忘了,动态规划的解题方法是,分阶段、找状态、做决策、状态转移方程、定目标、寻找终止条件** 。
+对于 O2O 领域的公司,尤其对于经常要遇到有限资源下,去最优化某个目标的岗位时,动态规划应该是高频考察的内容。我们依然是围绕动态规划的解题方法,从寻找最优子结构的视角去解决问题。**千万别忘了,动态规划的解题方法是,分阶段、找状态、做决策、状态转移方程、定目标、寻找终止条件** 。
 
 我们先看一下这个问题的阶段。很显然,从起点开始,每一个移动动作就是一个阶段的决策动作,移动后到达的新的格子就是一个状态。
 
@@ -193,7 +193,7 @@ else {
 
 ### 练习题
 
-下面我们给出一个练习题,帮助你巩固本课时讲解的解题思路和方法。 **【题目】** 小明从小就喜欢数学,喜欢在笔记里记录很多表达式。他觉得现在的表达式写法很麻烦,为了提高运算符优先级,不得不添加很多括号。如果不小心漏了一个右括号的话,就差之毫厘,谬之千里了。
+下面我们给出一个练习题,帮助你巩固本课时讲解的解题思路和方法。**【题目】** 小明从小就喜欢数学,喜欢在笔记里记录很多表达式。他觉得现在的表达式写法很麻烦,为了提高运算符优先级,不得不添加很多括号。如果不小心漏了一个右括号的话,就差之毫厘,谬之千里了。
 
 因此他改用前缀表达式,例如把 `(2 + 3) * 4`写成`* + 2 3 4`,这样就能避免使用括号了。这样的表达式虽然书写简单,但计算却不够直观。请你写一个程序帮他计算这些前缀表达式。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25420\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25420\350\256\262.md"
index 3f573d4a7..c431e6af3 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25420\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25420\350\256\262.md"
@@ -24,9 +24,9 @@
 
 根据我的经验,好的简历要满足以下 3 个必要条件,分别是信息完备、抽象概括、重点突出。
 
-1. **信息完备** ,指的是必备的东西不能缺。例如,姓名、学历、联系方式、工作经历等。
-   2. **抽象概括** ,指的是可有可无的东西不要写。例如,研究生的导师姓名(除非是院士级的)、政治面貌、户籍等。
-   3. **重点突出** ,指的是对你有利的东西要放大加粗。例如,电话号、S 绩效、系统性能提高 50% 等。
+1. **信息完备**,指的是必备的东西不能缺。例如,姓名、学历、联系方式、工作经历等。
+   2. **抽象概括**,指的是可有可无的东西不要写。例如,研究生的导师姓名(除非是院士级的)、政治面貌、户籍等。
+   3. **重点突出**,指的是对你有利的东西要放大加粗。例如,电话号、S 绩效、系统性能提高 50% 等。
 
 #### 自我介绍
 
@@ -48,7 +48,7 @@
 
 根据我面试候选人的结果来看,很多底层的技术研发工程师都在瞎忙。说得讽刺一些,就好像是电影《国产凌凌漆》中的达文西一样。记得剧中的达文西曾经发明了一款太阳能手电筒。它的功能是,手电筒在有光的情况下就会亮,在没有光的时候就绝对不会亮。很显然,这是一件毫无用处的发明。工程师就像是一个系统的发明者。如果每天都是瞎忙的话,就很可能用了很酷炫的技术,做了一件毫无用处的事情。
 
-关于项目介绍,我在这里给你提出 3 个问题,你可以结合自己以前的项目尝试回答。 **问题 1:在项目中,你解决了什么问题?不解决会有什么后果?** 这个问题想问的其实是 Why。候选人切记不可上来就说,我做了什么事情。正确的回答应该从问题出发。一定是公司遇到了某个必须解决的问题(系统问题、业务问题),最终导致你去做了什么对应的事情。 **问题 2:这个问题的复杂性在哪里?你在解决它的过程中需要具备哪些能力?** 这个问题想问的是 What。既然明确了问题,那么就要再进一步找到这个问题的关键点和复杂性。再以此,提炼出技术问题,寻找解决方案。 **问题 3:这个问题被你解决了多少?你取得了哪些业务收益?**
+关于项目介绍,我在这里给你提出 3 个问题,你可以结合自己以前的项目尝试回答。**问题 1:在项目中,你解决了什么问题?不解决会有什么后果?** 这个问题想问的其实是 Why。候选人切记不可上来就说,我做了什么事情。正确的回答应该从问题出发。一定是公司遇到了某个必须解决的问题(系统问题、业务问题),最终导致你去做了什么对应的事情。**问题 2:这个问题的复杂性在哪里?你在解决它的过程中需要具备哪些能力?** 这个问题想问的是 What。既然明确了问题,那么就要再进一步找到这个问题的关键点和复杂性。再以此,提炼出技术问题,寻找解决方案。**问题 3:这个问题被你解决了多少?你取得了哪些业务收益?**
 
 这个问题想问的是 How,也是最终的结果。比如,如果有你没你都一样,那么这就是瞎忙的一个项目。如果有了你,使得公司每年节约了 XX 元的成本,那这就是你真实取得的业务收益。
 
@@ -80,7 +80,7 @@
 
 **【题目】** 如下图所示,我们给出一段简历内容,要求你根据本课时学习到的知识,予以评价。
 
-![WechatIMG52.png](assets/CgqCHl8ax62AN-bjAACBRAY3SOM181.png) **【分析】** 不难发现,这段简历在简历 3 个要素上都出现了问题,具体分析如下: **首先,信息不完备** 。既然硕士阶段写了 GPA,本科阶段就应该写上与之对应的 GPA 或者加权平均分;或者统一都不写。 **其次,信息冗余** 。师从 XX 教授,以及导师毕业于哪个大学,这些对你的求职又有什么用呢?写了也只是浪费纸张、浪费篇幅。 **最后,重点信息不突出** 。总成绩 5%,这是非常好的名次,可以考虑加粗,让人一眼就看到。
+![WechatIMG52.png](assets/CgqCHl8ax62AN-bjAACBRAY3SOM181.png) **【分析】** 不难发现,这段简历在简历 3 个要素上都出现了问题,具体分析如下: **首先,信息不完备** 。既然硕士阶段写了 GPA,本科阶段就应该写上与之对应的 GPA 或者加权平均分;或者统一都不写。**其次,信息冗余** 。师从 XX 教授,以及导师毕业于哪个大学,这些对你的求职又有什么用呢?写了也只是浪费纸张、浪费篇幅。**最后,重点信息不突出** 。总成绩 5%,这是非常好的名次,可以考虑加粗,让人一眼就看到。
 
 再延伸一下。根据这个简历,可以初步判断出候选人做事情可能会丢三落四,对系统架构的设计缺少必备的审美,对事情重要性的判断能力欠缺。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25421\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25421\350\256\262.md"
index 77377d1a1..697dbc558 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25421\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25421\350\256\262.md"
@@ -6,11 +6,11 @@
 
 首先,我们要明确一点,手写代码要比在 IDE 里写代码难得多。在很多 IDE 中,敲一个 Str 出来,就会自动补全 ing,得到 String。反括号"}",也会自动与前面的括号呼应。即使代码敲错了,按下 backspace 就可以回到原来的位置重新写。
 
-而手写代码就没有这么便捷的“功能”了。如果你前面的代码写错了,或者忘记定义变量了,那么勾勾画画就会让纸上的卷面乱七八糟,这势必会影响代码的呈现。 **因此,手写代码必须谋定而后动** 。
+而手写代码就没有这么便捷的“功能”了。如果你前面的代码写错了,或者忘记定义变量了,那么勾勾画画就会让纸上的卷面乱七八糟,这势必会影响代码的呈现。**因此,手写代码必须谋定而后动** 。
 
 但是,我也曾多次听到这样的声音,很多人会说:“我入职之后是在 IDE 里写代码,为什么面试要给我增加难度,偏偏要在纸上写呢?”
 
-其实,原因就在于 IDE 帮助工程师减负,但工程师的能力不应该下降。在纸上写代码,特别锻炼一个候选人的全局视野。 **它考察的是候选人关于模块、函数的分解能力,对代码中变量的声明、初始化、赋值运算的设计框架以及对于编码任务的全方面把控能力** 。
+其实,原因就在于 IDE 帮助工程师减负,但工程师的能力不应该下降。在纸上写代码,特别锻炼一个候选人的全局视野。**它考察的是候选人关于模块、函数的分解能力,对代码中变量的声明、初始化、赋值运算的设计框架以及对于编码任务的全方面把控能力** 。
 
 如果一个候选人,通过勾勾抹抹完成了一个编码任务,其实是能反映出他不具备全局思考的能力,只能是走一步看一步地去解决问题。
 
diff --git "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25422\350\256\262.md" "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25422\350\256\262.md"
index aa5c34265..aaeae4e7c 100644
--- "a/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25422\350\256\262.md"
+++ "b/docs/Basic/\351\207\215\345\255\246\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\254\25422\350\256\262.md"
@@ -59,11 +59,11 @@ for (int j = 0; j \<= (100-7\*i) / 3; j++) {
 
 #### 03 | 增删查:掌握数据处理的基本操作,以不变应万变
 
-**【问题】** 对于一个包含 5 个元素的数组,如果要把这个数组元素的顺序翻转过来。你可以试着分析该过程需要对数据进行哪些操作? **【解析】** 假设原数组 a = {1,2,3,4,5},现在要更改为 a = {5,4,3,2,1}。要想得到新的数组,就要找到 “1” 和 “5”,再分别把它们赋值给对方。因此,这里主要会产生大量的基于索引位置的查找动作。
+**【问题】** 对于一个包含 5 个元素的数组,如果要把这个数组元素的顺序翻转过来。你可以试着分析该过程需要对数据进行哪些操作?**【解析】** 假设原数组 a = {1,2,3,4,5},现在要更改为 a = {5,4,3,2,1}。要想得到新的数组,就要找到 “1” 和 “5”,再分别把它们赋值给对方。因此,这里主要会产生大量的基于索引位置的查找动作。
 
-#### 04 | 如何完成线性表结构下的增删查? **【问题】** 给定一个包含 n 个元素的链表,现在要求每 k 个节点一组进行翻转,打印翻转后的链表结果。例如,链表为 1 -> 2 -> 3 -> 4 -> 5 -> 6,k = 3,则打印 321654。 **【解析】** 我们给出一些提示。利用链表翻转的算法,这个问题应该很简单。利用 3 个指针,prev、curr、next,执行链表翻转,每次得到了 k 个翻转的结点就执行打印。
+#### 04 | 如何完成线性表结构下的增删查?**【问题】** 给定一个包含 n 个元素的链表,现在要求每 k 个节点一组进行翻转,打印翻转后的链表结果。例如,链表为 1 -> 2 -> 3 -> 4 -> 5 -> 6,k = 3,则打印 321654。**【解析】** 我们给出一些提示。利用链表翻转的算法,这个问题应该很简单。利用 3 个指针,prev、curr、next,执行链表翻转,每次得到了 k 个翻转的结点就执行打印。
 
-#### 05 | 栈:后进先出的线性表,如何实现增删查? **【问题】** 给定一个包含 n 个元素的链表,现在要求每 k 个节点一组进行翻转,打印翻转后的链表结果。例如,链表为 1 -> 2 -> 3 -> 4 -> 5 -> 6,k = 3,则打印 321654。要求用栈来实现。 **【解析】** 我们用栈来实现它,就很简单了。你可以牢牢记住, **只要涉及翻转动作的题目,都是使用栈来解决的强烈信号** 。
+#### 05 | 栈:后进先出的线性表,如何实现增删查?**【问题】** 给定一个包含 n 个元素的链表,现在要求每 k 个节点一组进行翻转,打印翻转后的链表结果。例如,链表为 1 -> 2 -> 3 -> 4 -> 5 -> 6,k = 3,则打印 321654。要求用栈来实现。**【解析】** 我们用栈来实现它,就很简单了。你可以牢牢记住,**只要涉及翻转动作的题目,都是使用栈来解决的强烈信号** 。
 
 具体的操作如下,设置一个栈,不断将队列数据入栈,并且实时记录栈的大小。当栈的大小达到 k 的时候,全部出栈。我们给出核心代码:
 
@@ -88,13 +88,13 @@ System.out.print(stack.pop());
 }
 ```
 
-#### 07 | 数组:如何实现基于索引的查找? **详细分析和答案,请翻阅 18 课时例题 1** 。
+#### 07 | 数组:如何实现基于索引的查找?**详细分析和答案,请翻阅 18 课时例题 1** 。
 
-#### 08 | 字符串:如何正确回答面试中高频考察的字符串匹配算法? **详细分析和解题步骤,请参考 17 课时例题 1。** #### 10 | 哈希表:如何利用好高效率查找的“利器”? **详细分析和答案,请翻阅 15 课时例题 1** 。
+#### 08 | 字符串:如何正确回答面试中高频考察的字符串匹配算法?**详细分析和解题步骤,请参考 17 课时例题 1。** #### 10 | 哈希表:如何利用好高效率查找的“利器”?**详细分析和答案,请翻阅 15 课时例题 1** 。
 
-#### 11 | 递归:如何利用递归求解汉诺塔问题? **详细分析和答案,请翻阅 16 课时例题 1** 。
+#### 11 | 递归:如何利用递归求解汉诺塔问题?**详细分析和答案,请翻阅 16 课时例题 1** 。
 
-#### 12 | 分治:如何利用分治法完成数据查找? **【问题】** 在一个有序数组中,查找出第一个大于 9 的数字,假设一定存在。例如,arr = { -1, 3, 3, 7, 10, 14, 14 };则返回 10。 **【解析】** 在这里提醒一下,带查找的目标数字具备这样的性质:
+#### 12 | 分治:如何利用分治法完成数据查找?**【问题】** 在一个有序数组中,查找出第一个大于 9 的数字,假设一定存在。例如,arr = { -1, 3, 3, 7, 10, 14, 14 };则返回 10。**【解析】** 在这里提醒一下,带查找的目标数字具备这样的性质:
 
 *   第一,它比 9 大;
 *   第二,它前面的数字(除非它是第一个数字),比 9 小。
@@ -193,13 +193,13 @@ return null;
 }
 ```
 
-#### 16 | 真题案例(一):算法思维训练 **【问题】** 如果现在是个线上实时交互的系统。客户端输入 x,服务端返回斐波那契数列中的第 x 位。那么,这个问题使用上面的解法是否可行。 **【解析】** 这里给你一个小提示,既然我这么问,答案显然是不可行的。如果不可行,原因是什么呢?我们又该如何解决?
+#### 16 | 真题案例(一):算法思维训练 **【问题】** 如果现在是个线上实时交互的系统。客户端输入 x,服务端返回斐波那契数列中的第 x 位。那么,这个问题使用上面的解法是否可行。**【解析】** 这里给你一个小提示,既然我这么问,答案显然是不可行的。如果不可行,原因是什么呢?我们又该如何解决?
 
 注意,题目中给出的是一个实时系统。当用户提交了 x,如果在几秒内没有得到系统响应,用户就会卸载 App 啦。
 
 一个实时系统,必须想方设法在 O(1) 时间复杂度内返回结果。因此,一个可行的方式是,在系统上线之前,把输入 x 在 1~100 的结果预先就计算完,并且保存在数组里。当收到 1~100 范围内输入时,O(1) 时间内就可以返回。如果不在这个范围,则需要计算。计算之后的结果返回给用户,并在数组中进行保存。以方便后续同样输入时,能在 O(1) 时间内可以返回。
 
-#### 17 | 真题案例(二):数据结构训练 **【问题】** 对于树的层次遍历,我们再拓展一下。如果要打印的不是层次,而是蛇形遍历,又该如何实现呢?蛇形遍历就是 s 形遍历,即奇数层从左到右,偶数层从右到左。 **【解析】** 这里要对数据的顺序进行逆序处理,直观上,你需要立马想到栈。毕竟只有栈是后进先出的结构,是能快速实现逆序的。具体而言,需要建立两个栈 s1 和 s2。进栈的顺序是,s1 先右后左,s2 先左后右。两个栈交替出栈的结果就是 s 形遍历,代码如下:
+#### 17 | 真题案例(二):数据结构训练 **【问题】** 对于树的层次遍历,我们再拓展一下。如果要打印的不是层次,而是蛇形遍历,又该如何实现呢?蛇形遍历就是 s 形遍历,即奇数层从左到右,偶数层从右到左。**【解析】** 这里要对数据的顺序进行逆序处理,直观上,你需要立马想到栈。毕竟只有栈是后进先出的结构,是能快速实现逆序的。具体而言,需要建立两个栈 s1 和 s2。进栈的顺序是,s1 先右后左,s2 先左后右。两个栈交替出栈的结果就是 s 形遍历,代码如下:
 
 ```plaintext
 public ArrayList\> Print(TreeNodes pRoot) {
@@ -285,7 +285,7 @@ return list;
 }
 ```
 
-#### 18 | 真题案例(三): 力扣真题训练 **【问题】** 给定一个链表,删除链表的倒数第 n 个节点。例如,给定一个链表: 1 -> 2 -> 3 -> 4 -> 5, 和 n = 2。当删除了倒数第二个节点后,链表变为 1 -> 2 -> 3 -> 5。你可以假设,给定的 n 是有效的。额外要求就是,要在一趟扫描中实现,即时间复杂度是 O(n)。这里给你一个提示,可以采用快慢指针的方法。 **【解析】** 定义快慢指针,slow 和 fast 并同时指向 header。然后,让 fast 指针先走 n 步。接着,让二者保持同样的速度,一起往前走。最后,fast 指针先到达终点,并指向了 null。此时,slow 指针的结果就是倒数第 n 个结点。比较简单,我们就不给代码了。
+#### 18 | 真题案例(三): 力扣真题训练 **【问题】** 给定一个链表,删除链表的倒数第 n 个节点。例如,给定一个链表: 1 -> 2 -> 3 -> 4 -> 5, 和 n = 2。当删除了倒数第二个节点后,链表变为 1 -> 2 -> 3 -> 5。你可以假设,给定的 n 是有效的。额外要求就是,要在一趟扫描中实现,即时间复杂度是 O(n)。这里给你一个提示,可以采用快慢指针的方法。**【解析】** 定义快慢指针,slow 和 fast 并同时指向 header。然后,让 fast 指针先走 n 步。接着,让二者保持同样的速度,一起往前走。最后,fast 指针先到达终点,并指向了 null。此时,slow 指针的结果就是倒数第 n 个结点。比较简单,我们就不给代码了。
 
 #### 19 | 真题案例(四):大厂真题实战演练 **【问题】** 小明从小就喜欢数学,喜欢在笔记里记录很多表达式。他觉得现在的表达式写法很麻烦,为了提高运算符优先级,不得不添加很多括号。如果不小心漏了一个右括号的话,就差之毫厘,谬之千里了。因此他改用前缀表达式,例如把 `(2 + 3) * 4`写成`* + 2 3 4`,这样就能避免使用括号了。这样的表达式虽然书写简单,但计算却不够直观。请你写一个程序帮他计算这些前缀表达式。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25401\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25401\350\256\262.md"
index 92acdbb4a..99fc8b517 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25401\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25401\350\256\262.md"
@@ -50,15 +50,15 @@ private final Map logMap =
 
 你可能会说,这个代码错误看起来太幼稚、太低级、太可笑了吧? 确实是这样的。这段错误的代码,我的眼睛不知道看过了它们多少次,可是这个小虫子(bug)还是华丽丽地逃脱了我的注意,进入了 **JDK 11 的最终发布版** 。
 
-如果使用条件语句,而不是条件运算符,这个幼稚错误发生的概率会急剧下降。 **坚持使用最直观的编码方式,而不是追求代码简短,真的可以避免很多不必要的错误** 。所以说啊,选择适合的编码方式,强调代码的检查、评审、校验,真的怎么都不算过分。
+如果使用条件语句,而不是条件运算符,这个幼稚错误发生的概率会急剧下降。**坚持使用最直观的编码方式,而不是追求代码简短,真的可以避免很多不必要的错误** 。所以说啊,选择适合的编码方式,强调代码的检查、评审、校验,真的怎么都不算过分。
 
 现在,如果你要再问我喜欢哪种编码方式,毫无疑问,我喜欢使用条件语句,而不是条件运算符。因为,用条件语句这种编码方式,可以给我确定感,我也不需要挑战什么高难度动作;而看代码的人,也可以很确定,很轻松,不需要去查验什么模糊的东西。
 
-这种阅读起来的确定性至少有三点好处,第一点是可以减少代码错误;第二点是可以节省我思考的时间;第三点是可以节省代码阅读者的时间。 **减少错误、节省时间,是我们现在选择编码方式的一个最基本的原则。** 《C 程序设计语言》这本 C 程序员的圣经,初次发表于 1978 年。那个年代的代码,多数很简单直接。简短的代码,意味着节省昂贵的计算能力,是当时流行的编码偏好。而现在,计算能力不再是瓶颈,如何更高效率地开发复杂的软件,成了我们首先需要考虑的问题。
+这种阅读起来的确定性至少有三点好处,第一点是可以减少代码错误;第二点是可以节省我思考的时间;第三点是可以节省代码阅读者的时间。**减少错误、节省时间,是我们现在选择编码方式的一个最基本的原则。** 《C 程序设计语言》这本 C 程序员的圣经,初次发表于 1978 年。那个年代的代码,多数很简单直接。简短的代码,意味着节省昂贵的计算能力,是当时流行的编码偏好。而现在,计算能力不再是瓶颈,如何更高效率地开发复杂的软件,成了我们首先需要考虑的问题。
 
 有一些新设计的编程语言,不再提供条件运算符。 比如,Kotlin 语言的设计者认为,编写简短的代码绝对不是 Kotlin 的目标。所以,Kotlin 不支持条件运算符。 Go 语言的设计者认为,条件运算符的滥用,产生了许多难以置信的、难以理解的复杂表达式。所以,Go 语言也不支持条件运算符。
 
-## 我们看到, **现实环境的变化,影响着我们对于代码“好”与“坏”的判断标准。** “好”的代码与“坏”的代码
+## 我们看到,**现实环境的变化,影响着我们对于代码“好”与“坏”的判断标准。** “好”的代码与“坏”的代码
 
 虽然对于“什么是优秀的代码“难以形成一致意见,但是这么多年的经验,让我对代码“好”与“坏”积累了一些自己的看法。
 
@@ -83,7 +83,7 @@ private final Map logMap =
 
 ## 优秀的代码是“经济”的代码
 
-大概也没人想记住这么多条标准吧?所以, **关于优秀代码的特点,我想用“经济”这一个词语来表达** 。这里的“经济”,指的是使用较少的人力、物力、财力、时间、空间,来获取较大的成果或收益 。或者简单地说, **投入少、收益大、投资回报高** 。为了方便,你也可以先理解为节俭或者抠门儿的意思。
+大概也没人想记住这么多条标准吧?所以,**关于优秀代码的特点,我想用“经济”这一个词语来表达** 。这里的“经济”,指的是使用较少的人力、物力、财力、时间、空间,来获取较大的成果或收益 。或者简单地说,**投入少、收益大、投资回报高** 。为了方便,你也可以先理解为节俭或者抠门儿的意思。
 
 当然,使用一个词语表达肯定是以偏概全的。但是,比起一长串的准则,一个关键词的好处是,更容易让人记住。我想这点好处可以大致弥补以偏概全的损失。
 
@@ -107,7 +107,7 @@ private final Map logMap =
 
 对软件开发流程选择的差异,就带来了我们对代码质量理解,以及对代码质量重视程度的千差万别。 比如说,一个创业公司是万万不能照搬大型成熟软件的开发流程的。因为,全面的高质量、高可靠、高兼容性的软件可能并不是创业公司最核心的目标。如果过分纠缠于这些代码指标,创始人的时间、投资人的金钱可能都没有办法得到最有效的使用。
 
-当然,越成熟的软件开发机制越容易写出优秀的代码。但是, **最适合当前现实环境的代码,才是最优秀的代码。**
+当然,越成熟的软件开发机制越容易写出优秀的代码。但是,**最适合当前现实环境的代码,才是最优秀的代码。**
 
 所以,当我们考虑具体投入的时候,还要考虑我们所处的现实环境。 如果我们超出现实环境去讨论代码的质量,有时候会有失偏颇,丧失我们讨论代码质量的意义。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25402\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25402\350\256\262.md"
index fd89cc8db..ecc7934b7 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25402\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25402\350\256\262.md"
@@ -24,7 +24,7 @@ fail:
 
 这个具有重大杀伤力的 bug 是如此的“幼稚”,如此的“好玩”,如此的“萌萌哒”,以至于到现在,人们还可以买到印有“GoTo Fail”的 T 恤衫,更别提业界对于这个问题的兴趣了。有很多文章,专门研究这一个“低级”安全漏洞;甚至有人探讨这个“低级”错误对于计算机软件教育的积极影响。
 
-所有的危机都不应该被浪费,这一次也不例外。这些年,我也一直在思考 **为什么我们会犯如此“低级”的错误** ?即使是在苹果这样的大公司。反过来再想,我们应该如何尽可能避免类似的错误呢?
+所有的危机都不应该被浪费,这一次也不例外。这些年,我也一直在思考 **为什么我们会犯如此“低级”的错误**?即使是在苹果这样的大公司。反过来再想,我们应该如何尽可能避免类似的错误呢?
 
 ## 人人都会犯错误
 
@@ -36,9 +36,9 @@ fail:
 
 第二个更加普遍的观点是同样的错误不能犯第二次。作为一名程序员,我同样尊重这个观点背后代表的美好期望。但是,我想给这个观点加一点点限制。这个观点应该是我们对自身的期望和要求;对于他人,我们可以更宽容; **对于一个团队,我们首先要思考如何提供一种机制,**  **以减少此类错误的发生** 。如果强制要求他人错不过三,现实中,我们虽然发泄了怨气,但是往往错失了工作机制提升的机会。
 
-第三个深入人心的观点是一个人犯了错误并不可怕,怕的是不承认错误。同样的,我理解这个观点背后代表的美好诉求。这是一个深入人心的观点,具有深厚的群众基础,我万万不敢造次。在软件工程领域,我想,在犯错这件事情上,我们还是要再多一点对自己的谅解,以及对他人的宽容。错误并不可怕,你不必为此深深自责,更不应该责备他人。要不然, **一旦陷入自责和指责的漩涡,很多有建设意义的事情,我们可能没有意识去做;或者即使意识到了,也没法做,做不好** 。
+第三个深入人心的观点是一个人犯了错误并不可怕,怕的是不承认错误。同样的,我理解这个观点背后代表的美好诉求。这是一个深入人心的观点,具有深厚的群众基础,我万万不敢造次。在软件工程领域,我想,在犯错这件事情上,我们还是要再多一点对自己的谅解,以及对他人的宽容。错误并不可怕,你不必为此深深自责,更不应该责备他人。要不然,**一旦陷入自责和指责的漩涡,很多有建设意义的事情,我们可能没有意识去做;或者即使意识到了,也没法做,做不好** 。
 
-## 我这么说,你是不是开始有疑惑了:人人都会犯错误,还重复犯,还不能批评,这怎么能编写出优秀的代码呢?换句话说就是, **我们怎么样才会少犯错误呢?** 把错误关在笼子里
+## 我这么说,你是不是开始有疑惑了:人人都会犯错误,还重复犯,还不能批评,这怎么能编写出优秀的代码呢?换句话说就是,**我们怎么样才会少犯错误呢?** 把错误关在笼子里
 
 人人都会犯错误,苹果的工程师也不例外。所以,“GoTo Fail”的“幼稚”漏洞,实在是在情理之中。可是,这样的漏洞是如何逃脱重重“监管”,出现在最终的发布产品中,这多多少少让我有点出乎意料。
 
@@ -54,7 +54,7 @@ fail:
 
 在我看来,上面那段代码,起码有两个地方可以优化。如果那位程序员能够按照规范的方式写代码,那“GoTo Fail”的漏洞应该是很容易被发现。我们在遇到问题的时候,也应该尽量朝着规范以及可持续改进的角度去思考错误背后的原因,而非一味地自责。
 
-首先, **他应该正确使用缩进** 。你现在可以再看下我优化后的代码,是不是第三行的代码特别刺眼,是不是更容易被“逮住”?
+首先,**他应该正确使用缩进** 。你现在可以再看下我优化后的代码,是不是第三行的代码特别刺眼,是不是更容易被“逮住”?
 
 ```java
     if ((error = doSomething()) != 0)
@@ -66,7 +66,7 @@ fail:
     return error;
 ```
 
-其次, **他应该使用大括号** 。使用大括号后,这个问题是不是就自动消失了?虽然,多余的这一行依然是多余的,但已经是没有多大危害的一行代码了。
+其次,**他应该使用大括号** 。使用大括号后,这个问题是不是就自动消失了?虽然,多余的这一行依然是多余的,但已经是没有多大危害的一行代码了。
 
 ```java
     if ((error = doSomething()) != 0) {
@@ -82,7 +82,7 @@ fail:
 
 从上面这个例子里,不知道你有没有体会到,好的代码风格带来的好处呢?工作中,像苹果公司的那位程序员一样的错误,你应该没少遇到吧?那现在,你是不是可以思考如何从代码风格的角度来避免类似的错误呢?
 
-魔鬼藏于细节。很多时候, **优秀的代码源于我们对细节的热情和执着** 。可能,你遇到的或者想到的问题,不是每一个都有完美的答案或者解决办法。但是, **如果你能够找到哪怕仅仅是一个小问题的一个小小的改进办法,都有可能会给你的代码质量带来巨大的提升和改变** 。
+魔鬼藏于细节。很多时候,**优秀的代码源于我们对细节的热情和执着** 。可能,你遇到的或者想到的问题,不是每一个都有完美的答案或者解决办法。但是,**如果你能够找到哪怕仅仅是一个小问题的一个小小的改进办法,都有可能会给你的代码质量带来巨大的提升和改变** 。
 
 当然,你可能还会说,我代码风格不错,但是那个问题就是没看到,这也是极有可能的事情。是这样,所以也就有了第二道工序:编译器。
 
@@ -98,7 +98,7 @@ fail:
 
 ### 第三道关:回归测试 (Regression Testing)
 
-一般地,软件测试会尽可能地覆盖 **关键逻辑和负面清单** ,以确保关键功能能够正确执行,关键错误能够有效处理。一般情况下,无论是开发人员,还是测试人员,都要写很多测试代码,来测试软件是否达到预期的要求。
+一般地,软件测试会尽可能地覆盖 **关键逻辑和负面清单**,以确保关键功能能够正确执行,关键错误能够有效处理。一般情况下,无论是开发人员,还是测试人员,都要写很多测试代码,来测试软件是否达到预期的要求。
 
 另外,这些测试代码还有一个关键用途就是做回归测试 。如果有代码变更,我们可以用回归测试来检查这样的代码变更有没有使代码变得更坏。
 
@@ -134,9 +134,9 @@ if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
 
 ## 代码制造的流水线
 
-我们分析了这重重关卡,我特别想传递的一个想法就是, **编写优秀的代码,不能仅仅依靠一个人的战斗** 。代码的优秀级别,依赖于每个关卡的优秀级别。高质量的代码,依赖于高质量的流水线。每道关卡都应该给程序员提供积极的反馈。这些反馈,在保障代码质量的同时,也能帮助程序员快速学习和成长。
+我们分析了这重重关卡,我特别想传递的一个想法就是,**编写优秀的代码,不能仅仅依靠一个人的战斗** 。代码的优秀级别,依赖于每个关卡的优秀级别。高质量的代码,依赖于高质量的流水线。每道关卡都应该给程序员提供积极的反馈。这些反馈,在保障代码质量的同时,也能帮助程序员快速学习和成长。
 
-可是,即使我们设置了重重关卡,“GoTo Fail”依然“过关斩将”,一行代码一路恣意玩耍。这里面有关卡虚设的因素,也有我们粗心大意的因素。我们怎么样才能打造更好的关卡,或者设置更好的笼子?尤其是, **身为程序员,如何守好第一关?**
+可是,即使我们设置了重重关卡,“GoTo Fail”依然“过关斩将”,一行代码一路恣意玩耍。这里面有关卡虚设的因素,也有我们粗心大意的因素。我们怎么样才能打造更好的关卡,或者设置更好的笼子?尤其是,**身为程序员,如何守好第一关?**
 
 欢迎你在留言区说说自己的思考。下一讲,我们再接着聊这个话题。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25403\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25403\350\256\262.md"
index 22924fed9..dfe5e978c 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25403\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25403\350\256\262.md"
@@ -30,21 +30,21 @@ public class MyThread extends Thread {
 
 要解决具体的问题,仅仅做到熟练使用编程语言是远远不够的,我们还需要更多工具。如果做前端,需要理解 HTML 和浏览器;如果做后端,需要掌握数据库和操作系统;如果做云计算,需要掌握 Kubernetes 等等。就像学了分筋错骨手,还要学降龙十八掌;学了七十二路空明拳,还要学左右互搏。俗话说,艺多不压身,工具箱永远都不嫌满。
 
-有了工具还不够,优秀的程序员还要深入理解问题,懂得问题的最核心价值。只有理解了问题,看到了解决问题的价值,我们才能够真正解决好问题,并且从中获得满满的成就感。 **我们一定要记得,程序员的存在不是为了写代码,而是为了解决现实问题,实现现实价值** 。
+有了工具还不够,优秀的程序员还要深入理解问题,懂得问题的最核心价值。只有理解了问题,看到了解决问题的价值,我们才能够真正解决好问题,并且从中获得满满的成就感。**我们一定要记得,程序员的存在不是为了写代码,而是为了解决现实问题,实现现实价值** 。
 
-真实的作品,都带着我们对于现实问题的理解。而打磨一个这样的作品,需要缜密的逻辑、突破创新和贯彻执行。通过使用合适的工具,把简单的、一行一行的代码,耐心地粘合、打磨成优秀的作品。 **如果说花样的工具是外家功夫,思维能力和行为能力可以算是内功** 。
+真实的作品,都带着我们对于现实问题的理解。而打磨一个这样的作品,需要缜密的逻辑、突破创新和贯彻执行。通过使用合适的工具,把简单的、一行一行的代码,耐心地粘合、打磨成优秀的作品。**如果说花样的工具是外家功夫,思维能力和行为能力可以算是内功** 。
 
 优秀的程序员,是一个内外双修的程序员。如果一个程序员可以熟练使用工具,有清晰的解决问题思路,能明晰地传达产品价值,那么他编写代码就不存在什么巨大的困难了。
 
 ## 发现关键的问题
 
-有了工具,遇到问题能解决掉,我们就可以做事情了。优秀的程序员还有一项好本领,就是发现关键的问题。 **能够发现关键的问题,我觉得是一个好程序员和优秀程序员的分水岭** 。
+有了工具,遇到问题能解决掉,我们就可以做事情了。优秀的程序员还有一项好本领,就是发现关键的问题。**能够发现关键的问题,我觉得是一个好程序员和优秀程序员的分水岭** 。
 
 优秀的程序员,能够发现一门编程语言的缺陷,一个顺手工具的局限。所以,他知道该怎么选择最合适的工具,该怎么避免不必要的麻烦。
 
 优秀的程序员,能够发现解决方案背后的妥协和风险。所以,他可以预设风险防范措施,设置软件的适用边界。
 
-优秀的程序员,能够敏锐地观察到产品的关键问题,或者客户未被满足的需求。所以,他可以推动产品持续地进步和演化。 **能够发现关键的问题,意味着我们可以从一个被动的做事情的程序员,升级为一个主动找事情的程序员** 。
+优秀的程序员,能够敏锐地观察到产品的关键问题,或者客户未被满足的需求。所以,他可以推动产品持续地进步和演化。**能够发现关键的问题,意味着我们可以从一个被动的做事情的程序员,升级为一个主动找事情的程序员** 。
 
 能够发现关键的问题,往往需要我们对一个领域有很深入的研究和深厚的积累,并且对新鲜事物保持充分的好奇心和求知欲。
 
@@ -74,7 +74,7 @@ public class MyThread extends Thread {
 
 优秀的程序员是一个领导型的人。他能够倾听,持续地获取他人的优秀想法,以及不同的意见。他能够表达,准确地传递自己的想法,恰当地陈述自己的意见。他是一个给予者,给别人尊重,给别人启发,给别人指导,给别人施展才华的空间。他是一个索取者,需要获得尊重,需要获得支持,需要持续学习,需要一个自主决策的空间。他能够应对压力,承担责任,积极主动,大部分时候保持克制和冷静,偶尔也会表达愤怒。他具有一定的影响力,以及良好的人际关系,能够和各种类型的人相处,能够引发反对意见,但是又不损害人际关系。他知道什么时候可以妥协,什么时候应该坚持。
 
-上面的这些,通常称为“软技能”。 **如果说,编程语言、花样工具、逻辑思维、解决问题这些“硬技能”可以决定我们的起点的话,影响力、人际关系这些“软技能”通常影响着我们可以到达的高度。** 因为,无论我们是加入他人的团队,或者组建自己的团队,我们只有在团队中才能变得越来越出色,做的事情越来越重要。所以,我们需要成为优秀的团队成员,接受影响,也影响他人。
+上面的这些,通常称为“软技能”。**如果说,编程语言、花样工具、逻辑思维、解决问题这些“硬技能”可以决定我们的起点的话,影响力、人际关系这些“软技能”通常影响着我们可以到达的高度。** 因为,无论我们是加入他人的团队,或者组建自己的团队,我们只有在团队中才能变得越来越出色,做的事情越来越重要。所以,我们需要成为优秀的团队成员,接受影响,也影响他人。
 
 ## 时间管理者
 
@@ -86,9 +86,9 @@ public class MyThread extends Thread {
 
 你有没有听说过这样的故事? 一家工厂的发动机坏了,请了很多人都没有修好。无奈,请了一位工程师,他听了听声音,在发动机上画了一道线,说:“打开,把线圈拆了”。果然,发动机就修好了。不管这个小故事是真的也好,假的也好,类似的事情在软件公司时时刻刻都在发生。有经验的程序员三分钟就能发现的问题,外行可能需要折腾好几天。持续地提高我们的硬技能和软技能,可以让我们做事情更快更好。
 
-坚持把时间用在对的地方,用在价值更大的地方。事情总是做不完的。一般的工程师,都有一种打破砂锅问到底的精气神,这是好事。可是,这顺便带来了一点点的副作用,很多人有一点点小小的强迫症,很多事情,喜欢自己动手整个清楚明白。可是,事情又特别多,很多事情根本就顾不上。怎么办呢? **要做只有你才能做的事情** 。是的,有很多事情,只有你可以做,只有你做得最快最好。其他的同事也是一样的,有很多事情,只有他们能做,只有他们做得最快最好。选择最合适的人做最合适的事,这不仅是领导的工作分配,也可以是我们自己的协商选择。
+坚持把时间用在对的地方,用在价值更大的地方。事情总是做不完的。一般的工程师,都有一种打破砂锅问到底的精气神,这是好事。可是,这顺便带来了一点点的副作用,很多人有一点点小小的强迫症,很多事情,喜欢自己动手整个清楚明白。可是,事情又特别多,很多事情根本就顾不上。怎么办呢?**要做只有你才能做的事情** 。是的,有很多事情,只有你可以做,只有你做得最快最好。其他的同事也是一样的,有很多事情,只有他们能做,只有他们做得最快最好。选择最合适的人做最合适的事,这不仅是领导的工作分配,也可以是我们自己的协商选择。
 
-事情做不完,就需要面临选择。 **要坚持做需要做的事情** 。不需要的、不紧急的、价值不大的,我们可以暂时搁置起来。一个人,能做的事情是有限的,能把最重要的事情最好,就已经很了不起了。
+事情做不完,就需要面临选择。**要坚持做需要做的事情** 。不需要的、不紧急的、价值不大的,我们可以暂时搁置起来。一个人,能做的事情是有限的,能把最重要的事情最好,就已经很了不起了。
 
 学会选择,是我们进阶道路上的一个必修课。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25404\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25404\350\256\262.md"
index 09db0bde6..d3dee7e0a 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25404\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25404\350\256\262.md"
@@ -6,7 +6,7 @@
 
 一般而言,一份高质量的编码规范,是严格的、清晰的、简单的,也是权威的。但是我们有时候并不想从内心信服,更别提自觉遵守了。你可能想问,遵循这样的约定到底有什么用呢?
 
-编码规范可以帮我们选择编码风格、确定编码方法,以便更好地进行编码实践。 简单地说, **一旦学会了编码规范,并且严格地遵守它们,可以让我们的工作更简单,更轻松,少犯错误** 。
+编码规范可以帮我们选择编码风格、确定编码方法,以便更好地进行编码实践。 简单地说,**一旦学会了编码规范,并且严格地遵守它们,可以让我们的工作更简单,更轻松,少犯错误** 。
 
 这个问题弄明白了,我们就能愉快地遵守这些约定,改进我们的编程方式了。
 
@@ -59,7 +59,7 @@ fail:
 
 在代码分析这道关,编码规范也是可以执行检查分析的一个重要部分。类似于编译器,如果有警告出现,分析警告对我们的精力是一种不必要的浪费; 如果过度自由,同样会阻碍代码分析工具提供更丰富的特性。
 
-只要警报拉响,不管处在哪一个关卡,源代码编写者都需要回到流水线的第一关,重新评估反馈、更改代码、编译代码、提交评审、等待评审结果等等。每一次的返工,都是对时间和精力的消耗。 **总结一下,在代码制造的每一道关卡,规范执行得越早,问题解决得越早,整个流水线的效率也就越高。** 前一段时间,阿里巴巴发表了《阿里巴巴 Java 开发手册》。我相信,或许很快,执行阿里巴巴 Java 编码规约检查的工具就会出现,并且成为流水线的一部分。 对于违反强制规约的,报以错误;对于违反推荐或者规约参考的,报以警告。这样,流水线才会自动促进程序员的学习和成长,修正不符合规范的编码。
+只要警报拉响,不管处在哪一个关卡,源代码编写者都需要回到流水线的第一关,重新评估反馈、更改代码、编译代码、提交评审、等待评审结果等等。每一次的返工,都是对时间和精力的消耗。**总结一下,在代码制造的每一道关卡,规范执行得越早,问题解决得越早,整个流水线的效率也就越高。** 前一段时间,阿里巴巴发表了《阿里巴巴 Java 开发手册》。我相信,或许很快,执行阿里巴巴 Java 编码规约检查的工具就会出现,并且成为流水线的一部分。 对于违反强制规约的,报以错误;对于违反推荐或者规约参考的,报以警告。这样,流水线才会自动促进程序员的学习和成长,修正不符合规范的编码。
 
 ## 规范的代码,降低软件维护成本
 
@@ -69,7 +69,7 @@ fail:
 
 如果是开源代码,它会面临更多眼光的挑剔。即使是封闭代码,也有可能接受各种各样的考验。"出道”的代码有它自己的旅程,有时候超越我们的控制和想象。在它的旅程中,会有新的程序员加入进来,观察它,分析它,改造它,甚至毁灭它。软件的维护,是这个旅程中最值得考虑的部分。
 
-有统计数据表明, **在一个软件生命周期里,软件维护阶段花费了大约 80% 的成本** 。这个成分,当然包括你我投入到软件维护阶段的时间和精力。
+有统计数据表明,**在一个软件生命周期里,软件维护阶段花费了大约 80% 的成本** 。这个成分,当然包括你我投入到软件维护阶段的时间和精力。
 
 举例来说吧,让我们一起来看看,一个 Java 的代码问题,在 OpenJDK 社区会发生什么呢?
 
@@ -85,7 +85,7 @@ fail:
 
 如果确定了问题,开发人员或者维护人员会进一步评估、设计潜在的解决方案。如果原代码的作者不能提供任何帮助,比如已经离职,那么他们可以依靠的信息,就只有代码本身了。
 
-你看,这个代码问题修改的过程重包含了很多角色:代码的编写者、代码的使用者、问题的审阅者以及问题的解决者, 这些角色一般不是同一个人。在修改代码时,不管我们是其中的哪一个角色,遵守规范的代码都能够节省我们的时间。 **很多软件代码,其生命的旅程超越了它的创造者,超越了团队的界限,超越了组织的界限,甚至会进入我们难以预想的领域** 。即使像空格缩进这样的小问题,随着这段代码的扩散,以及接触到这段代码人数的增加,由它造成的效率问题也会对应的扩散、扩大。
+你看,这个代码问题修改的过程重包含了很多角色:代码的编写者、代码的使用者、问题的审阅者以及问题的解决者, 这些角色一般不是同一个人。在修改代码时,不管我们是其中的哪一个角色,遵守规范的代码都能够节省我们的时间。**很多软件代码,其生命的旅程超越了它的创造者,超越了团队的界限,超越了组织的界限,甚至会进入我们难以预想的领域** 。即使像空格缩进这样的小问题,随着这段代码的扩散,以及接触到这段代码人数的增加,由它造成的效率问题也会对应的扩散、扩大。
 
 而严格遵守共同的编码规范,提高代码的可读性,可以使参与其中的人更容易地理解代码,更快速地理解代码,更快速地解决问题。
 
@@ -113,7 +113,7 @@ fail:
 
 对于编码规范这件事,我特别想和你分享盐野七生在《罗马人的故事》这套书里的一句话:“ **一件东西,无论其实用性多强,终究比不上让人心情愉悦更为实用。** ”
 
-严格地遵守编码规范,可以使我们的工作更简单,更轻松,更愉快。 记住, **优秀的代码不光是给自己看的,也是给别人看的,而且首先是给别人看的** 。
+严格地遵守编码规范,可以使我们的工作更简单,更轻松,更愉快。 记住,**优秀的代码不光是给自己看的,也是给别人看的,而且首先是给别人看的** 。
 
 你有什么编码规范的故事和大家分享吗? 欢迎你在留言区写写自己的想法,我们可以进一步讨论。也欢迎你把今天的文章分享给跟你协作的同学,看看编码规范能不能让你们之间的合作更轻松愉快。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25405\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25405\350\256\262.md"
index ac11933c6..cf0705284 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25405\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25405\350\256\262.md"
@@ -60,7 +60,7 @@ public byte[] isEmpty();
 
 下面的表格列出了不同例子的正确转换形式,和容易出错的转换形式 (出自“Google Java Style Guide”)。
 
-![img](assets/f28217dc672df8bc968eccb57ce19c1d.png) **2. 蛇形命名法(snake_case)** 在蛇形命名法中,单词之间通过下划线“\_”连接,比如“out_of_range”。 **3. 串式命名法(kebab-case)** 在蛇形命名法中,单词之间通过连字符“-”连接,比如“background-color”。 **4. 匈牙利命名法**
+![img](assets/f28217dc672df8bc968eccb57ce19c1d.png) **2. 蛇形命名法(snake_case)** 在蛇形命名法中,单词之间通过下划线“\_”连接,比如“out_of_range”。**3. 串式命名法(kebab-case)** 在蛇形命名法中,单词之间通过连字符“-”连接,比如“background-color”。**4. 匈牙利命名法**
 
 在匈牙利命名法中,标识符由一个或者多个小写字母开始,这些字母用来标识标识符的类型或者用途。标识符的剩余部分,可以采取其他形式的命名法,比如大驼峰命名法。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25406\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25406\350\256\262.md"
index 1e3625279..a41be8da4 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25406\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25406\350\256\262.md"
@@ -82,15 +82,15 @@ public class CodingFormat {
 }
 ```
 
-那么到底如何利用空白空间呢?可以分为下面四个方法,我来一一讲解一下。 **同级别代码块靠左对齐** 我们阅读的习惯顺序是从左到右,代码也如此。因此不同行,但同级别的代码要靠左对齐。
+那么到底如何利用空白空间呢?可以分为下面四个方法,我来一一讲解一下。**同级别代码块靠左对齐** 我们阅读的习惯顺序是从左到右,代码也如此。因此不同行,但同级别的代码要靠左对齐。
 
 比如,上面的 CodingFormat 例子中,main() 方法和 Greeting 枚举类都是 CodingFormat 的下一级内容,属于同一级别的两个块。 两个代码块的左侧要对齐。
 
-上面的 CodingFormat 例子中的枚举常量、枚举类的变量、枚举类的方法,也是属于同一级别的内容。 对应地,左侧要对齐。 **同级别代码块空行分割** 我们阅读代码总是从上往下读,不同行的同级别的代码块之间,要使用空行分割。
+上面的 CodingFormat 例子中的枚举常量、枚举类的变量、枚举类的方法,也是属于同一级别的内容。 对应地,左侧要对齐。**同级别代码块空行分割** 我们阅读代码总是从上往下读,不同行的同级别的代码块之间,要使用空行分割。
 
 当我们读到一个空行的时候,我们的大脑就会意识到这部分的信息结束了,可以停留下来接受这段信息。 另外,我们阅读代码的时候,碰到空白行,我们也可以暂停,往回看几行,或者重新回顾一下整个代码块,梳理逻辑、加深理解。
 
-比如,上面的 CodingFormat 例子中,main() 方法和 Greeting 枚举类之间的空白行,getGreeting() 和 getLanguage() 方法之间的空行,都是用来分割不同的信息块的。greeting 变量和 Greeting 构造方法之间的空白行,表示变量声明结束,下面是开始定义类的方法,同样起到分割信息块的作用。 **下一级代码块向右缩进** 我们上面讲了同级别的代码格式,那么不同级别的呢?
+比如,上面的 CodingFormat 例子中,main() 方法和 Greeting 枚举类之间的空白行,getGreeting() 和 getLanguage() 方法之间的空行,都是用来分割不同的信息块的。greeting 变量和 Greeting 构造方法之间的空白行,表示变量声明结束,下面是开始定义类的方法,同样起到分割信息块的作用。**下一级代码块向右缩进** 我们上面讲了同级别的代码格式,那么不同级别的呢?
 
 区分不同行的不同级别的代码,可以使用缩进。缩进的目的是为了让我们更直观地看到缩进线,从而意识到代码之间的关系。
 
@@ -173,7 +173,7 @@ public class CodingFormat {
 
 另外,如果我们使用八个空格作为一个缩进单元,为了代码的整洁性,我们往往会被迫使用最少的缩进嵌套,这也导致了额外的复杂性,可读性就降低了。
 
-由于我们倾向于使用有准确意义的命名,标识符的长度往往是一个不能忽视的因素。现在的编码规范,四个空格的缩进最为常见,二个空格的缩进次之,八个空格的缩进使用的较少。 **同行内代码块空格区隔** 我们上面讲的都是不同行的代码该如何注意格式。位于同一行内的代码块,同样需要注意。我们可以使用空格区分开不同的逻辑单元。
+由于我们倾向于使用有准确意义的命名,标识符的长度往往是一个不能忽视的因素。现在的编码规范,四个空格的缩进最为常见,二个空格的缩进次之,八个空格的缩进使用的较少。**同行内代码块空格区隔** 我们上面讲的都是不同行的代码该如何注意格式。位于同一行内的代码块,同样需要注意。我们可以使用空格区分开不同的逻辑单元。
 
 比如,逗号分隔符后要使用空格,以区分开下一个信息:
 
@@ -212,7 +212,7 @@ if (variable != null) {
 
 一般一个完整的表达式可以看作是一个独立的行为。
 
-编辑器的宽度,屏幕的宽度,都是有限制的。当一个完整的表达式比较长时,就需要换行。 **基本的换行原则**
+编辑器的宽度,屏幕的宽度,都是有限制的。当一个完整的表达式比较长时,就需要换行。**基本的换行原则**
 
 我们前面讨论的代码分块的基本思想,同样适用于换行。基本的换行规范需要考虑以下三点。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25407\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25407\350\256\262.md"
index 18917e90c..188aca226 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25407\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25407\350\256\262.md"
@@ -16,9 +16,9 @@
 
 可是,注释也是一个麻烦鬼,可能会给我们带来三个麻烦。
 
-首先, **因为注释不需要运行,所以没有常规的办法来测试它** 。 注释对不对?有没有随着代码变更?这些问题都是写注释需要注意的地方。 **注释难以维护,这是使用注释带来的最大的麻烦** 。
+首先,**因为注释不需要运行,所以没有常规的办法来测试它** 。 注释对不对?有没有随着代码变更?这些问题都是写注释需要注意的地方。**注释难以维护,这是使用注释带来的最大的麻烦** 。
 
-另一个麻烦是, **注释为我们提供了一个借口** 。使用注释来解释代码,是注释的本意。但是,我们有时候会过度依赖解释,从而放弃了潜在的替代方案,比如更准确的命名,更清晰的结构,更顺畅的逻辑等等。 **注释,被我们用成万能的狗皮膏药,有时会让代码更糟糕** 。
+另一个麻烦是,**注释为我们提供了一个借口** 。使用注释来解释代码,是注释的本意。但是,我们有时候会过度依赖解释,从而放弃了潜在的替代方案,比如更准确的命名,更清晰的结构,更顺畅的逻辑等等。**注释,被我们用成万能的狗皮膏药,有时会让代码更糟糕** 。
 
 比如,下面的代码和注释,看起来没毛病,但读起来很吃力。
 
@@ -48,7 +48,7 @@ String lastName;
 
 ## 几种常见注释类型
 
-接下来,我们就聊聊几种常见的注释类型。一个典型的源代码文件,一般包含不同类型的注释。不同类型的注释,有着不相同的要求,适用于不同的注释风格和原则。 **第一种类型,是记录源代码版权和授权的** ,一般放在每一个源文件的开头,说明源代码的版权所有者,以及授权使用的许可方式,或者其他的公共信息。比如,如果是个人的代码,版权信息可以写成:
+接下来,我们就聊聊几种常见的注释类型。一个典型的源代码文件,一般包含不同类型的注释。不同类型的注释,有着不相同的要求,适用于不同的注释风格和原则。**第一种类型,是记录源代码版权和授权的**,一般放在每一个源文件的开头,说明源代码的版权所有者,以及授权使用的许可方式,或者其他的公共信息。比如,如果是个人的代码,版权信息可以写成:
 
 ```java
 /*
@@ -56,11 +56,11 @@ String lastName;
  */
 ```
 
-一般来说,版权和授权信息是固定的。版权和授权信息是法律条款,除了年份,一个字都不能更改。对于每个源代码文件,我们记得复制粘贴在文件开头就行。 **需要注意的是,如果文件有变更,记得更改版权信息的年份(比如上例中的 2018)** 。 **第二种类型,是用来生成用户文档的** ,比如 Java Doc。 这部分的作用,是用来生成独立的、不包含源代码的文档。 这些文档帮助使用者了解软件的功能和细节,主要面向的是该软件的 **使用者** ,而不是该软件的开发者。 比如 Java 的 API 规范的文档。 **第三种类型,是用来解释源代码的** 。换句话说,就是帮助代码的阅读者理解代码。这是大家默认的注释类型,也是我们今天讨论的重点。
+一般来说,版权和授权信息是固定的。版权和授权信息是法律条款,除了年份,一个字都不能更改。对于每个源代码文件,我们记得复制粘贴在文件开头就行。**需要注意的是,如果文件有变更,记得更改版权信息的年份(比如上例中的 2018)** 。**第二种类型,是用来生成用户文档的**,比如 Java Doc。 这部分的作用,是用来生成独立的、不包含源代码的文档。 这些文档帮助使用者了解软件的功能和细节,主要面向的是该软件的 **使用者**,而不是该软件的开发者。 比如 Java 的 API 规范的文档。**第三种类型,是用来解释源代码的** 。换句话说,就是帮助代码的阅读者理解代码。这是大家默认的注释类型,也是我们今天讨论的重点。
 
 ## 简化注释的风格
 
-上面我们介绍了三种常见的注释类型,下面就针对这三种注释类型,再给你介绍 **三种风格** 的注释。 **针对第一种注释类型,也就是固定的版权和授权信息,使用一般的星号注释符(/-/)** 。注释块的首行和尾行只使用星号注释符,中间行以缩进一个空格的星号开始,文字和星号之间使用一个空格。注释的每行长度限制,和代码块的每行长度限制保持一致。
+上面我们介绍了三种常见的注释类型,下面就针对这三种注释类型,再给你介绍 **三种风格** 的注释。**针对第一种注释类型,也就是固定的版权和授权信息,使用一般的星号注释符(/-/)** 。注释块的首行和尾行只使用星号注释符,中间行以缩进一个空格的星号开始,文字和星号之间使用一个空格。注释的每行长度限制,和代码块的每行长度限制保持一致。
 
 比如:
 
@@ -126,9 +126,9 @@ if (!myString.isEmpty()) {
 
 那么,用来解释源代码的注释有什么需要注意的地方吗?为了规避注释的种种麻烦,有没有什么原则我们必需要遵守呢?我总结了以下三点。
 
-1. **准确** ,错误的注释比没有注释更糟糕。
-2. **必要** ,多余的注释浪费阅读者的时间。
-3. **清晰** ,混乱的注释会把代码搞得更乱。
+1. **准确**,错误的注释比没有注释更糟糕。
+2. **必要**,多余的注释浪费阅读者的时间。
+3. **清晰**,混乱的注释会把代码搞得更乱。
 
 比如,当我们说编程语言时,一定不要省略“编程”这两个字。否则,就可能会被误解为大家日常说话用的语言。这就是准确性的要求。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25409\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25409\350\256\262.md"
index 31a7bb078..2a3a4b85b 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25409\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25409\350\256\262.md"
@@ -18,7 +18,7 @@ Java 注解是 Java 1.5 引入的一个工具,类似于给代码贴个标签
 
 ## 在声明继承关系中,Java 注解该如何使用?
 
-在代码编写中,继承和重写是面向对象编程的两个重要的机制。这两个机制,在给我们带来便利的同时,也顺便带来了一些麻烦,这就需要我们用到注解了。 **第一个麻烦是,识别子类的方法是不是重写方法** 。比如下面的例子,在一般情况下,对代码阅读者来说,最直觉的感受就是,getFirstName() 这个方法不是重写方法,父类 Person 没有定义这个方法。
+在代码编写中,继承和重写是面向对象编程的两个重要的机制。这两个机制,在给我们带来便利的同时,也顺便带来了一些麻烦,这就需要我们用到注解了。**第一个麻烦是,识别子类的方法是不是重写方法** 。比如下面的例子,在一般情况下,对代码阅读者来说,最直觉的感受就是,getFirstName() 这个方法不是重写方法,父类 Person 没有定义这个方法。
 
 ```java
 class Student extends Person {
@@ -43,7 +43,7 @@ class Student extends Person {
 }
 ```
 
-为什么要识别重写方法呢?这是因为继承的第二个麻烦。 **第二个麻烦是,重写方法可以不遵守父类方法的规范** 。面向对象编程的机制,理想的状况是,父类定义了方法和规范,子类严格地遵守父类的定义。 比如 Person.getFirstName() 要求返回值是一个人的名,不包括姓氏部分,而且不可以是空值。但是子类 Student.getFirstName() 的实现完全有可能没有严格遵守这样的规范,不管是有意的,或者是无意的。 比如,返回了姓氏,或者返回了包括姓氏的姓名,或者可以返回了空值。
+为什么要识别重写方法呢?这是因为继承的第二个麻烦。**第二个麻烦是,重写方法可以不遵守父类方法的规范** 。面向对象编程的机制,理想的状况是,父类定义了方法和规范,子类严格地遵守父类的定义。 比如 Person.getFirstName() 要求返回值是一个人的名,不包括姓氏部分,而且不可以是空值。但是子类 Student.getFirstName() 的实现完全有可能没有严格遵守这样的规范,不管是有意的,或者是无意的。 比如,返回了姓氏,或者返回了包括姓氏的姓名,或者可以返回了空值。
 
 ```java
 class Student extends Person {
@@ -99,7 +99,7 @@ public String(byte ascii[], int hibyte) {
 
 如果软件的维护者继续在废弃的接口上投入精力,意味着这个接口随着时间的推移,它的实现可能会存在各种各样的问题,包括严重的安全问题,就连使用者也要承担这些风险。而且还会有用户持续把它们运用到新的应用中去,这就违背了废弃接口的初衷。更多的使用者加入危险的游戏,也增加了删除废弃接口的难度。
 
-这就要求我们做好两件事情。 **第一件事情是,如果接口的设计存在不合理性,或者新方法取代了旧方法,我们应该尽早地废弃该接口** 。
+这就要求我们做好两件事情。**第一件事情是,如果接口的设计存在不合理性,或者新方法取代了旧方法,我们应该尽早地废弃该接口** 。
 
 及时止损!
 
@@ -143,7 +143,7 @@ public interface Certificate {
 }
 ```
 
-**第二件事情是,如果我们在现有的代码中使用了废弃的接口,要尽快转换、使用替换的方法** 。等到废弃方法删除的时候,再去更改,就太晚了, **不要等到压力山大的时候才救火** 。
+**第二件事情是,如果我们在现有的代码中使用了废弃的接口,要尽快转换、使用替换的方法** 。等到废弃方法删除的时候,再去更改,就太晚了,**不要等到压力山大的时候才救火** 。
 
 如果一个接口被废弃,编译器会警告继续使用的代码。Java 提供了一个不推荐使用的注解,SuppressWarnings。这个注解告诉编译器,忽略特定的警告。警告是非常有价值的信息,忽略警告永远不是一个最好的选项。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25412\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25412\350\256\262.md"
index a2ad79506..96a418d63 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25412\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25412\350\256\262.md"
@@ -38,9 +38,9 @@
 
 有时候,一个软件包含很多贡献者,不同的贡献者有不同的版权诉求。软件的不同部分,就有不同的版权。
 
-这种情况下, **版权描述一般放在每一个源文件的头部** 。不同的源文件可以有不同的版权,同一个源文件也可以有一个以上的版权所有者。
+这种情况下,**版权描述一般放在每一个源文件的头部** 。不同的源文件可以有不同的版权,同一个源文件也可以有一个以上的版权所有者。
 
-如果版权来源只有一个,而且源文件头部没有版权描述,我们就需要把版权描述放到最显眼的地方。 **这个地方就是软件工程的根目录,命名为 COPYRIGHT,全部使用大写字母** 。
+如果版权来源只有一个,而且源文件头部没有版权描述,我们就需要把版权描述放到最显眼的地方。**这个地方就是软件工程的根目录,命名为 COPYRIGHT,全部使用大写字母** 。
 
 没有版权描述的软件,并不是没有版权保护。如果一个软件没有版权描述或者版权描述不清晰,使用起来有很多法律风险。如果这个软件依赖外部的版权,那么问题就会变得更为复杂。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25413\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25413\350\256\262.md"
index dbe8d1506..a61d0fa72 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25413\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25413\350\256\262.md"
@@ -28,7 +28,7 @@
 
 **合约要成文** 无论对于调用者,还是实现者来说,外部接口的使用都要有章可循,有规可依。如果调用者需要去看实现代码来理解外部接口,那么外部接口和内部实现的分离还有什么用呢?不就背离了外部接口和内部实现分离的初衷吗?这样做既是对实现者的纵容,也是对调用者的无视。
 
-比如说,Java 的每个版本的 API 文档和指南,就是 Java 语言的合约。 **合约要清楚** 合约既然是我们协作的依靠,就一定要清晰可靠、容易遵循,不能有模棱两可的地方。如果接口规范描述不清,既误导调用者,也误导实现者。
+比如说,Java 的每个版本的 API 文档和指南,就是 Java 语言的合约。**合约要清楚** 合约既然是我们协作的依靠,就一定要清晰可靠、容易遵循,不能有模棱两可的地方。如果接口规范描述不清,既误导调用者,也误导实现者。
 
 如果接口规范复杂难懂,说明接口的设计也很糟糕。
 
@@ -36,9 +36,9 @@
 
 接口规范主要用来描述接口的设计和功能,包括确认边界条件、指定参数范围以及描述极端状况。比如,参数错了会出什么错误?
 
-这里需要注意的是,接口规范不是我们定义术语、交代概念、提供示例的地方。这些应该在其他文档中解决,比如我们下次要聊的面向最终用户的文档。 **合约要稳定** 既然是合约,意味着调用者必须依赖于现有的规范。比如 InputStream.read() 这个方法,接口规范描述的是读取一个字节(8-bit),返回值是介于 0 和 255 之间的一个整数。如果我们要把这一个规范改成返回值是介于 -128 到 127 之间的一个整数,或者是读取一个字符(比如一个汉字),都会对现有的使用代码造成灾难性的影响。
+这里需要注意的是,接口规范不是我们定义术语、交代概念、提供示例的地方。这些应该在其他文档中解决,比如我们下次要聊的面向最终用户的文档。**合约要稳定** 既然是合约,意味着调用者必须依赖于现有的规范。比如 InputStream.read() 这个方法,接口规范描述的是读取一个字节(8-bit),返回值是介于 0 和 255 之间的一个整数。如果我们要把这一个规范改成返回值是介于 -128 到 127 之间的一个整数,或者是读取一个字符(比如一个汉字),都会对现有的使用代码造成灾难性的影响。
 
-接口的设计和规范的制定,一定要谨慎再谨慎,小心再小心,反复推敲,反复精简。一旦接口合约制定,公布,然后投入使用,就尽最大努力保持它的稳定,即使这个接口或者合约存在很多不足。 **变更要谨慎** 世界上哪里有一成不变的东西呢!技术的进步、需求的演进,总是推着我们朝前走。合约也需要跟得上变化。
+接口的设计和规范的制定,一定要谨慎再谨慎,小心再小心,反复推敲,反复精简。一旦接口合约制定,公布,然后投入使用,就尽最大努力保持它的稳定,即使这个接口或者合约存在很多不足。**变更要谨慎** 世界上哪里有一成不变的东西呢!技术的进步、需求的演进,总是推着我们朝前走。合约也需要跟得上变化。
 
 可是,接口合约毕竟不是租房合约,可以一年一续,每年变更一次。租房合约的变更成本很小,但软件的接口合约变更的影响要严重得多。特别是兼容性问题,稍微一丁点儿的接口规范变化,都可能导致大面积的应用崩溃。越成功的接口,使用者越多,变更的影响也就越大,变更的成本也就变高,变更也就越困难。你可以试着想一想,如果 InputStream.read() 这个方法在 Java 中删除,会造成多大的影响?会有多少应用瘫痪?
 
@@ -54,7 +54,7 @@ JavaDoc 就是一种顾及了多方利益的一种组织形式。它通过文档
 
 JavaDoc 工具可以把文档注释,转换为便于阅读为 HTML 文档。这样就方便规范的使用者阅读了。
 
-当然,也不是所有的规范,都一定要使用 JavaDoc 的形式,特别是冗长的规范。如果有两种以上不同形式的规范组织文档, **我建议一定要互相链接、引用** 。比如,冗长的规范可以单独放在一个文件里。然后,在 Java Doc 对应的文件里,加上改规范的链接。
+当然,也不是所有的规范,都一定要使用 JavaDoc 的形式,特别是冗长的规范。如果有两种以上不同形式的规范组织文档,**我建议一定要互相链接、引用** 。比如,冗长的规范可以单独放在一个文件里。然后,在 Java Doc 对应的文件里,加上改规范的链接。
 
 比如下面的例子中,“Java Security Standard Algorithm Names Specification”就是一个独立的,较长的规范文档。当需要使用这个文档的时候,就要在对应的接口中指明该文档的位置,这样方便用户进行检索。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25414\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25414\350\256\262.md"
index 7295e011c..7e340b29b 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25414\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25414\350\256\262.md"
@@ -33,7 +33,7 @@
 
 估计很多用户立即就会不知所措。他们大部分都不知道这些协议到底是什么,更别提让他们做出有实际意义的选择了。
 
-## 我举这样的一个例子,想说明的是,有了用户的千差万别,才有了软件的千差万别。我们不能想当然地认为,有了详实的用户指南,用户就能够使用产品。 **事实上,用户指南,不能超越用户的理解能力和操作能力。** 什么时候确定产品用户?
+## 我举这样的一个例子,想说明的是,有了用户的千差万别,才有了软件的千差万别。我们不能想当然地认为,有了详实的用户指南,用户就能够使用产品。**事实上,用户指南,不能超越用户的理解能力和操作能力。** 什么时候确定产品用户?
 
 这是一个老生常谈的问题。之所以常谈,是因为我们很容易就忘了我们的用户。所以,不得不经常拎出来谈一谈,时不时地拽一拽这根弦。
 
@@ -71,15 +71,15 @@
 
 对于一个陌生的类库,我们一般要先阅读开发指南,然后检索接口和接口规范。如果开发指南让用户抓狂,你可以回顾一下开头讲到的跑步机的例子,想象下影响会有多糟糕!
 
-那么合格的开发指南都要符合哪几个规则呢?我总结为三点:需要交代清楚概念,可以快速上手,示例都可操作。 **交代概念** 一个合格的开发指南,不要假定用户具有和开发者一样的知识范围。对应的接口规范和开发指南里涉及到的概念,一定要交代清楚。我们可以假定一个程序员了解 IP 地址这个概念,这是计算机入门的基本概念。但是,不要假定他了解 IP 地址的计算方式,虽然也是基础知识,但是大部分人记不住知识的细节。
+那么合格的开发指南都要符合哪几个规则呢?我总结为三点:需要交代清楚概念,可以快速上手,示例都可操作。**交代概念** 一个合格的开发指南,不要假定用户具有和开发者一样的知识范围。对应的接口规范和开发指南里涉及到的概念,一定要交代清楚。我们可以假定一个程序员了解 IP 地址这个概念,这是计算机入门的基本概念。但是,不要假定他了解 IP 地址的计算方式,虽然也是基础知识,但是大部分人记不住知识的细节。
 
-所以说,交代清楚概念,很方便作者和读者之间建立共识,降低后续文档的阅读负担。 **快速上手** 一个好的开发指南,要尽最大可能,让开发者快速上手。
+所以说,交代清楚概念,很方便作者和读者之间建立共识,降低后续文档的阅读负担。**快速上手** 一个好的开发指南,要尽最大可能,让开发者快速上手。
 
 我们学习一门编程语言,往往从“Hello, World!”这个例子开始。它本身并没有太多玄妙的东西,但可以让一个初学者最快地玩耍起来,然后,再逐步探索更深入的内容。
 
 这是一个值得学习的方法。很多开发指南,都有一个类似于“Hello, World!”这样的简短的快速入门章节。你也可以试试这个办法。
 
-但需要注意的是,快速入门的章节一定要简单、靠前。让读者最快接触到,很容易学会,方便“玩耍”。 **示例都可操作**
+但需要注意的是,快速入门的章节一定要简单、靠前。让读者最快接触到,很容易学会,方便“玩耍”。**示例都可操作**
 
 可操作性是开发指南的一个命门。所有成文的方法和示例,都要求可以使用、可以操作、可以验证。虽然说起来简单,但是做到这一点并不简单。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25415\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25415\350\256\262.md"
index a44af1b3b..847968285 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25415\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25415\350\256\262.md"
@@ -16,7 +16,7 @@
 
 ## 为什么需要编码规范?
 
-**1. 提高编码的效率** 在不损害代码质量的前提下,效率可以节省我们的时间和成本。这种节省不仅仅停留在编码阶段,更体现在整个软件的生命周期里。我在第四篇已经详细解释了这一点。 **2. 提高编码的质量** 代码的质量在于它和预期规范的一致性。一致、简单、规范的代码易于测试。相反,复杂的代码会加大测试的难度,难以达到合适的测试覆盖率。另外,代码的复用也意味着质量的复用,所以代码的问题会随着它的复用成倍地叠加,提高了软件的使用或者维护成本。 **3. 降低维护的成本** 代码的维护要求代码必须能够修改,增加新功能,修复已知的问题。如果代码结构的清晰、易于阅读理解,那么问题就容易排查和定位。 **4. 扩大代码的影响**
+**1. 提高编码的效率** 在不损害代码质量的前提下,效率可以节省我们的时间和成本。这种节省不仅仅停留在编码阶段,更体现在整个软件的生命周期里。我在第四篇已经详细解释了这一点。**2. 提高编码的质量** 代码的质量在于它和预期规范的一致性。一致、简单、规范的代码易于测试。相反,复杂的代码会加大测试的难度,难以达到合适的测试覆盖率。另外,代码的复用也意味着质量的复用,所以代码的问题会随着它的复用成倍地叠加,提高了软件的使用或者维护成本。**3. 降低维护的成本** 代码的维护要求代码必须能够修改,增加新功能,修复已知的问题。如果代码结构的清晰、易于阅读理解,那么问题就容易排查和定位。**4. 扩大代码的影响**
 
 要想让更多的人参与,就需要一致的编码风格,恰当地使用文档。要方便他们阅读,便于解释。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25417\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25417\350\256\262.md"
index 38c6ceb9c..1fab9c8ab 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25417\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25417\350\256\262.md"
@@ -16,7 +16,7 @@
 
 **怎么理解代码的性能?** 为了理解这个问题,我们需要对代码的性能有一个共同的认识。代码的性能并不是可以多块地进行加减乘除,而是如何管理内存、磁盘、网络、内核等计算机资源。代码的性能与编码语言关系不大,就算是 JavaScript 编写的应用程序,也可以很快,C 语言编写的程序也可能很慢。
 
-事实上,代码的性能和算法密切相关,但是更重要的是,我们必须从架构层面来考虑性能,选择适当的技术架构和合适的算法。很多纸面上看起来优美的算法,实际上很糟糕。也有很多算法看起来不咋样,但实际上很高效。为了管理代码的性能,在一定程度上,我们需要很好地了解计算机的硬件、操作系统以及依赖库的基本运行原理和工作方式。一个好的架构师,一定会认真考虑、反复权衡性能要求。 **需不需要学习性能?** 一个程序员,可以从多个方面做出贡献。有人熟悉业务逻辑,有人熟悉类库接口,有人能够设计出色的用户界面。这都非常好,但是如果考察编程能力,有两件事情我们需要特别关注。
+事实上,代码的性能和算法密切相关,但是更重要的是,我们必须从架构层面来考虑性能,选择适当的技术架构和合适的算法。很多纸面上看起来优美的算法,实际上很糟糕。也有很多算法看起来不咋样,但实际上很高效。为了管理代码的性能,在一定程度上,我们需要很好地了解计算机的硬件、操作系统以及依赖库的基本运行原理和工作方式。一个好的架构师,一定会认真考虑、反复权衡性能要求。**需不需要学习性能?** 一个程序员,可以从多个方面做出贡献。有人熟悉业务逻辑,有人熟悉类库接口,有人能够设计出色的用户界面。这都非常好,但是如果考察编程能力,有两件事情我们需要特别关注。
 
 第一件事情是,我们的代码是不是正确?事实上,代码正确这个门槛特别低。如果代码出现了大范围的错误,说明编程还没有入门。
 
@@ -28,7 +28,7 @@
 
 一个好的程序员,他编写的代码一定兼顾正确和效率的。事实上,只有兼顾正确和效率,编程才有挑战性,实现起来才有成就感。如果丢弃其中一个指标,那么大多数任务都是小菜一碟。
 
-有过面试经验的小伙伴,你们有没有注意到,正确和有效地编码是面试官考察的两个重点?招聘广告可不会提到,程序员要能够编写正确的代码和有效的代码。但是一些大的企业,会考察算法,其中一条重要的评判标准就是算法够不够快。他们可能声称算法考察的是一个人的基本功,是他的聪明程度。但是如果算法设计不够快,主考官就会认为我们基本功不够扎实、不够聪明。 你看,算法快慢大多只是见识问题,但很多时候,会被迫和智商联系起来。这样做既无理,也无聊,但是我们也没有办法逃避开来,主考官可能也没有更好的办法筛选出更好的人才。 **需不需要考虑代码性能?** 具体到开发任务,对于软件的性能,有很多误解。这些误解,一部分来自我们每个人都难以避免的认知的局限性,一部分来自不合理的假设。
+有过面试经验的小伙伴,你们有没有注意到,正确和有效地编码是面试官考察的两个重点?招聘广告可不会提到,程序员要能够编写正确的代码和有效的代码。但是一些大的企业,会考察算法,其中一条重要的评判标准就是算法够不够快。他们可能声称算法考察的是一个人的基本功,是他的聪明程度。但是如果算法设计不够快,主考官就会认为我们基本功不够扎实、不够聪明。 你看,算法快慢大多只是见识问题,但很多时候,会被迫和智商联系起来。这样做既无理,也无聊,但是我们也没有办法逃避开来,主考官可能也没有更好的办法筛选出更好的人才。**需不需要考虑代码性能?** 具体到开发任务,对于软件的性能,有很多误解。这些误解,一部分来自我们每个人都难以避免的认知的局限性,一部分来自不合理的假设。
 
 比如说,有一种常见的观点是,我们只有一万个用户,不要去操百万用户的心。这种简单粗暴的思考方式很麻烦!你要是相信这样的简单论断,肯定会懵懂得一塌糊涂。百万用户的心是什么心?你根本没有进一步思考的余地。你唯一能够理解的,大概就是性能这东西,一边儿玩去吧。
 
@@ -36,7 +36,7 @@
 
 我们可以问自己一些简单的问题。比如说,一万个用户会同时访问吗?如果一秒钟你需要处理一万个用户的请求,这就需要有百万用户、千万用户,甚至亿万用户的架构设计。
 
-再比如说,会有一万个用户同时访问吗?也许系统没有一万个真实用户,但是可能会有一万个请求同时发起,这就是网络安全需要防范的网络攻击。系统保护的东西越重要,提供的服务越重要,就越要防范网络攻击。而防范网络攻击,只靠防火墙等边界防卫措施,是远远不够的, **代码的质量才是网络安全防护的根本** 。
+再比如说,会有一万个用户同时访问吗?也许系统没有一万个真实用户,但是可能会有一万个请求同时发起,这就是网络安全需要防范的网络攻击。系统保护的东西越重要,提供的服务越重要,就越要防范网络攻击。而防范网络攻击,只靠防火墙等边界防卫措施,是远远不够的,**代码的质量才是网络安全防护的根本** 。
 
 你看,哪怕我们没有一万个用户,我们都要操一万个用户的心;当我们操一万个用户的心的时候,我们可能还要操百万用户的心。
 
@@ -46,7 +46,7 @@
 
 但是,如果我们要去开发具有商业价值的软件,就要认真思考代码的性能能够给公司带来的价值,以及需要支付的成本。
 
-经验告诉我们, **越早考虑性能问题,我们需要支付的成本就越小,带来的价值就越大** 。甚至是,和不考虑性能的方案相比,考虑性能的成本可能还要更小。
+经验告诉我们,**越早考虑性能问题,我们需要支付的成本就越小,带来的价值就越大** 。甚至是,和不考虑性能的方案相比,考虑性能的成本可能还要更小。
 
 你可能会吃惊,难道优化代码性能是没有成本的吗? 当然有。这个成本通常就是我们拓展视野和经验积累所需要支付的学费。这些学费,当然也变成了我们自身市场价值的一部分。
 
@@ -54,15 +54,15 @@
 
 ## 什么时候开始考虑性能问题?
 
-为了进度,很多人的选择是不考虑什么性能问题,能跑就行,先跑起来再说;先把代码摞起来,再考虑性能优化;先把业务推出去,再考虑跑得快不快的问题。可是,如果真的不考虑性能,一旦出了问题,系统崩溃,你的老板不会只骂他自己,除非他是一个优秀的领导。 **硬件扩展能解决性能问题吗?** 有一个想法很值得讨论。很多人认为,如果碰到性能问题,我们就增加更多的机器去解决,使用更多的内存,更多的内核,更快的 CPU。网站频繁崩溃,为什么就不能多买点机器呢?!
+为了进度,很多人的选择是不考虑什么性能问题,能跑就行,先跑起来再说;先把代码摞起来,再考虑性能优化;先把业务推出去,再考虑跑得快不快的问题。可是,如果真的不考虑性能,一旦出了问题,系统崩溃,你的老板不会只骂他自己,除非他是一个优秀的领导。**硬件扩展能解决性能问题吗?** 有一个想法很值得讨论。很多人认为,如果碰到性能问题,我们就增加更多的机器去解决,使用更多的内存,更多的内核,更快的 CPU。网站频繁崩溃,为什么就不能多买点机器呢?!
 
-但遗憾的是,扩展硬件并不是总能够线性地提高系统的性能。出现性能问题,投入更多的设备,只是提高软件性能的一个特殊方法。而且,这不是一个廉价的方法。过去的经验告诉我们,提高一倍的性能,硬件投入成本高达四五倍;如果需要提高四五倍的性能,可能投入二三十倍的硬件也达不到预期的效果。硬件和性能的非线性关系,反而让代码的性能优化更有价值。 **性能问题能滞后处理吗?** 越来越多的团队开始使用敏捷开发模式,要求拥抱变化,快速迭代。很多人把这个作为一个借口:我们下一次迭代的时候,再讨论性能问题。他们忘了敏捷开发最重要的一个原则,就是高质量地工作。没有高质量的工作作为基础,敏捷开发模式就会越走越艰难,越走越不敏捷,越走成本越高。而性能问题,是最重要的质量指标之一。
+但遗憾的是,扩展硬件并不是总能够线性地提高系统的性能。出现性能问题,投入更多的设备,只是提高软件性能的一个特殊方法。而且,这不是一个廉价的方法。过去的经验告诉我们,提高一倍的性能,硬件投入成本高达四五倍;如果需要提高四五倍的性能,可能投入二三十倍的硬件也达不到预期的效果。硬件和性能的非线性关系,反而让代码的性能优化更有价值。**性能问题能滞后处理吗?** 越来越多的团队开始使用敏捷开发模式,要求拥抱变化,快速迭代。很多人把这个作为一个借口:我们下一次迭代的时候,再讨论性能问题。他们忘了敏捷开发最重要的一个原则,就是高质量地工作。没有高质量的工作作为基础,敏捷开发模式就会越走越艰难,越走越不敏捷,越走成本越高。而性能问题,是最重要的质量指标之一。
 
 性能问题,有很多是架构性问题。一旦架构性问题出现,往往意味着代码要推倒重来,这可不是我们可以接受的快速迭代。当然,也有很多性能问题,是技术性细节,是变化性的问题。对于这些问题,使用快速迭代就是一个经济的方式。
 
 很多年以来,我们有一个坏的研发习惯,就是性能问题滞后处理,通过质量保证 (QA) 环节来检测性能问题,然后返回来优化性能。这是一个效率低、耗费大的流程。
 
-当应用程序进入质量保证环节的时候,为时已晚。在前面的设计和开发阶段中,我们投入了大量时间和精力。业务也要求我们尽快把应用程序推向市场。如果等到最后一分钟,才能找到一个严重的性能问题,推迟产品的上市时间,错失市场良机,那么这个性能问题解决的成本是数量级的。没有一个企业喜欢事情需要做两遍才能做到正确的团队,所以我们需要在第一时间做到正确。 **要有性能工程的思维**
+当应用程序进入质量保证环节的时候,为时已晚。在前面的设计和开发阶段中,我们投入了大量时间和精力。业务也要求我们尽快把应用程序推向市场。如果等到最后一分钟,才能找到一个严重的性能问题,推迟产品的上市时间,错失市场良机,那么这个性能问题解决的成本是数量级的。没有一个企业喜欢事情需要做两遍才能做到正确的团队,所以我们需要在第一时间做到正确。**要有性能工程的思维**
 
 采用性能工程思维,才能确保快速交付应用程序,而不用担心因为性能耽误进度。性能工程思维通过流程“左移”,把性能问题从一个一次性的测试行为,变成一个贯穿软件开发周期的持续性行为;从被动地接受问题审查,变成主动地管理质量。也就是说,在软件研发的每一步,每一个参与人员,都要考虑性能问题。整个过程要有计划,有组织,能测量,可控制。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25418\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25418\350\256\262.md"
index 7ab61675d..685cff196 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25418\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25418\350\256\262.md"
@@ -38,7 +38,7 @@
 
 需要特别注意的是,这个等待时间是用户能够感受到的一个任务执行的时间,不是我们熟悉的代码片段执行的时间。比如说,打开一个网页,可能需要打开数十个连接,下载数十个文件。对于用户而言,打开一个网页就是一个完整的、不可分割的任务。它们并不需要去理解打开网页背后的技术细节。
 
-有了这个指数,我们就知道快是指多块,慢是指多慢;什么是满意,什么是不满意。这样我们就可以量化软件性能这个指标了,可以给软件性能测试、评级了。 **体验要一致** 为什么 90 分以上才算是好成绩呢? 这就牵涉到用户体验的一致性。一致性原则是一个非常基本的产品设计原则,它同样也适用于性能的设计和体验。
+有了这个指数,我们就知道快是指多块,慢是指多慢;什么是满意,什么是不满意。这样我们就可以量化软件性能这个指标了,可以给软件性能测试、评级了。**体验要一致** 为什么 90 分以上才算是好成绩呢? 这就牵涉到用户体验的一致性。一致性原则是一个非常基本的产品设计原则,它同样也适用于性能的设计和体验。
 
 一个服务,如果 10 次访问有 2 次不满意,用户就很难对这个服务有一个很高的评价。10 次访问有 2 次不满意,是不是说明用户可以给这个服务打 80 分呢?显然不是的。他们的真实感受更可能是,这个服务不及格。特别是如果有对比的话,他们甚至会觉得这样的服务真是垃圾。
 
@@ -64,11 +64,11 @@
 
 如何让用户对服务感到满意呢?这就需要我们通过代码管理好内存、磁盘、网络以及内核等计算机资源。
 
-管理好计算机资源主要包括两个方面,一个方面是把有限的资源使用得更有效率,另一个方面是能够使用好更多的资源。 **把资源使用得更有效率** 这个概念很好理解,指的就是完成同一件事情,尽量使用最少的计算机资源,特别是使用最少的内存、最少的 CPU 以及最少的网络带宽。
+管理好计算机资源主要包括两个方面,一个方面是把有限的资源使用得更有效率,另一个方面是能够使用好更多的资源。**把资源使用得更有效率** 这个概念很好理解,指的就是完成同一件事情,尽量使用最少的计算机资源,特别是使用最少的内存、最少的 CPU 以及最少的网络带宽。
 
 愿景很美好,但是我们的确又做不到,怎么可能“又要马儿跑,又要马儿不吃草”呢?这个时候,就需要我们在这些计算机资源的使用上做出合理的选择和分配。比如通过使用更多的内存,来提高 CPU 的使用效率;或者通过使用更多的 CPU,来减少网络带宽的使用;再或者,通过使用客户端的计算能力,来减轻服务端的计算压力。
 
-所以,有时候我们说效率的时候,其实我们说的是分配。计算机资源的使用,也是一个策略。不同的计算场景,需要匹配不同的策略。只有这样,才能最大限度地发挥计算机的整体的计算能力,甚至整个互联网的计算能力。 **能够使用好更多的资源**
+所以,有时候我们说效率的时候,其实我们说的是分配。计算机资源的使用,也是一个策略。不同的计算场景,需要匹配不同的策略。只有这样,才能最大限度地发挥计算机的整体的计算能力,甚至整个互联网的计算能力。**能够使用好更多的资源**
 
 这个概念也很好理解,就是当我们面对更多计算机资源的时候,能够用上它们、用好它们。遗憾的是,很多代码是做不到这一点的。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25419\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25419\350\256\262.md"
index d6796c1a6..ed99b34aa 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25419\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25419\350\256\262.md"
@@ -52,7 +52,7 @@
 
 所以,我们要回归到最终用户。只有从最终用户的眼里看需求,才能够识别什么是最核心的需求,什么是衍生的需求,什么是无效的需求。这样,我们才能找到一个最小的子集,那就是现在就必须满足的需求。
 
-首先就必须满足的需求,是优先级最高的、最重要的事情,这些事情要小而精致,是我们的时间、金钱、智力投入效率最高的地方,也是回报最丰厚的地方。我们要把这些事情做到让竞争对手望尘莫及的地步。 **不要一步到位**
+首先就必须满足的需求,是优先级最高的、最重要的事情,这些事情要小而精致,是我们的时间、金钱、智力投入效率最高的地方,也是回报最丰厚的地方。我们要把这些事情做到让竞争对手望尘莫及的地步。**不要一步到位**
 
 有一些需求很重要,但不是现在就必须做的。这就需要另外一个方法——迭代演进。第一次我们没有办法完成的事情,就放在第二次考虑。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25420\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25420\350\256\262.md"
index 079e53958..2591c6e79 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25420\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25420\350\256\262.md"
@@ -16,19 +16,19 @@
 
 我见过的优秀的程序员,无一例外,都对简洁代码有着偏执般的执着。甚至小到缩进空格该使用几个空格这样细枝末节的问题,都会严格地遵守编码的规范。乍一看,纠缠于缩进空格不是浪费时间吗?可是真相是,把小问题解决好,事实上节省了大量的时间。
 
-这些对代码整洁充满热情的工程师,会对整个团队产生积极的、至关重要的影响。这种影响,不仅仅关乎到工程进展的速度,还关系到工程的质量。真正能够使得产品获得成功,甚至扭转科技公司命运的,不是关键时刻能够救火的队员,而是从一开始就消除了火灾隐患的队员。 **简单直观减轻沟通成本** 简单直观的解决方案,有一个很大的优点,就是容易理解,易于传达。事情越简单,理解的门槛越低,理解的人越多,传达越准确。一个需要多人参与的事情,如果大家都能够清晰地理解这件事情,这就成功了一半。
+这些对代码整洁充满热情的工程师,会对整个团队产生积极的、至关重要的影响。这种影响,不仅仅关乎到工程进展的速度,还关系到工程的质量。真正能够使得产品获得成功,甚至扭转科技公司命运的,不是关键时刻能够救火的队员,而是从一开始就消除了火灾隐患的队员。**简单直观减轻沟通成本** 简单直观的解决方案,有一个很大的优点,就是容易理解,易于传达。事情越简单,理解的门槛越低,理解的人越多,传达越准确。一个需要多人参与的事情,如果大家都能够清晰地理解这件事情,这就成功了一半。
 
-我们不要忘了,客户也是一个参与者。简单直观的解决方案,降低了用户的参与门槛,减轻了学习压力,能够清晰地传递产品的核心价值,最有可能吸引广泛的用户。 **简单直观降低软件风险** 软件最大的风险,来源于软件的复杂性。软件的可用性,可靠性,甚至软件的性能,归根到底,都是软件的复杂性带来的副产品。越复杂的软件,我们越难以理解,越难以实现,越难以测量,越难以实施,越难以维护,越难以推广。如果我们能够使用简单直接的解决方案,很多棘手的软件问题都会大幅地得到缓解。
+我们不要忘了,客户也是一个参与者。简单直观的解决方案,降低了用户的参与门槛,减轻了学习压力,能够清晰地传递产品的核心价值,最有可能吸引广泛的用户。**简单直观降低软件风险** 软件最大的风险,来源于软件的复杂性。软件的可用性,可靠性,甚至软件的性能,归根到底,都是软件的复杂性带来的副产品。越复杂的软件,我们越难以理解,越难以实现,越难以测量,越难以实施,越难以维护,越难以推广。如果我们能够使用简单直接的解决方案,很多棘手的软件问题都会大幅地得到缓解。
 
 如果代码风格混乱,逻辑模糊,难以理解,我们很难想象,这样的代码会运行可靠。
 
 ## 该怎么做到简单直观?
 
-如果我们达成了共识,要保持软件的简单直观,那么,我们该怎么做到这一点呢?最重要的就是做小事,做简单的事情。 **使用小的代码块** 做小事的一个最直观的体现,就是代码的块要小,每个代码块都要简单直接、逻辑清晰。整洁的代码读起来像好散文,赏心悦目,不费力气。
+如果我们达成了共识,要保持软件的简单直观,那么,我们该怎么做到这一点呢?最重要的就是做小事,做简单的事情。**使用小的代码块** 做小事的一个最直观的体现,就是代码的块要小,每个代码块都要简单直接、逻辑清晰。整洁的代码读起来像好散文,赏心悦目,不费力气。
 
 如果你玩过乐高积木,或者组装过宜家的家具,可能对“小部件组成大家具”的道理会深有体会。代码也是这样,一小块一小块的代码,组合起来,可以成就大目标。作为软件的设计师,我们要做的事情,就是识别、设计出这些小块。如果有现成的小块代码可以复用,我们就拿来用。如果没有现成的,我们就自己来实现这些代码块。
 
-为了保持代码块的简单,给代码分块的一个重要原则就是, **一个代码块只做一件事情** 。前面,我们曾经使用过下面的例子。这个例子中,检查用户名是否符合用户名命名的规范,以及检查用户名是否是注册用户,放在了一个方法里。
+为了保持代码块的简单,给代码分块的一个重要原则就是,**一个代码块只做一件事情** 。前面,我们曾经使用过下面的例子。这个例子中,检查用户名是否符合用户名命名的规范,以及检查用户名是否是注册用户,放在了一个方法里。
 
 ```java
 /** 
@@ -65,7 +65,7 @@ boolean isRegisteredUser(String userName) {
 
 **遵守约定的惯例** 把代码块做小,背后隐含一个重要的假设:这些小代码块要容易组装。不能进一步组装的代码,如同废柴,没有一点儿价值。
 
-而能够组装的代码,接口规范一定要清晰。越简单、越规范的代码块,越容易复用。这就是我们前面反复强调的编码规范。 **花时间做设计** 对乐高或者宜家来说,我们只是顾客,他们已经有现成的小部件供我们组合。对于软件工程师而言,我们是软件的设计者,是需要找出识别、设计和实现这些小部件的人。
+而能够组装的代码,接口规范一定要清晰。越简单、越规范的代码块,越容易复用。这就是我们前面反复强调的编码规范。**花时间做设计** 对乐高或者宜家来说,我们只是顾客,他们已经有现成的小部件供我们组合。对于软件工程师而言,我们是软件的设计者,是需要找出识别、设计和实现这些小部件的人。
 
 识别出这些小部件,是一个很花时间的事情。
 
@@ -79,7 +79,7 @@ boolean isRegisteredUser(String userName) {
 
 一个优秀的程序员,可能 80% 的时间是在设计、拆解和验证,只有 20% 的时间是在写代码。但是,拿出 20% 的时间写的代码,可能要比拿出 150% 时间写的代码,还要多,还要好。这个世界真的不是线性的。
 
-有一句流传的话,说的是“跑得慢,到得早”。这句话不仅适用于健身,还适用于写程序。 **借助有效的工具**
+有一句流传的话,说的是“跑得慢,到得早”。这句话不仅适用于健身,还适用于写程序。**借助有效的工具**
 
 我自己最常使用的工具,就是圆珠笔和空白纸。大部分问题,一页纸以内,都可以解决掉。当然,这中间的过程,可能需要一打甚至一包纸。
 
diff --git "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25421\350\256\262.md" "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25421\350\256\262.md"
index 5673636bb..d99471421 100644
--- "a/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25421\350\256\262.md"
+++ "b/docs/Code/\344\273\243\347\240\201\347\262\276\350\277\233\344\271\213\350\267\257/\347\254\25421\350\256\262.md"
@@ -30,7 +30,7 @@ OpenJDK 的代码评审,很多时候代码量很大。代码评审的时候,
 
 **短期内代码写得多与否,我们可以把这个比喻成“走得慢,还是走得快”的问题。** 如果给我们半年的时间,那些质量差的代码,编写效率也许可以和质量好的代码保持在同一水准,特别是软件还没有见到用户的时候。
 
-如果给我们一年的时间,软件已经见到了用户,那么质量差的代码的编写效率,应该大幅度落后于优质代码了。甚至生产这些代码的团队,都被市场无情淘汰了。 **看谁的代码能够长期赢得竞争,我们可以把这个比喻成“到得慢,还是到得快”问题。** 为什么会这样呢? 一小时内,什么都不管,什么都不顾,怎么能不多产呢!
+如果给我们一年的时间,软件已经见到了用户,那么质量差的代码的编写效率,应该大幅度落后于优质代码了。甚至生产这些代码的团队,都被市场无情淘汰了。**看谁的代码能够长期赢得竞争,我们可以把这个比喻成“到得慢,还是到得快”问题。** 为什么会这样呢? 一小时内,什么都不管,什么都不顾,怎么能不多产呢!
 
 可是,不管不顾,并不意味真的可以高枕无忧。需求满足不了就会返工,程序出了问题也会返工,测试通不过还会返工······每一次的返工,都要你重新阅读代码,梳理逻辑,修改代码。
 
@@ -44,7 +44,7 @@ OpenJDK 的代码评审,很多时候代码量很大。代码评审的时候,
 
 你不妨记录一下三个月以来,你的工作时间,看看有多少时间是花在了修修补补上,有多少时间是花在了新的用户需求上。这样,对这个问题可能有不一样的感受。
 
-另外, **是不是关注代码质量,就一定走得慢呢?** 其实也不是这样的。比如说,如果一个定义清晰,承载功能单一的接口,我们就容易理解,编码思路也清晰,写代码就又快又好。可是,简单直观的接口怎么来?我们需要花费大量的时间,去设计接口,才能获得这样的效果。
+另外,**是不是关注代码质量,就一定走得慢呢?** 其实也不是这样的。比如说,如果一个定义清晰,承载功能单一的接口,我们就容易理解,编码思路也清晰,写代码就又快又好。可是,简单直观的接口怎么来?我们需要花费大量的时间,去设计接口,才能获得这样的效果。
 
 为什么有的人一天可以写几千行代码,有的人一天就只能写几十行代码呢?这背后最重要的一个区别就是心里有没有谱,思路是不是清晰。几千行的代码质量就比几十行的差吗? 也不一定。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25400\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25400\350\256\262.md"
index 9fcb845da..2d405db6b 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25400\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25400\350\256\262.md"	
@@ -2,11 +2,11 @@
 
 你好,我是王潇俊,从今天开始,我将会和你一起聊聊“持续交付”这个话题。
 
-“持续交付”已不再是一个陌生词汇了,绝大多数软件研发企业,都在或多或少地实施“持续交付”,因为大家都清楚,也都曾经体会或者听别人说过, **“持续交付”能够提高研发效率。** 但是要说实施得多好、多彻底,那我估计很多人都会面面相觑。
+“持续交付”已不再是一个陌生词汇了,绝大多数软件研发企业,都在或多或少地实施“持续交付”,因为大家都清楚,也都曾经体会或者听别人说过,**“持续交付”能够提高研发效率。** 但是要说实施得多好、多彻底,那我估计很多人都会面面相觑。
 
 做好持续交付并不是件易事,从我的经验来看,它主要难在三个地方。
 
-第一,实施“持续交付”,将会影响整个的研发生命周期,会涉及到流程、团队、工具等多个方面。很可能需要突破当前组织的束缚,引起大量的技术和组织变革。因为,实施“持续交付”需要组织从上到下的认可,需要有大勇气将一些可能属于黑箱操作的工作,公开出来给大家监督。所以, **这样的事情很难推进。** 第二,实施“持续交付”,对实施者和参与者的要求都很高,他们不仅需要了解开发,还要了解流程,了解测试,了解运维,甚至还需要有一定的架构知识和管理知识。所以, **这样的人才很难寻找。** 第三,实施“持续交付”,大多数团队都希望能够快速见效,立竿见影。但是,“持续交付”的改进过程本身就是一个持续迭代的过程,需要多次循环才能体现效果。甚至在实施的初期,因为开发习惯和流程变化,团队在适应的过程中效率会有暂时的下降。所以, **这样的效果很难度量。** 由于这三大难点,很多人对“持续交付”敬而远之,或者爱恨交加。因此,我希望这个专栏能够带你全面、立体地认识持续交付,当你了解得越多,理解得越透彻,你也就越有信心。简单来说,我认为: **无论企业在什么阶段,无论个人的能力如何,都可以去尝试“持续交付”。** 在实践中,我还经常看到一些错误的观点。
+第一,实施“持续交付”,将会影响整个的研发生命周期,会涉及到流程、团队、工具等多个方面。很可能需要突破当前组织的束缚,引起大量的技术和组织变革。因为,实施“持续交付”需要组织从上到下的认可,需要有大勇气将一些可能属于黑箱操作的工作,公开出来给大家监督。所以,**这样的事情很难推进。** 第二,实施“持续交付”,对实施者和参与者的要求都很高,他们不仅需要了解开发,还要了解流程,了解测试,了解运维,甚至还需要有一定的架构知识和管理知识。所以,**这样的人才很难寻找。** 第三,实施“持续交付”,大多数团队都希望能够快速见效,立竿见影。但是,“持续交付”的改进过程本身就是一个持续迭代的过程,需要多次循环才能体现效果。甚至在实施的初期,因为开发习惯和流程变化,团队在适应的过程中效率会有暂时的下降。所以,**这样的效果很难度量。** 由于这三大难点,很多人对“持续交付”敬而远之,或者爱恨交加。因此,我希望这个专栏能够带你全面、立体地认识持续交付,当你了解得越多,理解得越透彻,你也就越有信心。简单来说,我认为: **无论企业在什么阶段,无论个人的能力如何,都可以去尝试“持续交付”。** 在实践中,我还经常看到一些错误的观点。
 
 1. **过度强调自动化** 。认为只有自动化才能算是“持续”,但限于业务逻辑变化快,QA 能力不足等,又无法实现测试自动化,而发布自动化更是遥遥无期,所以只能放弃。
 2. **过度强调流程化** 。总觉得“持续交付”先要构建强流程来管控,结果就一直限于流程和实现流程的“泥潭”里,却忘了初衷。
@@ -22,25 +22,25 @@
 
 晚上我们睡觉的时候,老美们就开始干活了。因为《魔兽世界》的爆红,所以当时开发需求特别多,缺陷也特别多,几乎每天都要提测,我就干脆用按键精灵写了个脚本,实现了每天自动地处理这些事情。现在想想,这不就是每日构建嘛。
 
-你现在可能和当时的我一样,正在采用或借鉴一些“持续集成”或“持续交付”的最佳实践,但还停留在一个个小的、零散的点上,并没有形成统一的体系,还搞不定持续交付。 **所以,我希望这个专栏首先能够给你呈现一个体系化的“持续交付”课程,帮助你拓展高度和广度,形成对“持续交付”立体的认识。** 其实从这个角度来看,我想通过这个专栏与你分享的内容,不正好就是我自己在实际成长过程中一点一点学到的东西吗?那么,如果你不嫌厌烦,可以继续听一下我的故事。
+你现在可能和当时的我一样,正在采用或借鉴一些“持续集成”或“持续交付”的最佳实践,但还停留在一个个小的、零散的点上,并没有形成统一的体系,还搞不定持续交付。**所以,我希望这个专栏首先能够给你呈现一个体系化的“持续交付”课程,帮助你拓展高度和广度,形成对“持续交付”立体的认识。** 其实从这个角度来看,我想通过这个专栏与你分享的内容,不正好就是我自己在实际成长过程中一点一点学到的东西吗?那么,如果你不嫌厌烦,可以继续听一下我的故事。
 
 离开第九城市之后,由于经受不住帝都“干燥”的天气,2008 年我又回到魔都,加入了当时还默默无闻的大众点评网。在那里,我真正体验了一把“坐火箭”的感觉;也是在那里,我与“持续交付”真正结缘。
 
 点评是一家工程师文化很浓重的公司,一直以来都以工程师的能力为傲。但随着 O2O 和移动互联网的兴起,点评走到了风口浪尖,团队在不断扩大,而研发效率开始下降了。
 
-起初,大家都觉得是自己的能力跟不上,就开始拼命学习,公司也开始树立专家典型。但结果却事与愿违,个人越牛,杂事越多,不能专注,反而成了瓶颈。总结之后,我们发现,这种情况是研发流程、合作方式等低效造成的。 **个人再强放在一个低效的环境下,也无力可施。** 然后,QA 团队开始推动“持续交付”,试图改变现状。为什么是 QA 团队呢,因为 QA 在软件研发生命周期的最后一端,所有前期的问题,他们都得承担。低效的研发模式和体系,首先压死的就是 QA。但是,QA 团队最终还是以失败收场了。究其原因:
+起初,大家都觉得是自己的能力跟不上,就开始拼命学习,公司也开始树立专家典型。但结果却事与愿违,个人越牛,杂事越多,不能专注,反而成了瓶颈。总结之后,我们发现,这种情况是研发流程、合作方式等低效造成的。**个人再强放在一个低效的环境下,也无力可施。** 然后,QA 团队开始推动“持续交付”,试图改变现状。为什么是 QA 团队呢,因为 QA 在软件研发生命周期的最后一端,所有前期的问题,他们都得承担。低效的研发模式和体系,首先压死的就是 QA。但是,QA 团队最终还是以失败收场了。究其原因:
 
 1. **缺乏实践经验,多数“持续交付”相关的图书、分享都停留在“what”和“why”上,没有具体的“how”;** 2. **QA 团队本身缺乏开发能力,无法将“持续交付”通过工具进行落地,只能流于表面的流程和理念。** 但这场自底向上的革命,却让公司看到了变革的方向。
 
 之后,点评就开始了轰轰烈烈的“精益创业”运动。“持续交付”作为研发线变革的重点,得到了更多资源的支持和高度的关注。也是在这时,我获得了与国内众多的领域专家进行探讨和学习的机会。
 
-最终, **点评是以发布系统为切入点,从下游逐步向上游的方式推行“持续交付”。** 并且在这个过程中,形成了专职的工程效能团队,从而打造出了一套持续交付平台。 **所以,我希望这个专栏的第二个重点是,结合我个人多年的实践经验,与你分享“持续交付”涉及的工具、系统、平台,到底如何去设计,如何去实施,如何去落地。** 离开点评之后,我加入了携程。携程的规模、体量相比点评,又大了许多。比如,携程有近 20 个 BU,应用数量达到 6000+,研发人员有 3000 人;同时还有去哪儿、艺龙等兄弟公司,在系统上也息息相关;而且携程随着多年的业务发展,系统复杂度也远远高于点评。要在这么大的平台上推行“持续交付”,挑战是巨大的。
+最终,**点评是以发布系统为切入点,从下游逐步向上游的方式推行“持续交付”。** 并且在这个过程中,形成了专职的工程效能团队,从而打造出了一套持续交付平台。**所以,我希望这个专栏的第二个重点是,结合我个人多年的实践经验,与你分享“持续交付”涉及的工具、系统、平台,到底如何去设计,如何去实施,如何去落地。** 离开点评之后,我加入了携程。携程的规模、体量相比点评,又大了许多。比如,携程有近 20 个 BU,应用数量达到 6000+,研发人员有 3000 人;同时还有去哪儿、艺龙等兄弟公司,在系统上也息息相关;而且携程随着多年的业务发展,系统复杂度也远远高于点评。要在这么大的平台上推行“持续交付”,挑战是巨大的。
 
 其实,携程在“持续交付”方面一直以来都是有所尝试和努力的,引进、自研各种方式都有,但是收效甚微。其中构建的一些工具和平台,由于种种问题,反而给研发人员留下了坏印象。这里面自然有各方面的问题,但我认为最主要的问题是以下三点:
 
 1. **“持续交付”必须以平台化的思想去看待,单点突破是无力的;** 2. **“持续交付”的实施,也要顺应技术的变迁,善于利用技术红利;** 3. **“持续交付”与系统架构、运维体系息息相关,已经不分彼此。** 事实上,在携程推进“持续交付”时,我们联合了框架、OPS 等部门,将目标放在支持更未来的容器化、云原生(Cloud Native),以及微服务上,利用这些新兴技术的理念,和开源社区的红利,从“持续发布”开始,逐步推进“持续交付”。
 
-在推进的过程中,我们既兼容了老旧的系统架构,也为迁移到新一代架构做好了准备,并提供了支持。可以说,携程第四代架构的升级本身,就是在坚持“持续交付”,从而获得了成功。 **所以,在 DevOps 越来越火的今天,我希望这个专栏可以达到的第三个目的是,能够让你看到“持续交付”与新兴技术擦出的火花,并与你探讨“持续交付”的未来。** 除了以上内容,你还将通过我的专栏收获以下四个方面。
+在推进的过程中,我们既兼容了老旧的系统架构,也为迁移到新一代架构做好了准备,并提供了支持。可以说,携程第四代架构的升级本身,就是在坚持“持续交付”,从而获得了成功。**所以,在 DevOps 越来越火的今天,我希望这个专栏可以达到的第三个目的是,能够让你看到“持续交付”与新兴技术擦出的火花,并与你探讨“持续交付”的未来。** 除了以上内容,你还将通过我的专栏收获以下四个方面。
 
 1. **“持续交付”的主要组件:配置管理、环境管理、构建集成和测试管理。** 在这一部分里,我会深入浅出地,跟你聊聊“持续交付”的这“四大金刚”,帮你全方位地理解“持续交付”的各项主要活动。
 2. **如何实现“灰度发布”。** 如果你对“持续部署”有所期待,希望进一步了解,那么你大多数的问题都可以在这一部分得到解答。
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25401\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25401\350\256\262.md"
index 60ed0fcd3..483d4ae91 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25401\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25401\350\256\262.md"	
@@ -16,9 +16,9 @@
 
 了解了持续交付,你可能会说“持续集成”、“持续部署”又是什么意思, 它们和“持续交付”有什么关系呢。那我就给你简单解释一下。
 
-我们通常会把软件研发工作拆解,拆分成不同模块或不同团队后进行编码,编码完成后,进行集成构建和测试。 **这个从编码到构建再到测试的反复持续过程,就叫作“持续集成”。** “持续集成”一旦完成,则代表产品处在一个可交付状态,但并不代表这是最优状态,还需要根据外部使用者的反馈逐步优化。当然这里的使用者并不一定是真正的用户,还可能是测试人员、产品人员、用户体验工程师、安全工程师、企业领导等等。 **这个在“持续集成”之后,获取外部对软件的反馈再通过“持续集成”进行优化的过程就叫作“持续交付”,它是“持续集成”的自然延续。** 那“持续部署”又是什么呢?软件的发布和部署通常是最艰难的一个步骤。
+我们通常会把软件研发工作拆解,拆分成不同模块或不同团队后进行编码,编码完成后,进行集成构建和测试。**这个从编码到构建再到测试的反复持续过程,就叫作“持续集成”。** “持续集成”一旦完成,则代表产品处在一个可交付状态,但并不代表这是最优状态,还需要根据外部使用者的反馈逐步优化。当然这里的使用者并不一定是真正的用户,还可能是测试人员、产品人员、用户体验工程师、安全工程师、企业领导等等。**这个在“持续集成”之后,获取外部对软件的反馈再通过“持续集成”进行优化的过程就叫作“持续交付”,它是“持续集成”的自然延续。** 那“持续部署”又是什么呢?软件的发布和部署通常是最艰难的一个步骤。
 
-传统安装型软件,要现场调试,要用户购买等等,其难度可想而知。即使是可达度最高的互联网应用,由于生产环境的多样性(各种软件安装,配置等)、架构的复杂性(分布式,微服务)、影响的广泛性(需要灰度发布)等等,就算产品已是待交付的状态,要真正达到用户可用的标准,还有大量的问题需要解决。 **而“持续部署”就是将可交付产品,快速且安全地交付用户使用的一套方法和系统,它是“持续交付”的最后“一公里”。**
+传统安装型软件,要现场调试,要用户购买等等,其难度可想而知。即使是可达度最高的互联网应用,由于生产环境的多样性(各种软件安装,配置等)、架构的复杂性(分布式,微服务)、影响的广泛性(需要灰度发布)等等,就算产品已是待交付的状态,要真正达到用户可用的标准,还有大量的问题需要解决。**而“持续部署”就是将可交付产品,快速且安全地交付用户使用的一套方法和系统,它是“持续交付”的最后“一公里”。**
 
 可见,“持续交付”是一个承上启下的过程,它使“持续集成”有了实际业务价值,形成了闭环,而又为将来达到“持续部署”的高级目标做好了铺垫。
 
@@ -81,7 +81,7 @@
 
 那到底应该怎么评估持续交付的价值呢?这里和你分享一下我在携程是怎么解决这个问题的。
 
-我除了会评估一些常规的 KPI 外,更多地会换一种思考方式。 **既然很难量化持续交付的价值,那么我们就具象化,来看看整个工程生命周期中有多少被开发人员诟病,或者阻碍开发人员自助处理的问题点** ,即“不可持续点”:
+我除了会评估一些常规的 KPI 外,更多地会换一种思考方式。**既然很难量化持续交付的价值,那么我们就具象化,来看看整个工程生命周期中有多少被开发人员诟病,或者阻碍开发人员自助处理的问题点**,即“不可持续点”:
 
 > 开发不能按需产生隔离的测试环境; 生产代码回滚后,要手工处理代码分支; 预发布(Staging)流量要能自动分离,以便预发布测试。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25402\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25402\350\256\262.md"
index ae14152df..79d9c8cbd 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25402\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25402\350\256\262.md"	
@@ -12,7 +12,7 @@
 
 什么样的组织文化,才是“持续交付”成长的沃土(当然这也是定义好的组织的标准),我把它分成了三个层次:
 
-**第一个层次:紧密配合,这是组织发展,部门合作的基础。** 一般企业都会按照职能划分部门。不同的职能产生不同的角色;不同的角色拥有不同的资源;不同的资源又产生不同的工作方式。这些不同的部门紧密配合,协同工作于共同的目标,就能达到成效。 **第二个层次:集思广益,这就需要组织内各个不同部门,或不同职能的角色,跳出自身的“舒适区”。** 除思考和解决本身职能的问题外,各部门还要为达到组织的共同目标,通盘考虑和解决所遇到问题和困难。这个层次需要增加组织的透明度,需要接受互相批评和帮助。 **第三个层次:自我驱动,是理想中的完美组织形式。** 如果第二个层次能够持续地运转,就会形成自我学习、自我驱动的飞轮效应,并且越转越快,它甚至能自发式的预见困难,并自驱动解决问题。
+**第一个层次:紧密配合,这是组织发展,部门合作的基础。** 一般企业都会按照职能划分部门。不同的职能产生不同的角色;不同的角色拥有不同的资源;不同的资源又产生不同的工作方式。这些不同的部门紧密配合,协同工作于共同的目标,就能达到成效。**第二个层次:集思广益,这就需要组织内各个不同部门,或不同职能的角色,跳出自身的“舒适区”。** 除思考和解决本身职能的问题外,各部门还要为达到组织的共同目标,通盘考虑和解决所遇到问题和困难。这个层次需要增加组织的透明度,需要接受互相批评和帮助。**第三个层次:自我驱动,是理想中的完美组织形式。** 如果第二个层次能够持续地运转,就会形成自我学习、自我驱动的飞轮效应,并且越转越快,它甚至能自发式的预见困难,并自驱动解决问题。
 
 这三个层次看起是不是有点眼熟,和我在上一篇文章中讲到的持续集成的三个层次:
 
@@ -22,7 +22,7 @@
 
 好像是一样的。真是有趣,持续交付其实也是帮企业建立更好的组织形式的一种方法。
 
-那么,在形成理想组织的实际执行中会遇到哪些问题呢? **一般软件企业与交付有关的研发部门包括四个:产品、开发、测试和运维。而这四个部门天然地形成了一个生产流水线,所以形成理想组织的第一层次紧密配合,基本没什么问题。**
+那么,在形成理想组织的实际执行中会遇到哪些问题呢?**一般软件企业与交付有关的研发部门包括四个:产品、开发、测试和运维。而这四个部门天然地形成了一个生产流水线,所以形成理想组织的第一层次紧密配合,基本没什么问题。**
 
 但是,要达到第二层次集思广益的难度,往往就很大。因为,每个部门有自身的利益,以及自己的工作方式和目标。
 
@@ -43,7 +43,7 @@
 - 而独立的工程效能部门,虽然能最大化地去做好持续交付工作,但其研发成本的投入也是需要考虑的,小团队的话,就不太适用了;
 - 敏捷形式是比较适合中小团队的一种组织变革方式,但对个人能力的要求也会比较高,而且往往需要一个很长时间的磨合才能见效。
 
-## 所以,你需要根据当前组织的情况来选择。 **总而言之,持续交付必须有与其相适应的组织和文化,否则将很难实施。** 流程因素
+## 所以,你需要根据当前组织的情况来选择。**总而言之,持续交付必须有与其相适应的组织和文化,否则将很难实施。** 流程因素
 
 要说持续交付对企业和组织改变最多的是什么,那么一定是流程。
 
@@ -81,19 +81,19 @@
 1. SOA 架构,面向服务,通过服务间的接口和契约联系;
 1. 微服务架构,按业务领域划分为独立的服务单元,可独立部署,松耦合。
 
-那么,这些架构对持续交付又有什么影响和挑战呢? **对单体架构来说:** 1. 整个应用使用一个代码仓库,在系统简单的情况下,因为管理简单,可以快速简单地做到持续集成;但是一旦系统复杂起来,仓库就会越变越大,开发团队也会越来越大,多团队维护一个代码仓库简直就是噩梦,会产生大量的冲突;而且持续集成的编译时间也会随着仓库变大而变长,团队再也承受不起一次编译几十分钟,结果最终失败的痛苦。
+那么,这些架构对持续交付又有什么影响和挑战呢?**对单体架构来说:** 1. 整个应用使用一个代码仓库,在系统简单的情况下,因为管理简单,可以快速简单地做到持续集成;但是一旦系统复杂起来,仓库就会越变越大,开发团队也会越来越大,多团队维护一个代码仓库简直就是噩梦,会产生大量的冲突;而且持续集成的编译时间也会随着仓库变大而变长,团队再也承受不起一次编译几十分钟,结果最终失败的痛苦。
 2\. 应用变复杂后,测试需要全回归,因为不管多么小的功能变更,都会引起整个应用的重新编译和打包。即使在有高覆盖率的自动化测试的帮助下,测试所要花费的时间成本仍旧巨大,且错误成本昂贵。
 3\. 在应用比较小的情况下,可以做到单机部署,简单直接,这有利于持续交付;但是一旦应用复杂起来,每次部署的代价也变得越来越高,这和之前说的构建越来越慢是一个道理。而且部署代价高会直接影响生产稳定性。这显然不是持续交付想要的结果。
 
-总而言之,一个你可以完全驾驭的单体架构应用,是最有容易做到持续交付的,但一旦它变得复杂起来,一切就都会失控。 **对 SOA 架构来说:** 1. 由于服务的拆分,使得应用的代码管理、构建、测试都变得更轻量,这有利于持续集成的实施。
+总而言之,一个你可以完全驾驭的单体架构应用,是最有容易做到持续交付的,但一旦它变得复杂起来,一切就都会失控。**对 SOA 架构来说:** 1. 由于服务的拆分,使得应用的代码管理、构建、测试都变得更轻量,这有利于持续集成的实施。
 2\. 因为分布式的部署,使得测试环境的治理,测试部署变得非常复杂,这里就需要持续交付过程中考虑服务与服务间的依赖,环境的隔离等等。
 3\. 一些新技术和组件的引入,比如服务发现、配置中心、路由、网关等,使得持续交付过程中不得不去考虑这些中间件的适配。
 
-总体来说,SOA 架构要做到持续交付比单体架构要难得多。但也正因架构解耦造成的分散化开发问题,持续集成、持续交付能够在这样的架构下发挥更大的威力。 **对微服务架构来说:** 其实,微服务架构是一种 SOA 架构的演化,它给持续交付带来的影响和挑战也基本与 SOA 架构一致。
+总体来说,SOA 架构要做到持续交付比单体架构要难得多。但也正因架构解耦造成的分散化开发问题,持续集成、持续交付能够在这样的架构下发挥更大的威力。**对微服务架构来说:** 其实,微服务架构是一种 SOA 架构的演化,它给持续交付带来的影响和挑战也基本与 SOA 架构一致。
 
 当然,如果你采用容器技术来承载你的微服务架构,就另当别论了,这完全是一个持续交付全新的领域,这部分内容我将在后续文章中跟你分享。
 
-### 第二,部署架构 **部署架构指的是,系统在各种环境下的部署方法,验收标准,编排次序等的集合。它将直接影响你持续交付的“最后一公里”。**  **首先,你需要考虑,是否有统一的部署标准和方式。** 在各个环境,不同的设备上,应用的部署方式和标准应该都是一样的,可复用的;除了单个应用以外,最好能做到组织内所有应用的部署方式都是一样的。否则可以想象,每个应用在每个环境上都有不同的部署方式,都要进行持续交付的适配,成本是巨大的。 **其次,需要考虑发布的编排次序。** 特别是在大集群、多机房的情况下。我们通常会采用金丝雀发布(之后讲到灰度发布时,我会详解这部分内容),或者滚动发布等灰度发布策略。那么就需要持续交付系统或平台能够支持这样的功能了。 **再次,是 markdown 与 markup 机制。** 为了应用在部署时做到业务无损,我们需要有完善的服务拉入拉出机制来保证。否则每次持续交付都伴随着异常产生,肯定不是大家愿意见到的。 **最后,是预热与自检。** 持续交付的目的是交付有效的软件。而有些软件在启动后需要处理加载缓存等预热过程,这些也是持续交付所要考虑的关键点,并不能粗暴启动后就认为交付完成了。同理,如何为应用建立统一的自检体系,也就自然成为持续交付的一项内容了
+### 第二,部署架构 **部署架构指的是,系统在各种环境下的部署方法,验收标准,编排次序等的集合。它将直接影响你持续交付的“最后一公里”。**  **首先,你需要考虑,是否有统一的部署标准和方式。** 在各个环境,不同的设备上,应用的部署方式和标准应该都是一样的,可复用的;除了单个应用以外,最好能做到组织内所有应用的部署方式都是一样的。否则可以想象,每个应用在每个环境上都有不同的部署方式,都要进行持续交付的适配,成本是巨大的。**其次,需要考虑发布的编排次序。** 特别是在大集群、多机房的情况下。我们通常会采用金丝雀发布(之后讲到灰度发布时,我会详解这部分内容),或者滚动发布等灰度发布策略。那么就需要持续交付系统或平台能够支持这样的功能了。**再次,是 markdown 与 markup 机制。** 为了应用在部署时做到业务无损,我们需要有完善的服务拉入拉出机制来保证。否则每次持续交付都伴随着异常产生,肯定不是大家愿意见到的。**最后,是预热与自检。** 持续交付的目的是交付有效的软件。而有些软件在启动后需要处理加载缓存等预热过程,这些也是持续交付所要考虑的关键点,并不能粗暴启动后就认为交付完成了。同理,如何为应用建立统一的自检体系,也就自然成为持续交付的一项内容了
 
 关于部署的问题,我也会在之后的篇章中和你详细的讨论。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25403\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25403\350\256\262.md"
index 3d3664759..2e57fa347 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25403\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25403\350\256\262.md"	
@@ -35,7 +35,7 @@ DevOps 的故事,要从一个叫帕特里克 · 德博伊斯(Patrick Debois
 
 这届大会出人意料的成功,许多开发工程师和运维工程师参加了这次大会,甚至还有各种 IT 管理人员参加。人们开始在 Twitter 上大量讨论 DevOpsDays 的内容。
 
-## 由于 Twitter 对内容长度的限制是 140 个字符,所以大家在 Twitter 上讨论时去掉了“Days”,只保留了 “DevOps”。 **于是, DevOps 这个名称正式诞生。** 持续交付的姗姗来迟
+## 由于 Twitter 对内容长度的限制是 140 个字符,所以大家在 Twitter 上讨论时去掉了“Days”,只保留了 “DevOps”。**于是, DevOps 这个名称正式诞生。** 持续交付的姗姗来迟
 
 在 DevOps 的这段编年史里,持续交付又在哪里呢?
 
@@ -47,15 +47,15 @@ DevOps 的故事,要从一个叫帕特里克 · 德博伊斯(Patrick Debois
 
 从本质上说,帕特里克最初遇到的问题,在《持续交付:发布可靠软件的系统方法》一书中找到了最佳实践。如果这本书可以早两年问世,或许今天就不会有 DevOps 了。
 
-然而, **DevOps 的概念一直在向外延伸,包括了:运营和用户,以及快速、良好、及时的反馈机制等内容,已经超出了“持续交付”本身所涵盖的范畴。而持续交付则一直被视作 DevOps 的核心实践之一被广泛谈及。** 这么看来,持续交付真是打了一个大盹儿。
+然而,**DevOps 的概念一直在向外延伸,包括了:运营和用户,以及快速、良好、及时的反馈机制等内容,已经超出了“持续交付”本身所涵盖的范畴。而持续交付则一直被视作 DevOps 的核心实践之一被广泛谈及。** 这么看来,持续交付真是打了一个大盹儿。
 
 ## 认识 DevOps
 
-DevOps 这几年一直在不断地演化,那么它到底是什么呢? **目前,人们对 DevOps 的看法,可以大致概括为 DevOps 是一组技术,一个职能、一种文化,和一种组织架构四种。**  **第一,DevOps 是一组技术,包括:自动化运维、持续交付、高频部署、Docker 等内容。** 但是,如果你仅仅将 DevOps 认为是一组技术的集合的话,就有一些片面。任何技术都是为了解决某些问题而被创造出来的。比如 Docker,就是为了解决 DevOps 所提倡的“基础设施即代码”这个问题,而被创造出来的。
+DevOps 这几年一直在不断地演化,那么它到底是什么呢?**目前,人们对 DevOps 的看法,可以大致概括为 DevOps 是一组技术,一个职能、一种文化,和一种组织架构四种。**  **第一,DevOps 是一组技术,包括:自动化运维、持续交付、高频部署、Docker 等内容。** 但是,如果你仅仅将 DevOps 认为是一组技术的集合的话,就有一些片面。任何技术都是为了解决某些问题而被创造出来的。比如 Docker,就是为了解决 DevOps 所提倡的“基础设施即代码”这个问题,而被创造出来的。
 
 从这个角度来看的话,DevOps 的范畴应该远远大于一组技术了。
 
-其实,DevOps 是一组技术这个观点,还是只站在了工程师角度去思考问题而得出的结论。虽然“DevOps”中“Dev”和“Ops”这两个角色都是工程师,但是其本质还是希望跳出工程师的惯性思维来看待问题。 **第二,DevOps 是一个职能,这也是我在各个场合最常听到的观点。** 你的公司有没有或者正准备成立一个叫作 DevOps 的部门,并将这个部门的工程师命名为 DevOps 工程师?至少在各大招聘网站上,是随处可见这样的职位,而招聘要求往往就是:会 Ops 技能的 Dev,或者会 Dev 技能的 Ops;或者干脆叫全栈工程师。
+其实,DevOps 是一组技术这个观点,还是只站在了工程师角度去思考问题而得出的结论。虽然“DevOps”中“Dev”和“Ops”这两个角色都是工程师,但是其本质还是希望跳出工程师的惯性思维来看待问题。**第二,DevOps 是一个职能,这也是我在各个场合最常听到的观点。** 你的公司有没有或者正准备成立一个叫作 DevOps 的部门,并将这个部门的工程师命名为 DevOps 工程师?至少在各大招聘网站上,是随处可见这样的职位,而招聘要求往往就是:会 Ops 技能的 Dev,或者会 Dev 技能的 Ops;或者干脆叫全栈工程师。
 
 “DevOps 是一个职能”这个观点,源于设施的日趋完善,云服务的流行,以及各类开源工具的广泛使用,使传统 Ops 的工作重心发生了变化,使企业产生了不再需要 Ops 的错觉。
 
@@ -63,11 +63,11 @@ DevOps 这几年一直在不断地演化,那么它到底是什么呢? **目
 
 虽然在 DevOps 看来,Dev 和 Ops 的最终目标是一致的,都是为了快速向客户提供高质量的产品,但其达到目标的手段和方法是不一样的。比如,Ops 往往需要更多的在线处理问题的经验,而这未必是 Dev 所具备的。
 
-所以,简单地把 DevOps 看做是一个职能,是一个彻底错误的观点。 **第三,DevOps 是一种文化,推倒 Dev 与 Ops 之间的阻碍墙。** DevOps 是通过充分的合作解决责任模糊、相互推诿的问题和矛盾。在著名的演讲《每天部署 10 次以上:Flickr 公司的 Dev 与 Ops 的合作》 中,就明确的指出工具和文化是他们成功的原因。
+所以,简单地把 DevOps 看做是一个职能,是一个彻底错误的观点。**第三,DevOps 是一种文化,推倒 Dev 与 Ops 之间的阻碍墙。** DevOps 是通过充分的合作解决责任模糊、相互推诿的问题和矛盾。在著名的演讲《每天部署 10 次以上:Flickr 公司的 Dev 与 Ops 的合作》 中,就明确的指出工具和文化是他们成功的原因。
 
 其实,DevOps 通常想要告诉我们的是:什么行为是值得被鼓励的,而什么行为需要被惩罚。通过这样的方法,DevOps 可以促使我们形成良好的做事习惯,也就是 DevOps 文化。
 
-所以,我们可以发现引入 DevOps 的组织,其实都是希望塑造这样的一种:信任、合作、沟通、学习、分享、共担等鼓励协作的文化。 **第四,DevOps 是一种组织架构,将 Dev 和 Ops 置于一个团队内,一同工作,同化目标,以达到 DevOps 文化地彻底贯彻。**
+所以,我们可以发现引入 DevOps 的组织,其实都是希望塑造这样的一种:信任、合作、沟通、学习、分享、共担等鼓励协作的文化。**第四,DevOps 是一种组织架构,将 Dev 和 Ops 置于一个团队内,一同工作,同化目标,以达到 DevOps 文化地彻底贯彻。**
 
 这看起来确实没有什么问题,而且敏捷团队往往都是这么去做的。但是,从另一方面来看,Ops 作为公司的公共研发资源,往往与 Dev 的配比是不成比例。所以,虽然我们希望每一个敏捷团队都有 Ops,但这可能是一种奢求。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25404\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25404\350\256\262.md"
index 30d2be364..ffc92f96a 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25404\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25404\350\256\262.md"	
@@ -19,13 +19,13 @@
 
 ## 谈谈主干开发(TBD)
 
-**主干开发是一个源代码控制的分支模型,开发者在一个称为 “trunk” 的分支(Git 称 master) 中对代码进行协作,除了发布分支外没有其他开发分支。** Google 和 Facebook 都是采用“主干开发”的方式,代码一般直接提交到主干的头部,这样可以保证所有用户看到的都是同一份代码的最新版本。 **“主干开发”确实避免了合并分支时的麻烦,因此像 Google 这样的公司一般就不采用分支开发,分支只用来发布。** 大多数时候,发布分支是主干某个时点的快照。以后的改 Bug 和功能增强,都是提交到主干,必要时 cherry-pick (选择部分变更集合并到其他分支)到发布分支。与主干长期并行的特性分支极为少见。
+**主干开发是一个源代码控制的分支模型,开发者在一个称为 “trunk” 的分支(Git 称 master) 中对代码进行协作,除了发布分支外没有其他开发分支。** Google 和 Facebook 都是采用“主干开发”的方式,代码一般直接提交到主干的头部,这样可以保证所有用户看到的都是同一份代码的最新版本。**“主干开发”确实避免了合并分支时的麻烦,因此像 Google 这样的公司一般就不采用分支开发,分支只用来发布。** 大多数时候,发布分支是主干某个时点的快照。以后的改 Bug 和功能增强,都是提交到主干,必要时 cherry-pick (选择部分变更集合并到其他分支)到发布分支。与主干长期并行的特性分支极为少见。
 
 由于不采用“特性分支开发”,所有提交的代码都被集成到了主干,为了保证主干上线后的有效性,一般会使用特性切换(feature toggle)。特性切换就像一个开关可以在运行期间隐藏、启用或禁用特定功能,项目团队可以借助这种方式加速开发过程。
 
 特性切换在大型项目持续交付中变得越来越重要,因为它有助于将部署从发布中解耦出来。但据吉姆 · 伯德(Jim Bird)介绍,特性切换会导致代码更脆弱、更难测试、更难理解和维护、更难提供技术支持,而且更不安全。
 
-他的主要论据是,将未经测试的代码引入生产环境是一个糟糕的主意,它们引发的问题可能会在无意间暴露出来。另外,越来越多的特性切换会使得逻辑越来越混乱。 **特性切换需要健壮的工程过程、可靠的技术设计和成熟的特性切换生命周期管理** ,如果不具备这三个关键的条件,使用特性切换反而会降低生产力。
+他的主要论据是,将未经测试的代码引入生产环境是一个糟糕的主意,它们引发的问题可能会在无意间暴露出来。另外,越来越多的特性切换会使得逻辑越来越混乱。**特性切换需要健壮的工程过程、可靠的技术设计和成熟的特性切换生命周期管理**,如果不具备这三个关键的条件,使用特性切换反而会降低生产力。
 
 根据上面的分析,主干开发的分支策略虽然有利于开展持续交付,但是它对开发团队的能力要求也更高。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25407\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25407\350\256\262.md"
index 11c8539ee..77cf15c1d 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25407\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25407\350\256\262.md"	
@@ -1,6 +1,6 @@
 # 07 “两个披萨”团队的代码管理实际案例
 
-在亚马逊内部有所谓的“两个披萨”团队,指的是团队的人数不能多到两个披萨饼还不够吃的地步。也就是说, **团队要小到让每个成员都能做出显著贡献,并且相互依赖,有共同目标,以及统一的成功标准,这样团队的工作效率才会高。**
+在亚马逊内部有所谓的“两个披萨”团队,指的是团队的人数不能多到两个披萨饼还不够吃的地步。也就是说,**团队要小到让每个成员都能做出显著贡献,并且相互依赖,有共同目标,以及统一的成功标准,这样团队的工作效率才会高。**
 
 现在有很多互联网公司喜欢采用“两个匹萨”团队的模式,你可能很好奇,这些团队通常是如何实施代码管理的?
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25409\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25409\350\256\262.md"
index 03c9722ae..5bcb00592 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25409\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25409\350\256\262.md"	
@@ -47,7 +47,7 @@
 
 那么,你究竟应该怎样去规划和设计环境呢?
 
-### 第一,公共与泳道的 **第一个关键点是抽象公共环境,而其中的公共服务基本都属于底层服务,相对比较稳定,这是解耦环境的重中之重。** 比如我们经常会将中间件,框架类服务,底层业务公共(账户,登陆,基本信息)服务部署在这套公共环境下。 **在公共环境的基础上,可以通过泳道的方式隔离相关测试应用** ,利用 LB 和 SOA 中间件对路由功能的支持,在一个大的公共集成测试环境中隔离出一个个独立的功能测试环境,那么增加的机器成本就仅与被并行的项目多少有关系了
+### 第一,公共与泳道的 **第一个关键点是抽象公共环境,而其中的公共服务基本都属于底层服务,相对比较稳定,这是解耦环境的重中之重。** 比如我们经常会将中间件,框架类服务,底层业务公共(账户,登陆,基本信息)服务部署在这套公共环境下。**在公共环境的基础上,可以通过泳道的方式隔离相关测试应用**,利用 LB 和 SOA 中间件对路由功能的支持,在一个大的公共集成测试环境中隔离出一个个独立的功能测试环境,那么增加的机器成本就仅与被并行的项目多少有关系了
 
 为了帮助你理解,我跟你分享一个具体的案例。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25410\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25410\350\256\262.md"
index ef4c2aa7b..99f1493d7 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25410\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25410\350\256\262.md"	
@@ -22,7 +22,7 @@
 
 经过上面这 4 步,一个简单的运行时环境的配置就算是完成了, 可以开始运行一个程序了。是不是感觉有点复杂呢?
 
-而这,对正常的运行时配置管理来说,只不过是冰山一角而已。 **我们不光要考虑单个实例初始化配置,还要考虑每次 JDK、Tomcat 等基础软件的版本升级引起的运行时配置的变更,而且这些变更都需要被清晰地记录下来,从而保证扩容出新的服务器时能取到正确的、最新的配置。** 另外,对于一个集群的服务器组来说,还需要强制保证它们的运行时配置是一致的。
+而这,对正常的运行时配置管理来说,只不过是冰山一角而已。**我们不光要考虑单个实例初始化配置,还要考虑每次 JDK、Tomcat 等基础软件的版本升级引起的运行时配置的变更,而且这些变更都需要被清晰地记录下来,从而保证扩容出新的服务器时能取到正确的、最新的配置。** 另外,对于一个集群的服务器组来说,还需要强制保证它们的运行时配置是一致的。
 
 独立环境配置
 ------ **独立环境配置的主要目的是,保证一个环境能够完整运作的同时,又保证足够的隔离性,使其成为一个内聚的整体。** 所以,要让一个环境能够符合需求的正常运作,你需要考虑的内容包括:
@@ -42,7 +42,7 @@
 
 解决复杂问题的办法,无非是先将其分解,再将其简单化,对环境配置这个难题来说也是同样的道理。想要解决它,首先得要想办法分解、简化它。
 
-最好的简化方法,莫过于标准化了。 **所谓标准化,就是为了在一定范围内获得最佳秩序,对实际的或潜在的问题制定共同、可重复使用的规则。**  **标准化也就是让环境学会了一门统一的语言,是自己说话的前提。** 按照这个思路,我们首先可以实现对语言栈的使用、运行时配置模板、独立环境配置的方法等的标准化:
+最好的简化方法,莫过于标准化了。**所谓标准化,就是为了在一定范围内获得最佳秩序,对实际的或潜在的问题制定共同、可重复使用的规则。**  **标准化也就是让环境学会了一门统一的语言,是自己说话的前提。** 按照这个思路,我们首先可以实现对语言栈的使用、运行时配置模板、独立环境配置的方法等的标准化:
 
 1. 规定公司的主流语言栈;
 1. 统一服务器安装镜像;
@@ -75,13 +75,13 @@
 
 像代码的部署路径这种情况,我们就把它叫作“约定大于配置”,在实际工作中,还有很多类似的场景,你完全可以利用这套方法,简化环境配置。
 
-## 比如,每个环境的域名定义,可以遵循以环境名作为区分的泛域名实现;又比如,可以用 FAT,UAT 这样的关键词来表示环境的作用;又比如,可以约定单机单应用;再比如,可以约定所有服务的端口都是 8080。 **“约定大于配置”的好处是,除了简化配置工作外,还可以提高沟通效率。** 团队成员一旦对某项内容形成认知,他们的沟通将不再容易产生歧义。 **“约定大于配置”相当于赋予了环境天生的本能,进一步加强了环境的自我描述能力。** 让环境自己能开口说话
+## 比如,每个环境的域名定义,可以遵循以环境名作为区分的泛域名实现;又比如,可以用 FAT,UAT 这样的关键词来表示环境的作用;又比如,可以约定单机单应用;再比如,可以约定所有服务的端口都是 8080。**“约定大于配置”的好处是,除了简化配置工作外,还可以提高沟通效率。** 团队成员一旦对某项内容形成认知,他们的沟通将不再容易产生歧义。**“约定大于配置”相当于赋予了环境天生的本能,进一步加强了环境的自我描述能力。** 让环境自己能开口说话
 
 有了环境标准化,以及约定大于配置的基础,你就可以顺利地让环境自己开口说话了。
 
-也就是说, **通过环境的自描述文件,让环境能讲清楚自己的作用、依赖,以及状态,而不是由外部配置来解释这些内容。** 以一台服务器为例,一旦生成,除了不能控制自己的生死外,其他运行过程中的配置,都应该根据它自身的描述来决定。
+也就是说,**通过环境的自描述文件,让环境能讲清楚自己的作用、依赖,以及状态,而不是由外部配置来解释这些内容。** 以一台服务器为例,一旦生成,除了不能控制自己的生死外,其他运行过程中的配置,都应该根据它自身的描述来决定。
 
-那么,如何让服务器自己说话呢? **首先,需要定义 Server Spec。** 这是重中之重,在服务器生成时,写入它自己的描述文件。我们通常把这个文件命名为“Server Spec”。在这个文件里,记录了这台服务器的所有身份信息,包括:IDC,型号,归属环境,作用,所属应用,服务类型,访问路径等。 **其次,解决配置中心寻址。** 中间件根据 Server Spec 的描述,寻找到它所在环境对应的配置中心,从而进一步获取其他配置,如数据库连接字符串,短信服务地址等等。 **最后,完成服务自发现。** 其实这就是一个服务自发现的过程。根据服务类型,访问路径等,还可以自动生成对应的路由配置,负载均衡配置等。
+那么,如何让服务器自己说话呢?**首先,需要定义 Server Spec。** 这是重中之重,在服务器生成时,写入它自己的描述文件。我们通常把这个文件命名为“Server Spec”。在这个文件里,记录了这台服务器的所有身份信息,包括:IDC,型号,归属环境,作用,所属应用,服务类型,访问路径等。**其次,解决配置中心寻址。** 中间件根据 Server Spec 的描述,寻找到它所在环境对应的配置中心,从而进一步获取其他配置,如数据库连接字符串,短信服务地址等等。**最后,完成服务自发现。** 其实这就是一个服务自发现的过程。根据服务类型,访问路径等,还可以自动生成对应的路由配置,负载均衡配置等。
 
 总结来说,我们是在尝试把环境配置的方向调个个儿:由原来外部通过配置告知环境应该干什么,转变成环境根据自身的能力和属性,决定自己应该去干什么。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25411\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25411\350\256\262.md"
index 4e0d6e8f7..2fed88955 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25411\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25411\350\256\262.md"	
@@ -2,13 +2,13 @@
 
 很多人分不清配置和配置管理,但其实它们是完全不同的概念。
 
-**配置管理:** 是通过技术或行政手段对软件产品及其开发过程和生命周期进行控制、规范的一系列措施。 它的目标是记录软件产品的演化过程,确保软件开发者在软件生命周期的各个阶段都能得到精确的产品配置信息。 **配置:** 是指独立于程序之外,但又对程序产生作用的可配变量。也就是说,同一份代码在不同的配置下,会产生不同的运行结果。
+**配置管理:** 是通过技术或行政手段对软件产品及其开发过程和生命周期进行控制、规范的一系列措施。 它的目标是记录软件产品的演化过程,确保软件开发者在软件生命周期的各个阶段都能得到精确的产品配置信息。**配置:** 是指独立于程序之外,但又对程序产生作用的可配变量。也就是说,同一份代码在不同的配置下,会产生不同的运行结果。
 
 从上面的定义中,你可以看到配置和配置管理有着本质上的不同:配置管理服务于软件研发过程,而配置则服务于程序本身。
 
 作为一名程序员,开发时经常要面对不同的运行环境:开发环境、测试环境、生产环境、内网环境、外网环境等等。不同的环境,相关的配置一般不一样,比如数据源配置、日志文件配置,以及一些软件运行过程中的基本配置等。
 
-另外,你也会遇到一些业务上的,以及逻辑上的配置。比如,针对不同地域采取不同的计费逻辑,计费逻辑又要根据这些地域的需要随时调整。 **如果我们把这些信息都硬编码在代码里,结果就是:每次发布因为环境不同,或者业务逻辑的调整,都要修改代码。而代码一旦被修改,就需要完整的测试,那么变更的代价将是巨大的。** 因此,我们往往会通过“配置”来解决这些问题。
+另外,你也会遇到一些业务上的,以及逻辑上的配置。比如,针对不同地域采取不同的计费逻辑,计费逻辑又要根据这些地域的需要随时调整。**如果我们把这些信息都硬编码在代码里,结果就是:每次发布因为环境不同,或者业务逻辑的调整,都要修改代码。而代码一旦被修改,就需要完整的测试,那么变更的代价将是巨大的。** 因此,我们往往会通过“配置”来解决这些问题。
 
 但是,“配置”本身也很讲究。在什么阶段进行配置,采用什么手段进行配置,都将直接影响持续交付的效果。
 
@@ -61,7 +61,7 @@ maven initialize –Pprod
 
 为什么要独立分离出打包这个步骤呢?你可能会问,Maven 在构建过程中不是已经完成了 package 步骤吗?
 
-正因为构建时配置,需要针对多个 profile 编译多次,而持续交付有一个核心概念,即: **一次构建多次部署** 。打包就是为了解决这个问题而被发明的。 **打包时配置的基本思想是:构建时完全不清楚程序所要部署的环境,因此只完成最基本的默认配置;而发布时清晰地知晓环境信息,因此可根据环境信息,进行相关配置的替换。** 在携程,我们开发了一个叫作 ConfigGen 的工具,用以替换配置文件。 这样,你就不需要每次更改配置时,都重新编译整个代码,大幅缩短了整个发布流程的时间, 而且 ConfigGen 完全基于 XML,适用于任何语言。
+正因为构建时配置,需要针对多个 profile 编译多次,而持续交付有一个核心概念,即: **一次构建多次部署** 。打包就是为了解决这个问题而被发明的。**打包时配置的基本思想是:构建时完全不清楚程序所要部署的环境,因此只完成最基本的默认配置;而发布时清晰地知晓环境信息,因此可根据环境信息,进行相关配置的替换。** 在携程,我们开发了一个叫作 ConfigGen 的工具,用以替换配置文件。 这样,你就不需要每次更改配置时,都重新编译整个代码,大幅缩短了整个发布流程的时间, 而且 ConfigGen 完全基于 XML,适用于任何语言。
 
 ConfigGen 的使用也很简单,只要一个 ConfigProfile.xml 文件即可,dev 和 prd 指两个入参,根据这两个入参分别定义了 currentENV 的具体值,如下图所示。
 
@@ -129,7 +129,7 @@ Apollo 有详尽的文档,其功能基本可以覆盖绝大多数业务对配
 
 > 当你使用构建配置和打包配置时,配置是随着代码的一起发布的。这样的话,如果代码回滚了,配置自然而然的也会跟着一起回滚,旧版本的代码和旧版本的配置在绝大多数情况下是兼容的。但如果你用了配置中心,配置就不会随着代码回滚,就可能引发意想不到的问题。
 
-此时, **先回滚配置还是先回滚代码就成了一个死循环的问题。最好的办法是保证配置与代码的兼容性,这有点类似于数据库的 schema 变更。** 比如,只增加配置不删减配置、不改变配置的数据类型而是新增一个配置等方法。同时,也要做好代码版本与配置版本的对应管理。
+此时,**先回滚配置还是先回滚代码就成了一个死循环的问题。最好的办法是保证配置与代码的兼容性,这有点类似于数据库的 schema 变更。** 比如,只增加配置不删减配置、不改变配置的数据类型而是新增一个配置等方法。同时,也要做好代码版本与配置版本的对应管理。
 
 那你可能会问,是不是只要做到代码和配置一起回滚就行了呢?其实不是,配置是一个很复杂的问题,像之前所说,绝大多数情况下,回滚配置能够兼容,但也有不行的时候。
 
@@ -137,7 +137,7 @@ Apollo 有详尽的文档,其功能基本可以覆盖绝大多数业务对配
 
 所以,对于配置回滚这个复杂问题,没有一劳永逸的办法, 只能根据实际情况选择最适合自己的方案。
 
-但是, **我有一个推荐做法就是,每次回滚时,将可能发生变化的配置进行 diff 操作,由负责回滚的具体人根据结果去做最后的判断。**
+但是,**我有一个推荐做法就是,每次回滚时,将可能发生变化的配置进行 diff 操作,由负责回滚的具体人根据结果去做最后的判断。**
 
 ## 总结
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25412\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25412\350\256\262.md"
index 0c01e2398..5398cfe8e 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25412\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25412\350\256\262.md"	
@@ -8,11 +8,11 @@
 
 当开发人员向你申请一套新环境时,作为测试环境的维护者,你首先需要明确打造环境构建流水线需要关注的三大内容:
 
-1. **虚拟机环境准备** ,根据环境的应用数、每个应用需要的硬件配置,准备好环境的硬件资源。
+1. **虚拟机环境准备**,根据环境的应用数、每个应用需要的硬件配置,准备好环境的硬件资源。
 
-2. **应用部署流水线** ,在标准化的虚拟机上进行应用部署,当出现问题时如何容错。
+2. **应用部署流水线**,在标准化的虚拟机上进行应用部署,当出现问题时如何容错。
 
-3. **环境变更** ,在 SOA 或微服务的架构体系下,常常会因为测试的需求,将几套环境合并或拆分,创建环境时,你需要考虑如何高效地完成这些操作。
+3. **环境变更**,在 SOA 或微服务的架构体系下,常常会因为测试的需求,将几套环境合并或拆分,创建环境时,你需要考虑如何高效地完成这些操作。
 
 接下来,我会针对这三大内容进行展开,带你快速搭建一套环境。
 
@@ -34,7 +34,7 @@
 
 ## 应用部署流水线
 
-由于不同公司的中间件和运维标准不同,部署流水线的差异也会很大,所以这里我只会从单应用部署标准化、应用部署的并行度,以及流水线的容错机制,这三个关键的角度,分享如何提速环境的搭建。 **单应用部署标准化** ,这是整个环境部署的基础。对一套测试环境而言,每个应用就像是环境上的一个零件,如果单个应用无法自动发布或者发布失败率很高,那么整个环境就更难以构建起来。而如何实现一个好的发布系统,提升单应用部署速度,我会在后面的文章中详细介绍。 **应用部署的并行度** ,为了提高环境的部署速度,需要尽可能得最大化应用部署的并行度。理想的情况下,环境中的所有应用都可以一次性地并行部署。 然而,做到一次性并行部署并不容易,需要保证:应用都是无状态的,并且可以不依赖别的应用进行启动,或者仅仅依赖于基础环境中的应用就可以启动,且可以随时通过中间件进行调用链的切换。 **在携程,我们力求做到所有应用都可以一次性并行部署,但这条运维标准并不通用。** 当我们需要更复杂的应用部署调度规则时,一个原则是将应用部署的次序、并行方式的描述交给开发人员去实现,并基于 DevOps 的理念,即调度策略和规则可以通过工具代码化,保证同一套环境反复创建的流水线是一致的。
+由于不同公司的中间件和运维标准不同,部署流水线的差异也会很大,所以这里我只会从单应用部署标准化、应用部署的并行度,以及流水线的容错机制,这三个关键的角度,分享如何提速环境的搭建。**单应用部署标准化**,这是整个环境部署的基础。对一套测试环境而言,每个应用就像是环境上的一个零件,如果单个应用无法自动发布或者发布失败率很高,那么整个环境就更难以构建起来。而如何实现一个好的发布系统,提升单应用部署速度,我会在后面的文章中详细介绍。**应用部署的并行度**,为了提高环境的部署速度,需要尽可能得最大化应用部署的并行度。理想的情况下,环境中的所有应用都可以一次性地并行部署。 然而,做到一次性并行部署并不容易,需要保证:应用都是无状态的,并且可以不依赖别的应用进行启动,或者仅仅依赖于基础环境中的应用就可以启动,且可以随时通过中间件进行调用链的切换。**在携程,我们力求做到所有应用都可以一次性并行部署,但这条运维标准并不通用。** 当我们需要更复杂的应用部署调度规则时,一个原则是将应用部署的次序、并行方式的描述交给开发人员去实现,并基于 DevOps 的理念,即调度策略和规则可以通过工具代码化,保证同一套环境反复创建的流水线是一致的。
 
 流水线的容错机制
 
@@ -52,10 +52,10 @@
 
 1. 当存在多个子环境时,可能在某个时间点需要做多个项目的集成,这时开发人员需要合并多个环境。
 1. 和合并的情况相反,有些情况下,开发人员需要将一个子环境中的应用切分开来,分为两个或者多个环境分别进行隔离测试。
-1. 已经存在一个子环境,当多个并行项目时,开发人员会克隆一套完整的子环境做测试。 **对于这 4 个场景,我们需要关注的是在多并行环境的情况下应用拓扑图,包括用户访问应用的入口、应用之间调用链的管理,以及应用对数据库之类的基础设施的访问。** 1. **用户访问应用的入口管理。** 以最常用的访问入口(域名)为例,我推荐的做法是根据约定大于配置的原则,当环境管理平台识别到这是一个 Web 应用时,通过应用在生产环境中的域名、路由,环境名等参数,自动生产一个域名并在域名服务上注册。 这里需要注意的是,域名的维护尽量是在 SLB(负载均衡,Server Load Balancer)类似的软负载中间件上实现,而不要在 DNS 上实现。因为域名变更时,通过泛域名的指向,SLB 二次解析可以做到域名访问的实时切换。而如果配置在 DNS 上,域名的变更就无法做到瞬时生效了。
+1. 已经存在一个子环境,当多个并行项目时,开发人员会克隆一套完整的子环境做测试。**对于这 4 个场景,我们需要关注的是在多并行环境的情况下应用拓扑图,包括用户访问应用的入口、应用之间调用链的管理,以及应用对数据库之类的基础设施的访问。** 1. **用户访问应用的入口管理。** 以最常用的访问入口(域名)为例,我推荐的做法是根据约定大于配置的原则,当环境管理平台识别到这是一个 Web 应用时,通过应用在生产环境中的域名、路由,环境名等参数,自动生产一个域名并在域名服务上注册。 这里需要注意的是,域名的维护尽量是在 SLB(负载均衡,Server Load Balancer)类似的软负载中间件上实现,而不要在 DNS 上实现。因为域名变更时,通过泛域名的指向,SLB 二次解析可以做到域名访问的实时切换。而如果配置在 DNS 上,域名的变更就无法做到瞬时生效了。
    2. **应用之间调用链的管理。** 对于 service 的调用关系,我在《“配置”是把双刃剑,带你了解各种配置方法》这篇文章中,提到了携程开源的配置中心 Apollo 的实现策略,所有的服务调用的路由都是通过环境描述文件 server.spec 自发现的,你只要保证文件的环境号、IDC 等属性是正确的,整个调用链就不会被混淆。 同时,服务调用中间件需要可以做到自动判断,被隔离的环境内是否有需要被调用的服务,并在当前环境以及基础环境中间进行自动选择,以保证服务被正确调用到。
    3. **对数据库的访问。** 一是,数据库连接串的维护问题,与 SOA 调用链(即服务之间的调用关系)的维护类似,完全可以借鉴;二是,数据库的快速创建策略。 对于数据库中的表结构和数据,我们采取的方式是根据生产中实际的数据库结构,产生一个基准库,由用户自己来维护这个基准库的数据,保证数据的有效性。并在环境创建时,提供数据库脚本变更的接口,根据之前的基准库创建一个新的实例,由此保证环境中的数据符合预期。
-   4. **对于环境的创建和拆分** ,最主要的问题就是如何复制和重新配置环境中的各个零件。环境创建,就是不断提高虚拟机准备和应用部署两个流水线的速度和稳定性;环境拆分,则需要关注以上所说的三个最重要的配置内容。 **而环境的合并需要注意的问题是,合并后的环境冲突。** 比如,两套环境中都存在同一个服务应用,而两者的版本是不一致的;又或者,两个环境各自配置了一套数据库。此时该如何处理呢。
+   4. **对于环境的创建和拆分**,最主要的问题就是如何复制和重新配置环境中的各个零件。环境创建,就是不断提高虚拟机准备和应用部署两个流水线的速度和稳定性;环境拆分,则需要关注以上所说的三个最重要的配置内容。**而环境的合并需要注意的问题是,合并后的环境冲突。** 比如,两套环境中都存在同一个服务应用,而两者的版本是不一致的;又或者,两个环境各自配置了一套数据库。此时该如何处理呢。
 
 因为环境的描述已经被代码化了,所以我们解决这些问题的方式类似于解决代码合并的冲突问题。在环境合并前,先进行一次环境的冲突检测,如果环境中存在不可自动解决的冲突,就将这些冲突罗列出来,由用户选择合适的服务版本。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25413\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25413\350\256\262.md"
index 6d789b1d7..cd7995267 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25413\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25413\350\256\262.md"	
@@ -62,7 +62,7 @@
 
 没有容器之前,交付标准包括软件环境(也就所谓的机器)和软件代码两部分。交付系统更关注的是软件代码,环境一旦产生后,我们就不再关心或者很难再干预用户后期是如何对其做变更的了。
 
-也就是说,环境的变更没有版本,没有记录,甚至当事人也会忘记当时变更了什么, 不言而喻,这会带来很多未知的安全隐患。 **而,容器技术统一了软件环境和软件代码,交付产物中既包括了软件环境,又包括了软件代码。也就是说,容器帮我们重新定义了交付标准。** 那么,容器技术到底是如何做到的呢?被重新定义后的交付,又有哪些特点呢?
+也就是说,环境的变更没有版本,没有记录,甚至当事人也会忘记当时变更了什么, 不言而喻,这会带来很多未知的安全隐患。**而,容器技术统一了软件环境和软件代码,交付产物中既包括了软件环境,又包括了软件代码。也就是说,容器帮我们重新定义了交付标准。** 那么,容器技术到底是如何做到的呢?被重新定义后的交付,又有哪些特点呢?
 
 ### 第一,交付结果一致
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25414\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25414\350\256\262.md"
index a795ffda3..855c2657b 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25414\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25414\350\256\262.md"	
@@ -36,7 +36,7 @@
 
 总之,搭建私有仓库一定物超所值。当然,维护和管理这一大批工具需要投入不少人力和经济成本,在公司 / 团队没有成一定规模的前提下,会有一定的负担。
 
-## 所以, **如果你的团队暂时没有条件自己搭建私有仓库的话,可以使用国内已有的一些私有仓库,来提升下载速度。当然,在选择私有仓库时,你要尽量挑选那些被广泛使用的仓库,避免安全隐患。** 使用本地缓存
+## 所以,**如果你的团队暂时没有条件自己搭建私有仓库的话,可以使用国内已有的一些私有仓库,来提升下载速度。当然,在选择私有仓库时,你要尽量挑选那些被广泛使用的仓库,避免安全隐患。** 使用本地缓存
 
 虽然搭建私有仓库可以解决代码或者依赖下载的问题,但是私有仓库不能滥用,还是要结合构建机器本地的磁盘缓存才能达到利益最大化。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25415\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25415\350\256\262.md"
index c17d85a20..310143dfc 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25415\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25415\350\256\262.md"	
@@ -44,7 +44,7 @@ Maven Enforcer 提供了非常丰富的内置检查规则,在这里,我给
 
 ### 第一,bannedDependencies 规则
 
-**该规则表示禁止使用某些依赖,或者某些依赖的版本** ,使用示例:
+**该规则表示禁止使用某些依赖,或者某些依赖的版本**,使用示例:
 
 ![img](assets/27f936902309c3ee2e71a8dd007018ba.png)
 
@@ -61,7 +61,7 @@ bannedDependencies 规则的常见应用场景包括:
 
 在《手把手教你依赖管理》一文中,我介绍了 Maven 的依赖仲裁的两个原则:最短路径优先原则和第一声明优先原则。
 
-但是,Maven 基于这两个原则处理依赖的方式过于简单粗暴。毕竟在一个成熟的系统中,依赖的关系错综复杂,用户很难一个一个地排查所有依赖的关系和冲突,稍不留神便会掉进依赖的陷阱里,这时 dependencyConvergence 就可以粉墨登场了。 **dependencyConvergence 规则的作用是: 当项目中的 A 和 B 分别引用了不同版本的 C 时, Enforce 检查失败。** 下面这个实例,可以帮你理解这个规则的作用。
+但是,Maven 基于这两个原则处理依赖的方式过于简单粗暴。毕竟在一个成熟的系统中,依赖的关系错综复杂,用户很难一个一个地排查所有依赖的关系和冲突,稍不留神便会掉进依赖的陷阱里,这时 dependencyConvergence 就可以粉墨登场了。**dependencyConvergence 规则的作用是: 当项目中的 A 和 B 分别引用了不同版本的 C 时, Enforce 检查失败。** 下面这个实例,可以帮你理解这个规则的作用。
 
 ![img](assets/ef9194165537330d5d8e0bbc6ce1ded0.png)
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25417\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25417\350\256\262.md"
index 65cc00265..58c294463 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25417\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25417\350\256\262.md"	
@@ -30,7 +30,7 @@
 
 如果真要这样操作的话,容器镜像也就不会有今天如此庞大的用户群体了。Docker 帮我们解决这个问题的方式,就是提供了 Dockerfile。
 
-简单来说, **Dockerfile 第一个好处就是,可以通过文本格式的配置文件描述镜像,这个配置文件里面可以运行功能丰富的指令,你可以通过运行 docker build 将这些指令转化为镜像。** 比如,我要更改 Ubuntu 镜像安装一个 Vim 编辑器,那么我的 Dockerfile 可以这样写:
+简单来说,**Dockerfile 第一个好处就是,可以通过文本格式的配置文件描述镜像,这个配置文件里面可以运行功能丰富的指令,你可以通过运行 docker build 将这些指令转化为镜像。** 比如,我要更改 Ubuntu 镜像安装一个 Vim 编辑器,那么我的 Dockerfile 可以这样写:
 
 ```bash
 FROM ubuntu 
@@ -43,7 +43,7 @@ RUN apt-get install vim -y
 
 运行 docker build 后会产生一个新镜像,我们可以通过 docker tag 给这个新镜像起一个名字,然后 docker push 到仓库,就可以从仓库下载这个镜像了,后续的其他镜像也可以继承这个镜像进行其他改动。
 
-镜像就是这样通过 Dockerfile 一层一层的继承,不断增加新的内容,直到变成你想要的样子。 **Dockerfile 的另外一个好处就是可以描述镜像的变化** ,通过一行命令就可以直观描述出环境变更的过程,如果再通过 git 进行版本控制,就可以让环境的管理更加可靠与简单。
+镜像就是这样通过 Dockerfile 一层一层的继承,不断增加新的内容,直到变成你想要的样子。**Dockerfile 的另外一个好处就是可以描述镜像的变化**,通过一行命令就可以直观描述出环境变更的过程,如果再通过 git 进行版本控制,就可以让环境的管理更加可靠与简单。
 
 了解了 Dockerfile 之后,你就可以利用它进行代码更新了,最主要的步骤就以下三步:
 
@@ -57,7 +57,7 @@ RUN apt-get install vim -y
 
 那么,如何做好镜像的优化呢?你可以从 3 个方面入手:
 
-1. **选择合适的 Base 镜像;** 2. **减少不必要的镜像层的产生;** 3. **充分利用指令的缓存。**  **为什么第一条说要选择合适的 Base 镜像呢?因为,这是最直接和有效的方式。** 举个例子就更好理解了。比如,我只想运行一个 Java 进程,那么镜像里就只有这个 Java 进程所需的环境就可以了,而没必要使用一个完整 Ubuntu 或者 CentOS 镜像。 **关于第二点,减少不必要的镜像层,是因为使用 Dockerfile 时,每一条指令都会创建一个镜像层,继而会增加整体镜像的大小。** 比如,下面这个 Dockerfile:
+1. **选择合适的 Base 镜像;** 2. **减少不必要的镜像层的产生;** 3. **充分利用指令的缓存。**  **为什么第一条说要选择合适的 Base 镜像呢?因为,这是最直接和有效的方式。** 举个例子就更好理解了。比如,我只想运行一个 Java 进程,那么镜像里就只有这个 Java 进程所需的环境就可以了,而没必要使用一个完整 Ubuntu 或者 CentOS 镜像。**关于第二点,减少不必要的镜像层,是因为使用 Dockerfile 时,每一条指令都会创建一个镜像层,继而会增加整体镜像的大小。** 比如,下面这个 Dockerfile:
 
 ```bash
 FROM ubuntu 
@@ -67,7 +67,7 @@ RUN apt-get remove vim -y
 
 虽然这个操作创建的镜像中没有安装 Vim,但是镜像的大小和有 Vim 是一样的。原因就是,每条指令都会新加一个镜像层,执行 install vim 后添加了一层,执行 remove vim 后也会添加一层,而这一删除命令并不会减少整个镜像的大小。
 
-因此,当我们编写 Dockerfile 时,可以合并多个 RUN 指令,减少不必要的镜像层的产生,并且在之后将多余的命令清理干净,只保留运行时需要的依赖。就好比我买了两斤橘子,只需要把橘子肉保留下来就好,橘子皮可以直接丢掉,不用保留在房间里。 **Dockerfile 构建的另外一个重要特性是指令可以缓存,可以极大地缩短构建时间。** 因为之前也说了,每一个 RUN 都会产生一个镜像,而 Docker 在默认构建时,会优先选择这些缓存的镜像,而非重新构建一层镜像。比如,一开始我的 Dockerfile 如下:
+因此,当我们编写 Dockerfile 时,可以合并多个 RUN 指令,减少不必要的镜像层的产生,并且在之后将多余的命令清理干净,只保留运行时需要的依赖。就好比我买了两斤橘子,只需要把橘子肉保留下来就好,橘子皮可以直接丢掉,不用保留在房间里。**Dockerfile 构建的另外一个重要特性是指令可以缓存,可以极大地缩短构建时间。** 因为之前也说了,每一个 RUN 都会产生一个镜像,而 Docker 在默认构建时,会优先选择这些缓存的镜像,而非重新构建一层镜像。比如,一开始我的 Dockerfile 如下:
 
 ```bash
 FROM ubuntu 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25418\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25418\350\256\262.md"
index fad124eef..d4455d480 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25418\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25418\350\256\262.md"	
@@ -91,7 +91,7 @@ yum install wget -y
 
 满足了用户对镜像的个性化需求,也就意味着会引入不可控因素,因此对镜像的安全合规检查也就变得尤为重要了。我们必须通过合规检查,来确认用户是否在容器里做了危险的事情。
 
-只有这样,用户个性化的自由,才不会损害整个环境。毕竟,有克制的自由才是真正的自由。 **对自定义镜像,首先必须保证它是基于公司官方 Base 镜像的,这是携程最不可动摇的底线。** 在其他情况下,就算真的不继承公司官方 Base 镜像,建议也必须要满足 Base 镜像的一些强制性规定,比如应用进程不能是 root 等类似的安全规范。 **关于自定义镜像是否继承了公司官方镜像,我们采取的方法是对比镜像 Layer,即自定义镜像的 Layer 中必须包含官方 Base 镜像的 Layer。**
+只有这样,用户个性化的自由,才不会损害整个环境。毕竟,有克制的自由才是真正的自由。**对自定义镜像,首先必须保证它是基于公司官方 Base 镜像的,这是携程最不可动摇的底线。** 在其他情况下,就算真的不继承公司官方 Base 镜像,建议也必须要满足 Base 镜像的一些强制性规定,比如应用进程不能是 root 等类似的安全规范。**关于自定义镜像是否继承了公司官方镜像,我们采取的方法是对比镜像 Layer,即自定义镜像的 Layer 中必须包含官方 Base 镜像的 Layer。**
 
 但是,对比 Layer 也不是最靠谱的方式,因为用户虽然继承了 Base 镜像,但还是有可能在用户创建的上层 Layer 中破坏镜像结构。目前,Docker 的部署流程中,还有许多潜在漏洞,有可能让一些有企图的人有机可乘,发起攻击。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25419\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25419\350\256\262.md"
index 3459790bf..16d4ff592 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25419\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25419\350\256\262.md"	
@@ -78,11 +78,11 @@
 
 ### 第三,扩展到集群
 
-如今应用架构基本告别了单点世界,面向集群的发布带来了更高维度的问题。 **当发布的目标是一组机器而不是一台机器时,主要问题就变成了如何协调整个过程。** 比如,追踪、同步一组机器目前发布进行到了哪一步,编排集群的发布命令就成为了更核心功能。好消息是,集群提供了新的、更易行的方法提高系统的发布时稳定性,其中最有用的一项被称为灰度发布。 **灰度发布是指,渐进式地更新每台机器运行的版本,一段时期内集群内运行着多个不同的版本,同一个 API 在不同机器上返回的结果很可能不同。** 虽然灰度发布涉及到复杂的异步控制流,但这种模式相比简单粗暴的“一波流”显然要安全得多。
+如今应用架构基本告别了单点世界,面向集群的发布带来了更高维度的问题。**当发布的目标是一组机器而不是一台机器时,主要问题就变成了如何协调整个过程。** 比如,追踪、同步一组机器目前发布进行到了哪一步,编排集群的发布命令就成为了更核心功能。好消息是,集群提供了新的、更易行的方法提高系统的发布时稳定性,其中最有用的一项被称为灰度发布。**灰度发布是指,渐进式地更新每台机器运行的版本,一段时期内集群内运行着多个不同的版本,同一个 API 在不同机器上返回的结果很可能不同。** 虽然灰度发布涉及到复杂的异步控制流,但这种模式相比简单粗暴的“一波流”显然要安全得多。
 
 不仅如此,当对灰度发布的进度有很高的控制能力时,事实上这种方式可以提供 A/B 测试可能性。 比如,你可以说,将 100 台机器分成 4 批,每天 25 台发布至新的版本,并逐步观察新版本的效果。
 
-其实, **集群层面的设计,某种程度上是对单机部署理念的重复,只不过是在更高的维度上又实现了一遍。** 例如,单机部署里重启服务线程堆逐批停止实现,与集群层面的分批发布理念,有异曲同工之妙。
+其实,**集群层面的设计,某种程度上是对单机部署理念的重复,只不过是在更高的维度上又实现了一遍。** 例如,单机部署里重启服务线程堆逐批停止实现,与集群层面的分批发布理念,有异曲同工之妙。
 
 ## 几种常见的灰度方式
 
@@ -95,9 +95,9 @@
    - 这种发布方法需要额外的服务器集群支持,对于负载高的核心应用机器需求可观,实现难度巨大且成本较高。
    - 蓝绿发布的好处是所有服务都使用这种方式时,实际上创造了蓝绿两套环境,隔离性最好、最可控,回滚切换几乎没有成本。
 
-1. **滚动发布** ,是不添加新机器,从同样的集群服务器中挑选一批,停止上面的服务,并更新为新版本,进行验证,验证完毕后接入流量。重复此步骤,一批一批地更新集群内的所有机器,直到遍历完所有机器。 这种滚动更新的方法比蓝绿发布节省资源,但发布过程中同时会有两个版本对外提供服务,无论是对自身或是调用者都有较高的兼容性要求,需要团队间的合作妥协。但这类问题相对容易解决,实际中往往会通过功能开关等方式来解决。
+1. **滚动发布**,是不添加新机器,从同样的集群服务器中挑选一批,停止上面的服务,并更新为新版本,进行验证,验证完毕后接入流量。重复此步骤,一批一批地更新集群内的所有机器,直到遍历完所有机器。 这种滚动更新的方法比蓝绿发布节省资源,但发布过程中同时会有两个版本对外提供服务,无论是对自身或是调用者都有较高的兼容性要求,需要团队间的合作妥协。但这类问题相对容易解决,实际中往往会通过功能开关等方式来解决。
 
-3. **金丝雀发布** ,从集群中挑选特定服务器或一小批符合要求的特征用户,对其进行版本更新及验证,随后逐步更新剩余服务器。这种方式,比较符合携程对灰度发布的预期,但可能需要精细的流控和数据的支持,同样有版本兼容的需求。 **结合实际情况,携程最终选择的方式是:综合使用滚动发布和金丝雀发布。** 首先允许对一个较大的应用集群,特别是跨 IDC 的应用集群,按自定义规则进行切分,形成较固定的发布单元。基于这种设计,我们开发了携程开源灰度发布系统,并命名为 Tars 。其开源地址为:[https://github.com/ctripcorp/tars](https://github.com/ctripcorp/tars)
+3. **金丝雀发布**,从集群中挑选特定服务器或一小批符合要求的特征用户,对其进行版本更新及验证,随后逐步更新剩余服务器。这种方式,比较符合携程对灰度发布的预期,但可能需要精细的流控和数据的支持,同样有版本兼容的需求。**结合实际情况,携程最终选择的方式是:综合使用滚动发布和金丝雀发布。** 首先允许对一个较大的应用集群,特别是跨 IDC 的应用集群,按自定义规则进行切分,形成较固定的发布单元。基于这种设计,我们开发了携程开源灰度发布系统,并命名为 Tars 。其开源地址为:[https://github.com/ctripcorp/tars](https://github.com/ctripcorp/tars)
 
 关于携程灰度发布的设计和实施,以及如何把灰度发布的理念贯穿到你的持续交付体系中,我会在后面的第 22 篇文章《发布系统架构功能设计实例》中详细介绍。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25420\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25420\350\256\262.md"
index 6e149f3a1..fb4857bd7 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25420\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25420\350\256\262.md"	
@@ -31,7 +31,7 @@
 1. 将引用指向这个新的基础设施;
 1. 保留原有基础设施以备回滚。
 
-## 虽然不可变模型的设想很好,但其中也会有一些特殊情况存在。比如,涉及数据的部分,特别是数据库,你不可能每次都重建一个数据库实例来达到“不可变”的目的。为什么呢?其根本原因是,数据库是有状态的。所以,从这里可以清楚地看到, **不可变(Immutable)的前提是无状态。** 不可变基础设施的神话
+## 虽然不可变模型的设想很好,但其中也会有一些特殊情况存在。比如,涉及数据的部分,特别是数据库,你不可能每次都重建一个数据库实例来达到“不可变”的目的。为什么呢?其根本原因是,数据库是有状态的。所以,从这里可以清楚地看到,**不可变(Immutable)的前提是无状态。** 不可变基础设施的神话
 
 说到为什么会需要“不可变基础设施”这种方法论,还是挺有意思的。
 
@@ -65,7 +65,7 @@
 2. **频率问题** :假设你可以通过一些方法保证顺序,在面对大型基础设施时,应该如何制定收敛频率呢?最简单的回答,自然是越频繁越好。 那么你就会陷入巨大的陷阱中,你会发现完全无法支撑并发的收敛工作。而且收敛工作与设施的规模成正比,直接否定了系统的可扩展性。
 3. **蝴蝶效应** :你始终无法确定一个绝对的基准点,认为是系统的初始或者当前应该有的状态。因为你始终还在收敛中,只是无限趋近。因此任何小偏差,都会引起将来重大的、不可预知的问题。这就是蝴蝶效应。
 
-但是,容器却通过分层镜像与镜像发布技术,解决了上面的顺序问题、频率问题和蝴蝶效应。所以说,容器是一个惊人的发明, **它使得每一次变更都成为了一次发布,而每一次发布都成为了系统的重新构建,** 从而使得“一致”模型的目标能够达成。
+但是,容器却通过分层镜像与镜像发布技术,解决了上面的顺序问题、频率问题和蝴蝶效应。所以说,容器是一个惊人的发明,**它使得每一次变更都成为了一次发布,而每一次发布都成为了系统的重新构建,** 从而使得“一致”模型的目标能够达成。
 
 ### 第二,Immutable 的衍生
 
@@ -86,10 +86,10 @@
 
 “不可变”模型的好处,已经显而易见。而对于容器时代的持续交付,也显然已经从原来单纯交付可运行软件的范畴,扩展为连带基础环境一起交付了,所以我们需要为此做好准备。
 
-上文中,我已经总结了一句话, **每一次变更都是一次发布,而每一次发布都是系统重新构建,更形象点说,每一次发布都是一个独立镜像的启动** 。所有持续交付的变化也都可以表现为这样一句话,那具体怎么理解呢。 **首先,任何的变更,包括代码的、配置的、环境的,甚至是 CPU、内存、磁盘的大小变化,都需要制作成独立版本的镜像。**  **其次,变更的镜像可以提前制作,但必须通过发布才能生效。** 这有 2 个好处:
+上文中,我已经总结了一句话,**每一次变更都是一次发布,而每一次发布都是系统重新构建,更形象点说,每一次发布都是一个独立镜像的启动** 。所有持续交付的变化也都可以表现为这样一句话,那具体怎么理解呢。**首先,任何的变更,包括代码的、配置的、环境的,甚至是 CPU、内存、磁盘的大小变化,都需要制作成独立版本的镜像。**  **其次,变更的镜像可以提前制作,但必须通过发布才能生效。** 这有 2 个好处:
 
 1. 重新生成新的实例进行生效,完全遵循不可变模型的做法;
-1. 发布内容既包含代码也包含基础设施,更有利于 DevOps 的实施。 **再次,一组运行中的同一个镜像的实例,因为“不可变”的原因,其表现和实质都是完全一样的,所以不再需要关心顺序的问题。因为任何一个都等价,所以也就没有发布或替换的先后问题了。**  **最后,根据“一致”模型的要求,我们需要记录系统从第一天发展到今天的所有有序变更。** 对 Docker 而言,不仅要能向上追溯层层 Base 镜像的情况,更建议将系统和软件的配置以 Dockerfile 的方式进行处理,以明确整个过程的顺序。
+1. 发布内容既包含代码也包含基础设施,更有利于 DevOps 的实施。**再次,一组运行中的同一个镜像的实例,因为“不可变”的原因,其表现和实质都是完全一样的,所以不再需要关心顺序的问题。因为任何一个都等价,所以也就没有发布或替换的先后问题了。**  **最后,根据“一致”模型的要求,我们需要记录系统从第一天发展到今天的所有有序变更。** 对 Docker 而言,不仅要能向上追溯层层 Base 镜像的情况,更建议将系统和软件的配置以 Dockerfile 的方式进行处理,以明确整个过程的顺序。
 
 这些理念,不仅传统的持续交付中没有涉及,甚至有些还与我们日常的理解和习惯有所不同。比如,你通常认为一个集群中的不同服务器的配置是可以不一样的,但在“不可变”模型中,它是不被允许的。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25421\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25421\350\256\262.md"
index 4e38f7997..dde3e02a2 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25421\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25421\350\256\262.md"	
@@ -20,7 +20,7 @@
 
 所有相关人员的注意力都会优先集中在这个大屏幕上,只有发生异常时,才会由具体的负责人在自己的岗位上进行处理。
 
-这也就说明一个很重要的问题,对于发布这件事儿来说, **首先应该有 1 张页面,且仅有 1 张页面,能够展示发布当时的绝大多数信息、数据和内容,这个页面既要全面,又要精准。** 全面指的是内容清晰完整,精准指的是数据要实时、可靠。
+这也就说明一个很重要的问题,对于发布这件事儿来说,**首先应该有 1 张页面,且仅有 1 张页面,能够展示发布当时的绝大多数信息、数据和内容,这个页面既要全面,又要精准。** 全面指的是内容清晰完整,精准指的是数据要实时、可靠。
 
 除了以上的要求外,对于实际的需求,还要考虑 2 个时态,即发布中和未发布时,展示的内容应该有所区别。
 
@@ -74,10 +74,10 @@
 
 将发布结果高度概括为成功、失败和中断后,配合这三种状态,我们可以进一步地定义出最精简的 4 种用户操作行为,即开始发布、停止发布、发布回退和发布重试。
 
-- **开始发布** ,指的是用户操作开始发布时,需要选择版本、发布集群、发布参数,配置提交后,即可立即开始发布。
-- **停止发布** ,指的是发布过程中如果遇到了异常情况,用户可以随时停止发布,发布状态也将停留在操作“停止发布”的那一刻。
-- **发布回退** ,指的是如果需要回退版本,用户可以在任意时刻操作“发布回退”,回退到历史上最近一次发布成功的版本。
-- **发布重试** ,指的是在发布的过程中,因为种种原因导致一些机器发布失败后,用户可以在整个事务发布结束后,尝试重新发布失败的机器,直到发布完成。
+- **开始发布**,指的是用户操作开始发布时,需要选择版本、发布集群、发布参数,配置提交后,即可立即开始发布。
+- **停止发布**,指的是发布过程中如果遇到了异常情况,用户可以随时停止发布,发布状态也将停留在操作“停止发布”的那一刻。
+- **发布回退**,指的是如果需要回退版本,用户可以在任意时刻操作“发布回退”,回退到历史上最近一次发布成功的版本。
+- **发布重试**,指的是在发布的过程中,因为种种原因导致一些机器发布失败后,用户可以在整个事务发布结束后,尝试重新发布失败的机器,直到发布完成。
 
 ## 5 个发布步骤
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25422\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25422\350\256\262.md"
index b10099a08..2916bf53c 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25422\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25422\350\256\262.md"	
@@ -32,13 +32,13 @@ Roll Engine,即发布引擎,主要负责创建发布批次,按批次粒度
 
 ## 发布系统核心模型
 
-发布系统的核心模型主要包括 Group、DeploymentConfig、Deployment、DeploymentBatch,和 DeploymentTarget 这 5 项。 **Group** ,即集群,一组相同应用的实例集合,是发布的最小单元,其概念如图 2 所示。
+发布系统的核心模型主要包括 Group、DeploymentConfig、Deployment、DeploymentBatch,和 DeploymentTarget 这 5 项。**Group**,即集群,一组相同应用的实例集合,是发布的最小单元,其概念如图 2 所示。
 
 ![img](assets/db5fe492e30a7b598a3eb578ad5256fd.png)
 
 图 2 Group 概念示意图
 
-同时,Group 的属性非常重要,包括 Site 站点、Path 虚拟路径、docBase 物理路径、Port 应用端口、HealthCheckUrl 健康检测地址等,这些属性都与部署逻辑息息相关。 **携程之所以这样设计,是因为 group 这个对象直接表示一个应用的一组实例,这样既可以支持单机单应用的部署架构,也可以支持单机多应用的情况。**  **DeploymentConfig** ,即发布配置,提供给用户的可修改配置项要通俗易懂,包括:单个批次可拉出上限比、批次间等待时间、应用启动超时时间、是否忽略点火。 **Deployment** ,即一个发布实体,主要包括 Group 集群、DeploymentConfig 发布配置、Package 发布包、发布时间、批次、状态等等信息。 **DeploymentBatch** ,即发布批次,通常发布系统选取一台服务器作为堡垒批次,集群里的其他服务器会按照用户设置的单个批次可拉出上限比划分成多个批次,必须先完成堡垒批次的发布和验证,才能继续其他批次的发布。 **DeploymentTarget** ,即发布目标服务器或实例,它与该应用的 Server 列表中的对象为一对一的关系,包括主机名、IP 地址、发布状态信息。 **这里一定要注意,发布系统的对象模型和你所采用的部署架构有很大关系。** 比如,携程发布系统的设计中并没有 pool 这个对象,而很多其他企业却采用 pool 实现对实例的管理。又比如,在针对 Kubernetes 时,我们也需要根据它的特性,针对性地处理 Set 对象等等。
+同时,Group 的属性非常重要,包括 Site 站点、Path 虚拟路径、docBase 物理路径、Port 应用端口、HealthCheckUrl 健康检测地址等,这些属性都与部署逻辑息息相关。**携程之所以这样设计,是因为 group 这个对象直接表示一个应用的一组实例,这样既可以支持单机单应用的部署架构,也可以支持单机多应用的情况。**  **DeploymentConfig**,即发布配置,提供给用户的可修改配置项要通俗易懂,包括:单个批次可拉出上限比、批次间等待时间、应用启动超时时间、是否忽略点火。**Deployment**,即一个发布实体,主要包括 Group 集群、DeploymentConfig 发布配置、Package 发布包、发布时间、批次、状态等等信息。**DeploymentBatch**,即发布批次,通常发布系统选取一台服务器作为堡垒批次,集群里的其他服务器会按照用户设置的单个批次可拉出上限比划分成多个批次,必须先完成堡垒批次的发布和验证,才能继续其他批次的发布。**DeploymentTarget**,即发布目标服务器或实例,它与该应用的 Server 列表中的对象为一对一的关系,包括主机名、IP 地址、发布状态信息。**这里一定要注意,发布系统的对象模型和你所采用的部署架构有很大关系。** 比如,携程发布系统的设计中并没有 pool 这个对象,而很多其他企业却采用 pool 实现对实例的管理。又比如,在针对 Kubernetes 时,我们也需要根据它的特性,针对性地处理 Set 对象等等。
 
 ## 发布流程及状态流转
 
@@ -56,7 +56,7 @@ Roll Engine,即发布引擎,主要负责创建发布批次,按批次粒度
 
 那么,我们就一起来看看整个状态流转如何通过状态机进行控制: **首先,借助于 Celery 分布式任务队列的 Chain 函数,发布系统将上述的 Markdown、Download、Install、Verify、Markup 五个阶段定义为一个完整的链式任务工作流,保证一个 Chain 函数里的子任务会依次执行。**  **其次,每个子任务执行成功或失败,都会将 DeploymentTarget 设置为对应的发布状态。** 例如,堡垒批次中的 DeploymentTarget 执行到 Verify 点火这个任务时,如果点火验证成功,那么 DeploymentTarget 会被置为 VERIFY_SUCCESS(点火成功)的状态,否则置为 VERIFY_FAILURE(点火失败)的状态。
 
-发布过程中,如果有任意一台 DeploymentTarget 发布失败,都会被认为是发布局部失败,允许用户重试发布。因此,重试发布只针对于有失败的服务器批次进行重试,对于该批次中已经发布成功的服务器,发布系统会对比当前运行版本与发布目标版本是否一致,如果一致且点火验证通过的话,则直接跳过。 **这里需要注意的是,** 堡垒批次不同于其他批次:堡垒批次中 DeploymentTarget 的 Chain 的最后一个子任务是 Verify 点火,而不是 Markup。 **再次,点火验证成功,DeploymentTarget 的状态流转到 VERIFY_SUCCESS 后,需要用户在发布系统页面上触发 Baking 操作** ,即堡垒批次中 DeploymentTarget 的 Markup,此时执行的是一个独立的任务事务,会将堡垒批次中的服务器拉入集群,接入生产流量。也就是说,这部分是由用户触发而非自动拉入。BAKE_SUCCESS 堡垒拉入成功之后,就是其他批次的 RollingOut 事务了,这也是一个独立的任务,需要由用户触发其他批次开始发布的操作。 **最后,设置发布批次。** 除堡垒批次外,其他的机器会按照用户自主设置的最大拉出比来分批,每个批次间允许用户设置等待时间,或者由用户手动执行启动下个批次发布的操作。从第 3 个批次起,允许用户设置较短的或者不设置等待批次的间隔时间,以提高最后几个批次的速度,即尾单加速,这样可以提高整个发布过程的效率。
+发布过程中,如果有任意一台 DeploymentTarget 发布失败,都会被认为是发布局部失败,允许用户重试发布。因此,重试发布只针对于有失败的服务器批次进行重试,对于该批次中已经发布成功的服务器,发布系统会对比当前运行版本与发布目标版本是否一致,如果一致且点火验证通过的话,则直接跳过。**这里需要注意的是,** 堡垒批次不同于其他批次:堡垒批次中 DeploymentTarget 的 Chain 的最后一个子任务是 Verify 点火,而不是 Markup。**再次,点火验证成功,DeploymentTarget 的状态流转到 VERIFY_SUCCESS 后,需要用户在发布系统页面上触发 Baking 操作**,即堡垒批次中 DeploymentTarget 的 Markup,此时执行的是一个独立的任务事务,会将堡垒批次中的服务器拉入集群,接入生产流量。也就是说,这部分是由用户触发而非自动拉入。BAKE_SUCCESS 堡垒拉入成功之后,就是其他批次的 RollingOut 事务了,这也是一个独立的任务,需要由用户触发其他批次开始发布的操作。**最后,设置发布批次。** 除堡垒批次外,其他的机器会按照用户自主设置的最大拉出比来分批,每个批次间允许用户设置等待时间,或者由用户手动执行启动下个批次发布的操作。从第 3 个批次起,允许用户设置较短的或者不设置等待批次的间隔时间,以提高最后几个批次的速度,即尾单加速,这样可以提高整个发布过程的效率。
 
 携程的发布系统,利用了一个分布式异步任务框架来处理整个发布过程的事务,然后通过状态机来控制这些任务的开始和停止。当然,由于我们使用 Python 语言,所以选择了 Celery 框架,其他语言也有很多成熟的类似框架,也建议你在实施过程中,充分利用这些框架的优势。
 
@@ -86,7 +86,7 @@ Roll Engine,即发布引擎,主要负责创建发布批次,按批次粒度
 
 对外部系统的元数据依赖,例如从 CMDB 同步 Group 信息的场景下,发布系统可以使用 Redis 锁合并重复的请求,提高同步数据的吞吐能力,以解决重试并发的问题。另外,由于发布系统做了数据缓存,也就同时具备了一键降级 CMDB 等其他外部系统依赖的能力。
 
-降级机制能够保证在突发异常情况下,发布系统可以解除所有外部依赖,独立完成任何发布应用的任务。也就是说, **降级机制可以保证发布系统做到,只有部署包存在,就能恢复服务。**
+降级机制能够保证在突发异常情况下,发布系统可以解除所有外部依赖,独立完成任何发布应用的任务。也就是说,**降级机制可以保证发布系统做到,只有部署包存在,就能恢复服务。**
 
 ## 总结
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25423\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25423\350\256\262.md"
index ce2efdf55..8ad647cbc 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25423\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25423\350\256\262.md"	
@@ -25,7 +25,7 @@
 
 另外,单机单应用在故障排除、配置管理等方面同样具有很多优势。一言以蔽之,简单的才是最好的。
 
-当然, **简单直接,也正是发布系统所希望看到的情况** 。
+当然,**简单直接,也正是发布系统所希望看到的情况** 。
 
 ## 增量发布还是全量发布?
 
@@ -37,7 +37,7 @@
 
 比如,增量发布对回滚非常不友好,你很难确定增量发布前的具体内容是什么。如果你真的要确定这些具体内容的话,就要做全版本的差异记录,获取每个版本和其他版本间的差异,这是一个巨大的笛卡尔积,需要耗费大量的计算资源,简直就是得不偿失。很显然,这是一个不可接受的方案。
 
-## 反之,全量发布就简单多了,每个代码包只针对一个版本,清晰明了,回滚也非常简单。所以, **我的建议是,全量发布是互联网应用发布的最好方式。** 如何控制服务的 Markup 和 Markdown?
+## 反之,全量发布就简单多了,每个代码包只针对一个版本,清晰明了,回滚也非常简单。所以,**我的建议是,全量发布是互联网应用发布的最好方式。** 如何控制服务的 Markup 和 Markdown?
 
 首先,你需要明确一件事儿,除了发布系统外,还有其他角色会对服务进行 Markup 和 Markdown 操作。比如,运维人员在进行机器检修时,人为的 Markdown 操作。因此,我们需要从发布系统上能够清晰地知晓服务的当前状态,和最后进行的操作。
 
@@ -45,7 +45,7 @@
 
 比如,发布系统如果发现服务最后进行的操作是 Markdown,那么还能不能继续发布呢?如果发布,那发布之后需不需要执行 Markup 操作呢?有些情况下,用户希望利用发布来修复服务,因此需要在发布之后执行 Markup;而有些情况下,用户发布后不能执行 Markup,很可能运维人员正在维护网络。
 
-为了解决这个问题, **携程在设计系统时,用不同的标志位来标识发布系统、运维操作、健康检测,以及服务负责人对服务的 Markup 和 Markdown 状态。4 个标志位的任何一个为 Markdown 都代表暂停服务,只有所有标志位都是 Markup 时,服务中心才会向外暴露这个服务实例。**
+为了解决这个问题,**携程在设计系统时,用不同的标志位来标识发布系统、运维操作、健康检测,以及服务负责人对服务的 Markup 和 Markdown 状态。4 个标志位的任何一个为 Markdown 都代表暂停服务,只有所有标志位都是 Markup 时,服务中心才会向外暴露这个服务实例。**
 
 这样做的好处是,将 4 种角色对服务的操作完全解耦,他们只需要关心自己的业务逻辑,既不会发生冲突,也不会影响事务完整性,更无需采用其他复杂的锁和 Token 机制进行排他操作。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25424\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25424\350\256\262.md"
index 9cdd5d8f5..bc6f29a8c 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25424\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25424\350\256\262.md"	
@@ -37,7 +37,7 @@
 
 ### 第二,网络监控
 
-网络是整个系统通路的保障。因为大型生产网络配置的复杂度通常比较高,以及系统网络架构的约束,所以网络监控一般比较难做。 **一般情况下,从持续交付的角度来说,网络监控并不需要做到太细致和太深入,因为大多数网络问题最终也会表现为其他应用层面的故障问题。但是,如果你的诉求是要快速定位 root cause,那就需要花费比较大的精力去做好网络监控了。** 网络监控,大致可以分为两大部分:
+网络是整个系统通路的保障。因为大型生产网络配置的复杂度通常比较高,以及系统网络架构的约束,所以网络监控一般比较难做。**一般情况下,从持续交付的角度来说,网络监控并不需要做到太细致和太深入,因为大多数网络问题最终也会表现为其他应用层面的故障问题。但是,如果你的诉求是要快速定位 root cause,那就需要花费比较大的精力去做好网络监控了。** 网络监控,大致可以分为两大部分:
 
 1. 公网监控。这部分监控,可以利用模拟请求的手段(比如,CDN 节点模拟、用户端模拟),获取对 CDN、DNS 等公网资源,以及网络延时等监控的数据。当然,你也可以通过采样的方式获取这部分数据。
 1. 内网监控。这部分监控,主要是对机房内部核心交换机数据和路由数据的监控。如果你能打造全局的视图,形成直观的路由拓扑,可以大幅提升监控效率。
@@ -46,7 +46,7 @@
 
 如果你的业务具有连续性,业务量达到一定数量后呈现比较稳定的变化趋势,那么你就可以利用业务指标来进行监控了。一般情况下,单位时间内的订单预测线,是最好的业务监控指标。
 
-任何的系统故障或问题,影响最大的就是业务指标,而一般企业最重要的业务指标就是订单和支付。因此, **监控企业的核心业务指标,能够以最快的速度反应系统是否稳定。** 反之,如果系统故障或问题并不影响核心业务指标,那么也就不太会造成特别严重的后果,监控的优先级和力度也就没有那么重要。
+任何的系统故障或问题,影响最大的就是业务指标,而一般企业最重要的业务指标就是订单和支付。因此,**监控企业的核心业务指标,能够以最快的速度反应系统是否稳定。** 反之,如果系统故障或问题并不影响核心业务指标,那么也就不太会造成特别严重的后果,监控的优先级和力度也就没有那么重要。
 
 当然,核心业务指标是需要经验去细心挑选的。不同业务的指标不同,而即使定义了指标,如何准确、高效地收集这些指标也是一个很重要的技术问题。比如,能不能做到实时,能不能做到预测。这些问题都需要获得技术的有力支持。
 
@@ -54,7 +54,7 @@
 
 分布式系统下,应用监控除了要解决常规的单个应用本身的监控问题外,还需要解决分布式系统,特别是微服务架构下,服务与服务之间的调用关系、速度和结果等监控问题。因此,应用监控一般也被叫作调用链监控。
 
-调用链监控一般需要收集应用层全量的数据进行分析,要分析的内容包括:调用量、响应时长、错误量等;面向的系统包括:应用、中间件、缓存、数据库、存储等;同时也支持对 JVM 等的监控。 **调用链监控系统,一般采用在框架层面统一定义的方式,以做到数据采集对业务开发透明,但同时也需要允许开发人员自定义埋点监控某些代码片段。** 另外,除了调用链监控,不要忘了最传统的应用日志监控。将应用日志有效地联合,并进行分析,也可以起到同样的应用监控作用,但其粒度和精准度比中间件采集方式要弱得多。
+调用链监控一般需要收集应用层全量的数据进行分析,要分析的内容包括:调用量、响应时长、错误量等;面向的系统包括:应用、中间件、缓存、数据库、存储等;同时也支持对 JVM 等的监控。**调用链监控系统,一般采用在框架层面统一定义的方式,以做到数据采集对业务开发透明,但同时也需要允许开发人员自定义埋点监控某些代码片段。** 另外,除了调用链监控,不要忘了最传统的应用日志监控。将应用日志有效地联合,并进行分析,也可以起到同样的应用监控作用,但其粒度和精准度比中间件采集方式要弱得多。
 
 所以,我的建议是利用中间件作为调用链监控的基础,如果不具备中间件的能力,则可以采用日志监控的方式。
 
@@ -98,13 +98,13 @@
 
 如果生产发布过程本身就是一个灰度发布过程的话,那么你基本就没有必要进行延时监控了。
 
-但是,如果整个灰度过程本身耗时并不长的话,我的建议是要进行一定时间的延时监控。我们通常认为,发布完成 30 分钟以后的异常,都属于运行时异常。所以, **我建议的发布后监控时间为 30 分钟。** ### 第三个问题,如何确定异常是由我的发布引起的?
+但是,如果整个灰度过程本身耗时并不长的话,我的建议是要进行一定时间的延时监控。我们通常认为,发布完成 30 分钟以后的异常,都属于运行时异常。所以,**我建议的发布后监控时间为 30 分钟。** ### 第三个问题,如何确定异常是由我的发布引起的?
 
 具备了持续部署能力之后,你最直观的感受就是发布频次变高了。
 
 以携程为例,我们每天的生产发布频次超过 2000 次,如果算上测试环境的发布,则要达到 1 万次左右。如此高频率的发布,我怎么确定某个异常是由我这次的发布引起的呢?而且除了发布,还同时进行着各类运维变更操作,我怎么确定某个异常是发布造成的,而不是变更造成的呢?
 
-解决这个问题, **你需要建立一套完整的运维事件记录体系,并将发布纳入其中,记录所有的运维事件。当有异常情况时,你可以根据时间线进行相关性分析。**
+解决这个问题,**你需要建立一套完整的运维事件记录体系,并将发布纳入其中,记录所有的运维事件。当有异常情况时,你可以根据时间线进行相关性分析。**
 
 那么,如何构建一套完整的运维事件记录体系呢?很简单,你可以通过消息总线的形式去解决这个问题。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25425\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25425\350\256\262.md"
index 2091c469d..0d4937d42 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25425\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25425\350\256\262.md"	
@@ -24,7 +24,7 @@
 
 看到这个统计结果,相信你已经蠢蠢欲动,准备好好执行代码静态检查了,这也是为什么我们要做代码静态检查的原因。
 
-但是,代码静态检查规则的建立往往需要大量的时间沉淀和技术积累,因此对初学者来说, **挑选合适的静态代码分析工具,自动化执行代码检查和分析,可以极大地提高代码静态检查的可靠性,节省测试成本。**
+但是,代码静态检查规则的建立往往需要大量的时间沉淀和技术积累,因此对初学者来说,**挑选合适的静态代码分析工具,自动化执行代码检查和分析,可以极大地提高代码静态检查的可靠性,节省测试成本。**
 
 ## 静态检查工具的优势
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25426\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25426\350\256\262.md"
index 35c20220e..f31919b52 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25426\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25426\350\256\262.md"	
@@ -25,7 +25,7 @@
 
 这里需要注意的是,我们需要使用的测试手段必须是有效的。为什么这样说呢,有两点原因。
 
-**第一,破坏性测试的手段和过程,并不是无的放矢,它们是被严格设计和执行的** 。不要把破坏性测试和探索性测试混为一谈。也就是说,破坏性测试不应该出现,“试试这样会不会出问题”的假设,而且检验破坏性测试的结果也都应该是有预期的。 **第二,破坏性测试,会产生切实的破坏作用,你需要权衡破坏的量和度** 。因为破坏不仅仅会破坏软件,还可能会破坏硬件。通常情况下,软件被破坏后的修复成本不会太大,而硬件部分被破坏后,修复成本就不好说了。所以,你必须要事先考虑好破坏的量和度。
+**第一,破坏性测试的手段和过程,并不是无的放矢,它们是被严格设计和执行的** 。不要把破坏性测试和探索性测试混为一谈。也就是说,破坏性测试不应该出现,“试试这样会不会出问题”的假设,而且检验破坏性测试的结果也都应该是有预期的。**第二,破坏性测试,会产生切实的破坏作用,你需要权衡破坏的量和度** 。因为破坏不仅仅会破坏软件,还可能会破坏硬件。通常情况下,软件被破坏后的修复成本不会太大,而硬件部分被破坏后,修复成本就不好说了。所以,你必须要事先考虑好破坏的量和度。
 
 ## 破坏性测试的流程与用例设计
 
@@ -38,7 +38,7 @@
 所以,在设计破坏性测试的测试用例时,我们通常会考虑两个维度: **第一个维度是,一个破坏点的具体测试,即设计一个或一组操作,能够导致应用或系统奔溃或异常** 。此时,你需要注意两个问题:
 
 1. 出现问题后的系统或软件是否有能力按预期捕获和处理异常;
-1. 确认被破坏的系统是否有能力按照预期设计进行必要的修复,以确保能够继续处理后续内容。 **第二个维度是,整个系统的破坏性测试** ,我们通常会采用压力测试、暴力测试、阻断链路去除外部依赖等方法,试图找到需要进行破坏性测试的具体的点。
+1. 确认被破坏的系统是否有能力按照预期设计进行必要的修复,以确保能够继续处理后续内容。**第二个维度是,整个系统的破坏性测试**,我们通常会采用压力测试、暴力测试、阻断链路去除外部依赖等方法,试图找到需要进行破坏性测试的具体的点。
 
 这两个维度的测试方法、流程基本一致,区别只是第二维度的测试通常不知道具体要测试的点,所以破坏范围会更大,甚至可能破坏整个系统。
 
@@ -48,7 +48,7 @@
 
 一般情况下,在发布前执行破坏性测试相对比较安全。但这也不是绝对的,比如你一不小心把 UAT 等大型联调环境搞坏了,其代价还是很可观的。
 
-因此, **绝大部分破坏性测试都会在单元测试、功能测试阶段执行。而执行测试的环境也往往是局部的测试子环境。**
+因此,**绝大部分破坏性测试都会在单元测试、功能测试阶段执行。而执行测试的环境也往往是局部的测试子环境。**
 
 那么问题又来了,真实环境要比测试子环境更复杂多变,在测试子环境进行的破坏性测试真的有效吗?这真是一个极好的问题。
 
@@ -87,10 +87,10 @@
 
 混沌工程也有几个高级原则:
 
-1. **使用改变现实世界的事件** ,就是要在真实的场景中进行实验,而不要想象和构造一些假想和假设的场景;
-   2. **在生产环境运行** ,为了发现真实场景的弱点,所以更建议在生产环境运行这些实验;
-   3. **自动化连续实现** ,人工的手工操作是劳动密集型的、不可持续的,因此要把混沌工程自动化构建到系统中;
-   4. **最小爆破半径** ,与第二条配合,要尽量减少对用户的负面影响,即使不可避免,也要尽力减少范围和程度。
+1. **使用改变现实世界的事件**,就是要在真实的场景中进行实验,而不要想象和构造一些假想和假设的场景;
+   2. **在生产环境运行**,为了发现真实场景的弱点,所以更建议在生产环境运行这些实验;
+   3. **自动化连续实现**,人工的手工操作是劳动密集型的、不可持续的,因此要把混沌工程自动化构建到系统中;
+   4. **最小爆破半径**,与第二条配合,要尽量减少对用户的负面影响,即使不可避免,也要尽力减少范围和程度。
 
 这样,就更符合持续交付的需求和胃口了。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25427\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25427\350\256\262.md"
index 3f2e1b4f3..7a6e68040 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25427\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25427\350\256\262.md"	
@@ -32,7 +32,7 @@
 1. 集成测试时,如何保证依赖服务的稳定性,或者说排除由稳定性带来的干扰,所以到底是依赖服务的问题,还是被测服务的问题很难确定;
 1. 真实的业务系统中,往往还存在多层依赖的问题,你还要想办法解决被测应用依赖的服务的依赖服务。
 
-我的天呢,“这座大山”简直难以翻越。 **“第三座大山”:测试用例的高度仿真。** 如何才能模拟出和用户一样的场景,一直困扰着我们。
+我的天呢,“这座大山”简直难以翻越。**“第三座大山”:测试用例的高度仿真。** 如何才能模拟出和用户一样的场景,一直困扰着我们。
 
 如果我们的回归测试不是自己设计的假想用例,而是真实用户在生产环境中曾经发生过的实际用例的话,那么肯定可以取得更好的回归测试效果。那么,有没有什么办法或技术能够帮助我们做到这一点呢?
 
@@ -99,7 +99,7 @@ Mockito 或者 EasyMock 这两个框架的实现原理,都是在运行时,
 Mock 技术,通过模拟,绕过了实际的数据调用和服务调用问题,横在我们面前的“三座大山”中的其中两座,测试数据的准备和清理、分布式系统的依赖算是铲平了。但是如何解决“第三座大山”呢,即如何做到模拟用户真正的操作行为呢?
 
 两大利器之二“回放”技术
------------- **要做到和实际用户操作一致,最好的方法就是记录实际用户在生产环境的操作,然后在测试环境中回放。** 当然,我们要记录的并不是用户在客户端的操作过程,而是用户产生的最终请求。这样做,我们就能规避掉客户端产生的干扰,直接对功能进行测试了。 **首先,我们一起来看一下如何把用户的请求记录下来。**
+------------ **要做到和实际用户操作一致,最好的方法就是记录实际用户在生产环境的操作,然后在测试环境中回放。** 当然,我们要记录的并不是用户在客户端的操作过程,而是用户产生的最终请求。这样做,我们就能规避掉客户端产生的干扰,直接对功能进行测试了。**首先,我们一起来看一下如何把用户的请求记录下来。**
 
 这里我们需要明确一个前提原则,即:我们并不需要记录所有用户的请求,只要抽样即可,这样既可以保持用例的新鲜度,又可以减少成本。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25428\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25428\350\256\262.md"
index 16b0b96c7..56157c8b5 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25428\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25428\350\256\262.md"	
@@ -22,7 +22,7 @@
 
 之后,这种平台化的设计和制造方法,在航空制造业和汽车制造业得到了广泛运用,获得了极大的成功,并一直被沿用至今。
 
-而, **互联网又再次给“平台化”插上了新的翅膀。互联网厂商平台化的玩法,往往是指自己搭台子,让其他人唱戏** 。也就是说,由互联网厂商自己提供一些基础保障能力,建立必要的标准,从而形成底层支撑平台;而由其他供应商或用户利用这个底层平台提供的服务,自己完成具体业务、功能流程设计,从而达到千人千面的个性化服务能力。
+而,**互联网又再次给“平台化”插上了新的翅膀。互联网厂商平台化的玩法,往往是指自己搭台子,让其他人唱戏** 。也就是说,由互联网厂商自己提供一些基础保障能力,建立必要的标准,从而形成底层支撑平台;而由其他供应商或用户利用这个底层平台提供的服务,自己完成具体业务、功能流程设计,从而达到千人千面的个性化服务能力。
 
 互联网厂商的这种做法,就使得企业的服务能力被放大到了极致。
 
@@ -78,7 +78,7 @@
 
 正如我在第一篇文章\[《持续交付到底有什么价值》\]中所说,并不是只有完整的端到端自动化才叫“持续交付”,代码管理,集成编译,环境管理、发布部署这四大核心模块,其实就是一个交付的闭环,只是交付的内容不同,但这些交付都是可测的、可评定的,所以并不是半成品。
 
-因此, **我们就可以考虑挑选最为重要或最为急迫的模块,优先加以实施。甚至,你可以优先实现这四个模块中的一个,先解决一部分问题。这样做减法的方式,我们称为横向缩小范围。**  **另外一种做减法的方式是减少纵向的深度** 。也就是优先支持单一的技术栈,或特定的、比较简单的场景,比如先搞定组织内的单体应用。
+因此,**我们就可以考虑挑选最为重要或最为急迫的模块,优先加以实施。甚至,你可以优先实现这四个模块中的一个,先解决一部分问题。这样做减法的方式,我们称为横向缩小范围。**  **另外一种做减法的方式是减少纵向的深度** 。也就是优先支持单一的技术栈,或特定的、比较简单的场景,比如先搞定组织内的单体应用。
 
 通过做减法先完成这个平台最核心模块的方式,可以控制平台的初建成本,而且效果也比较容易预期。比如,携程就是优先完成了发布部署模块,再逐步向持续交付的上游拓展。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25429\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25429\350\256\262.md"
index 6b241828d..ef9e8ed2b 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25429\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25429\350\256\262.md"	
@@ -21,9 +21,9 @@
 
 在《持续交付:发布可靠软件的系统方法》一书中,作者给我们列举了几个反模式,比如:
 
-1. **手工部署软件** ,即由详尽的文档描述一个部署过程,部署需要手工操作及验证;
-   2. **开发完之后才向类生产环境部署** ,即开发完成后才第一次向类生产环境部署,如果是全新的应用,第一次部署往往会失败;
-   3. **生产环境需要手工配置管理** ,即有专门的团队负责生产环境的配置变更,修改配置时,需要这个专门的团队手工登录到服务器进行操作。
+1. **手工部署软件**,即由详尽的文档描述一个部署过程,部署需要手工操作及验证;
+   2. **开发完之后才向类生产环境部署**,即开发完成后才第一次向类生产环境部署,如果是全新的应用,第一次部署往往会失败;
+   3. **生产环境需要手工配置管理**,即有专门的团队负责生产环境的配置变更,修改配置时,需要这个专门的团队手工登录到服务器进行操作。
 
 你可以按照我在发布及监控这个系列分享的内容,通过合理打造一套发布系统,解决“手工部署软件”这个反模式的问题。
 
@@ -52,7 +52,7 @@
 
 ## 重塑持续交付平台的相关部分
 
-有了云计算,或者说私有 IaaS 平台这个强大的底层支持,我们下一步要解决的就是充分发挥它的能力。所以,现在我就和你分享一下,持续交付平台的哪些部分可以利用云计算的强大能力。 **首先,弹性的集成编译环境。**
+有了云计算,或者说私有 IaaS 平台这个强大的底层支持,我们下一步要解决的就是充分发挥它的能力。所以,现在我就和你分享一下,持续交付平台的哪些部分可以利用云计算的强大能力。**首先,弹性的集成编译环境。**
 
 不同技术栈的应用需要不同的编译环境,而且要保证编译环境和运行时环境一致,否则会发生意料之外的问题。这样一来,如果组织内部同时有多个技术栈存在,或应用对环境有多种要求时,就需要有多个独立的编译环境了。
 
@@ -73,11 +73,11 @@
 
 比如,原先一个测试子环境的生命周期往往与某个功能研发或是项目研发不一致,会提前准备,或是多次复用;又或者由于资源紧缺的原因,测试环境只能模拟部分实际环境;另外还会有一些环境被作为公共的资源一直保留,从不释放。这些问题都增加了环境管理的复杂度。
 
-现在,有了云计算平台的强大能力,我们完全可以打破这些限制,将环境的生命周期设计得与项目生命周期一致,每个项目或每个功能都可以拥有自己独立的测试环境;另外,你还可以动态定义所需的任何环境,或者利用模板技术,快速复制一个已存在的环境。总之, **环境管理变得越来越灵活了。** 除了计算资源之外,云计算也同时提供了非常强大的网络定义能力,为环境管理插上了翅膀。
+现在,有了云计算平台的强大能力,我们完全可以打破这些限制,将环境的生命周期设计得与项目生命周期一致,每个项目或每个功能都可以拥有自己独立的测试环境;另外,你还可以动态定义所需的任何环境,或者利用模板技术,快速复制一个已存在的环境。总之,**环境管理变得越来越灵活了。** 除了计算资源之外,云计算也同时提供了非常强大的网络定义能力,为环境管理插上了翅膀。
 
 我们可以通过 VPC(专有网络),对任何环境定义网段划分、路由策略和安全策略等。这样环境与环境之间就拥有了快速处理网络隔离和相通的能力。借此,我们也可以很容易地创造沙箱环境、专用测试环境等。
 
-有了云计算的支持,环境管理真的可以飞起来。 **最后,充分利用存储。**
+有了云计算的支持,环境管理真的可以飞起来。**最后,充分利用存储。**
 
 云计算除了可以提供计算资源和网络资源的便利外,同时也可以解决资源存储的问题。分布式存储的能力,同样能给持续交付提供有利的帮助。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25430\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25430\350\256\262.md"
index e0efc662e..f4a6627f5 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25430\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25430\350\256\262.md"	
@@ -24,7 +24,7 @@
 
 其实上面的这三个问题,也会在真实的业务系统上碰到。所以,我们借鉴了携程业务系统的稳定性评估方案,最后决定采用如下的实施方案:
 
-**首先,我们通过监控、保障、人为记录等手段,统计所有的故障时间** 。需要统计的指标包括:开始时间、结束时间和故障时长。 **然后,计算过去三个月内这个时间段产生的持续交付平均业务量** 。所谓业务量,就是这个时间段内,处理的代码提交、code review;进行的编译、代码扫描、打包;测试部署;环境处理;测试执行和生产发布的数量。 **最后,计算这个时间段内的业务量与月平均量相比的损失率** 。这个损失率,就代表了系统的不稳定性率,反之就是稳定性率了。
+**首先,我们通过监控、保障、人为记录等手段,统计所有的故障时间** 。需要统计的指标包括:开始时间、结束时间和故障时长。**然后,计算过去三个月内这个时间段产生的持续交付平均业务量** 。所谓业务量,就是这个时间段内,处理的代码提交、code review;进行的编译、代码扫描、打包;测试部署;环境处理;测试执行和生产发布的数量。**最后,计算这个时间段内的业务量与月平均量相比的损失率** 。这个损失率,就代表了系统的不稳定性率,反之就是稳定性率了。
 
 这样计算得到的不稳定性率指标,就要比简单粗暴地用宕机时间要精确得多,也不再会遇到前面提到的三种问题。
 
@@ -64,7 +64,7 @@
 
 虽然这种做法和流程没什么问题,但却有违于我们推崇的“谁开发,谁运行”理念,并且也因此增加了一个实际不是必须的工作角色。在这之后,我们改造了这几个团队的流程,相当于是推动了整个公司的持续交付。
 
-这个案例第一次让我们认识到, **我们可以用手上的数据去推动、去优化持续交付体系。**
+这个案例第一次让我们认识到,**我们可以用手上的数据去推动、去优化持续交付体系。**
 
 这三个案例,都充分说明了数据对持续交付、持续交付平台的重要性,所以我们也要善用这些宝贵的数据。接下来,我再和你分享一下,持续交付体系中还有哪些数据值得我们关注。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25431\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25431\350\256\262.md"
index 86fb2d5ae..dba6cd704 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25431\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25431\350\256\262.md"	
@@ -22,7 +22,7 @@
 
 对于移动 App 来说,业界流行的做法是采用“分支开发,主干发布”的方式,并且采用交付快车的方式进行持续的版本发布。
 
-关于这种代码管理方式,我会在下一篇文章《细谈移动 App 的持续交付流水线(pipline)》中进行详细介绍。 **其次,移动 App 的开发已经走向了组件化,所以也需要处理好依赖管理的问题** 。
+关于这种代码管理方式,我会在下一篇文章《细谈移动 App 的持续交付流水线(pipline)》中进行详细介绍。**其次,移动 App 的开发已经走向了组件化,所以也需要处理好依赖管理的问题** 。
 
 移动端的技术栈往往要比统一技术栈的后端服务更复杂,所以在考虑依赖管理时,我们需要多方位地为多种技术栈做好准备。比如:
 
@@ -36,7 +36,7 @@
 
 ## 项目信息管理
 
-项目信息管理主要包括版本信息管理和功能信息管理这两大方面。 **对于移动 App 的持续交付来说,我们特别需要维护版本的相关信息,并对每个版本进行管理** 。
+项目信息管理主要包括版本信息管理和功能信息管理这两大方面。**对于移动 App 的持续交付来说,我们特别需要维护版本的相关信息,并对每个版本进行管理** 。
 
 对后端服务来说,它只要做到向前兼容,就可以一直以最新版本的形式进行发布;而且,它的发布相对自主,控制权比较大。
 
@@ -44,7 +44,7 @@
 
 所以,在移动 App 的持续交付中,我们需要管理好每个版本的相关信息。
 
-另外,为了提高移动 App 的构建和研发效率,我们会把整个项目拆分多个子项目,而主要的拆分依据就是功能模块。也就是说,除了从技术角度来看,移动 App 的持续交付会存在依赖管理的内容外,从项目角度来看,也常常会存在功能依赖和功能集成的需要。所以, **为了项目的协调和沟通,我们需要重点管理每个功能的信息。**
+另外,为了提高移动 App 的构建和研发效率,我们会把整个项目拆分多个子项目,而主要的拆分依据就是功能模块。也就是说,除了从技术角度来看,移动 App 的持续交付会存在依赖管理的内容外,从项目角度来看,也常常会存在功能依赖和功能集成的需要。所以,**为了项目的协调和沟通,我们需要重点管理每个功能的信息。**
 
 可见,做好项目信息管理在移动 App 的持续交付中尤为重要,而在后端服务的持续交付中却没那么受重视了,这也是移动 App 的持续交付体系与服务端的一大不同点。以携程或美团点评为例,它们都各自研发了 MCD 或 MCI 平台,以求更好地管理项目信息。
 
@@ -64,13 +64,13 @@
 
 移动 App 和后端服务的持续交付体系,在构建管理上的不同点,主要体现在以下三个方面:
 
-1. **你需要准备 Android 和 iOS 两套构建环境** ,而且 iOS 的构建环境还需要一套独立的管理方案。因为,iOS 的构建环境,你不能直接使用 Linux 虚拟机处理,而是要采用 Apple 公司的专用设备。
-   2. **在整个构建过程中,你还要考虑证书的管理** ,不同的版本或使用场景需要使用不同的证书。如果证书比较多的话,还会涉及到管理的逻辑问题,很多组织都会选择自行开发证书管理服务。
+1. **你需要准备 Android 和 iOS 两套构建环境**,而且 iOS 的构建环境还需要一套独立的管理方案。因为,iOS 的构建环境,你不能直接使用 Linux 虚拟机处理,而是要采用 Apple 公司的专用设备。
+   2. **在整个构建过程中,你还要考虑证书的管理**,不同的版本或使用场景需要使用不同的证书。如果证书比较多的话,还会涉及到管理的逻辑问题,很多组织都会选择自行开发证书管理服务。
    3. **为了解决组件依赖的问题,你需要特别准备独立的中央组件仓库,并用缓存等机制加快依赖组件下载的速度** 。其实,这一点会和后端服务比较相像。
 
 ## 发布管理
 
-移动 App 的发布管理,和后端服务相比,相差就比较大了。 **首先,移动 App 无法做到强制更新,决定权在终端用户** 。移动 App 的发布,你所能控制的只是将新版本发布到市场而已,而最终是否更新新版本,使得新版本的功能起效,则完全取决于用户。这与后端服务强制更新的做法完全不同。 **其次,移动 App 在正式发布到市场前,会进行时间比较长的内测或公测** 。这些测试会使用类似 Fabric Beta 或者 TestFlight 这样的 Beta 测试平台,使部分用户优先使用,完成灰度测试;或者在公司内部搭建一个虚拟市场,利用内部资源优先完成内测。而且,这个测试周期往往都比较长,其中也会迭代多个版本。 **最后,移动 App 的分发渠道比较多样** 。还可能会利用一些特殊的渠道进行发布。为了应对不同的渠道的需求,比如标准渠道版本,控制部分内容,一些字样的显示等等。在完成基本的构建和打包之后,还需要做一些额外的配置替换、增删改查的动作。比如,更新渠道配置和说明等。
+移动 App 的发布管理,和后端服务相比,相差就比较大了。**首先,移动 App 无法做到强制更新,决定权在终端用户** 。移动 App 的发布,你所能控制的只是将新版本发布到市场而已,而最终是否更新新版本,使得新版本的功能起效,则完全取决于用户。这与后端服务强制更新的做法完全不同。**其次,移动 App 在正式发布到市场前,会进行时间比较长的内测或公测** 。这些测试会使用类似 Fabric Beta 或者 TestFlight 这样的 Beta 测试平台,使部分用户优先使用,完成灰度测试;或者在公司内部搭建一个虚拟市场,利用内部资源优先完成内测。而且,这个测试周期往往都比较长,其中也会迭代多个版本。**最后,移动 App 的分发渠道比较多样** 。还可能会利用一些特殊的渠道进行发布。为了应对不同的渠道的需求,比如标准渠道版本,控制部分内容,一些字样的显示等等。在完成基本的构建和打包之后,还需要做一些额外的配置替换、增删改查的动作。比如,更新渠道配置和说明等。
 
 以上这些因素,就决定了移动 App 与后端服务的发布管理完全不同。关于移动 App 的发布,我会在下一篇文章《细谈移动 App 的持续交付流水线(pipeline)》中进行详细介绍。
 
@@ -90,14 +90,14 @@
 
 但是,我们也无法避免 Bug。所以,对移动 App 来说,我们就要通过特定的热修复技术,做到在用户不重新安装客户端的前提下,就可以修复 Bug。这也就是我所说的热修复。
 
-关于热修复,比如 **Android 系统** ,主要的方式就是以下两步:
+关于热修复,比如 **Android 系统**,主要的方式就是以下两步:
 
 1. 下发补丁(内含修复好的 class)到用户手机;
 1. App 通过类加载器,调用补丁中的类。
 
 其实现原理,主要是利用了 Android 的类加载机制,即从 DexPathList 对象的 Element 数组中获取对应的类进行加载,而获取的方式则是遍历。也就是说,我们只需要把修复的类放置在这个 Element 数组的第一位就可以保证加载到新的类了,而此时有 Bug 的类其实还是存在的,只是不会被加载到而已。
 
-当然技术发展到今天,我们已经无需重复造轮子了,完全可以利用一些大厂开放的方案和平台完成热修复。比如,百川的 hHotFix、美团 Robust、手机 QQ 空间、微信 Tinker,都是很好的方案。 **iOS 系统方面** ,Apple 公司一直对热修复抓的比较严。但是,从 iOS7 之后,iOS 系统引入了 JavaScriptCore,这样就可以在 Objective-C 和 JavaScript 之间传递值或对象了,从而使得创建混合对象成为了可能。因此,业界产生了一些成熟的热修复方案。比如:
+当然技术发展到今天,我们已经无需重复造轮子了,完全可以利用一些大厂开放的方案和平台完成热修复。比如,百川的 hHotFix、美团 Robust、手机 QQ 空间、微信 Tinker,都是很好的方案。**iOS 系统方面**,Apple 公司一直对热修复抓的比较严。但是,从 iOS7 之后,iOS 系统引入了 JavaScriptCore,这样就可以在 Objective-C 和 JavaScript 之间传递值或对象了,从而使得创建混合对象成为了可能。因此,业界产生了一些成熟的热修复方案。比如:
 
 1. [Rollout.io](http://rollout.io/)、JSPatch、DynamicCocoa 这三个方案,只针对 iOS 的热更新。目前,Rollout.io 和 JSPatch 已经实现了平台化,脚本语言用的都是 JavaScript。Rollout.io 除了支持 OC 的热更新外,还支持 Swift。 DynamiCocoa 源自滴滴,目前还没开源,所以我也没怎么体验过。但是,它号称可以通过 OC 编码,自动转换成 JavaScript 脚本,这对编码来说好处多多。
 1. React Native、Weex 这两个方案,都是跨平台的热更新方案。其中,React Native 是由 Facebook 开发的, Weex 是由阿里开发的。就我个人的体验来说,Weex 从语法上更贴近编程思路,而且还实现了平台化,使用起来更加便捷。
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25432\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25432\350\256\262.md"
index 0a78e97bf..b9786cc7a 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25432\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25432\350\256\262.md"	
@@ -28,7 +28,7 @@
 
 关于发布快车还有三个关键点,容易被误解或者疏忽。
 
-**第一个关键点是,并不是说所有开发的功能,都一定要集成到最近的那节车厢、最近的那个版本中** 。任何功能都应该按照既定计划,规划纳入到适合的那节车厢、那个版本中。这也是为什么移动端 App 的持续交付需要良好的信息管理的原因。 **第二个关键点是,我们必须要保证固定间隔的发车时间,每周、每两周都可以,但必须保证每个车厢到点即发** 。只有这样,我们才能保证持续交付流水线的持续运行,以及不间断地产出。这里需要注意的是,对于一些特殊的、不规则的发布,我们要把它们归类到热修复的流程,而不是在发布快车中处理。 **第三个关键点是,这个过程的最终产物是可以发布到市场的版本,而不是发布到用户侧的版本** 。虽然我们把这个发布模式叫作发布快车,但其实它的最终产物是可以发布的待发布版本。所以这个流程完成后的版本没有被正式发布,或出现了部分缺陷无法发布的情况是很正常的,可以被接受。我们并不需要保证每个版本都一定能发布到用户手上。
+**第一个关键点是,并不是说所有开发的功能,都一定要集成到最近的那节车厢、最近的那个版本中** 。任何功能都应该按照既定计划,规划纳入到适合的那节车厢、那个版本中。这也是为什么移动端 App 的持续交付需要良好的信息管理的原因。**第二个关键点是,我们必须要保证固定间隔的发车时间,每周、每两周都可以,但必须保证每个车厢到点即发** 。只有这样,我们才能保证持续交付流水线的持续运行,以及不间断地产出。这里需要注意的是,对于一些特殊的、不规则的发布,我们要把它们归类到热修复的流程,而不是在发布快车中处理。**第三个关键点是,这个过程的最终产物是可以发布到市场的版本,而不是发布到用户侧的版本** 。虽然我们把这个发布模式叫作发布快车,但其实它的最终产物是可以发布的待发布版本。所以这个流程完成后的版本没有被正式发布,或出现了部分缺陷无法发布的情况是很正常的,可以被接受。我们并不需要保证每个版本都一定能发布到用户手上。
 
 发布快车的发布模式,特别是以上说的三个特性,非常符合移动 App 对持续交付的需求,即:分散开发,定期集成,控制发布。所以绝大部分的移动 App 团队,都选择采用发布快车的发布方式。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25433\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25433\350\256\262.md"
index b7e750f13..4521ce6a7 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25433\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25433\350\256\262.md"	
@@ -85,7 +85,7 @@
 
 图 4 二进制交付及集成
 
-使用二进制包的方式,可以帮我们大幅提升移动 App 的编译速度。而且,因为有了中间交付物,我们可以采用与后端服务一样的方式,在本地缓存需要依赖的组件,进一步加速编译过程。 **通过对开发、构建过程的优化,我们已经将原来的交付效率至少提高了 1 倍。** 接下来,我们再一起看看,如何优化测试和发布流程,以求移动 App 的持续交付体系更高效。
+使用二进制包的方式,可以帮我们大幅提升移动 App 的编译速度。而且,因为有了中间交付物,我们可以采用与后端服务一样的方式,在本地缓存需要依赖的组件,进一步加速编译过程。**通过对开发、构建过程的优化,我们已经将原来的交付效率至少提高了 1 倍。** 接下来,我们再一起看看,如何优化测试和发布流程,以求移动 App 的持续交付体系更高效。
 
 ## 如何提升测试效率?
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25434\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25434\350\256\262.md"
index 035200c2d..58c524f29 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25434\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25434\350\256\262.md"	
@@ -100,13 +100,13 @@ App
 
 我们对编译与集成的要求,具体可以概括为以下几点:
 
-首先, **能够同时支持传统的部署包、Docker 镜像,以及移动 App 的编译和集成** 。而且能够在触发编译时自动进行适配支持,这样才能保证各个团队有新项目时无须再进行额外配置。
+首先,**能够同时支持传统的部署包、Docker 镜像,以及移动 App 的编译和集成** 。而且能够在触发编译时自动进行适配支持,这样才能保证各个团队有新项目时无须再进行额外配置。
 
-其次, **所有构建产物及构建历史,都能被有效、永久地记录和存储** 。因为单从传统的编译驱动管理角度看,它以编译任务为基准,需要清除过久、过大的编译任务,从而释放更多的资源用于集成编译。但是,从持续交付的角度看,我们需要完全保留这些内容,用于版本追溯。
+其次,**所有构建产物及构建历史,都能被有效、永久地记录和存储** 。因为单从传统的编译驱动管理角度看,它以编译任务为基准,需要清除过久、过大的编译任务,从而释放更多的资源用于集成编译。但是,从持续交付的角度看,我们需要完全保留这些内容,用于版本追溯。
 
-再次, **各构建产物有自己独立的版本体系,并与代码 commit ID 相关联** 。这是非常重要的,交付产物的版本就是它的唯一标识,任何交付物都可以通过版本进行辨识和追溯。
+再次,**各构建产物有自己独立的版本体系,并与代码 commit ID 相关联** 。这是非常重要的,交付产物的版本就是它的唯一标识,任何交付物都可以通过版本进行辨识和追溯。
 
-最后, **构建通道必须能够支持足够的并发量** 。这也就要求集成构建服务要做到高可用和可扩展,最好能做到资源弹性利用。
+最后,**构建通道必须能够支持足够的并发量** 。这也就要求集成构建服务要做到高可用和可扩展,最好能做到资源弹性利用。
 
 ## 打包与发布相关的需求
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25435\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25435\350\256\262.md"
index fd3690e9f..af45e65cb 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25435\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25435\350\256\262.md"	
@@ -209,7 +209,7 @@ end
 
 图 2 GitLab 官方 HA 方案(引自 GitLab 官网)
 
-我们先看一下社区版的 GitLab,官方提供的 HA 方案的整体架构图可参考图 2。从整体架构上看,PostgreSQL、Redis 这两个模块的高可用,都有通用的解决方案。而 GitLab 在架构上最大的问题是,需要通过文件系统在本地访问仓库文件。于是, **水平扩展时,如何把本地的仓库文件当做数据资源在服务器之间进行读写就变成了一个难题。**
+我们先看一下社区版的 GitLab,官方提供的 HA 方案的整体架构图可参考图 2。从整体架构上看,PostgreSQL、Redis 这两个模块的高可用,都有通用的解决方案。而 GitLab 在架构上最大的问题是,需要通过文件系统在本地访问仓库文件。于是,**水平扩展时,如何把本地的仓库文件当做数据资源在服务器之间进行读写就变成了一个难题。**
 
 官方推荐的方案是通过 NFS 进行多机 Git 仓库共享。但这个方案在实际使用中并不可行,git 本身是 IO 密集型应用,对于真正在性能上有水平扩展诉求的用户来说,NFS 的性能很快就会成为整个系统的瓶颈。我早期在美团点评搭建持续交付体系时,曾尝试过这个方案,当达到几百个仓库的规模时,NFS 就撑不住了。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25436\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25436\350\256\262.md"
index 9237c0823..662e9db97 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25436\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25436\350\256\262.md"	
@@ -161,7 +161,7 @@ node {
 
 ![img](assets/94ce8fb5b4446a1caf9e015e6668f8c8.png)
 
-图 5 GitLab Merge Request **第二个 stage:** 比较好理解,就是执行 Maven 命令对项目编译和打包。 **第三个 stage:**
+图 5 GitLab Merge Request **第二个 stage:** 比较好理解,就是执行 Maven 命令对项目编译和打包。**第三个 stage:**
 
 通过 Maven 调用 Sonar 的静态代码扫描,并在结束后更新 Merge Request 的 commit 状态,使得 Merge Request 允许被合并。同时将单元测试结果展现在 GitLab 上。
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25437\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25437\350\256\262.md"
index 082434672..c03ec359e 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25437\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25437\350\256\262.md"	
@@ -107,7 +107,7 @@ ssh-copy-id [email protected]
 
 1. yum 调用目标机器上的包管理工具完成软件安装 。Ansible 对于不同的 Linux 操作系统包管理进行了封装,在 CentOS 上相当于 yum, 在 Ubuntu 上相当于 APT。
 2.Template 远程文件渲染,可以把本地机器的文件模板渲染后放到远程主机上。
-3.Service 服务管理,同样封装了不同 Linux 操作系统实际执行的 Service 命令。 **通常情况下,我们用脚本的方式使用 Ansible,只要使用好 Inventory 和 PlayBook 这两个组件就可以了,即:使用 PlayBook 编写 Ansible 脚本,然后用 Inventory 维护好需要管理的机器列表** 。这样,就能解决 90% 以上使用 Ansible 的需求。
+3.Service 服务管理,同样封装了不同 Linux 操作系统实际执行的 Service 命令。**通常情况下,我们用脚本的方式使用 Ansible,只要使用好 Inventory 和 PlayBook 这两个组件就可以了,即:使用 PlayBook 编写 Ansible 脚本,然后用 Inventory 维护好需要管理的机器列表** 。这样,就能解决 90% 以上使用 Ansible 的需求。
 
 但如果你有一些更复杂的需求,比如通过代码调用 Ansible,可能还要用到 API 组件。感兴趣的话,你可以参考 Ansible 的官方文档。
 
@@ -115,7 +115,7 @@ ssh-copy-id [email protected]
 
 我先来整理下,针对 Java 后端服务部署的需求:
 
-> 完成 Ansible 的 PlayBook 后,在 Jenkins Pipeline 中调用相关的脚本,从而完成 Java Tomcat 应用的发布。 **首先,在目标机器上安装 Tomcat,并初始化。** 我们可以通过编写 Ansible PlayBook 完成这个操作。一个最简单的 Tomcat 初始化脚本只要十几行代码,但是如果我们要对 Tomcat 进行更复杂的配置,比如修改 Tomcat 的 CATALINA_OPTS 参数,工作量就相当大了,而且还容易出错。
+> 完成 Ansible 的 PlayBook 后,在 Jenkins Pipeline 中调用相关的脚本,从而完成 Java Tomcat 应用的发布。**首先,在目标机器上安装 Tomcat,并初始化。** 我们可以通过编写 Ansible PlayBook 完成这个操作。一个最简单的 Tomcat 初始化脚本只要十几行代码,但是如果我们要对 Tomcat 进行更复杂的配置,比如修改 Tomcat 的 CATALINA_OPTS 参数,工作量就相当大了,而且还容易出错。
 
 在这种情况下,一个更简单的做法是,使用开源第三方的 PlayBook 的复用文件 roles。你可以访问[https://galaxy.Ansible.com](https://galaxy.ansible.com/) ,这里有数千个第三方的 roles 可供使用。
 
@@ -132,7 +132,7 @@ ssh-copy-id [email protected]
 
 你只需要这简单的三行代码,就可以完成 Tomcat 的安装,以及服务注册。与此同时,你只要添加 Tomcat_default_catalina_opts 参数,就可以修改 CATALINA_OPTS 了。
 
-这样一来,Java 应用所需要的 Web 容器就部署好了。 **然后,部署具体的业务代码** 。
+这样一来,Java 应用所需要的 Web 容器就部署好了。**然后,部署具体的业务代码** 。
 
 这个过程就是指,把编译完后的 War 包推送到目标机器上的指定目录下,供 Tomcat 加载。
 
@@ -151,7 +151,7 @@ ssh-copy-id [email protected]
 
 而在上一篇文章\[《快速构建持续交付系统(三):Jenkins 解决集成打包问题》\]中,我提到,要在编译之后,把构建产物统一上传到 Nexus 或者 Artifactory 之类的构建产物仓库中。
 
-所以, **此时更好的做法是直接在部署本地从仓库下载 War 包** 。这样,之后我们有独立部署或者回滚的需求时,也可以通过在 Ansible 的脚本中选择版本实现。当然,此处你仍旧可以使用 Ansible 的 SCP 模块复制 War 包,只不过是换成了在部署机上执行而已。 **最后,重启 Tomcat 服务,整个应用的部署过程就完成了** 。
+所以,**此时更好的做法是直接在部署本地从仓库下载 War 包** 。这样,之后我们有独立部署或者回滚的需求时,也可以通过在 Ansible 的脚本中选择版本实现。当然,此处你仍旧可以使用 Ansible 的 SCP 模块复制 War 包,只不过是换成了在部署机上执行而已。**最后,重启 Tomcat 服务,整个应用的部署过程就完成了** 。
 
 ## Ansible Tower 简介
 
diff --git "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25438\350\256\262.md" "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25438\350\256\262.md"
index a7bbce7c8..2e1ae5e68 100644
--- "a/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25438\350\256\262.md"	
+++ "b/docs/Code/\346\214\201\347\273\255\344\272\244\344\273\230 36 \350\256\262/\347\254\25438\350\256\262.md"	
@@ -52,9 +52,9 @@ FWS 和 FAT 这两类环境,在网络上完全相同,并共用一组数据
 
 在携程,我们有一套完整的测试环境自助管理平台,开发人员或 QA 团队可以按需自助完成对对测试环境的任意操作。这里,我也分享一下,在携程创建一个测试环境的大致步骤。
 
-**第一步,选择一个已经存在的 FAT 环境,或者重新创建一个 FAT 环境** 。如果是重新创建的话,可以选择重新创建一个空的环境,或者是复制一个已有的环境。 **第二步,选择要在这个 FAT 环境下部署的服务应用,先进行关系绑定(即,这个 FAT 环境下要部署的所有服务应用的描述)再部署** 。如果该服务属于其他团队,则可以要求该团队协助部署(由平台来处理)。
+**第一步,选择一个已经存在的 FAT 环境,或者重新创建一个 FAT 环境** 。如果是重新创建的话,可以选择重新创建一个空的环境,或者是复制一个已有的环境。**第二步,选择要在这个 FAT 环境下部署的服务应用,先进行关系绑定(即,这个 FAT 环境下要部署的所有服务应用的描述)再部署** 。如果该服务属于其他团队,则可以要求该团队协助部署(由平台来处理)。
 
-在携程,一个团队只能部署属于自己的服务应用,如果你的 FAT 环境中包含了其他团队的应用,则要由其他团队部署。这样做的好处是各司其职,能更好地控制联调版本。 **第三步,配置这个 FAT 环境相关的信息** 。携程的配置中心,同样也支持多测试环境的功能,可以做到同一个配置 key 在不同环境有不同的 value。 **第四步,对于特殊的服务调用,进行单独配置。**
+在携程,一个团队只能部署属于自己的服务应用,如果你的 FAT 环境中包含了其他团队的应用,则要由其他团队部署。这样做的好处是各司其职,能更好地控制联调版本。**第三步,配置这个 FAT 环境相关的信息** 。携程的配置中心,同样也支持多测试环境的功能,可以做到同一个配置 key 在不同环境有不同的 value。**第四步,对于特殊的服务调用,进行单独配置。**
 
 经过这样的四步,一个测试环境就被创建起来了。期间测试环境的任何变化,都可以通过环境管理平台完成。比如,增减服务应用、修改配置,或是扩容 / 缩容服务器等。
 
@@ -75,7 +75,7 @@ FWS 和 FAT 这两类环境,在网络上完全相同,并共用一组数据
 
 整个数据库发布的持续交付流程,是以测试通过为驱动的。这个过程,要经历开发、功能,以及集成测试 3 个环境。而数据库的发布又与代码发布不同步,所以如果有兼容问题的话,就容易被发现了。
 
-那么,怎么做到兼容呢? **携程对数据库变更的要求是:**
+那么,怎么做到兼容呢?**携程对数据库变更的要求是:**
 
 - 第一,与业务相关的,只能新增字段,不能删除字段,也不能修改已有字段的定义,并且新增字段必须有默认值。
 - 第二,对于必须要修改原有数据库结构的场景,则必须由 DBA 操作,不纳入持续交付流程。
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25400\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25400\350\256\262.md"
index 9b3285fb2..1325cda0c 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25400\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25400\350\256\262.md"	
@@ -34,7 +34,7 @@
 
 ## 如何进行学习
 
-**熟悉一门面向对象语言** 首先,至少要熟悉一门面向对象的计算机语言。如果没有,请根据自己的学习爱好,或希望从事的工作,先选择一门面向对象语言(C++、Java、Python、Go 等都可以)进行学习和实战,对抽象、继承、多态、封装有一定的基础之后,再来看本系列的文章内容。 **了解 Python 的基本语法** 对 Python 的基本语法有一个简单了解。Python 语法非常简单,只要有一定的编程语言基础,通过下文的介绍很快就能理解的。 **学会阅读 UML 图**
+**熟悉一门面向对象语言** 首先,至少要熟悉一门面向对象的计算机语言。如果没有,请根据自己的学习爱好,或希望从事的工作,先选择一门面向对象语言(C++、Java、Python、Go 等都可以)进行学习和实战,对抽象、继承、多态、封装有一定的基础之后,再来看本系列的文章内容。**了解 Python 的基本语法** 对 Python 的基本语法有一个简单了解。Python 语法非常简单,只要有一定的编程语言基础,通过下文的介绍很快就能理解的。**学会阅读 UML 图**
 
 UML(Unified Modeling Language)称为统一建模语言或标准建模语言,是面向对象软件的标准化建模语言。UML 规范用来描述建模的概念有:类(对象的)、对象、关联、职责、行为、接口、用例、包、顺序、协作以及状态。
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25401\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25401\350\256\262.md"
index a1741f9ec..778c7c636 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25401\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25401\350\256\262.md"	
@@ -157,9 +157,9 @@ class DrinkingMode(Observer):
 
 #### 推模型和拉模型
 
-观察者模式根据其侧重的功能还可以分为推模型和拉模型。 **推模型** :被观察者对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。一般这种模型的实现中,会把被观察者对象中的全部或部分信息通过 update 的参数传递给观察者 \[update(Object obj) ,通过 obj 参数传递\]
+观察者模式根据其侧重的功能还可以分为推模型和拉模型。**推模型** :被观察者对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。一般这种模型的实现中,会把被观察者对象中的全部或部分信息通过 update 的参数传递给观察者 \[update(Object obj) ,通过 obj 参数传递\]
 
-> 如某应用 App 的服务要在凌晨1:00开始进行维护,1:00-2:00期间所有服务将会暂停,这里你就需要向所有的 App 客户端推送完整的通知消息:“本服务将在凌晨1:00开始进行维护,1:00-2:00期间所有服务将会暂停,感谢您的理解和支持!” 不管用户想不想知道,也不管用户会不会在这段期间去访问,消息都需要被准确无误地通知到。这就是典型的推模型的应用。 **拉模型** :被观察者在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到被观察者对象中获取,相当于是观察者从被观察者对象中拉数据。一般这种模型的实现中,会把被观察者对象自身通过 update 方法传递给观察者 \[update(Observable observable ),通过 observable 参数传递 \],这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
+> 如某应用 App 的服务要在凌晨1:00开始进行维护,1:00-2:00期间所有服务将会暂停,这里你就需要向所有的 App 客户端推送完整的通知消息:“本服务将在凌晨1:00开始进行维护,1:00-2:00期间所有服务将会暂停,感谢您的理解和支持!” 不管用户想不想知道,也不管用户会不会在这段期间去访问,消息都需要被准确无误地通知到。这就是典型的推模型的应用。**拉模型** :被观察者在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到被观察者对象中获取,相当于是观察者从被观察者对象中拉数据。一般这种模型的实现中,会把被观察者对象自身通过 update 方法传递给观察者 \[update(Observable observable ),通过 observable 参数传递 \],这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
 
 > 如某应用 App 有新的版本推出,则需要发送一个版本升级的通知消息,而这个通知消息只会简单地列出版本号和下载地址,如果你需要升级你的 App 还需要调用下载接口去下载安装包完成升级。这其实也可以理解成是拉模型。
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25403\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25403\350\256\262.md"
index 1c6a9eacc..0430767c6 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25403\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25403\350\256\262.md"	
@@ -129,7 +129,7 @@ def testState():
 1. 水,具有流动性;
 1. 水蒸汽,质轻,肉眼看不见,却存在于空气中。这三种状态的特性是不是相差巨大?简直就不像是同一种东西,但事实却是不管它在什么状态,其内部组成都是一样的,都是水分子(H2O)。
 
-如水一般, **状态** 即事物所处的某一种形态。 **状态模式** 是说一个对象在其内部状态发生改变时,其表现的行为和外在属性不一样,这个对象看上去就像是改变了它的类型一样。因此,状态模式又称为对象的行为模式。
+如水一般,**状态** 即事物所处的某一种形态。**状态模式** 是说一个对象在其内部状态发生改变时,其表现的行为和外在属性不一样,这个对象看上去就像是改变了它的类型一样。因此,状态模式又称为对象的行为模式。
 
 ## 状态模式的模型抽象
 
@@ -218,12 +218,12 @@ class Water(Context):
         if(isinstance(state, State)):
             state.behavior(self)
 # 单例的装饰器
-def singleton(cls, *args, **kwargs):
+def singleton(cls, *args,**kwargs):
     "构造一个单例的装饰器"
     instance = {}
-    def __singleton(*args, **kwargs):
+    def __singleton(*args,**kwargs):
         if cls not in instance:
-            instance[cls] = cls(*args, **kwargs)
+            instance[cls] = cls(*args,**kwargs)
         return instance[cls]
     return __singleton
 @singleton
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25404\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25404\350\256\262.md"
index c5fcbb1ad..9cd376443 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25404\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25404\350\256\262.md"	
@@ -8,7 +8,7 @@
 >
 > 然而好景不太长,由于种种的原因,最后 Jenny 还是和 Tony 分开了……
 >
-> 人生就像一种旅行,蜿蜒曲折,一路向前!沿途你会看到许多的风景,也会经历很多的黑夜,但我们无法回头!有一些风景可能很短暂,而有一些风景我们希望能够伴随自己走完余生。Tony 经历过一次被爱,也经历过一次追爱;他希望下次能找到一个可陪伴自己走完余生的她,也是 **他的唯一!** ![img](assets/b6726f60-c390-11e8-b5ca-0da8fdb41124.jpg)
+> 人生就像一种旅行,蜿蜒曲折,一路向前!沿途你会看到许多的风景,也会经历很多的黑夜,但我们无法回头!有一些风景可能很短暂,而有一些风景我们希望能够伴随自己走完余生。Tony 经历过一次被爱,也经历过一次追爱;他希望下次能找到一个可陪伴自己走完余生的她,也是 **他的唯一!**![img](assets/b6726f60-c390-11e8-b5ca-0da8fdb41124.jpg)
 
 ## 用程序来模拟生活
 
@@ -71,7 +71,7 @@ id(jenny): 47127888  id(kimi): 47127888
 
 ### 设计思想
 
-有一些人,你希望是唯一的,程序也一样,有一些类,你希望实例是唯一的。 **单例** 就是一个类只能有一个对象(实例),单例就是用来控制某些事物只允许有一个个体,比如在我们生活的世界中,有生命的星球只有一个——地球(至少到目前为止人类所发现的世界中是这样的)。
+有一些人,你希望是唯一的,程序也一样,有一些类,你希望实例是唯一的。**单例** 就是一个类只能有一个对象(实例),单例就是用来控制某些事物只允许有一个个体,比如在我们生活的世界中,有生命的星球只有一个——地球(至少到目前为止人类所发现的世界中是这样的)。
 
 人如果脚踏两只船,你的生活将会翻船!程序中的部分关键类如果有多个实例,将容易使逻辑混乱,程序崩溃!
 
@@ -153,10 +153,10 @@ class Singleton2(type):
     def __init__(cls, what, bases=None, dict=None):
         super().__init__(what, bases, dict)
         cls._instance = None # 初始化全局变量cls._instance为None
-    def __call__(cls, *args, **kwargs):
+    def __call__(cls, *args,**kwargs):
         # 控制对象的创建过程,如果cls._instance为None则创建,否则直接返回
         if cls._instance is None:
-            cls._instance = super().__call__(*args, **kwargs)
+            cls._instance = super().__call__(*args,**kwargs)
         return cls._instance
 class CustomClass(metaclass=Singleton2):
     """用户自定义的类"""
@@ -184,12 +184,12 @@ tony == karry: True
 #### 3. 装饰器的方法
 
 ```python
-def singletonDecorator(cls, *args, **kwargs):
+def singletonDecorator(cls, *args,**kwargs):
     """定义一个单例装饰器"""
     instance = {}
-    def wrapperSingleton(*args, **kwargs):
+    def wrapperSingleton(*args,**kwargs):
         if cls not in instance:
-            instance[cls] = cls(*args, **kwargs)
+            instance[cls] = cls(*args,**kwargs)
         return instance[cls]
     return wrapperSingleton
 @singletonDecorator
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25408\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25408\350\256\262.md"
index 45e8caa96..707da55d2 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25408\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25408\350\256\262.md"	
@@ -217,7 +217,7 @@ def testDecorator2():
 
 1. 可以用不同的装饰器进行多重装饰,装饰的顺序不同,可能产生不同的效果。
 
-1. 装饰类和被装饰类可以独立发展,不会相互耦合;装饰模式相当于是继承的一个替代模式。 **装饰模式的缺点:**
+1. 装饰类和被装饰类可以独立发展,不会相互耦合;装饰模式相当于是继承的一个替代模式。**装饰模式的缺点:**
 
 1. 与继承相比,用装饰的方式拓展功能更加容易出错,排错也更困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25409\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25409\350\256\262.md"
index 971eb1dcd..c913b21e7 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25409\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25409\350\256\262.md"	
@@ -263,7 +263,7 @@ AppliancesFactory 是一个抽象的工厂类,定义了三个方法,分别
 
 抽象工厂模式适合于有多个系列的产品,且每一个系列下有相同子分类的产品。我们定义一个抽象的工厂类 AbstractFactory,AbstractFactory 中定义生产每一个系列产品的方法;而两个具体的工厂实现类 Factory1 和 Factory2 分别生产子分类1的每一系列产品和子分类2的每一系列产品。
 
-如上面家电的例子中,有冰箱、空调、洗衣机三个系列的产品,而每一个系列都有相同的子分类高效型和节能型。通过抽象工厂模式的类图,我们知道 Refrigerator、AirConditioner、WashingMachine 其实也可以不用继承自 HomeAppliances,因为可以把它们看成是独立的系列。当然真实项目中要根据实际应用场景而定,如果这三种家电有很多相同的属性,可以抽象出一个父类 HomeAppliances,如果差别很大则没有必要。 **优点:**
+如上面家电的例子中,有冰箱、空调、洗衣机三个系列的产品,而每一个系列都有相同的子分类高效型和节能型。通过抽象工厂模式的类图,我们知道 Refrigerator、AirConditioner、WashingMachine 其实也可以不用继承自 HomeAppliances,因为可以把它们看成是独立的系列。当然真实项目中要根据实际应用场景而定,如果这三种家电有很多相同的属性,可以抽象出一个父类 HomeAppliances,如果差别很大则没有必要。**优点:**
 
 - 解决了具有二级分类的产品的创建。
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25410\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25410\350\256\262.md"
index a697405fb..ec79bf2fd 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25410\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25410\350\256\262.md"	
@@ -8,7 +8,7 @@
 >
 > 这里每一个诊室的医生诊断完一个病人之后,会呼叫下一位病人,这时外面的显示屏和语音系统就会自动播报下一位病人的名字。Tony 无聊地看着显示屏,下一位病人0170 Panda,请进入3号分诊室准备就诊;下一位病人0171 Lily……
 >
-> 因为人太多,等到12点前面仍然还有12个人,Tony 不得不下去吃个中饭,回来继续等。下一位病人0213 Nick,请进入3号分诊室准备就诊!Tony 眼睛一亮, **哎,妈呀!终于快到了,下一个就是我了!** 看了一个时间,正好14:00……
+> 因为人太多,等到12点前面仍然还有12个人,Tony 不得不下去吃个中饭,回来继续等。下一位病人0213 Nick,请进入3号分诊室准备就诊!Tony 眼睛一亮,**哎,妈呀!终于快到了,下一个就是我了!** 看了一个时间,正好14:00……
 
 ![enter image description here](assets/47b62430-de18-11e7-9fb6-af685862fa1c.jpg)
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25413\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25413\350\256\262.md"
index f4ee36170..7d10e545b 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25413\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25413\350\256\262.md"	
@@ -65,7 +65,7 @@ def testProtoType():
 
 ### 从剧情中思考克隆模式
 
-像上面的 Demo 一样,通过拷贝自身的属性来创建一个新对象的过程叫做 **克隆模式** 。在很多书籍和资料中被称为 **原型模式** ,但我觉得克隆一词更能切中其主旨。
+像上面的 Demo 一样,通过拷贝自身的属性来创建一个新对象的过程叫做 **克隆模式** 。在很多书籍和资料中被称为 **原型模式**,但我觉得克隆一词更能切中其主旨。
 
 克隆模式的核心就是一个 Clone 方法,Clone 方法的功能就是拷贝父本的所有属性,主要包括两个过程:
 
@@ -133,7 +133,7 @@ def testProtoType2():
 小狗Coco        小兔Ricky
 ```
 
-在上面这个例子中,我们看到“副本 tony1”是通过深拷贝的方式创建的,我们对 tony1 对象增加宠物,不会影响 tony 对象。而副本 tony2 是通过浅拷贝的方式创建的,我们对 tony2 对象增加宠物时,tony 对象也更着改变。这是因为 Person 类`__petList`成员是一个可变的引用类型, **浅拷贝只拷贝引用类型对象的指针(指向),而不拷贝引用类型对象指向的值;深拷贝到同时拷贝引用类型对象及其指向的值。** 引用类型:对象本身可以修改,Python 中的引用类型有列表(List)、字典(Dictionary)、类对象。Python 在赋值的时候默认是浅拷贝,如
+在上面这个例子中,我们看到“副本 tony1”是通过深拷贝的方式创建的,我们对 tony1 对象增加宠物,不会影响 tony 对象。而副本 tony2 是通过浅拷贝的方式创建的,我们对 tony2 对象增加宠物时,tony 对象也更着改变。这是因为 Person 类`__petList`成员是一个可变的引用类型,**浅拷贝只拷贝引用类型对象的指针(指向),而不拷贝引用类型对象指向的值;深拷贝到同时拷贝引用类型对象及其指向的值。** 引用类型:对象本身可以修改,Python 中的引用类型有列表(List)、字典(Dictionary)、类对象。Python 在赋值的时候默认是浅拷贝,如
 
 ```python
 def testList():
@@ -165,7 +165,7 @@ list1: [1, 2, 3, 4]
 
 通过 Clone 的方式创建对象时,浅拷贝往往是很危险的,因为一个对象的改变另一个对象也同时改变。深拷贝会对一个对象的发生进行完全拷贝,这样两个对象之间就不会相互影响了,你改你的,我改我的。
 
-在使用克隆模式时,除非一些特殊情况(如需求本身就要求两个对象一起改变), **尽量使用深拷贝的方式** (称其为 **安全模式** )。
+在使用克隆模式时,除非一些特殊情况(如需求本身就要求两个对象一起改变),**尽量使用深拷贝的方式** (称其为 **安全模式** )。
 
 ## 克隆模式的模型抽象
 
@@ -230,7 +230,7 @@ class Person(Clone):
 
 #### 克隆模式的缺点
 
-通过克隆的方式创建对象, **不会执行类的构造函数** ,这不一定是缺点,但大家使用的时候需要注意这一点
+通过克隆的方式创建对象,**不会执行类的构造函数**,这不一定是缺点,但大家使用的时候需要注意这一点
 
 ## 应用场景
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25415\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25415\350\256\262.md"
index 337dd22d3..fb4ade8d2 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25415\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25415\350\256\262.md"	
@@ -363,7 +363,7 @@ def testGame():
 testGame()
 ```
 
-**测试结果:** ![img](assets/9fa581e0-7da5-11e8-8748-9f97e9dc7c3b.jpg)
+**测试结果:**![img](assets/9fa581e0-7da5-11e8-8748-9f97e9dc7c3b.jpg)
 
 在上面的 Demo 中 MacroCommand 是一种组合命令,也叫 **宏命令** (Macro Command)。宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其他命令对象的引用,如上面的弹跳命令是上跳、攻击、下蹲 3 个命令的组合,引用了 3 个命令对象。
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25416\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25416\350\256\262.md"
index 1d870fe70..dab1e7655 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25416\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25416\350\256\262.md"	
@@ -6,7 +6,7 @@
 >
 > 工作压力变大之后,Tony 就经常忙的忘了这事、忘了那事!为了解决这个问题,不至于落下重要的工作,Tony 想了一个办法:每天 9 点到公司,花 10 分钟想一下今天有哪些工作项,有哪些线上问题必须要解决的,有哪些任务需要完成的,然后把这些列一个今日待工作项(To Do List),最后就是看一下新闻,刷一下朋友圈,等到 9:30 大家来齐后开始每日的晨会,接下来就是一整天的忙碌……
 >
-> 因此在每天工作开始(头脑最清醒的一段时间)之前,把今天需要完成的主要事项记录下来,列一个 To Do List,是非常有必要的。这样,当你忘记了要做什么事情时,只要看一下 To Do List 就能想起所有今天要完成的工作项,就不会因忘记某项工作而影响项目的进度, **好记性不如烂笔头** 嘛!
+> 因此在每天工作开始(头脑最清醒的一段时间)之前,把今天需要完成的主要事项记录下来,列一个 To Do List,是非常有必要的。这样,当你忘记了要做什么事情时,只要看一下 To Do List 就能想起所有今天要完成的工作项,就不会因忘记某项工作而影响项目的进度,**好记性不如烂笔头** 嘛!
 
 ![img](assets/fb30bb20-7da9-11e8-8748-9f97e9dc7c3b.jpg)
 
@@ -97,7 +97,7 @@ Tony的工作项:
 
 ## 从剧情中思考备忘模式
 
-在上面的示例中,Tony 将自己的工作项写在 TodoList 中作为备忘,这样,在自己忘记工作内容时,可以通过 TodoList 来快速恢复记忆。像 TodoList 一样,将一个对象的状态或内容记录起来,在状态发生改变或出现异常时,可以恢复对象之前的状态或内容,这在程序中叫做 **备忘录模式** ,也可简称备忘模式。
+在上面的示例中,Tony 将自己的工作项写在 TodoList 中作为备忘,这样,在自己忘记工作内容时,可以通过 TodoList 来快速恢复记忆。像 TodoList 一样,将一个对象的状态或内容记录起来,在状态发生改变或出现异常时,可以恢复对象之前的状态或内容,这在程序中叫做 **备忘录模式**,也可简称备忘模式。
 
 ### 备忘录模式
 
@@ -257,7 +257,7 @@ while (True):
 testTerminal()
 ```
 
-**输出结果:** ![enter image description here](assets/4328cda0-7daa-11e8-be78-bb5c0f92d7f1.jpg)
+**输出结果:**![enter image description here](assets/4328cda0-7daa-11e8-be78-bb5c0f92d7f1.jpg)
 
 ## 应用场景
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25417\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25417\350\256\262.md"
index 1379ea163..563b649e9 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25417\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25417\350\256\262.md"	
@@ -4,7 +4,7 @@
 
 > 团队的拓展培训是很多大公司都组织的活动,因为素质拓展培训能将企业培训、团队建设、企业文化融入到有趣的体验活动中。Tony 所在的公司今年也举行了这样的活动,形式是团体活动 + 自由行,团体活动(第一天)就是素质拓展和技能培训,自由行(第二天)就是自主选择、轻松游玩,因为我们的活动地点是一个休闲娱乐区,还是有很多可玩的东西。
 >
-> 团体活动中有一个项目非常有意思,活动内容是:6 个人一组,每个组完成一幅作画,每个组会拿到一张彩绘原型图,然后根据原型图完成一幅彩绘图。素材:原型图每组一张、铅笔每组一支、空白画布每组一张、画刷每组若干;而颜料却是所有组共用的,有红、黄、蓝、绿、紫五种颜色各一大桶,足够使用。开始前 3 分钟时间准备,采用什么样的合作方式每组自己讨论,越快完成的组获得的分数越高!颜料之所以是共用的,原因也很简单, **颜料很贵,必须充分利用。** >
+> 团体活动中有一个项目非常有意思,活动内容是:6 个人一组,每个组完成一幅作画,每个组会拿到一张彩绘原型图,然后根据原型图完成一幅彩绘图。素材:原型图每组一张、铅笔每组一支、空白画布每组一张、画刷每组若干;而颜料却是所有组共用的,有红、黄、蓝、绿、紫五种颜色各一大桶,足够使用。开始前 3 分钟时间准备,采用什么样的合作方式每组自己讨论,越快完成的组获得的分数越高!颜料之所以是共用的,原因也很简单,**颜料很贵,必须充分利用。** >
 > Tony 所在的 _梦之队_ 组经过讨论后,采用的合作方式是:绘画天分最高的 Anmin 负责描边(也就是素描),Tony 负责选择和调配颜料(取到颜料后必须加水并搅拌均匀),而喜欢跑步的 Simon 负责传送颜料(因为颜料放中间,离每个组都有一段距离),其他人负责涂色。因为梦之队成员配合的比较好,所以最后取得了最优的成绩。
 
 ![img](assets/06a6b600-833e-11e8-81ea-e357bbe10665.jpg)
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25418\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25418\350\256\262.md"
index 25248169a..591559995 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25418\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25418\350\256\262.md"	
@@ -86,13 +86,13 @@ def testRegister():
 
 在上面的示例中,迎新志愿者陪同并帮助入学新生完成报到登记、缴纳学费、领日用品、入住宿舍等一系列的报到流程。新生不用知道具体的报到流程,不用去寻找各个场地;只要跟着志愿者走,到指定的地点,根据志愿者的指导,完成指定的任务即可。志愿者虽然不是直接提供这些报到服务,但也相当于间接提供了报到登记、缴纳学费、领日用品、入住宿舍等一条龙的服务,帮新生减轻了不少麻烦和负担。
 
-在这里志愿者就相当于一个对接人,将复杂的业务通过一个对接人来提供一整套统一的(一条龙式的)服务,让用户不用关心内部复杂的运行机制。这种方式在程序中叫 **外观模式** ,也是门面模式。
+在这里志愿者就相当于一个对接人,将复杂的业务通过一个对接人来提供一整套统一的(一条龙式的)服务,让用户不用关心内部复杂的运行机制。这种方式在程序中叫 **外观模式**,也是门面模式。
 
 ### 外观模式
 
 > Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
 >
-> 为子系统中的一组接口提供一个一致的界面称为 **外观模式** ,外观模式定义了一个高层接口,这个接口使得这一子系统更容易使用。
+> 为子系统中的一组接口提供一个一致的界面称为 **外观模式**,外观模式定义了一个高层接口,这个接口使得这一子系统更容易使用。
 
 外观模式的核心思想:用一个简单的接口来封装一个复杂的系统,使这个系统更容易使用。
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25419\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25419\350\256\262.md"
index 33c2ffc73..8a4601856 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25419\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25419\350\256\262.md"	
@@ -61,7 +61,7 @@ IT 圈外的朋友读《从生活的角度解读设计模式》一书后的感
 
 ## 从剧情中思考访问模式
 
-在上面的示例中,同样内容的一本书,不同类型的读者看到了不同的内容,读到了不同的味道。这里读者和书是两类事物,他们虽有联系,却是比较弱的联系,因此我我们将其分开处理,这种方式在程序中叫 **访问者模式** ,也可简称为访问模式。这里的读者就是访问者,书就是被访问的对象,阅读是访问的行为。
+在上面的示例中,同样内容的一本书,不同类型的读者看到了不同的内容,读到了不同的味道。这里读者和书是两类事物,他们虽有联系,却是比较弱的联系,因此我我们将其分开处理,这种方式在程序中叫 **访问者模式**,也可简称为访问模式。这里的读者就是访问者,书就是被访问的对象,阅读是访问的行为。
 
 ### 访问模式
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25423\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25423\350\256\262.md"
index b849f1265..ed0d4dcd7 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25423\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25423\350\256\262.md"	
@@ -6,7 +6,7 @@
 >
 > 第一次来杭州,西湖必然是非去不可的。正值周末,风和日丽,最适合游玩。上午 9 点出发,Tony 和 Sam 打一辆滴滴快车从滨江到西湖的南山路,然后从大华饭店步行到断桥,之后是穿越断桥,漫步白堤,游走孤山岛,就这样一路走走停停,闲聊、拍照,很快就到了中午。中午在岳王庙附近找了一家生煎,简单解决午餐(大餐留着晚上吃)。因为拍照拍的比较多,手机没电了,正好看到店里有共享充电宝,便借了一个给手机充满电,也多休息了一个小时。 下午,他们准备骑行最美西湖路;吃完饭,找了两辆共享自行车,从杨公堤开始骑行,路过太子湾、雷峰塔,然后再到柳浪闻莺。之后就是沿湖步行走到龙翔桥,找了一家最具杭州特色的饭店解决晚餐……
 >
-> 这一路行程他们从共享汽车(滴滴快车)到共享自行车,再到共享充电宝,共享的生活方式已如影随形地渗透到了生活的方方面面。 **共享,不仅让我们出行更便捷,而且资源更节约!** ![img](assets/9ba61de0-9608-11e8-9f67-05ec09da262a.jpg)
+> 这一路行程他们从共享汽车(滴滴快车)到共享自行车,再到共享充电宝,共享的生活方式已如影随形地渗透到了生活的方方面面。**共享,不仅让我们出行更便捷,而且资源更节约!**![img](assets/9ba61de0-9608-11e8-9f67-05ec09da262a.jpg)
 
 ## 用程序来模拟生活
 
@@ -119,9 +119,9 @@ def testPowerBank():
 
 对象池机制正好可以解决享元模式的这个缺陷。它通过借、还的机制,让一个对象在某段时间内被一个使用者独占,用完之后归还该对象,在独占的这段时间内使用者可以修改对象的部分属性(因为这段时间内其他用户不会使用这个对象);而享元模式因为没有这种机制,享元对象在整个生命周期都是被所有使用者共享的。
 
-> 什么就 **独占** ?就是你用着这个充电宝,(同一时刻)别人就不能用了,因为只有一个接口,只能给一个手机充电。
+> 什么就 **独占**?就是你用着这个充电宝,(同一时刻)别人就不能用了,因为只有一个接口,只能给一个手机充电。
 >
-> 什么叫 **共享** ?就是深夜中几个人围一圆桌坐着,头顶上挂着一盏电灯,大家都享受着这盏灯带来的光明,这盏电灯就是共享的。而且一定范围内来讲它是无限共享的,因为圆桌上坐着 5 个人和坐着 10 个人,他们感觉到的光亮是一样的。
+> 什么叫 **共享**?就是深夜中几个人围一圆桌坐着,头顶上挂着一盏电灯,大家都享受着这盏灯带来的光明,这盏电灯就是共享的。而且一定范围内来讲它是无限共享的,因为圆桌上坐着 5 个人和坐着 10 个人,他们感觉到的光亮是一样的。
 
 对象池机制是享元模式的一个延伸,可以理解为享元模式的升级版。
 
@@ -302,7 +302,7 @@ def testObjectPool():
 
 这就类似于 C 语言中对象内存的分配和释放,程序员必须自己负责内存的申请和释放,给程序带来了很大的负担。
 
-要解决这个问题,就要使用引用计数的技术。 **引用计数的核心相思** 是:这个对象每多一个使用者(如对象的赋值和传递时),引用就自动加 1;每少一个使用者(如 del 一个变量,或退出作用域),引用就自动减 1。
+要解决这个问题,就要使用引用计数的技术。**引用计数的核心相思** 是:这个对象每多一个使用者(如对象的赋值和传递时),引用就自动加 1;每少一个使用者(如 del 一个变量,或退出作用域),引用就自动减 1。
 
 当引用为1时(只有对象池指向这个对象),自动归还(returnObject)给对象池,这样使用者只需要申请一个对象(borrowObject),而不用关心什么时候归还。
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25424\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25424\350\256\262.md"
index c3954395f..fbde6025f 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25424\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25424\350\256\262.md"	
@@ -1,6 +1,6 @@
 # 24 深入解读回调机制:把你技能亮出来
 
-> 铁打的公司,流水的员工!职场中经常有新的员工来,也有老的员工走。为迎接新员工的到来,Tony 所在的公司每个月都有一个新人见面会,在见面会上每个新人都要给大家表演一个节目,节目类型不限,内容随意!只要 **把你的技能都亮出来** ,把最有趣的一面展示给大家就行。有的人选择唱一首歌,有的人拉一曲 Ukulele,有的人会说一搞笑段子,有的人会表演魔术,还有的人耍起了滑板,真是各种鬼才……
+> 铁打的公司,流水的员工!职场中经常有新的员工来,也有老的员工走。为迎接新员工的到来,Tony 所在的公司每个月都有一个新人见面会,在见面会上每个新人都要给大家表演一个节目,节目类型不限,内容随意!只要 **把你的技能都亮出来**,把最有趣的一面展示给大家就行。有的人选择唱一首歌,有的人拉一曲 Ukulele,有的人会说一搞笑段子,有的人会表演魔术,还有的人耍起了滑板,真是各种鬼才……
 
 ![img](assets/a9827b90-9624-11e8-9c35-b59aad3fef8b.jpg)
 
@@ -126,7 +126,7 @@ list2 = list(filter(lambda x: x > 10, elements))
 
 ## 实战应用
 
-下面,我们用策略模式来实现示例中的这种回调机制。 **源码示例:**
+下面,我们用策略模式来实现示例中的这种回调机制。**源码示例:**
 
 ```python
 from abc import ABCMeta, abstractmethod
@@ -193,7 +193,7 @@ Kerry.doPerformance(Skateboarding())
 自己测试一下,结果和回调函数的方式是一样的。
 这种用面向对象的方式实现的类图如下:
 ![enter image description here](assets/9ba733b0-9626-11e8-9f67-05ec09da262a.jpg)
-有人可能会问上面这个类图和策略模式不太一样啊!策略模式中 Context 和 Strategy 是一种聚合关系,即 Context 中存有 Strategy 的对象;而这里 NewEmployee 和 Skill 是一个依赖关系,NewEmployee 不存 Skill 的对象。这里要说明的设计模式不是一成不变的,是可以根据实现情况灵活变通的。如果你愿意,依然可以写成聚合关系,但代码将不会这么优雅。 **Java 的实现** :
+有人可能会问上面这个类图和策略模式不太一样啊!策略模式中 Context 和 Strategy 是一种聚合关系,即 Context 中存有 Strategy 的对象;而这里 NewEmployee 和 Skill 是一个依赖关系,NewEmployee 不存 Skill 的对象。这里要说明的设计模式不是一成不变的,是可以根据实现情况灵活变通的。如果你愿意,依然可以写成聚合关系,但代码将不会这么优雅。**Java 的实现** :
 用 Java 这种支持匿名类的语言来实现,更能感受到回调的味道,代码也更简洁和优雅,如下:
 
 ```java
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25425\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25425\350\256\262.md"
index eefcd42d2..ac3f9035e 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25425\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25425\350\256\262.md"	
@@ -26,7 +26,7 @@
 
 ## 如何区分不同的模式
 
-设计模式是对面向对象思想的常见使用场景的模型总结和归纳。 **设计模式之间的区分,要更多地从我们含义和应用场景去区别,而不应该从他们的类图结构来区分。** 看策略模式、状态模式、桥接模式这三种模式的类图几乎是完全一样的(如下图)。从面向的对象的继承、多态、封装的角度来分析,他们是完全一样的。
+设计模式是对面向对象思想的常见使用场景的模型总结和归纳。**设计模式之间的区分,要更多地从我们含义和应用场景去区别,而不应该从他们的类图结构来区分。** 看策略模式、状态模式、桥接模式这三种模式的类图几乎是完全一样的(如下图)。从面向的对象的继承、多态、封装的角度来分析,他们是完全一样的。
 
 ![enter image description here](assets/9176a840-9ba2-11e8-870d-e9db50847c4e.jpg)
 
@@ -38,7 +38,7 @@
 
 ## 编程思想的三重境界
 
-所以有人说: **设计模式这东西很虚!** 要我说, **它确实也虚!** 如果它看得见摸得着,那我就没必要讲了。我说过,设计模式是一套被反复使用、多数人知晓的、无数工程师实践的代码设计经验的总结,它是面向对象思想的高度提炼和模板化。既然是思想,能不虚吗?它就想道家里面的“道”的理念,每个人对道的理解是不样的,对道的认知也有不同的境界,而不同的境界对应着不同的修为。
+所以有人说: **设计模式这东西很虚!** 要我说,**它确实也虚!** 如果它看得见摸得着,那我就没必要讲了。我说过,设计模式是一套被反复使用、多数人知晓的、无数工程师实践的代码设计经验的总结,它是面向对象思想的高度提炼和模板化。既然是思想,能不虚吗?它就想道家里面的“道”的理念,每个人对道的理解是不样的,对道的认知也有不同的境界,而不同的境界对应着不同的修为。
 
 宋代禅宗大师青原行思提出参禅的三重境界:
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25426\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25426\350\256\262.md"
index 55041b0d7..46a1f3ea6 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25426\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25426\350\256\262.md"	
@@ -78,7 +78,7 @@ Animal("狗").running()
 Animal("鱼").swimming()
 ```
 
-这种写法在代码的方法级别是符合单一职责原则的,一个方法负责一项功能,因水质的原因修改 swimming 方法不会影响陆地上跑的动物。但在类的级别它是不符合单一职责原则的,因为它同时可以干两件事情:跑和游。而且这种写法给用户增加了麻烦,调用方需要时刻明白那种动物是会跑的,那种动物是会游泳的;不然就很可能会出现“狗调用了 swimming 方法,鱼调用了 running 方法”窘境。 **方法三:**
+这种写法在代码的方法级别是符合单一职责原则的,一个方法负责一项功能,因水质的原因修改 swimming 方法不会影响陆地上跑的动物。但在类的级别它是不符合单一职责原则的,因为它同时可以干两件事情:跑和游。而且这种写法给用户增加了麻烦,调用方需要时刻明白那种动物是会跑的,那种动物是会游泳的;不然就很可能会出现“狗调用了 swimming 方法,鱼调用了 running 方法”窘境。**方法三:**
 
 ```bash
 class TerrestrialAnimal():
@@ -455,7 +455,7 @@ zooAdmin.getPandaBeiBeiInfo()
 
 类 A 通过接口 interface 依赖类 C,类 B 通过接口 interface 依赖类 D,如果接口 interface 对于类 A 和类 B 来说不是最小接口,则类 C 和类 D 必须去实现他们不需要的方法。
 
-#### **说人话:** 建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。 **接口尽量小,但是要有限度** 。当发现一个接口过于臃肿时,就要对这个接口进行适当的拆分,但是如果过小,则会造成接口数量过多,使设计复杂化;所以一定要适度
+#### **说人话:** 建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。**接口尽量小,但是要有限度** 。当发现一个接口过于臃肿时,就要对这个接口进行适当的拆分,但是如果过小,则会造成接口数量过多,使设计复杂化;所以一定要适度
 
 #### **案例分析** 我们知道在生物分类学中,从高到低有界、门(含亚门)、纲、目、科、属、种七个等级的分类。脊椎动物就是脊索动物的一个亚门,是万千动物世界中数量最多、结构最复杂的一个门类。哺乳动物(也称兽类)、鸟类、鱼类是脊椎动物中最重要的三个子分类;哺乳动物大都生活于陆地,鱼类都生活在水里,而鸟类大都能飞行
 
diff --git "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25427\350\256\262.md" "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25427\350\256\262.md"
index b2388bb0e..9b25924f7 100644
--- "a/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25427\350\256\262.md"	
+++ "b/docs/Code/\347\231\275\350\257\235\350\256\276\350\256\241\346\250\241\345\274\217 28 \350\256\262/\347\254\25427\350\256\262.md"	
@@ -27,13 +27,13 @@
 
 要进行代码重构的原因,总结一下,常见的原因有以下几种:
 
-- **重复的代码太多** ,没有复用性;难于维护,需要修改时处处都得改。
-- **代码的结构混乱** ,注释也不清晰;没有人能清楚地理解这段代码的含义。
-- **程序没有拓展性** ,遇到新的变化,不能灵活的处理。
-- **对象结构强耦合** ,业务逻辑太复杂,牵一发而动全身,维护时排查问题非常困难。
-- **部分模块性能低** ,随着用户的增长,已无法满足响应速度的要求。
+- **重复的代码太多**,没有复用性;难于维护,需要修改时处处都得改。
+- **代码的结构混乱**,注释也不清晰;没有人能清楚地理解这段代码的含义。
+- **程序没有拓展性**,遇到新的变化,不能灵活的处理。
+- **对象结构强耦合**,业务逻辑太复杂,牵一发而动全身,维护时排查问题非常困难。
+- **部分模块性能低**,随着用户的增长,已无法满足响应速度的要求。
 
-这些导致代码重构的原因,称为代码的坏味道,我称它为 **脏乱差** ,这些脏乱差的代码是怎样形成的呢?大概有以下几种因素:
+这些导致代码重构的原因,称为代码的坏味道,我称它为 **脏乱差**,这些脏乱差的代码是怎样形成的呢?大概有以下几种因素:
 
 1. 上一个写这段代码程序员经验不足、水平太差,或写代码时不够用心。
 1. 奇葩的产品经理提出奇葩的需求。
@@ -125,7 +125,7 @@ def dynamic():
 
 ### 命名的学问
 
-程序中的命名包括变量名、常量名、函数名、类名、文件名等。一个良好的名称能让你的代码具有更好的可读性,让你的程序更容易被人理解;相反,一个不好的名称不仅会降低代码的可读性,甚至会有误导的作用。 **良好的名称应当是可读的、恰当的并且容易记忆的。** 好的命名还可以取代注释的作用,因为注释通常会滞后于代码,经常会出现忘记添加注释或注释更新不及时的情况。
+程序中的命名包括变量名、常量名、函数名、类名、文件名等。一个良好的名称能让你的代码具有更好的可读性,让你的程序更容易被人理解;相反,一个不好的名称不仅会降低代码的可读性,甚至会有误导的作用。**良好的名称应当是可读的、恰当的并且容易记忆的。** 好的命名还可以取代注释的作用,因为注释通常会滞后于代码,经常会出现忘记添加注释或注释更新不及时的情况。
 
 #### **语义相反的词汇要成对出现** 正确的使用词义相反的单词做名称,可以提高代码的可读性。比如 “first / last” 比 “first / end” 通常更让人容易理解。下面是一些常见的例子
 
@@ -201,7 +201,7 @@ date, bookPD, x
 
 使用面向对象的语言时,在一些描述类属性的函数命名中类名是多余的,因为对象本身会包含在调用的代码中。例如,使用 book.getTitle() 而不是 book.getBookTitle(),使用 report.print() 而不是 report.printReport()。
 
-#### **变量名的缩写**  **习惯性缩写:** 始终使用相同的缩写。例如,对 number 的缩写,可以使用 num 也可以使用 no,但不要两个同时使用,始终保证使用同一个缩写。同样的,也不要在一些地方用缩写而另外一些地方不用,如果用了 number 这个单词,就不要在别的地方再用到 num 这个缩写。 **使用的缩写要可以发音:** 尽量让你的缩写可以发音。例如,用 curSetting 而不用 crntSetting,这样可以方便开发人员进行交流。 **避免罕见的缩写:** 尽量避免不常见的缩写。例如,msg(message)、min(Minmum) 和 err(error) 就是一些常见的缩写,而 cal(calender) 大家就不一定都能够理解
+#### **变量名的缩写**  **习惯性缩写:** 始终使用相同的缩写。例如,对 number 的缩写,可以使用 num 也可以使用 no,但不要两个同时使用,始终保证使用同一个缩写。同样的,也不要在一些地方用缩写而另外一些地方不用,如果用了 number 这个单词,就不要在别的地方再用到 num 这个缩写。**使用的缩写要可以发音:** 尽量让你的缩写可以发音。例如,用 curSetting 而不用 crntSetting,这样可以方便开发人员进行交流。**避免罕见的缩写:** 尽量避免不常见的缩写。例如,msg(message)、min(Minmum) 和 err(error) 就是一些常见的缩写,而 cal(calender) 大家就不一定都能够理解
 
 #### **常见命名规则**
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25405\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25405\350\256\262.md"
index bce778128..795dec9aa 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25405\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25405\350\256\262.md"
@@ -35,7 +35,7 @@
 1. 连接交互:在切分的各部分之间建立连接交互的原则和机制。
 1. 组装整合:把切分的各部分按预期定义的规则和方法组装整合为一体,完成系统目标。
 
-有时,你会认为架构师的职责是要交付 “一种架构”,而这“一种架构” 的载体通常又会以某种文档的形式体现。所以,很容易误解架构师的工作就是写文档。但实际上 **架构师的交付成果是一整套决策流,文档仅仅是交付载体** ,而且仅仅是过程交付产物,最终的技术决策流实际体现在线上系统的运行结构中。
+有时,你会认为架构师的职责是要交付 “一种架构”,而这“一种架构” 的载体通常又会以某种文档的形式体现。所以,很容易误解架构师的工作就是写文档。但实际上 **架构师的交付成果是一整套决策流,文档仅仅是交付载体**,而且仅仅是过程交付产物,最终的技术决策流实际体现在线上系统的运行结构中。
 
 而对于实现,你应该已经很清楚是在做什么了。但我在这里不妨更清晰地分解一下。实现的最终交付物是程序代码,但这个过程中会发生什么?一般会有下面 6 个方面的考虑:选型评估;程序设计;执行效率;稳定健壮;维护运维;集成部署。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25409\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25409\350\256\262.md"
index 8457400f1..49b80c971 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25409\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25409\350\256\262.md"
@@ -51,7 +51,7 @@
 
 后来,这位同学在动手实现他的完美设计时,发现程序越写越复杂,交作业的时间已经不够了,只好借用我的不完美的第一版代码改改凑合交了。而我在这第一版代码基础上,又按领悟到的正确思路重构了一次、改进了一番后交了作业。
 
-所以,别被所谓 “完美“ 的程序所困扰,只管先去盯住你要用编程解决的问题,把问题解决,把任务完成。 **编程,其实一开始哪有什么完美,只有不断变得更好。** 工作后,我做了大量的项目,发现这些项目都有很多类似之处。每次,即使项目上线后,我也必然重构项目代码,提取其中可复用的代码,然后在下一个项目中使用。循环往复,一直干了七八年。每次提炼重构,都是一次从 “更多” 走向 “更好” 的过程。我想,很多程序员都有类似的经历吧?
+所以,别被所谓 “完美“ 的程序所困扰,只管先去盯住你要用编程解决的问题,把问题解决,把任务完成。**编程,其实一开始哪有什么完美,只有不断变得更好。** 工作后,我做了大量的项目,发现这些项目都有很多类似之处。每次,即使项目上线后,我也必然重构项目代码,提取其中可复用的代码,然后在下一个项目中使用。循环往复,一直干了七八年。每次提炼重构,都是一次从 “更多” 走向 “更好” 的过程。我想,很多程序员都有类似的经历吧?
 
 回到开头修改 Bug 的例子,我用半天的时间改一个 Bug,感觉效率不算高,这符合精益编程的思路吗?先来回顾下这半天改这个 Bug 的过程。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25410\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25410\350\256\262.md"
index 6a36b7a5f..8bb42fa18 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25410\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25410\350\256\262.md"
@@ -44,7 +44,7 @@
 
 这个替换过程,是个纯粹的搬砖体力活,弄完了还没松口气就又有了新问题:Hibernate 在某些场景下出现了性能问题。陆陆续续把这些新问题处理好,着实让我累了一阵子。后来反思这个决策感觉确实不太妥当,替换带来的好处仅仅是每次新增一个 DAO 类时少写几行代码,却带来很多当时未知的风险。
 
-那时年轻,有激情啊,对新技术充满好奇与冲动。 **其实对于新技术,即使从我知道、我了解到我熟悉、我深谙,这时也还需要克制,要等待合适的时机** 。这让我想起了电影《勇敢的心》中的一个场景,是战场上华莱士看着对方冲过来,高喊:“Hold!Hold!”新技术的应用,也需要等待一个合适的出击时刻,也许是应用在新的服务上,也许是下一次架构升级。
+那时年轻,有激情啊,对新技术充满好奇与冲动。**其实对于新技术,即使从我知道、我了解到我熟悉、我深谙,这时也还需要克制,要等待合适的时机** 。这让我想起了电影《勇敢的心》中的一个场景,是战场上华莱士看着对方冲过来,高喊:“Hold!Hold!”新技术的应用,也需要等待一个合适的出击时刻,也许是应用在新的服务上,也许是下一次架构升级。
 
 不克制的一种形态是容易做出臆想的、通用化的假设,而且我们还会给这种假设安一个非常正当的理由:扩展性。不可否认,扩展性很重要,但扩展性也应当来自真实的需求,而非假设将来的某天可能需要扩展,因为扩展性的反面就是带来设计抽象的复杂性以及代码量的增加。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25411\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25411\350\256\262.md"
index 5bc4b7c46..bf891404a 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25411\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25411\350\256\262.md"
@@ -36,7 +36,7 @@
 
 用户说需要一匹更快的马,你就跑去 “养” 只更壮、更快的马;后来用户需求又变了,说要让马能在天上飞,你可能就傻眼了,只能拒绝用户说:“这需求不合理,技术上实现不了。”可见,用户所说的也不可 “信” 矣。只有真正挖掘并理解了用户的原始需求,最后通过编程实现的程序系统才是符合 “信” 的标准的。
 
-但在这一条的修行上几乎没有止境,因为要做到 “信” 的标准,编写行业软件程序的程序员需要在一个行业长期沉淀,才能慢慢搞明白用户的真实需求。 **达,指不拘泥于原文的形式,表达通顺明白,让读者对所述内容明达** 。
+但在这一条的修行上几乎没有止境,因为要做到 “信” 的标准,编写行业软件程序的程序员需要在一个行业长期沉淀,才能慢慢搞明白用户的真实需求。**达,指不拘泥于原文的形式,表达通顺明白,让读者对所述内容明达** 。
 
 这条应用在编程上就是在说程序的可读性、可理解性和可维护性。
 
@@ -48,7 +48,7 @@
 
 一些流行建议的解决方案是:多沟通,深入理解别人的代码思路和风格,不要轻易盲目地修改。但这些年实践下来,这个方法在现实中走得并不顺畅。
 
-随着微服务架构的流行,倒是提供了另一种解决方案:每个服务对应一个唯一的负责人(Owner)。长期由一个人来维护的代码,就不会那么容易腐烂,因为一个人不存在沟通问题。而一个人所能 “达” 到的层次,完全由个人的经验水平和追求来决定。 **雅,指选用的词语要得体,追求文章本身的古雅,简明优雅** 。
+随着微服务架构的流行,倒是提供了另一种解决方案:每个服务对应一个唯一的负责人(Owner)。长期由一个人来维护的代码,就不会那么容易腐烂,因为一个人不存在沟通问题。而一个人所能 “达” 到的层次,完全由个人的经验水平和追求来决定。**雅,指选用的词语要得体,追求文章本身的古雅,简明优雅** 。
 
 雅的标准,应用在编程上已经从技艺上升到了艺术的追求,这当然是很高的要求与自我追求了,难以强求。而只有先满足于 “信” 和 “达” 的要求,你才有余力来追求 “雅” 。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25412\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25412\350\256\262.md"
index b3743cef0..924d759ff 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25412\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25412\350\256\262.md"
@@ -57,7 +57,7 @@ Backblaze 2014 年发布的硬盘统计报告指出,根据对其数据中心 3
 
 当然,如果能对网络的具体层次有更深刻的理解,自然也是更好的。事实上,如果你和一个对网络具体层次缺乏理解的人调试两端的网络程序,碰到问题时,经常会发现沟通不在一个层面上,产生理解困难。(这里推荐下隔壁的“趣谈网络协议”专栏)
 
-了解了环境,也难免不出 Bug。因为我们对环境的理解是渐进式的,不可能一下子就完整掌握,全方位,无死角。当出现了因为环境产生的过敏反应时,收集足够多相关的信息才能帮助快速定位和解决问题,这就是前面《代码与分类》文章中 “运维” 类代码需要提供的服务。 **收集信息** ,不仅仅局限于相关直接依赖环境的配置和参数,也包括用户输入的一些数据。真实场景确实大量存在这样一种情况:同样的环境只针对个别用户发生异常过敏反应。
+了解了环境,也难免不出 Bug。因为我们对环境的理解是渐进式的,不可能一下子就完整掌握,全方位,无死角。当出现了因为环境产生的过敏反应时,收集足够多相关的信息才能帮助快速定位和解决问题,这就是前面《代码与分类》文章中 “运维” 类代码需要提供的服务。**收集信息**,不仅仅局限于相关直接依赖环境的配置和参数,也包括用户输入的一些数据。真实场景确实大量存在这样一种情况:同样的环境只针对个别用户发生异常过敏反应。
 
 有一种药叫抗过敏药,那么也可以有一种代码叫 “抗过敏代码”。在收集了足够的信息后,你才能编写这样的代码,因为现实中,程序最终会运行在一些一开始你可能没考虑到的环境中。收集到了这样的环境信息,你才能写出针对这种环境的 “抗过敏代码”。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25416\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25416\350\256\262.md"
index 9a9dd6d21..9c1637490 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25416\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25416\350\256\262.md"
@@ -51,7 +51,7 @@
 
 目标是愿望层面的,计划是执行层面的,而计划的方式也有不同的认识维度。
 
-从 **时间维度** ,可以拟定 “短、中、长” 三阶段的计划:
+从 **时间维度**,可以拟定 “短、中、长” 三阶段的计划:
 
 - 短期:拟定一年内的几个主要事项、行动周期和检查标准。
 - 中期:近 2~3 年内的规划,对一年内不足以取得最终成果的事项,可以分成每年的阶段性结果。
@@ -59,7 +59,7 @@
 
 短期一年可以完成几件事或任务,中期两三年可以掌握精熟一门技能,长期的 “一辈子” 达成一个愿景,实现一个成长的里程碑。
 
-从 **路径维度** ,订计划可以用一种 SMART 方法,该方法是百年老店通用电气创造的。在 20 世纪 40 年代的时候,通用电气就要求每一个员工把自己的年度目标、实现方法及标准写信告诉自己的上级。上级也会根据这个年度目标来考核员工。这种方法进化到了 20 世纪 80 年代,就成了著名的 SMART 原则。
+从 **路径维度**,订计划可以用一种 SMART 方法,该方法是百年老店通用电气创造的。在 20 世纪 40 年代的时候,通用电气就要求每一个员工把自己的年度目标、实现方法及标准写信告诉自己的上级。上级也会根据这个年度目标来考核员工。这种方法进化到了 20 世纪 80 年代,就成了著名的 SMART 原则。
 
 SMART 也是 5 个英文词的首字母缩写:
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25417\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25417\350\256\262.md"
index a56e18cf8..9863101f6 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25417\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25417\350\256\262.md"
@@ -56,7 +56,7 @@
 
 通过兴趣来启动,但要靠承诺才能有效地执行下去。感兴趣和做承诺的差别在于,只是感兴趣的事,到了执行的时候,总可以给自己找出各种各样的原因、借口或外部因素的影响去延期执行;而承诺就是这件事是每天的最高优先级,除非不可抗力的因素,都应该优先执行。
 
-比如,写作本是我的兴趣,但接下 “极客时间” 的专栏后,这就是承诺了,所以为此我就只能放弃很多可以用于休闲、娱乐的时间。 **兴趣让计划更容易启动,而承诺让计划得以完成。**
+比如,写作本是我的兴趣,但接下 “极客时间” 的专栏后,这就是承诺了,所以为此我就只能放弃很多可以用于休闲、娱乐的时间。**兴趣让计划更容易启动,而承诺让计划得以完成。**
 
 而在现实生活中,让计划不可行或半途而废的常见错误有:
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25418\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25418\350\256\262.md"
index 7c96ba7c2..64c4df1d3 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25418\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25418\350\256\262.md"
@@ -34,7 +34,7 @@
 
 > 真正的自由,是在所有时候都能控制自己。
 
-如蒙田所说, **计划才能给你真正的自由,你对计划的控制力越强,离自由也就更近了** 。
+如蒙田所说,**计划才能给你真正的自由,你对计划的控制力越强,离自由也就更近了** 。
 
 ## 结果与收益
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25420\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25420\350\256\262.md"
index 536d34f1d..1e89d807b 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25420\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25420\350\256\262.md"
@@ -8,7 +8,7 @@
 
 一个计划被制定出来后,我们通常会根据它的周期设定一个执行的节奏。
 
-**长期** ,就像长跑,跑五千米是长跑,跑马拉松(四万多米)也是长跑,但我们知道跑五千米和跑拉松肯定是用不同的节奏在跑。
+**长期**,就像长跑,跑五千米是长跑,跑马拉松(四万多米)也是长跑,但我们知道跑五千米和跑拉松肯定是用不同的节奏在跑。
 
 一个长期的目标可以是五年,也可以是十年,因目标而异。要精熟一门技能领域,比如编程,确切地说应该是编程中的某一分支领域,对于一般人来说,可能就需要三五年不等了。而像精通一门外语,可能需要的时间更长,我是从初中开始学习英语的,如今二十多年过去了,别说精,可能连熟都谈不上。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25424\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25424\350\256\262.md"
index 1aaf0f2c8..6b5fe8ca8 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25424\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25424\350\256\262.md"
@@ -54,7 +54,7 @@
 
 而对于学习语言本身我觉得最高效的方法就是看一本该领域的经典入门书。比如,对于 Java 就是《Java 核心技术》或《Java 编程思想》,这是我称之为 **第一维度的书,聚焦于一个技术领域并讲得透彻清晰** 。
 
-在有了该语言的一些实际编程和工程经验后,就可以看一些该领域 **第二维度的书** ,比如:《Effective Java》《UNIX 编程艺术》等,这些是聚焦于特定领域经验总结型的书,这类书最有价值的地方是其 **聚焦于领域的思想和思路** 。
+在有了该语言的一些实际编程和工程经验后,就可以看一些该领域 **第二维度的书**,比如:《Effective Java》《UNIX 编程艺术》等,这些是聚焦于特定领域经验总结型的书,这类书最有价值的地方是其 **聚焦于领域的思想和思路** 。
 
 如果过早地看这类书反而没什么帮助,甚至还会可能造成误解与困扰。例如,我看豆瓣上关于《UNIX 编程艺术》的书评,有这么一条:“很多例子和概念已经成了古董,当历史书看,无所获。”这显然就是过早接触了第二维度的书,却预期得到第一维度的收获,自然无所获了。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25425\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25425\350\256\262.md"
index 7d41282b3..7e6f416ca 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25425\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25425\350\256\262.md"
@@ -56,8 +56,8 @@
 
 > 它会使你对时间的感觉越来越精确。每个人都感觉 “时间越过越快”,为什么会有这样的感觉?这种感觉会使得我们产生很多不必要的焦虑。而基于 “事件 - 时间日志” 的记录可以调整你对时间的感觉。在估算任何工作时,都更容易确定 “真正现实可行的目标”。
 
-是的,这就是关于时间的第一个 “基石” 习惯, **在精确地感知与测量时间之后,你才可能更准确地“预知” 未来** 。
+是的,这就是关于时间的第一个 “基石” 习惯,**在精确地感知与测量时间之后,你才可能更准确地“预知” 未来** 。
 
 站在当下这端实际是很难看清未来的另一端的,因为存在太多的可能情况。在一部叫《彗星来的那一夜》的电影中假设了这种场景,一个人有很多种平行分支的存在,时间经过的部分形成了稳定的唯一一个版本。
 
-也许,过去的十年你也像我一样,曾站在起点却看不清十年后的终点。而现今,我总会想象站在每一个未来可能的终点去审视当前的自己、当下的决策与行动,就会得到一个完全不一样的理解,从此开启完全不一样的平行分支。 **过去的你也许不曾是时间的旧爱,但未来的你可以成为时间的新欢。** 
\ No newline at end of file
+也许,过去的十年你也像我一样,曾站在起点却看不清十年后的终点。而现今,我总会想象站在每一个未来可能的终点去审视当前的自己、当下的决策与行动,就会得到一个完全不一样的理解,从此开启完全不一样的平行分支。**过去的你也许不曾是时间的旧爱,但未来的你可以成为时间的新欢。** 
\ No newline at end of file
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25426\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25426\350\256\262.md"
index 6b882121f..dc4e9fe3b 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25426\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25426\350\256\262.md"
@@ -2,7 +2,7 @@
 
 上篇,我讲述了关于建立时间习惯的第一步: **感知与测量** ;之所以需要先感知与测量,就是为了更好地了解你的时间都花在哪里去了。
 
-下一步, **为了更有效地利用好你每天有限的时间,就需要你重新审视并调整你的时间切割与构建方式** 。
+下一步,**为了更有效地利用好你每天有限的时间,就需要你重新审视并调整你的时间切割与构建方式** 。
 
 ## 切割时间
 
@@ -54,7 +54,7 @@
 
 每一个习惯的构建成本是不同的,甚至同样的习惯对不同的人来讲,构建成本也是不同的。比如,跑步这个事,对有些爱运动的人来说就是每天跑或每周跑几次的习惯,而于我而言,建立跑步这个习惯,从心理到生理都有更大的消耗。
 
-任何行动的发生,都需要两种努力才有可能:第一种,是行动本身固有需要的努力,如跑步,跑一公里和跑十公里固有需要的努力是不等量的;第二种,指决策是否执行这种行动的努力,决定跑一公里还是跑十公里的决策意志力消耗,我想也不会一样。 **构建习惯的目的,以及它们能起作用的原因在于:它能消除行动中第二种努力的决策消耗。** 我之所以选择构建写作习惯而不是跑步习惯的原因是,对一个像我这样的程序员而言,写文章和写代码的感觉很接近,它就好像是一种刚好在程序员的能力边界线外不远处的事情。这样,写作行动所需要付出的两种努力,感觉都还在可以应对的范围,属于能力边界附近不远的事情,也是正适合用来扩张能力边界的事,有一种挑战的刺激感,又不至于望而生畏。
+任何行动的发生,都需要两种努力才有可能:第一种,是行动本身固有需要的努力,如跑步,跑一公里和跑十公里固有需要的努力是不等量的;第二种,指决策是否执行这种行动的努力,决定跑一公里还是跑十公里的决策意志力消耗,我想也不会一样。**构建习惯的目的,以及它们能起作用的原因在于:它能消除行动中第二种努力的决策消耗。** 我之所以选择构建写作习惯而不是跑步习惯的原因是,对一个像我这样的程序员而言,写文章和写代码的感觉很接近,它就好像是一种刚好在程序员的能力边界线外不远处的事情。这样,写作行动所需要付出的两种努力,感觉都还在可以应对的范围,属于能力边界附近不远的事情,也是正适合用来扩张能力边界的事,有一种挑战的刺激感,又不至于望而生畏。
 
 电影《功夫熊猫 3》里,师父对阿宝说:
 
@@ -62,11 +62,11 @@
 
 所以,在时间 “基石” 习惯之上构建的习惯应该是你能力范围之外的行动。如果一项行动通过习惯慢慢变成了能力范围之内的事,那么你以后再去做类似的事,其实就不需要再付出什么决策努力了,也就不再需要习惯来帮忙了。
 
-有时,习惯会让你产生日复一日、年复一年做一件事的感觉,这样日积月累下来消耗了大量的时间,但付出了这么多未必会产生真正的收获。怎么会这样呢? **习惯,它的表象和形式给人的感觉是在重复一件事,但它的内在与核心其实是不断产生交付,持续的交付。** 好多万年前,人类的蛮荒时期,还没有进入农业社会,人类是如何生存的?那时的人类,以采集和狩猎为生,每天年轻力壮的男人负责出去狩猎和采集野果,女人则在部落内照料一家老小。这就是进化史上,自然选择让人类养成的共同习惯,并且这个习惯持续了数十万年。
+有时,习惯会让你产生日复一日、年复一年做一件事的感觉,这样日积月累下来消耗了大量的时间,但付出了这么多未必会产生真正的收获。怎么会这样呢?**习惯,它的表象和形式给人的感觉是在重复一件事,但它的内在与核心其实是不断产生交付,持续的交付。** 好多万年前,人类的蛮荒时期,还没有进入农业社会,人类是如何生存的?那时的人类,以采集和狩猎为生,每天年轻力壮的男人负责出去狩猎和采集野果,女人则在部落内照料一家老小。这就是进化史上,自然选择让人类养成的共同习惯,并且这个习惯持续了数十万年。
 
 采集与狩猎这个行动习惯的核心就是必须每天产生交付,得有收获,否则一家老小都得饿肚子。而像狩猎这样的活动,就需要高度集中的注意力、熟练的技能运用和瞬间的爆发,它需要狩猎者所有的感官都高度专注在猎物的运动上,并随时调整适应猎物的运动变化。而采集,就需要采集者不断扩大或走出熟悉的边界,因为熟悉的地方可能早就没了果实,而陌生的地界又可能潜藏着未知的危险。
 
-这样的行动习惯,通过数十万年的进化,甚至已经刻画在了我们的基因中。这就是我想说的, **如果你要构建一个习惯,就要运用好基因中本已存在的关于 “采集和狩猎” 的本能:高度专注,跨出边界,持续交付** 。
+这样的行动习惯,通过数十万年的进化,甚至已经刻画在了我们的基因中。这就是我想说的,**如果你要构建一个习惯,就要运用好基因中本已存在的关于 “采集和狩猎” 的本能:高度专注,跨出边界,持续交付** 。
 
 末了,我把上、下两篇的内容一起提炼总结为如下:
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25427\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25427\350\256\262.md"
index ac16e7da7..b97e2af83 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25427\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25427\350\256\262.md"
@@ -65,7 +65,7 @@
 
 从理解的角度,这类技术切换的尝试事实上扩大了你的知识边界,尝试的也许是孤点,但你可以进一步找到它们的连接处,形成体系。因为很多现实的原因,每个人的起点和路径都不会一样,但我们都是从某一点开始去慢慢摸索、尝试,最终走出一个属于自己的体系来的。
 
-最后,当你有了自己的体系,也可能有了更多的尝试选择权,就可以体系为中心,去有选择地尝试对你更有意义或价值的事了。 **总结来说** :
+最后,当你有了自己的体系,也可能有了更多的尝试选择权,就可以体系为中心,去有选择地尝试对你更有意义或价值的事了。**总结来说** :
 
 试一试,是走出舒适区的一次行动,这本是一个好的出发点,但若只有一个模糊的终点,那么它带来的更可能就是无谓的浪费。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25429\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25429\350\256\262.md"
index 9c524c57e..fa3ab85c5 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25429\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25429\350\256\262.md"
@@ -61,7 +61,7 @@
 
 但在实际中,结对编程也有它的缺点和劣势,比如更高的开发成本(毕竟要同时占用两个人)。而且,有些人可能从心理上就很不喜欢结对编程的,比如我,因为坐在一起编程,难免分心而无法进入完美的心流状态,所以会感觉自己的工作效率都会下降一半以上;并且我也很难接受别人在看代码讨论时,用手戳屏幕指指点点。当然,不仅仅是我,还有更甚者,除了代码洁癖,还有生活洁癖,根本接受不了任何其他人和自己共用一个键盘的。
 
-也许稍微松散点,没有那么物理上的严格结对,而是确保每一个程序员写的每一行代码,都能有一个配对的程序员去进行检视,虽说这个过程完全是异步或远程的,但效果应该也是可以保障的。这几乎就是开源项目的协作模式。开源项目的繁荣与成功,也证明了其实践的协作模式是一种好方法。 **总结来说** :
+也许稍微松散点,没有那么物理上的严格结对,而是确保每一个程序员写的每一行代码,都能有一个配对的程序员去进行检视,虽说这个过程完全是异步或远程的,但效果应该也是可以保障的。这几乎就是开源项目的协作模式。开源项目的繁荣与成功,也证明了其实践的协作模式是一种好方法。**总结来说** :
 
 在你从程序新人成长起来的过程中,要学会区分,哪些确实是值得学习与推广的好方法,哪些仅仅是自己的个人习惯,特别是在你成长到开始成为技术管理者之后。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25430\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25430\350\256\262.md"
index 1065dc693..019a9bfae 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25430\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25430\350\256\262.md"
@@ -2,7 +2,7 @@
 
 程序员群体有个共同的弱点,那就是写得了代码,解决得了问题,但却不能很好地展现自己的能力。从今天开始,咱们专栏即进入一个关于 “展现” 的主题,聊聊(写作、画图和演讲)三类最常见的展现手段。
 
-其中,展现的最常见形式之一就是: **写作** ,它是一种能随着时间去沉淀的长尾展现形式。
+其中,展现的最常见形式之一就是: **写作**,它是一种能随着时间去沉淀的长尾展现形式。
 
 曾有多人问起,写作除了坚持写、持续写、长期写,还有什么其他技巧么?答案是:有的。虽说我并没有上过专业的写作课,但在长期的写作过程中,我已通过实践摸索出来了一套符合程序员这种理性逻辑思维的写作技法,简言之,就是:写字如编码。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25431\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25431\350\256\262.md"
index b94bda321..5a30dadeb 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25431\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25431\350\256\262.md"
@@ -50,11 +50,11 @@
 
 彩虹七色包括:红、橙、黄、绿、青、蓝、紫。但七种颜色的选择也是有优先级,在一本讲设计的书中 Designing with the Mind in Mind(中文译本《认知与设计》)提出了下面一些色彩使用准则:
 
-- **使用饱和度、亮度以及色相区分颜色,确保颜色的高反差** ,因为人的视觉是为边缘反差而优化的。
-- **使用独特的颜色** ,因为人最容易区分的颜色包括:红、绿、黄、蓝、白和黑。
-- **避免使用色盲无法区分的颜色对** ,比如:深红-黑,深红-深绿,蓝色-紫色,浅绿-白色。
-- **使用颜色之外的其他提示** ,对有颜色视觉障碍的人友好,而且也增强了可理解性。
-- **避免强烈的对抗色** ,比如:红黑,黄黑。
+- **使用饱和度、亮度以及色相区分颜色,确保颜色的高反差**,因为人的视觉是为边缘反差而优化的。
+- **使用独特的颜色**,因为人最容易区分的颜色包括:红、绿、黄、蓝、白和黑。
+- **避免使用色盲无法区分的颜色对**,比如:深红-黑,深红-深绿,蓝色-紫色,浅绿-白色。
+- **使用颜色之外的其他提示**,对有颜色视觉障碍的人友好,而且也增强了可理解性。
+- **避免强烈的对抗色**,比如:红黑,黄黑。
 
 以你看为什么交通灯是:红、黄、绿?为什么乔布斯选择这三个颜色作为 Mac 操作系统中所有应用窗体的按纽颜色,这也是暗合人类的视觉认知原则的。所以我现在多选择的是白底、黑字、黑色线条,色块优先选择红、绿、黄、蓝,实在不够用了才会选择橙、青、紫。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25432\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25432\350\256\262.md"
index afd5a561c..7d914f648 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25432\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25432\350\256\262.md"
@@ -82,13 +82,13 @@ TED 的演讲以前多是 18 分钟,而现在分长、短两种:短的约 6
 
 我们的正常语速大约是每分钟 150~200 个汉字,但在演讲的压力环境下,可能会出现不自觉地加速,无意识地跑偏,甚至语无伦次。如果想要提供更精确的信息传递和表达,那么演讲稿就是必需的。
 
-让演讲的每一个字,都体现它的价值。 **第三类是小故事** 。人是情感动物,故事的影响效应远高于数据和逻辑,即使是在做技术分享时。
+让演讲的每一个字,都体现它的价值。**第三类是小故事** 。人是情感动物,故事的影响效应远高于数据和逻辑,即使是在做技术分享时。
 
 以前听过一些技术分享感觉比较枯燥、催眠,就在于技术基本都在讲逻辑、讲数据,听久了自然疲劳。而穿插一些 “小” 故事,则可以加深前面数据和逻辑的影响效应。这一点很多慈善募捐组织早就学会了,再大比例的穷困数据,也比不上一张衣不蔽体的小女孩照片来得有效。
 
 ### 3. 节奏
 
-一段持续时间的演讲中,有没有一些关键的时间点呢?当然是有的。 **一个是开场** 。据研究统计,一场演讲给人留下的印象和评价,开场的数秒至关重要。这可能和一开始是否能抓住听众的注意力有关。 **另一个是峰终** 。管理界有一个 “峰终定律(Peak-End Rule)”:在 “峰” 和 “终” 时的体验,主宰了对一段体验好或者不好的感受,而在过程中好或不好体验的比重、时间长短,对记忆的感觉差不多没有影响。也就是说,如果在一段体验的高峰和结尾,你的体验是愉悦的,那么你对整个体验的感受就是愉悦的,即使这次体验总体来看,更多是无聊和乏味的时刻。
+一段持续时间的演讲中,有没有一些关键的时间点呢?当然是有的。**一个是开场** 。据研究统计,一场演讲给人留下的印象和评价,开场的数秒至关重要。这可能和一开始是否能抓住听众的注意力有关。**另一个是峰终** 。管理界有一个 “峰终定律(Peak-End Rule)”:在 “峰” 和 “终” 时的体验,主宰了对一段体验好或者不好的感受,而在过程中好或不好体验的比重、时间长短,对记忆的感觉差不多没有影响。也就是说,如果在一段体验的高峰和结尾,你的体验是愉悦的,那么你对整个体验的感受就是愉悦的,即使这次体验总体来看,更多是无聊和乏味的时刻。
 
 峰终定律,在管理上决定了用户体验的资源投入分布,只需要重点投入设计好 “峰终” 体验。而演讲,也是一门体验艺术,它的 “峰” 前面说了一处——开场(抓注意力);另一处,可能是中间某一处关键点(提供独特的高价值内容或观点)。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25433\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25433\350\256\262.md"
index 52c865623..3ab555258 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25433\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25433\350\256\262.md"
@@ -36,7 +36,7 @@
 
 初级同学经过两三年工作历练,对实现各种业务功能、开发规范流程都很熟练了,摆脱了对基本指导的依赖性,这时就进入了中级阶段。中级工程师已经能够独立承担开发任务,设计实现他们负责的系统模块,以及通过搜集有效信息、资料和汲取过往经验来解决自己工作范围内遇到的问题。
 
-中级这个层面的基本要求就是: **完成动作、达成品质和优化效率** ,属于公司 “动作执行” 层面的中坚力量。观察下来,这个级别的工程师多数都能做到完成,但品质可能有瑕疵,效率上甚至也有很多无效耗散。不过,效率和品质总是在不断的迭代中去完善,自身也会在这个过程中不断成长并向着下一个阶梯迈进。
+中级这个层面的基本要求就是: **完成动作、达成品质和优化效率**,属于公司 “动作执行” 层面的中坚力量。观察下来,这个级别的工程师多数都能做到完成,但品质可能有瑕疵,效率上甚至也有很多无效耗散。不过,效率和品质总是在不断的迭代中去完善,自身也会在这个过程中不断成长并向着下一个阶梯迈进。
 
 不少同学卡在这一阶段,就是因为虽然不断在完成工作,但却没有去反思、沉淀、迭代并改进,从而导致自己一直停留在了不断的重复中。所以,在工作中要保持迭代与改进,并把你的经验分享给新来的初级同学,这样在未来之路你不仅会走得更快,而且也可能走得更轻松。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25435\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25435\350\256\262.md"
index 2fd52e098..f0e3d18cc 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25435\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25435\350\256\262.md"
@@ -84,7 +84,7 @@
 
 这像不像一个现实版的 “令狐冲” 与 “风清扬” 的故事?而这,就是我期待的一种师徒关系。
 
-现实中,对于师徒关系,会有人有这样的疑问:“教会徒弟,会饿死师傅吗?”也许中世纪时期的师徒关系会有这样的担忧,但如今这个信息时代,知识根本不稀缺,也没有所谓的 “一招鲜,吃遍天” 的绝招。反过来说, **带好了徒弟,接手并取代了你当前正在做的事情,你才有可能解放出来去做更高层次和更大维度的事情** 。
+现实中,对于师徒关系,会有人有这样的疑问:“教会徒弟,会饿死师傅吗?”也许中世纪时期的师徒关系会有这样的担忧,但如今这个信息时代,知识根本不稀缺,也没有所谓的 “一招鲜,吃遍天” 的绝招。反过来说,**带好了徒弟,接手并取代了你当前正在做的事情,你才有可能解放出来去做更高层次和更大维度的事情** 。
 
 而作为学徒,你需要吸取德里克的经验: **学习和成长是自己的事,严肃待之,行动起来,自助者,人亦助之** 。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25436\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25436\350\256\262.md"
index f76b71e8a..c8370c7ba 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25436\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25436\350\256\262.md"
@@ -18,7 +18,7 @@
 
 老先生说:“这就是电厂。如果烧的是煤炭,这就是燃煤电厂;如果烧的天然气,这就是燃气电厂;如果获得热能的方式是核裂变,这就是核电厂;如果带动叶片的能量来自从高处流向低处的水流,这就是水电厂。”
 
-“你们或许会问:那我们看到的电站怎么这么复杂?答案其实很简单,电站需要复杂系统的目的:一是为了确保安全(Safety),二是为了提高效率(Efficiency)。 **安全与效率的平衡,是所有工程技术的核心** 。”
+“你们或许会问:那我们看到的电站怎么这么复杂?答案其实很简单,电站需要复杂系统的目的:一是为了确保安全(Safety),二是为了提高效率(Efficiency)。**安全与效率的平衡,是所有工程技术的核心** 。”
 
 听完这个故事,我觉着所谓 “大道至简” 大概就是这样的感觉了。
 
@@ -38,9 +38,9 @@
 - 运维
 - 运行
 
-**安全开发** ,就是为了保障交付的程序代码是高质量、低 Bug 率、无漏洞的。从开发流程、编码规范到代码评审、单元测试等,都是为了保障开发过程中的 “安全”。 **安全运维** ,就是为了保障程序系统在线上的变化过程中不出意外,无故障。但无故障是个理想状态,现实中总会有故障产生,当其发生时最好是对用户无感知或影响范围有限的。
+**安全开发**,就是为了保障交付的程序代码是高质量、低 Bug 率、无漏洞的。从开发流程、编码规范到代码评审、单元测试等,都是为了保障开发过程中的 “安全”。**安全运维**,就是为了保障程序系统在线上的变化过程中不出意外,无故障。但无故障是个理想状态,现实中总会有故障产生,当其发生时最好是对用户无感知或影响范围有限的。
 
-通过自动部署来避免人为的粗心大意,资源隔离保障程序故障影响的局部化;当一定要有人参与操作时,操作规范和日志保证了操作的标准化和可追溯性;线上程序的版本化管理与灰度发布机制,保障了若有代码 Bug 出现时的影响局部化与快速恢复能力。 **安全运行** ,就是为了应对 “峰值” 等极端或异常运行状态,提供高可靠和高可用的服务能力。
+通过自动部署来避免人为的粗心大意,资源隔离保障程序故障影响的局部化;当一定要有人参与操作时,操作规范和日志保证了操作的标准化和可追溯性;线上程序的版本化管理与灰度发布机制,保障了若有代码 Bug 出现时的影响局部化与快速恢复能力。**安全运行**,就是为了应对 “峰值” 等极端或异常运行状态,提供高可靠和高可用的服务能力。
 
 ## 效率
 
@@ -48,7 +48,7 @@
 
 ![img](assets/2b3a679cc254af1701c6a1f53c1a666f.png)
 
-“效率”的划分 **开发效率** ,可以从 “个体” 和 “群体” 两个方面来看。
+“效率”的划分 **开发效率**,可以从 “个体” 和 “群体” 两个方面来看。
 
 个体,就是程序员个人了,其开发效率除了受自身代码设计与编写能力的影响,同时还要看其利用工具的水平。更好的源码管理工具与技巧可以避免无谓的冲突与混乱;代码模板与开发框架能大幅度提升代码产出效率;而持续集成工具体系则能有助于快速推进代码进入可测试状态。
 
@@ -56,7 +56,7 @@
 
 以后端服务架构技术演进的变化为例,从单体应用到面向服务架构思想,再到如今已成主流的微服务架构实践,它最大的作用在于有利于大规模开发团队的并行化开发,从而提升了团队整体的效率。理想情况下,每个微服务的代码库都不大,变化锁闭在每个服务内部,不会影响其他服务。
 
-微服务化一方面提升了整体的开发效率,但因为服务多了,部署就变复杂了,所以降低了部署的效率。但部署效率可以通过自动化的手段来得到弥补,而开发则没法自动化。另一方面,每个微服务都是一个独立的进程,从而在应用进程层面隔离了资源冲突,提升了程序运行的 “安全” 性。 **运维效率** ,可以从 “检查”“诊断” 和 “处理” 三个方面来看。
+微服务化一方面提升了整体的开发效率,但因为服务多了,部署就变复杂了,所以降低了部署的效率。但部署效率可以通过自动化的手段来得到弥补,而开发则没法自动化。另一方面,每个微服务都是一个独立的进程,从而在应用进程层面隔离了资源冲突,提升了程序运行的 “安全” 性。**运维效率**,可以从 “检查”“诊断” 和 “处理” 三个方面来看。
 
 一个运行的系统,是一个有生命力的系统,并有其生命周期。在其生命周期内,我们需要定期去做检查,以得到系统的 “生命体征” 的多维度信息数据汇总,以供后续的诊断分析。
 
@@ -68,7 +68,7 @@
 - 变更:修改配置或回滚程序版本;
 - 限制:故障断路或过载限流。
 
-**运行效率** ,关键就是提高程序的 “响应性”,若是服务还包括其 “吞吐量”。
+**运行效率**,关键就是提高程序的 “响应性”,若是服务还包括其 “吞吐量”。
 
 程序运行的高效率,也即高响应、高吞吐能力,所有的优化手段都可以从下面两个维度来分类:
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25437\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25437\350\256\262.md"
index 540265cd1..6a011a02d 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25437\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25437\350\256\262.md"
@@ -43,7 +43,7 @@
 
 成为程序员后,你一开始可能会习惯于一个人完成系统开发,自己做架构设计、技术选型、代码调测,最后发布上线,但这只适合代码量在一定范围内的系统开发模式。在一定范围内,你可以实现从头到尾“一条龙”做完,并对系统的每一处细节都了如指掌,但当系统规模变大后,需要更多的人共同参与时,整个设计和开发的模式就完全不一样了。
 
-一定规模的系统,下面又会划分子系统,子系统又可能由多个服务构成,而每个服务又有多个模块,每个模块包含多个对象。比如,我现在所在团队负责的产品,就由数个系统、十数个子系统、上百个服务构成,这样规模的系统就不太可能光靠一个人来设计的,而是在不同的层次上都由不同的人来共同参与设计并开发完成的。 **规模化的设计思路,一方面是自顶向下去完成顶层设计** 。顶层设计主要做两件事:
+一定规模的系统,下面又会划分子系统,子系统又可能由多个服务构成,而每个服务又有多个模块,每个模块包含多个对象。比如,我现在所在团队负责的产品,就由数个系统、十数个子系统、上百个服务构成,这样规模的系统就不太可能光靠一个人来设计的,而是在不同的层次上都由不同的人来共同参与设计并开发完成的。**规模化的设计思路,一方面是自顶向下去完成顶层设计** 。顶层设计主要做两件事:
 
 - 一是去建立系统的边界。系统提供什么?不提供什么?以怎样的形式提供?
 - 二是去划定系统的区域。也就是系统的层次与划分,以及它们之间的通信路径。
@@ -52,7 +52,7 @@
 
 而系统的区域划分,也是为了让系统内部各部分之间的耦合降低,从而让开发人员在属于自己的区域内更自由地发挥。而在区域内的 “控球”“传球” 与 “跑位”,就更多属于开发人员个体能力的发挥,这个过程中区域的大小、边界都可能发生变化,进而导致区域之间的通信路径也跟随变化。这样的变化,就属于自底向上的演化过程。
 
-所以, **规模化设计思路的另一面,就是要让系统具备自底向上的演化机能** 。因为,自顶向下的设计是前瞻性的设计,但没有人能做到完美的前瞻性设计;而自底向上的演化机能,是后验性的反应,它能调整修复前瞻性设计中可能的盲点缺陷。
+所以,**规模化设计思路的另一面,就是要让系统具备自底向上的演化机能** 。因为,自顶向下的设计是前瞻性的设计,但没有人能做到完美的前瞻性设计;而自底向上的演化机能,是后验性的反应,它能调整修复前瞻性设计中可能的盲点缺陷。
 
 记得,我曾经看过一个视频名字大概是 “梅西的十大不可思议进球”,视频里的每一个进球都体现了梅西作为超级明星球员的价值,而在前面提及的万维钢的文章中,有一个核心观点:“普通的团队指望明星,最厉害的球队依靠系统”。其实二者并不矛盾,好的系统不仅依靠 “明星” 级的前瞻顶层设计,也指望 “明星” 级的底层演化突破能力。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25438\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25438\350\256\262.md"
index 856b85ae4..a3d3f44f8 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25438\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25438\350\256\262.md"
@@ -2,7 +2,7 @@
 
 写了多年代码,做了好多的工程,不停地完成项目,但如果你一直仅仅停留在重复这个过程,那么就不会得到真正的成长与提高。你得从这些重复做工程的过程中,抽象提炼出真正解决问题的工程思维,用来指导未来的工程实践。
 
-什么是 **工程思维** ?我从自己过往经验中提炼出的理解是: **一种具备科学理论支撑,并成体系的系统化思维** 。做了多年的软件开发工程,碰到和解决了数不清的问题,最终这些问题,我发现稍微抽象一下,可以归为以下两类:
+什么是 **工程思维**?我从自己过往经验中提炼出的理解是: **一种具备科学理论支撑,并成体系的系统化思维** 。做了多年的软件开发工程,碰到和解决了数不清的问题,最终这些问题,我发现稍微抽象一下,可以归为以下两类:
 
 1. 可以简单归因的问题:属于直接简单的因果关系;
 1. 难以简单归因的问题:属于间接复杂的因果关系。
@@ -33,7 +33,7 @@
 
 这就是用科学思维来指导工程实践,科学理论指出方向,探明边界,工程实践在边界的约束范围内修通道路,达成目标。正如前面故事中,造船理论往大的方向走也有其极限,因为除了能源利用率的经济性外,越大的船对其他建造、施工和运营方面也会带来边际成本的提高,所以也就没法一直往大里造,这就是工程现实的约束。
 
-所以,理论的意义不在于充当蓝图,而在于为工程设计实践提供有约束力的原理;而工程设计则依循一切有约束力的理论,为实践作切实可行的筹划。 **简言之,科学理论确定了上限,工程实践画出了路线** 。
+所以,理论的意义不在于充当蓝图,而在于为工程设计实践提供有约束力的原理;而工程设计则依循一切有约束力的理论,为实践作切实可行的筹划。**简言之,科学理论确定了上限,工程实践画出了路线** 。
 
 ## 系统与反馈
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25439\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25439\350\256\262.md"
index 9776f7f99..92038d5fd 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25439\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25439\350\256\262.md"
@@ -2,7 +2,7 @@
 
 从今天起,咱们专栏即进入第 4 个大主题——“ **徘徊:道中彷徨** ”。成长的途中,我们总会面临很多的困扰与惶惑,这些困扰和彷徨很多都关乎选择,只有了解并认清这类困惑,我们才可能做出最合适的选择。
 
-职业生涯的路上,每个人都会碰到 **职业倦怠期** ,我也不例外。曾经好几次,我都陷入其中。如今从中摆脱出来后,我就想尝试搞清楚这样一种状态的根源,思考一种方法来缩短它持续的时间,或者说增加它出现的时间间隔。
+职业生涯的路上,每个人都会碰到 **职业倦怠期**,我也不例外。曾经好几次,我都陷入其中。如今从中摆脱出来后,我就想尝试搞清楚这样一种状态的根源,思考一种方法来缩短它持续的时间,或者说增加它出现的时间间隔。
 
 那职业倦怠到底是怎样的一种感受呢?
 
@@ -10,11 +10,11 @@
 
 1974 年,美国临床心理学家弗罗伊登贝格尔(Herbert J. Freudenberger)首次提出“ **职业倦怠** ”的概念,用来指 **人面对过度工作时产生的身体和情绪的极度疲劳** 。
 
-职业倦怠感想必你也不陌生,一般将可以明显感知到的分为两种。 **一种是短期的倦怠感** 。它出现的状态,可以用两个英文单词来形象地表达:Burnout(燃尽,精疲力尽)和 Overwhelm(难以承受)。
+职业倦怠感想必你也不陌生,一般将可以明显感知到的分为两种。**一种是短期的倦怠感** 。它出现的状态,可以用两个英文单词来形象地表达:Burnout(燃尽,精疲力尽)和 Overwhelm(难以承受)。
 
 作为程序员的我们想必最能感知这样的状态,因为我们处在现代信息工业时代的最前沿,快节奏、高压力、大变化的环境很是常见。应对这样的环境,我们就需要更多的 “燃料” 和更强的承受力。但有时,环境压力突然增加,短期内超出了我们的负载能力,难免出现“燃尽”(Burnout)的时刻,并感到难以承受(Overwhelming)。
 
-此时,就进入了短时的倦怠期。这种短期的倦怠感觉其实和感冒差不多常见,年年都能碰上一两次,应对的办法其实也很简单:休个年假,脱离当前的环境,换换节奏,重新补充 “燃料”,恢复精力。就像感冒,其实并不需要什么治疗,自然就能恢复。人,无论生理还是心理,都是一个 “反脆弱” 体,“凡不能打垮我的,必使我更强大”。 **另一种更可怕的倦怠感是长期的** ,它与你对当前工作的感受有关。
+此时,就进入了短时的倦怠期。这种短期的倦怠感觉其实和感冒差不多常见,年年都能碰上一两次,应对的办法其实也很简单:休个年假,脱离当前的环境,换换节奏,重新补充 “燃料”,恢复精力。就像感冒,其实并不需要什么治疗,自然就能恢复。人,无论生理还是心理,都是一个 “反脆弱” 体,“凡不能打垮我的,必使我更强大”。**另一种更可怕的倦怠感是长期的**,它与你对当前工作的感受有关。
 
 有些人把 “上班” 看作工作的全部,那么这样的人去上班一般都是被动的、勉强的。这样的人就是普遍存在的 “混日子” 的上班族,虽不情愿,但又没有别的办法,毕竟不能失去这份工作的收入。而这种 “混日子” 的状态,其实就是处在一种长期的职业倦怠期。
 
@@ -56,7 +56,7 @@
 - 职业生涯 Career
 - 工作岗位 Job
 
-**目的意义** ,这是工作的终极之问。它决定了你的很多选择,以及你接受什么、拒绝什么,是工作愿景背后的灵魂所在。每个人工作都会有自己的目的与意义,而且还会随着工作过程发生变化(或者说进化更合适些)。追寻目的与意义,这可能是你、我一生的工作。 **职业生涯** ,是个人一生职业愿望的追寻过程。它由长期目标驱动,是追寻 “目的意义” 的一条你所期望的路径。而这条路径并不唯一,它因人而异,因你的 “目的意义” 而异。它构建在你工作过程中的所有经历、经验、决策、知识、技能与时运的基础之上。 **工作岗位** ,这不过是你现在上班的地方,包括了位置、角色、关系、职责与薪酬的总和。
+**目的意义**,这是工作的终极之问。它决定了你的很多选择,以及你接受什么、拒绝什么,是工作愿景背后的灵魂所在。每个人工作都会有自己的目的与意义,而且还会随着工作过程发生变化(或者说进化更合适些)。追寻目的与意义,这可能是你、我一生的工作。**职业生涯**,是个人一生职业愿望的追寻过程。它由长期目标驱动,是追寻 “目的意义” 的一条你所期望的路径。而这条路径并不唯一,它因人而异,因你的 “目的意义” 而异。它构建在你工作过程中的所有经历、经验、决策、知识、技能与时运的基础之上。**工作岗位**,这不过是你现在上班的地方,包括了位置、角色、关系、职责与薪酬的总和。
 
 这三个区域会有交集,这里我举个实际的例子。假如你工作的 “目的意义” 非常现实,就是希望有更多的收入改善家庭生活,住更大的房子,开更好的车。而现在的 “工作岗位” 能够提供这样让你满意的收入水平,那么你就会感到 “快乐幸福”。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25441\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25441\350\256\262.md"
index 64058642e..2a707ceb0 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25441\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25441\350\256\262.md"
@@ -42,7 +42,7 @@
 
 ### 1. 以理服人
 
-首先,把握一个度, **对事不对人** ,切勿意气用事。
+首先,把握一个度,**对事不对人**,切勿意气用事。
 
 有些程序员之间的分歧点是非常诡异的,这可能和程序员自身的洁癖、口味和偏好有关。比如:大小写啦、命名规则啦、大括号要不要独立一行啦、驼峰还是下划线啦、Tab 还是空格啦,这些都能产生分歧。
 
@@ -98,13 +98,13 @@
 - 形式
 - 风格
 
-**从内容上看** ,虽说你想沟通的本质是同一样东西或事情,但针对不同的人,你就需要准备不同的内容。比如,同内行与外行谈同一个技术方案,内容就是不同的。这里就需要你发挥同理心和换位思考的能力。保罗·格雷厄姆(Paul Graham)曾在他的书《黑客与画家》中写道:
+**从内容上看**,虽说你想沟通的本质是同一样东西或事情,但针对不同的人,你就需要准备不同的内容。比如,同内行与外行谈同一个技术方案,内容就是不同的。这里就需要你发挥同理心和换位思考的能力。保罗·格雷厄姆(Paul Graham)曾在他的书《黑客与画家》中写道:
 
 > 判断一个程序员是否具备 “换位思考” 的能力有一个好方法,那就是看他怎样向没有技术背景的人解释技术问题。
 
-换位思考本质上就是沟通技巧中的一种。 **从形式上看** ,沟通其实不局限于面对面的谈话。面对面交谈是一种形式,书面写作又是另外一种形式,连写代码本身都是在和未来的自己或某个你尚未谋面的程序员沟通。
+换位思考本质上就是沟通技巧中的一种。**从形式上看**,沟通其实不局限于面对面的谈话。面对面交谈是一种形式,书面写作又是另外一种形式,连写代码本身都是在和未来的自己或某个你尚未谋面的程序员沟通。
 
-程序员确实有很多都不擅长面对面的沟通形式。面对面沟通的场景是很复杂的,因为这种沟通中交流传递的载体不仅仅是言语本身,眼神、姿态、行为、语气、语调高低,甚至一种很虚幻的所谓“气场”,都在传递着各种不同的信息。而大部分人都不具备这种同时控制好这么多传递渠道的能力,也即我们通常说的“缺乏控场能力”,这里面隐含着对你其他能力的要求,比如:临场应变、思维的活跃度与变化等。 **从风格上看** ,不同方式和场景的沟通可以有不同的风格。比如面对面沟通,有一对一的私下沟通,风格就可以更随性柔和些;也有一对多的场景,比如演讲、汇报和会议,风格就要正式一些,语言的风格可能就需要更清晰、准确和锐利一些。
+程序员确实有很多都不擅长面对面的沟通形式。面对面沟通的场景是很复杂的,因为这种沟通中交流传递的载体不仅仅是言语本身,眼神、姿态、行为、语气、语调高低,甚至一种很虚幻的所谓“气场”,都在传递着各种不同的信息。而大部分人都不具备这种同时控制好这么多传递渠道的能力,也即我们通常说的“缺乏控场能力”,这里面隐含着对你其他能力的要求,比如:临场应变、思维的活跃度与变化等。**从风格上看**,不同方式和场景的沟通可以有不同的风格。比如面对面沟通,有一对一的私下沟通,风格就可以更随性柔和些;也有一对多的场景,比如演讲、汇报和会议,风格就要正式一些,语言的风格可能就需要更清晰、准确和锐利一些。
 
 沟通之难就在于清晰地传递内容和观点。当你要向其他人详细解释某样东西的时候,你经常会惊讶地发现你有多无知,于是,你不得不开始一个全新的探索过程。这一点可以从两个方面来体会:
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25442\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25442\350\256\262.md"
index 10e6a1646..84296dd89 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25442\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25442\350\256\262.md"
@@ -83,7 +83,7 @@
 
 技术也许会停滞,技能也可能会过时,但其中的能力却可以沉淀下来,应用于下一代的技能之上。
 
-汉语中容易把能力和技能混为一谈,在英语中,技能对应的词是 Skill,而能力对应的是 Ability。 **技能是你习得的一种工具,那么能力就是你运用工具的思考和行为方式** ,它是你做成一件事并取得成果的品质。
+汉语中容易把能力和技能混为一谈,在英语中,技能对应的词是 Skill,而能力对应的是 Ability。**技能是你习得的一种工具,那么能力就是你运用工具的思考和行为方式**,它是你做成一件事并取得成果的品质。
 
 程序员爱说自己是手艺人,靠手艺总能吃口饭。五百年前,鞋匠也是手艺人,但进入工业革命后,制鞋基本就由机器取代了。手工制鞋是一门技能,它的过时用了几百年时间,但如何做一双好鞋的能力是不会过时的,五百年后人们还是要穿鞋,还要求穿更好的鞋。这时鞋匠需要应对的变化是:换一种工具(现代流水线机器生产)来制作好鞋。而现代化的制鞋机器技术实际上还进一步放大了好鞋匠的能力,提升了他们的价值。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25444\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25444\350\256\262.md"
index 312b402bc..41280f2c7 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25444\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25444\350\256\262.md"
@@ -58,11 +58,11 @@
 
 如果产品无法获得市场的价值认同,技术作品自然也就埋没其中了。
 
-有个说法是:要做好技术需要懂业务和产品。这大体没什么问题,但需要提到的细节是懂的方向。技术不需要了解业务产品的每一个显性特征,一个足够大的业务产品,有无数的显性特征细节,这些全部的细节可能分散在一群各自分工的产品经理们中。所以应该说, **技术需要懂的是产品提供的核心服务和流程,并清晰地将其映射到技术的支撑能力与成本上** 。
+有个说法是:要做好技术需要懂业务和产品。这大体没什么问题,但需要提到的细节是懂的方向。技术不需要了解业务产品的每一个显性特征,一个足够大的业务产品,有无数的显性特征细节,这些全部的细节可能分散在一群各自分工的产品经理们中。所以应该说,**技术需要懂的是产品提供的核心服务和流程,并清晰地将其映射到技术的支撑能力与成本上** 。
 
 另外,技术不仅仅需要支撑满足产品的全部显性和隐性服务特性,这些对于技术都相当于显性服务特性。而技术还有自己的隐性服务特性,这一点恰恰也正是高级和资深程序员需要重点关注的。所谓技术的隐性特性,通俗点就是程序员常说的非功能性需求,它的产生与来源都是源自程序和程序员本身。
 
-用一段新算法实现的函数取代了旧函数,那么多余的旧函数就变成了负债而非资产,是需要去清理的。重构代码变得更简洁和优雅,可读性增强,节省了未来的维护成本。一个能同时服务一万人的程序实例,你知道你没法加十个实例就能简单变成能同时服务十万人的系统。这些都是技术冰山下的隐性特征, **显性的错误会有测试、产品甚至最终用户来帮你纠正,但隐性的错误却很难有人能及时帮你发现并纠正** 。
+用一段新算法实现的函数取代了旧函数,那么多余的旧函数就变成了负债而非资产,是需要去清理的。重构代码变得更简洁和优雅,可读性增强,节省了未来的维护成本。一个能同时服务一万人的程序实例,你知道你没法加十个实例就能简单变成能同时服务十万人的系统。这些都是技术冰山下的隐性特征,**显性的错误会有测试、产品甚至最终用户来帮你纠正,但隐性的错误却很难有人能及时帮你发现并纠正** 。
 
 产品的显性特性就如泰坦尼克号,而技术的隐性特性则是泰坦尼克号撞上冰山后的反应。一旦隐性的错误爆发,就像泰坦尼克号撞上了冰山,一切外显的繁华最终都将沉入海底。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25445\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25445\350\256\262.md"
index 06aef220d..fde73eb42 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25445\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25445\350\256\262.md"
@@ -32,7 +32,7 @@
 
 ## 多种困境
 
-从感性和理性两个角度认知和分析了代码评审的好处,但其适用的场景和花费的成本代价也需要去平衡。除了这点,如果把代码评审作为一个必要环节引入到研发流程中,也许还有一些关于如何实施代码评审的困境。 **困境一** ,项目期限 Deadline 已定,时间紧迫,天天加班忙成狗了,谁还愿意搞代码评审?这是一个最常见的客观阻碍因素,因为 Deadline 很多时候都不是我们自己能确定和改变的。 **困境二** ,即使强制推行下去,如何保障其效果?团队出于应付,每次走个过场,那么也就失去了评审的初衷和意义。 **困境三** ,团队人员结构搭配不合理,新人没经验的多,有经验的少。新人交叉评审可能效果不好,而老是安排经验多的少数人帮助 Review 多数新人的代码,新人或有收获,但对高级或资深程序员又有多大裨益?一个好的规则或制度,总是需要既符合多方参与者的个体利益又能满足组织或团队的共同利益,这样的规则或制度才能更顺畅、有效地实施和运转。 **困境四** ,有人就是不喜欢别人 Review 他的代码,他会感觉是在找茬。比如,团队中存在一些自信超强大的程序员,觉得自己写的代码绝对没问题,不需要别人来给他 Review。
+从感性和理性两个角度认知和分析了代码评审的好处,但其适用的场景和花费的成本代价也需要去平衡。除了这点,如果把代码评审作为一个必要环节引入到研发流程中,也许还有一些关于如何实施代码评审的困境。**困境一**,项目期限 Deadline 已定,时间紧迫,天天加班忙成狗了,谁还愿意搞代码评审?这是一个最常见的客观阻碍因素,因为 Deadline 很多时候都不是我们自己能确定和改变的。**困境二**,即使强制推行下去,如何保障其效果?团队出于应付,每次走个过场,那么也就失去了评审的初衷和意义。**困境三**,团队人员结构搭配不合理,新人没经验的多,有经验的少。新人交叉评审可能效果不好,而老是安排经验多的少数人帮助 Review 多数新人的代码,新人或有收获,但对高级或资深程序员又有多大裨益?一个好的规则或制度,总是需要既符合多方参与者的个体利益又能满足组织或团队的共同利益,这样的规则或制度才能更顺畅、有效地实施和运转。**困境四**,有人就是不喜欢别人 Review 他的代码,他会感觉是在找茬。比如,团队中存在一些自信超强大的程序员,觉得自己写的代码绝对没问题,不需要别人来给他 Review。
 
 以上种种,仅仅是我过去经历的一些执行代码评审时面临的困境与障碍,我们需要找到一条路径来绕过或破除这样的障碍与困境。
 
@@ -68,7 +68,7 @@ Google 以一种强硬的姿态来制定了关于代码评审的规则,规则
 
 剑是好剑,但还需要配合好剑客与好剑法。
 
-即使在最差的环境下,完全没有人关心代码评审这件事,一个有追求的程序员依然可以做到一件事,自己给自己 Review。就像写文章,我写完一篇文章不会立刻发布,而是从头脑中放下(Unload),过上一段时间,也许是几天后,再自己重新细读一遍,改掉其中必然会出现的错别字或文句不通畅之处,甚或论据不充分或逻辑不准确的地方,因为我知道不管我写了多少文字,总还会有这些 Bug,这就是给自己的 Review。 **给自己 Review 是一种自省,自我的成长总是从自省开始的。**
+即使在最差的环境下,完全没有人关心代码评审这件事,一个有追求的程序员依然可以做到一件事,自己给自己 Review。就像写文章,我写完一篇文章不会立刻发布,而是从头脑中放下(Unload),过上一段时间,也许是几天后,再自己重新细读一遍,改掉其中必然会出现的错别字或文句不通畅之处,甚或论据不充分或逻辑不准确的地方,因为我知道不管我写了多少文字,总还会有这些 Bug,这就是给自己的 Review。**给自己 Review 是一种自省,自我的成长总是从自省开始的。**
 
 代码评审,能提升质量,降低缺陷;代码评审,也能传播知识,促进沟通;代码评审,甚至还能影响心理,端正姿势。代码评审,好处多多,让人寄予希望,执行起来却又不免哀伤,也许正是因为每一次评审的收益是不确定的、模糊的,但付出的代价却是固定的,包括固定的评审时间、可能延期的发布等。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25446\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25446\350\256\262.md"
index c63c0137d..08d9785e9 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25446\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25446\350\256\262.md"
@@ -54,7 +54,7 @@
 
 最可怕的失业就来自变革引发的技能性淘汰(如:国企下岗),其次是环境引发的萧条(如:金融危机),再次是技能虽然还有普适价值,但自身却适应不了环境变化带来的改变与调整(如:Tony 的危机)。
 
-Tony 面对的危机还是比较少见,属于个人问题。而金融危机也不多见,面对萧条 “血”(储蓄)够厚也可以撑得过去。只有第一种,技能性淘汰,积重难返。四十而不惑,不过四十岁程序员的悲哀在于,他们拥有十五年以上的经历与经验,有时却在和只有五年经验的年轻程序员竞争同样的岗位。 **中年人和年轻人本应在不同的战场上。年轻时,拼的是体力、学习力和适应能力,是做解答题的效率与能力;中年了,拼的是脑力、心力和决策能力,是做对选择题的概率。** 年轻时,是用体力和时间积累经历,换取成长与机会。就拿我来说,从年轻到中年我的体力状态变化是:20 岁以前可以通宵游戏后再接着上一天课;30 岁以前连续一个月加班通宵颠倒,睡一觉后就又精神满满;35 岁以前,还能上线到凌晨两、三点,睡上几小时后,早上 9 点又正常上班;35 岁以后,就要尽可能保持规律作息,否则可能第二天就精神不振了。
+Tony 面对的危机还是比较少见,属于个人问题。而金融危机也不多见,面对萧条 “血”(储蓄)够厚也可以撑得过去。只有第一种,技能性淘汰,积重难返。四十而不惑,不过四十岁程序员的悲哀在于,他们拥有十五年以上的经历与经验,有时却在和只有五年经验的年轻程序员竞争同样的岗位。**中年人和年轻人本应在不同的战场上。年轻时,拼的是体力、学习力和适应能力,是做解答题的效率与能力;中年了,拼的是脑力、心力和决策能力,是做对选择题的概率。** 年轻时,是用体力和时间积累经历,换取成长与机会。就拿我来说,从年轻到中年我的体力状态变化是:20 岁以前可以通宵游戏后再接着上一天课;30 岁以前连续一个月加班通宵颠倒,睡一觉后就又精神满满;35 岁以前,还能上线到凌晨两、三点,睡上几小时后,早上 9 点又正常上班;35 岁以后,就要尽可能保持规律作息,否则可能第二天就精神不振了。
 
 所以,中年了体力下降是自然生理规律,但和脑力有关的学习能力并不会有明显改变。记得以前看过一篇万维钢的文章讲了一本书《成年大脑的秘密生活:令人惊讶的中年大脑天赋》,其中提到:
 
@@ -68,6 +68,6 @@ Tony 面对的危机还是比较少见,属于个人问题。而金融危机也
 
 简言之,人到中年,转换了战场,重新定位自己的优势,转变核心竞争力,浴火重生,开启人生的下半场。
 
-年轻时,我们打的是突击站,左冲右突;中年了,我们打的是阵地战,稳步推进;如今,我们进入了人生的中场战事。 **这场战事从谋生的恐惧感开始,给予我们警示;到舍生的无惧感,让我们摆脱束缚,整装待发;最后经过重生的安全感,推动我们再次上升** 。
+年轻时,我们打的是突击站,左冲右突;中年了,我们打的是阵地战,稳步推进;如今,我们进入了人生的中场战事。**这场战事从谋生的恐惧感开始,给予我们警示;到舍生的无惧感,让我们摆脱束缚,整装待发;最后经过重生的安全感,推动我们再次上升** 。
 
 关于中年之惑,你有哪些看法呢?
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25447\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25447\350\256\262.md"
index 93966f436..99635b33f 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25447\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25447\350\256\262.md"
@@ -30,7 +30,7 @@
 
 到了 C 轮,公司上市的可能性大大增强,前景可期。但这时加入,能拿到的比例进一步下降一个量级到 0.005%,如果这时公司的上市预期市值就 100 亿,估计也吸引不到什么人了。
 
-变身土豪,其实需要的是增值 100 倍的机会,而最低的下注金额是一年的收入。加入创业公司就是用你的时间下注,能否撞上 100 倍的机会,很多时候就是靠时运。 **因上努力,果上随缘,尽人事,听天命** 。
+变身土豪,其实需要的是增值 100 倍的机会,而最低的下注金额是一年的收入。加入创业公司就是用你的时间下注,能否撞上 100 倍的机会,很多时候就是靠时运。**因上努力,果上随缘,尽人事,听天命** 。
 
 ### 3. 追求梦想
 
@@ -48,15 +48,15 @@
 
 搞清楚了自身的期望与需要付出的成本和代价,再来理性地看看其他方面的因素。
 
-1、 **创始人创业的目的是什么** ? **期望是什么** ?创业毕竟是一段长期的旅程,大家的目的、价值观、期望差距太大,必然走不长远,身边就目睹过这样的例子。
+1、 **创始人创业的目的是什么**?**期望是什么**?创业毕竟是一段长期的旅程,大家的目的、价值观、期望差距太大,必然走不长远,身边就目睹过这样的例子。
 
-2、 **创始人以前的口碑和信用如何** ?有信用污点的人并不值得合作与跟随,而且前面说的创业公司期权,最终能否兑现?就国内的创业环境而言,这一点也很是关键。
+2、 **创始人以前的口碑和信用如何**?有信用污点的人并不值得合作与跟随,而且前面说的创业公司期权,最终能否兑现?就国内的创业环境而言,这一点也很是关键。
 
-3、 **公司的核心团队成员如何** ?看看之前都有些什么样的人,你对这个团队的感觉契合么?价值观对味么?这个团队是合作融洽,还是各怀鬼胎?有些小公司,人虽不多,办公室政治比大公司还厉害。
+3、 **公司的核心团队成员如何**?看看之前都有些什么样的人,你对这个团队的感觉契合么?价值观对味么?这个团队是合作融洽,还是各怀鬼胎?有些小公司,人虽不多,办公室政治比大公司还厉害。
 
-4、 **对你的定位是什么** ?创业公司在发展初期容易遇到技术瓶颈,会以招 CTO 的名义,来找一个能解决当前技术瓶颈的专业人才。也许你会被名头(Title)吸引,解决完问题,渡过了这个瓶颈,但这之后老板会觉得你的价值还足够大么?有句话是这么说的:“技术总是短期被高估,长期被低估”。而你自身还能跟得上公司的发展需要么?
+4、 **对你的定位是什么**?创业公司在发展初期容易遇到技术瓶颈,会以招 CTO 的名义,来找一个能解决当前技术瓶颈的专业人才。也许你会被名头(Title)吸引,解决完问题,渡过了这个瓶颈,但这之后老板会觉得你的价值还足够大么?有句话是这么说的:“技术总是短期被高估,长期被低估”。而你自身还能跟得上公司的发展需要么?
 
-5、 **公司是否有明确的业务方向** ? **业务的天花板多高** ? **有哪些对手** ? **相对竞争的核心优势是什么** ?很多做技术的同学都不太关心业务的商业模式,也许这在大公司可以,毕竟船大,一般也就感觉不到风浪。但在创业公司则不然,业务的天花板有多高?也就是能做到多大?如果公司业务没有明确的方向和优势,你憧憬着踏上了火箭,结果却是小舢板,起风时感觉还走得挺快,风一停,就只好随波荡漾了。
+5、 **公司是否有明确的业务方向**?**业务的天花板多高**?**有哪些对手**?**相对竞争的核心优势是什么**?很多做技术的同学都不太关心业务的商业模式,也许这在大公司可以,毕竟船大,一般也就感觉不到风浪。但在创业公司则不然,业务的天花板有多高?也就是能做到多大?如果公司业务没有明确的方向和优势,你憧憬着踏上了火箭,结果却是小舢板,起风时感觉还走得挺快,风一停,就只好随波荡漾了。
 
 也许还有很多其他方面你关注的条件和因素,在选择前都可以先列出来比较。只是最后我比较确定的一件事是,不会有任何一家公司满足你所有心仪的条件,这时就需要你做决策了。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25448\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25448\350\256\262.md"
index e648ecdcd..cb425a4b9 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25448\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25448\350\256\262.md"
@@ -12,9 +12,9 @@
 
 但总的来说,来这里接单的很大一部分程序员应该都是想要赚点工资之外的钱吧。赚钱本无错,只是程序员除了接兼职外包项目还有什么其他赚钱方式吗?我想了想,程序员赚钱的方式大概有下面这些。
 
-**咨询 / 培训** 。一般被外部企业邀请去做咨询或培训的程序员,根据个体差异可能报酬从几千到几万不等吧,但能够提供此类服务的程序员,对其本身的素质要求较高,而且来源不稳定,所以不具有普适性。 **演讲 / 分享** 。程序员圈子经常会有一些技术分享大会,有些组织者会对提供分享的讲师支付一点报酬,具体数额可能因“会”而异吧,但一般不会比咨询和培训类更多。 **投稿 / 翻译** 。一些写作和英语能力都不错的程序员可以向技术媒体去投稿或翻译稿件。原创千字标准一百五左右,而翻译会更低些,看译者的水平从千字几十到一百左右。 **写书** 。也有不少程序员写书出版的,但基本都是技术类图书。对于图书版税,一个非著名作者不太可能超过 10%,而能卖到一万册的国内技术书籍其实并不多,假如一本书销售均价 50 元,那你可以自己算下大概写一本书能挣多少。畅销和长销的技术类图书,基本都成了教材,而现实中要写一本优秀的教材保持十数年长盛不衰,是件极困难的事。 **写博客 / 公众号** 。十年前大家写博客,现在很多人都写公众号。博客是免费阅读,靠广告流量分成赚钱,但其实几乎就没几个有流量的独立博客,都是聚合性的博客站赚走了这部分钱。
+**咨询 / 培训** 。一般被外部企业邀请去做咨询或培训的程序员,根据个体差异可能报酬从几千到几万不等吧,但能够提供此类服务的程序员,对其本身的素质要求较高,而且来源不稳定,所以不具有普适性。**演讲 / 分享** 。程序员圈子经常会有一些技术分享大会,有些组织者会对提供分享的讲师支付一点报酬,具体数额可能因“会”而异吧,但一般不会比咨询和培训类更多。**投稿 / 翻译** 。一些写作和英语能力都不错的程序员可以向技术媒体去投稿或翻译稿件。原创千字标准一百五左右,而翻译会更低些,看译者的水平从千字几十到一百左右。**写书** 。也有不少程序员写书出版的,但基本都是技术类图书。对于图书版税,一个非著名作者不太可能超过 10%,而能卖到一万册的国内技术书籍其实并不多,假如一本书销售均价 50 元,那你可以自己算下大概写一本书能挣多少。畅销和长销的技术类图书,基本都成了教材,而现实中要写一本优秀的教材保持十数年长盛不衰,是件极困难的事。**写博客 / 公众号** 。十年前大家写博客,现在很多人都写公众号。博客是免费阅读,靠广告流量分成赚钱,但其实几乎就没几个有流量的独立博客,都是聚合性的博客站赚走了这部分钱。
 
-而公众号开创了阅读打赏模式,有些人看见一些超级大 V 随便写篇文章就有几千人赞赏,觉得肯定赚钱。但其实写公众号的人真没有靠赞赏赚钱的,赞赏顶多算个正向鼓励罢了。一个拥有十万读者的公众号,实际平均每篇的打赏人数可能不到 50 人,而平均打赏单价可能不到 5 元。这么一算,假如一篇文章 2000 字,还不如投稿的稿费多。所以持续的博客或公众号写作基本靠兴趣,而能积累起十万读者的程序员几乎属于万中无一吧。 **课程 / 专栏** 。这是今年才兴起的形式,一些有技术积累和总结表达(包括:写和讲)能力的程序员有机会抓住这个形式的一些红利,去把自己掌握的知识和经验梳理成作品出售。但能通过这个形式赚到钱的程序员,恐怕也是百里挑一的,普适性和写书差不多。 **兼职 / 外包** 。这就是前面说的外包平台模式,平台发布项目,程序员注册为签约开发者,按人天标价,自己给自己的人天时间估值。我看平台上的跨度是一天从 300 到 2000 不等。
+而公众号开创了阅读打赏模式,有些人看见一些超级大 V 随便写篇文章就有几千人赞赏,觉得肯定赚钱。但其实写公众号的人真没有靠赞赏赚钱的,赞赏顶多算个正向鼓励罢了。一个拥有十万读者的公众号,实际平均每篇的打赏人数可能不到 50 人,而平均打赏单价可能不到 5 元。这么一算,假如一篇文章 2000 字,还不如投稿的稿费多。所以持续的博客或公众号写作基本靠兴趣,而能积累起十万读者的程序员几乎属于万中无一吧。**课程 / 专栏** 。这是今年才兴起的形式,一些有技术积累和总结表达(包括:写和讲)能力的程序员有机会抓住这个形式的一些红利,去把自己掌握的知识和经验梳理成作品出售。但能通过这个形式赚到钱的程序员,恐怕也是百里挑一的,普适性和写书差不多。**兼职 / 外包** 。这就是前面说的外包平台模式,平台发布项目,程序员注册为签约开发者,按人天标价,自己给自己的人天时间估值。我看平台上的跨度是一天从 300 到 2000 不等。
 
 各种赚钱方式,分析了一圈下来,发现其实对于大部分程序员而言,最具普适性的还是兼职外包方式。因为其他方式都需要编程之外的一些其他技能,而且显然兼职外包方式相比较而言属于赚钱效率和收入最高的一种方式,无怪乎会有那么多程序员去外包平台注册为签约开发者。
 
@@ -44,13 +44,13 @@
 
 ## 值钱与选择
 
-该不该接外包的选择本质是: **选择做赚钱的事,还是值钱的事** ?
+该不该接外包的选择本质是: **选择做赚钱的事,还是值钱的事**?
 
 梁宁有篇文章就叫《赚钱的事和值钱的事》,文中总结了这两点的差别:
 
 > 赚钱的事,核心是当下的利差,现金现货,将本求利。
 >
-> 值钱的事,核心是结构性价值,兑现时间,在某个未来。 **从赚钱的角度看** ,前面分析的所有赚钱方式的赚钱性价比都很低,完全不值得做。你可能会反驳说,外包项目的收入可能也不低,甚至比你的全职工资还高,怎么会认为赚钱性价比很低呢?一方面,全职工作提供的收入是稳定的;另一方面,兼职外包的收入多是临时的,一次性而不稳定的。若你能持续稳定地获得高于全职工资的外包收入来源,那么仅从赚钱角度看,更好的选择可能应该是去全职做外包了。 **从值钱的角度看** ,前面分析的所有赚钱方式,在以个人价值增值为出发点的前提下,是值得尝试的。正因为兼职外包接单对很多程序员具有普适性,所以针对这件事情的 **出发点应该是看是否以个人价值及其增长为归依** ,而非是为了当下能多赚点钱。过于专注短期的收入提升,可能会“一叶障目”,忽视了长期的价值增值。
+> 值钱的事,核心是结构性价值,兑现时间,在某个未来。**从赚钱的角度看**,前面分析的所有赚钱方式的赚钱性价比都很低,完全不值得做。你可能会反驳说,外包项目的收入可能也不低,甚至比你的全职工资还高,怎么会认为赚钱性价比很低呢?一方面,全职工作提供的收入是稳定的;另一方面,兼职外包的收入多是临时的,一次性而不稳定的。若你能持续稳定地获得高于全职工资的外包收入来源,那么仅从赚钱角度看,更好的选择可能应该是去全职做外包了。**从值钱的角度看**,前面分析的所有赚钱方式,在以个人价值增值为出发点的前提下,是值得尝试的。正因为兼职外包接单对很多程序员具有普适性,所以针对这件事情的 **出发点应该是看是否以个人价值及其增长为归依**,而非是为了当下能多赚点钱。过于专注短期的收入提升,可能会“一叶障目”,忽视了长期的价值增值。
 
 为了多赚点外快牺牲当下所有的业余时间,这值得吗?这种兼职外包项目对于自身的价值增值有多大的帮助?这是你需要反问和思考的问题。我估计很多兼职项目都是低水平的重复劳动,其实不止是兼职,甚至很多全职工作亦是如此。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25449\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25449\350\256\262.md"
index 5c1df3145..25e6a3939 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25449\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25449\350\256\262.md"
@@ -14,7 +14,7 @@
 >
 > 这时,Rod Johnson 发出振聋发聩的一呼:尔等不必向泥胎偶像顶礼膜拜,圣灵正在尔等自身 —— 这就是他在书中一直倡导的 “循证架构”。选择一种架构和种技术的依据是什么?Rod Johnson 认为,应该是基于实践的证据、来自历史项目或亲自试验的经验……
 
-所以,我们去阅读技术干货文章, **想从别人的分享中获得对自己技术方案的一个印证。这就是一种行业的实践证据,毕竟想通过听取分享去印证的,通常都是走过了一条与自己类似的道路** 。技术道路的旅途中充满着迷雾与不确定性,我们不过是想在别人已走过的类似道路中获得指引和启发,并得到迈出坚实下一步的信心。
+所以,我们去阅读技术干货文章,**想从别人的分享中获得对自己技术方案的一个印证。这就是一种行业的实践证据,毕竟想通过听取分享去印证的,通常都是走过了一条与自己类似的道路** 。技术道路的旅途中充满着迷雾与不确定性,我们不过是想在别人已走过的类似道路中获得指引和启发,并得到迈出坚实下一步的信心。
 
 这就是 **循证方式的技术决策路径** 。
 
@@ -78,6 +78,6 @@
 
 简单算一下,大概就是 228.8 万单机长连接的接入能力,14.6 亿怕是以当时全国人口作为预估上限了。实际当然没有那么多,但估计单机百万长连接左右应该是有的。这是一个相当不错的数量了,而采用 Java 技术栈要实现这个单机数量,恐怕也需要多进程,不然大内存堆的 GC 停顿就是一个不可接受和需要单独调优的工作了。
 
-以上就是从干货中提取知识和经验总结的案例,形成对已有知识的连结。这就是不断加固并扩大自己的技术知识体系之网。 **总结来说:面对众多的技术干货,从循证出发,找到参考,做出技术决策,决定后续演进路线;在演进路上,不断切磋,升级思考方式,调整路径,走出合适的道路;在路上,把遇到的独立的知识点,不断吸收连结进入自己的技术知识体系之网** 。
+以上就是从干货中提取知识和经验总结的案例,形成对已有知识的连结。这就是不断加固并扩大自己的技术知识体系之网。**总结来说:面对众多的技术干货,从循证出发,找到参考,做出技术决策,决定后续演进路线;在演进路上,不断切磋,升级思考方式,调整路径,走出合适的道路;在路上,把遇到的独立的知识点,不断吸收连结进入自己的技术知识体系之网** 。
 
 回答了标题的问题,这篇文章也该结束了。面对技术这片大海,我们都是一个渔民,三天打鱼,两天结网。愿你的“网”越结越大,捞的“鱼”也越来越多,也欢迎留言分享下你的“打鱼”和“结网”经历。
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25450\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25450\350\256\262.md"
index 1410e6db9..27d1476a9 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25450\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25450\350\256\262.md"
@@ -14,7 +14,7 @@
 
 刚工作不久,同事和我讨论当用户删除自己的数据时,我们到底应不应该删掉它?我那时觉得理所应当写个 Delete 的 SQL 语句把它删掉。因为当时是这么想的:既然用户都不要他的数据了,我们还把它保留下来做什么呢?不是浪费资源嘛,而且服务器存储资源还算挺贵的。
 
-但今天的互联网大数据时代,用户主动或非主动提交的任何数据,你都别想再将它真正地删除了。这个时代,受益于 **摩尔定律** ,存储设备容量不断增加,而价格不断降低,所有关于用户的数据总是可能有用的,都先存下来再说。
+但今天的互联网大数据时代,用户主动或非主动提交的任何数据,你都别想再将它真正地删除了。这个时代,受益于 **摩尔定律**,存储设备容量不断增加,而价格不断降低,所有关于用户的数据总是可能有用的,都先存下来再说。
 
 做技术这么些年下来,关于技术方案的判断,曾经以为的绝对标准,今天再看都是相对的。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25451\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25451\350\256\262.md"
index 953ab451f..d7b1ff8a4 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25451\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25451\350\256\262.md"
@@ -1,6 +1,6 @@
 # 51 技术债务,有意或无意的选择?
 
-在编程的路上,我们总会碰到历史系统,接手遗留代码,然后就会忍不住抱怨,那我们是在抱怨什么呢?是债务,技术债务。以前说过, **代码既是资产也是债务** ,而历史系统的遗留代码往往是大量技术债务的爆发地。
+在编程的路上,我们总会碰到历史系统,接手遗留代码,然后就会忍不住抱怨,那我们是在抱怨什么呢?是债务,技术债务。以前说过,**代码既是资产也是债务**,而历史系统的遗留代码往往是大量技术债务的爆发地。
 
 然而,技术债务到底是如何产生的?它是我们有意还是无意的选择?这里就先从技术债务的认知开始谈起吧。
 
@@ -18,11 +18,11 @@
 - 战术债务
 - 疏忽债务
 
-**战略债务** ,是为了战略利益故意为之,并长期存在。我理解就是在公司或业务高速发展的阶段,主动放弃了一些技术上的完备与完美性,而保持快速的迭代与试错性。在这个阶段,公司的战略利益是业务的抢占,所以此阶段的公司都有一些类似的口号,比如:先完成,再完美;优雅的接口,糟糕的实现。
+**战略债务**,是为了战略利益故意为之,并长期存在。我理解就是在公司或业务高速发展的阶段,主动放弃了一些技术上的完备与完美性,而保持快速的迭代与试错性。在这个阶段,公司的战略利益是业务的抢占,所以此阶段的公司都有一些类似的口号,比如:先完成,再完美;优雅的接口,糟糕的实现。
 
-这类债务的特点是,负债时间长,但利息不算高且稳定,只要保持长期 “付息”,不还本金也能维持下去。 **战术债务** ,一般是为了应对短期紧急情况采取的折衷办法。这种债务的特点就是高息,其实说高利贷也不为过。
+这类债务的特点是,负债时间长,但利息不算高且稳定,只要保持长期 “付息”,不还本金也能维持下去。**战术债务**,一般是为了应对短期紧急情况采取的折衷办法。这种债务的特点就是高息,其实说高利贷也不为过。
 
-这类债务,一直以来经常碰到。比如,曾经做电信项目时,系统处理工单,主流程上有缺陷,对某一类工单处理会卡住。这时又不太方便停机更新程序,于是就基于系统的动态脚本能力去写了个脚本临时处理这类工单,可以应对当时业务经营的连续性,但缺陷是资源开销大,当超过一定量时 CPU 就会跑满了。这样的技术方案就属于战术债务的应用。为避免“夜长梦多”,当天半夜的业务低谷,我就重新修复上线了新程序,归还了这笔短期临时债务。 **疏忽债务** ,这类债务一般都是无意识的。从某种意义上来说,这就是程序员的成长性债务,随着知识、技能与经验的积累,这类债务会逐步减少。另一方面,如果我们主动创造一个关注技术债务的环境,这类债务就会被有意识地还掉。
+这类债务,一直以来经常碰到。比如,曾经做电信项目时,系统处理工单,主流程上有缺陷,对某一类工单处理会卡住。这时又不太方便停机更新程序,于是就基于系统的动态脚本能力去写了个脚本临时处理这类工单,可以应对当时业务经营的连续性,但缺陷是资源开销大,当超过一定量时 CPU 就会跑满了。这样的技术方案就属于战术债务的应用。为避免“夜长梦多”,当天半夜的业务低谷,我就重新修复上线了新程序,归还了这笔短期临时债务。**疏忽债务**,这类债务一般都是无意识的。从某种意义上来说,这就是程序员的成长性债务,随着知识、技能与经验的积累,这类债务会逐步减少。另一方面,如果我们主动创造一个关注技术债务的环境,这类债务就会被有意识地还掉。
 
 从上面的分类可以看出,战略和战术债务都是我们有意识的选择,而疏忽债务正如其名,是无意识的。但不论技术债务是有意的还是无意的,我们都需要有意识地管理它们。
 
@@ -36,7 +36,7 @@
 
 举个例子来说明下,好几年前团队接手继续开发并维护一个系统,系统的业务一开始发展很快,不停地添加功能,每周都要上好几次线。一年后,还是每周都要上好几次线,但每次上线的时间越来越长,回归测试的工作量越来越大。再后来,系统迎来了更多的新业务,我们不得不复制了整个系统的代码,修改,再重新部署,以免影响现有线上系统的正常运行…
 
-到了这样的状况,每个人都知道,债务在报警了,债主找上门了。一次重大的还债行动计划开始了,由于还债的名声不太好听,所以我们喜欢叫:架构升级。架构升级除了还债,还是为未来铺路。当然,前提是要有未来。如果未来还能迎来更大的业务爆发增长,架构升级就是为了在那时能消化更多的长短期债务。 **管理债务的目标就是识别出债务,并明了不同类型的债务应该在何时归还,不要让债务持续累积并导致技术破产** 。一般来说,只要感觉到团队生产力下降,很可能就是因为有技术债的影响。这时,我们就需要识别出隐藏的债务,评估其 “利率” 并判断是否需要还上这笔债,以及何时还。
+到了这样的状况,每个人都知道,债务在报警了,债主找上门了。一次重大的还债行动计划开始了,由于还债的名声不太好听,所以我们喜欢叫:架构升级。架构升级除了还债,还是为未来铺路。当然,前提是要有未来。如果未来还能迎来更大的业务爆发增长,架构升级就是为了在那时能消化更多的长短期债务。**管理债务的目标就是识别出债务,并明了不同类型的债务应该在何时归还,不要让债务持续累积并导致技术破产** 。一般来说,只要感觉到团队生产力下降,很可能就是因为有技术债的影响。这时,我们就需要识别出隐藏的债务,评估其 “利率” 并判断是否需要还上这笔债,以及何时还。
 
 有时,我们会为债务感到焦虑,期望通过一次大规模重构或架构升级还掉所有的债务,从此无债一身轻。其实,这是理想状态,长期负债才是现实状态。
 
@@ -50,9 +50,9 @@
 
 创业公司从小到大的发展过程中,业务在高速增长,系统服务的实现即使没那么优化,但只要能通过加机器扩展,就暂时没必要去归还实现层面的负债。无非是早期多浪费点机器资源,等业务到了一定规模、进入平稳期后,再一次性清偿掉这笔实现负债,降低运营成本。
 
-这就是技术上的战略债务, **业务高速发展期保持付息,稳定期后一次性归还** 。
+这就是技术上的战略债务,**业务高速发展期保持付息,稳定期后一次性归还** 。
 
-战术债务,因为利息很高,所以一般都是 **快借快还** 。而疏忽债务,需要坚持 **成长性归还策略** ,一旦发现过去的自己写下了愚蠢的代码,就需要主动积极地确认并及时优化,清偿这笔代码实现债务。
+战术债务,因为利息很高,所以一般都是 **快借快还** 。而疏忽债务,需要坚持 **成长性归还策略**,一旦发现过去的自己写下了愚蠢的代码,就需要主动积极地确认并及时优化,清偿这笔代码实现债务。
 
 其次,还债时,我们主要考虑债务的大小和还债的时机,在不同的时间还债,也许研发成本相差不大,但机会成本相差很大,这一点前面分析战略债务时已有提及。而按不同债务的大小,又可以分为大债务和小债务。一般,我把需要以周或月为单位计算的债务算作大债务,而只需一个程序员两三天时间内归还的债务算作小债务,所以这不是一个精确的定义。
 
@@ -76,7 +76,7 @@
 
 任何一个程序系统或其一部分都会与某一个程序员建立关联,如果这个程序员此时就负责这部分系统,那么他在此基础上继续创造代码时,既增加了资产也可能引入了新的债务。这时他的一个重要职责就是,维持好资产与债务的平衡关系。如果在此期间,系统的债务失衡导致技术破产,需要被迫大规模重构或重写时,那么这个程序员的信用必将受到关联伤害。
 
-所以, **程序员的信用,更多体现在面对技术债务的态度和能力** ——有意识地引入债务,并有计划地归还债务;无意识地引入债务,发现之后,有意识地归还。
+所以,**程序员的信用,更多体现在面对技术债务的态度和能力** ——有意识地引入债务,并有计划地归还债务;无意识地引入债务,发现之后,有意识地归还。
 
 再有代码洁癖的人也没法写出无债务的代码,而无债务的系统也不存在。面对负债高的系统,我们不必过于焦虑,高负债的系统往往活力还比较强。作为程序员的我们,应把技术债务当作一门工具,而不是一种负担,那么可能就又获得了新的技能,得到了成长。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25453\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25453\350\256\262.md"
index 8bebc54f3..1690333be 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25453\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25453\350\256\262.md"
@@ -22,7 +22,7 @@
 
 > 人若没有目标,就只好盯着感受,没有平衡,只有妥协。
 
-认清自己当前阶段的目标,定义清楚这个阶段的平衡点。一个阶段内,就不用太在意每一天生活与工作的平衡关系,应放到整个阶段中一个更长的周期来看,达到阶段的平衡即可。 **通过短期的逃避带来的平衡,只会让你在更长期的范围内失衡** 。
+认清自己当前阶段的目标,定义清楚这个阶段的平衡点。一个阶段内,就不用太在意每一天生活与工作的平衡关系,应放到整个阶段中一个更长的周期来看,达到阶段的平衡即可。**通过短期的逃避带来的平衡,只会让你在更长期的范围内失衡** 。
 
 作为个人,你需要承担起定义并掌握自己生活轨迹的重任,如果你不去规划和定义自己的生活,那么别人就会为你规划,而别人对平衡的处理你往往并不认同。
 
@@ -42,7 +42,7 @@
 
 当想清楚了这点后,工作就不过是生活的一部分,何必需要去平衡。与其去平衡两者,不如从整体长期的角度去选择一种平衡的生活。一段时间,也许你的生活中充满了工作;一段时间,你决定减少一些工作,去交交朋友,谈个恋爱。再一段时间后,有了孩子,你决定把曾经生活里的一部分,比如玩游戏,换成陪孩子玩游戏。也许你没法每一天都能做到这样自如地选择,但从一个长期的角度(五到十年)你总是可以选择的。
 
-紧要的是, **去过你想要的生活,而非不得不过的生活** 。
+紧要的是,**去过你想要的生活,而非不得不过的生活** 。
 
 而这里所指的“工作”已不再仅仅是“上班、打工”这样的狭义含义,而是更广义上的“工作”。比如,现在我正在写这篇文字,写到这里,时间已过了凌晨,窗外有点淅沥声,下起了小雨。我喜欢成都夜晚的小雨,突然想起了杜甫《春夜喜雨》中的某几句:
 
@@ -64,7 +64,7 @@
 
 把这个思路用在平衡工作与生活上的话,大概是这样,假如对于一个非常有事业心和野望的人(可以理解为风险偏好大的人),大学毕业平均是 22 岁,那么就应该是 120 - 22 = 98,也就是 98% 的精力花在工作上,当然这里是广义上的 “工作”。而对于那些刚毕业但没有那么大野心的年轻人,也应该投入大约 80%(这是用 100 来减) 的精力在 “工作” 上。
 
-对于这个策略,我的理解是 **早期的高投入,是为了将来需要更多平衡时,能获得这种平衡的能力** 。在我有限的见识和理解能力之内,我是认同这个比例的。一开始就想获得安稳与平衡,人过中年之后是否还能获得这样的安稳与平衡,感觉就比较靠运气。掌控自己能把握的,剩下的再交给时代和运气。 **人生,就是在风险中沉浮,平衡的交易策略就是用来应对风险与波动的** 。
+对于这个策略,我的理解是 **早期的高投入,是为了将来需要更多平衡时,能获得这种平衡的能力** 。在我有限的见识和理解能力之内,我是认同这个比例的。一开始就想获得安稳与平衡,人过中年之后是否还能获得这样的安稳与平衡,感觉就比较靠运气。掌控自己能把握的,剩下的再交给时代和运气。**人生,就是在风险中沉浮,平衡的交易策略就是用来应对风险与波动的** 。
 
 工作是我们度过很长一段生命的方式,还有句话是这么说的:“我不喜欢工作,但我喜欢存在于工作里的东西 —— 发现自己的机会”,工作才会让我们找到属于自己的真正生活。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25454\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25454\350\256\262.md"
index b638f93d1..e8d46aa76 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25454\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25454\350\256\262.md"
@@ -1,6 +1,6 @@
 # 54 侠客行:一技压身,天下行走
 
-从今天开始,我们进入了专栏的第 5 部分 —— **寻路:路在何方** ?这是一条关于方向、角色和自我定位的探索,那就让我们开始一起走走这条程序江湖路吧。
+从今天开始,我们进入了专栏的第 5 部分 —— **寻路:路在何方**?这是一条关于方向、角色和自我定位的探索,那就让我们开始一起走走这条程序江湖路吧。
 
 大约三年前吧,读到一篇文章《为何我工作十年,内心仍无比恐慌》,来自一位腾讯产品总监的演讲分享。文中分析了一个让其感到恐慌与焦虑的深层次原因:好像不会什么技能,技能门槛低。
 
@@ -28,7 +28,7 @@
 
 有时可能我们会有一个职业理想,叫 “一技压身,天下行走”,就像一名侠客一样,学好了功夫,从此闯荡江湖,好不逍遥自在。
 
-之前看过一本武侠玄幻小说,里面有一些角色就叫 “天下行走”,他们都有自己厉害的独门绝技,不厉害怎能天下行走。其中,剑客的剑快,野人的身体坚硬如铁,和尚从不说话修的闭口蝉,一开口就人人色变,这些就是他们独特的技能模型。 **技能模型才是区分不同专业人才特点和价值的核心关键点** 。
+之前看过一本武侠玄幻小说,里面有一些角色就叫 “天下行走”,他们都有自己厉害的独门绝技,不厉害怎能天下行走。其中,剑客的剑快,野人的身体坚硬如铁,和尚从不说话修的闭口蝉,一开口就人人色变,这些就是他们独特的技能模型。**技能模型才是区分不同专业人才特点和价值的核心关键点** 。
 
 而技能模型的形成是一系列选择的结果。以前玩过一个游戏叫《暗黑破坏神》,正常不作弊地玩,一个角色是很难点亮所有技能的,游戏是故意这样设计的。所以你可以反复玩来尝试点亮不同的技能组合方式,这样游戏才具备反复的可玩性。而与游戏不同的是,人生只有一次,你无法点亮所有技能,只有唯一的一种点亮路径选择塑造独一无二的你。
 
@@ -50,7 +50,7 @@
 
 程序员作为工程师的一种,必须得有一项核心硬技能,这是需要长时间积累和磨练的技能,要花大力气的,而这个大力气和长时间,也正是这门技能的门槛。关于技能的习得有一个流行的看法是:花 20% 的时间快速获得某个领域 80% 的知识和技能。这看起来像是一种学习的捷径,但一个硬技能领域最核心的竞争力往往都是最后那 20% —— 也就是你用那 80% 的功夫反复磨练出来的最后 20% 的技艺。
 
-古龙小说中有个角色叫荆无命,他腰带右边插着一柄剑,剑柄向左,是个左撇子,江湖中都知道他左手剑快,但其实他右手剑更快。荆无命要是个程序员的话,那可能就同时具备了两个核心硬技能,属于那种 Java 很强,但 C++ 更牛的人。但我从业这些年还没碰到过同时点亮两者的,无论 Java 还是 C++,因为各自都有足够大的生态和体系,已经需要很长的时间来积累和打磨了。 **我们大部分普通人,拥有的是有限的时间与才华,面对的是无限的兴趣和技能,同时修炼多个核心硬技能是不明智,甚至是不可行的** 。记得以前读万维钢有篇文章介绍了一本书叫《达芬奇诅咒》,文艺复兴时期的达芬奇是一位多才多艺的人,但一个人如果像达芬奇一样对什么东西都感兴趣,但又没有和达芬奇匹敌的才华,很可能尝试了很多,最终却一事无成,这就中了 “达芬奇诅咒”。
+古龙小说中有个角色叫荆无命,他腰带右边插着一柄剑,剑柄向左,是个左撇子,江湖中都知道他左手剑快,但其实他右手剑更快。荆无命要是个程序员的话,那可能就同时具备了两个核心硬技能,属于那种 Java 很强,但 C++ 更牛的人。但我从业这些年还没碰到过同时点亮两者的,无论 Java 还是 C++,因为各自都有足够大的生态和体系,已经需要很长的时间来积累和打磨了。**我们大部分普通人,拥有的是有限的时间与才华,面对的是无限的兴趣和技能,同时修炼多个核心硬技能是不明智,甚至是不可行的** 。记得以前读万维钢有篇文章介绍了一本书叫《达芬奇诅咒》,文艺复兴时期的达芬奇是一位多才多艺的人,但一个人如果像达芬奇一样对什么东西都感兴趣,但又没有和达芬奇匹敌的才华,很可能尝试了很多,最终却一事无成,这就中了 “达芬奇诅咒”。
 
 所以,构建核心技能模型其实是关于才华和技能的战略。《达芬奇诅咒》一书作者就选择技能领域推荐了三个标准:
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25455\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25455\350\256\262.md"
index 457516cde..995c4a9a4 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25455\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25455\350\256\262.md"
@@ -30,7 +30,7 @@
 
 然而这只是从一个领域的核心硬技能转换到了另一个领域,但这两个领域基本是独立的,关联性很弱,而且交叉的区域也很薄,也就意味着很多经验和能力要重新积累。这就是从问题本身的维度去寻找到的解决方案,而爱因斯坦说了,我们需要到更高的维度去寻找答案。而更高的维度就是认知的维度,所以首先需要的是 **升维我们的认知结构** 。
 
-在我修行成术的过程中出现了好多新技术,当时我总想忙完这阵就抽空去学习了解下。但一过几年也一直没能抽出空去看,如今再去看时发现好些当年的新技术已不需再看了。五年成术是立足于一点,成立身之本;而下一阶段不该是寻找更多的点,而是 **由点及线、由线成网、由网化形** 。围绕一个点去划线,由一组线结成网,最后由网化成形, **“化形” 表达了一种更高级的知识和技能运用形态,比一堆离散的知识技能点有价值得多** 。
+在我修行成术的过程中出现了好多新技术,当时我总想忙完这阵就抽空去学习了解下。但一过几年也一直没能抽出空去看,如今再去看时发现好些当年的新技术已不需再看了。五年成术是立足于一点,成立身之本;而下一阶段不该是寻找更多的点,而是 **由点及线、由线成网、由网化形** 。围绕一个点去划线,由一组线结成网,最后由网化成形,**“化形” 表达了一种更高级的知识和技能运用形态,比一堆离散的知识技能点有价值得多** 。
 
 而对于认知升维,由点及线、由线成网、由网化形,其实走的是一种 “升维学习” 之道。这个过程几乎没有终点,是一个持续学习、不断完善的过程,最终结多大的网,成什么样的形,全看个人修为。一条线至少要两个点才能画出,那么第二个点的选择就要看能不能和第一个点连起来了,而这比在一个维度上去预测和乱踩点要有效得多。
 
@@ -38,7 +38,7 @@
 
 其实郭靖还学了另一个更有体系、形态更牛的武功秘籍——《九阴真经》。除了郭靖,《九阴真经》还有很多人看过、学过,有高手如:黄药师、王重阳等,也有一般人如:梅超风。高手们本身有自己的武功体系和形态,所以看了《九阴真经》也仅仅是从中领悟,融入自己的体系中甚至因此创造出新的武功形态。而梅超风之流则仅仅是学点其中的招式,如:九阴白骨爪,和之前自身所学其实没有太多关联,武功境界终究有限。
 
-所以, **升维化形,化的正是技能模型,而这套模型基本决定了你的功力高低** 。
+所以,**升维化形,化的正是技能模型,而这套模型基本决定了你的功力高低** 。
 
 再回到前面那位桌面端程序员的瓶颈问题,升一点维度看更泛的终端,桌面端不过是这棵技能模型树上的一个分枝。树并没有死,甚至更壮大了,只是自己这棵枝干瘪了些,所以可以去嫁接其他分枝获取营养,而非想要跳到另一棵树上去,重新发芽开枝。
 
@@ -46,7 +46,7 @@
 
 结网化形,走上升维之道,因而战场也变大了,但你的时间并没有增多,这就存在一个理论学习和战场实战的矛盾。
 
-到底是应该更宽泛地看书学习建立理论边界,还是在实战中领悟提升?关于这点,你需要选择建立适当的平衡,走两边的极端都不合适。在学校的学习更多是在建立理论体系,而在工作前五年的成术过程则更多是偏实战。 **再之后的阶段又可能需要回归偏理论,提升抽象高度,从具体的问题中跳出来,尝试去解决更高层次、更长远也更本质的问题。而从更现实的角度来看,你的环境也会制约你能参与实战的经历,导致有些东西靠实战可能永远接触不到,不去抽象地思考是无法获得和领悟的。** 历史上关于理论和实战有很多争论,还留下了一些著名的成语。理论派的负面历史代表人物有:赵括。还有一个关于他的成语:纸上谈兵。他谈起军事理论来一套一套的,一上战场真打起来就葬送了数十万将士的性命,所以大家都会以赵括为例来批评没有实战经验支撑的理论靠不住。
+到底是应该更宽泛地看书学习建立理论边界,还是在实战中领悟提升?关于这点,你需要选择建立适当的平衡,走两边的极端都不合适。在学校的学习更多是在建立理论体系,而在工作前五年的成术过程则更多是偏实战。**再之后的阶段又可能需要回归偏理论,提升抽象高度,从具体的问题中跳出来,尝试去解决更高层次、更长远也更本质的问题。而从更现实的角度来看,你的环境也会制约你能参与实战的经历,导致有些东西靠实战可能永远接触不到,不去抽象地思考是无法获得和领悟的。** 历史上关于理论和实战有很多争论,还留下了一些著名的成语。理论派的负面历史代表人物有:赵括。还有一个关于他的成语:纸上谈兵。他谈起军事理论来一套一套的,一上战场真打起来就葬送了数十万将士的性命,所以大家都会以赵括为例来批评没有实战经验支撑的理论靠不住。
 
 但其实还有另一个更著名的历史人物,也是理论派出身,在真正拜将之前也没什么实战经验。并且也有关于他的成语,如:背水一战,这是他抽象地思考过很久的战法,但也是第一次上战场使用,一战而青史留名。
 
@@ -56,7 +56,7 @@
 
 所以,关于理论和实战的关系,从这个历史故事可以有所体会。而 **“十面埋伏” 这样的技能维度显然比 “霸王举鼎” 要高出不少,而升维后的技能,也需要升级后的战场才发挥得出来** 。
 
-技能的成长速度总会进入平缓阶段,并慢慢陷入瓶颈点,然后也许你就会感到焦虑;而焦虑只是一种预警,此时你还未真正陷入困境,但若忽视这样的预警,不能及时进行 **认知和技能升维** ,将有可能陷入越来越勤奋,却越来越焦虑的状态,结果走入 “ **三穷之地** ”(包括如下三种“穷”):
+技能的成长速度总会进入平缓阶段,并慢慢陷入瓶颈点,然后也许你就会感到焦虑;而焦虑只是一种预警,此时你还未真正陷入困境,但若忽视这样的预警,不能及时进行 **认知和技能升维**,将有可能陷入越来越勤奋,却越来越焦虑的状态,结果走入 “ **三穷之地** ”(包括如下三种“穷”):
 
 1. 结果穷:技能增长的边际收益递减;
 1. 方法穷:黔驴技穷,维度过于单一;
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25456\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25456\350\256\262.md"
index 067986547..f72207a55 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25456\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25456\350\256\262.md"
@@ -36,7 +36,7 @@
 
 图中蓝色三角区域表明,随着你从入门初级成长到高级程序员的过程中,需要得到的帮助和指导越来越少;而红色三角区域表明,你能提供的帮助和指导应该越来越多。所在,在前面那个想象的 “泡面拔刀” 的场景中,作为高级程序员的你,更理想的做法应该是去指导年轻程序员如何解决问题的思路,而不是自己拔刀,唰唰两下搞定。
 
-对,很多高级程序员都会以 “等把他教会,我自己早都搞定了” 为由,忍不住自己拔刀。 **理解、掌握并应用好一种知识和技巧是你的 “拔刀术”,但分享传递并教授指导这种知识和技巧才是 “御剑术”** ,而 “剑” 就是你面前更年轻、更初级的程序员。
+对,很多高级程序员都会以 “等把他教会,我自己早都搞定了” 为由,忍不住自己拔刀。**理解、掌握并应用好一种知识和技巧是你的 “拔刀术”,但分享传递并教授指导这种知识和技巧才是 “御剑术”**,而 “剑” 就是你面前更年轻、更初级的程序员。
 
 曾经多少次面对年轻初级程序员交付的结果,我都有一种懊恼的心情,怀疑当初是不是该自己拔刀?那时就突然理解了驾校老司机为何总是满腔怒火地吼着:“让你松点离合,只松一点儿就好…”,而当初的我刚学开车时,一开始不是松少了,就是熄火了。
 
@@ -48,7 +48,7 @@
 
 所有的程序员都是从修行 “拔刀术” 开始,但只有极少数人最终走到了剑心 “天翔龙闪” 的境界,所有未能突破的我们都进入了瓶颈停滞区。我们不断学习和练习,终于练到拔刀由心,收发自如,终成习惯,但要将这个技能升维,跨越战场,却正是需要打破这个习惯。
 
-其中, **从 “拔刀术” 到 “御剑术” 是习惯的打破;从 “御剑术” 到 “万剑诀” 则是量级的变化** 。因而,“御剑术” 是修行 “万剑诀” 的必经之路。嗯,游戏里也是这么设定的。
+其中,**从 “拔刀术” 到 “御剑术” 是习惯的打破;从 “御剑术” 到 “万剑诀” 则是量级的变化** 。因而,“御剑术” 是修行 “万剑诀” 的必经之路。嗯,游戏里也是这么设定的。
 
 “万剑诀” 正如其名,御万剑而破敌。回到现实中,这是一项高杠杆率的技能。而高杠杆率的活动包括:
 
@@ -58,7 +58,7 @@
 
 这就是 “万剑诀” 的核心要诀。应用到程序员修行之路上:如果走上同时影响多人的路线,这就是一条团队管理和领导者之路;如果走上影响长远的路线,你可能乐于分享、传授,这可能是一条布道师的路线;如果你通过提供知识和技能来影响其他一群人的工作,那么这可能是一条架构师的路线。
 
-“万剑诀” 和 “御剑术” 的共通之处在于都以人为剑,观察、揣摩每把剑的特性,先养剑再御剑最后以诀引之。 **若 “拔刀术” 是自己实现的能力,那 “御剑术” 和 “万剑诀” 都是借助他人使之实现的自信和能力** ,只是后者相比而言规模更大,杠杆率更高。“万剑诀” 的重心在追求问题解决的覆盖面,而面临每个具体问题时就需要依赖每把剑的锋利度了。
+“万剑诀” 和 “御剑术” 的共通之处在于都以人为剑,观察、揣摩每把剑的特性,先养剑再御剑最后以诀引之。**若 “拔刀术” 是自己实现的能力,那 “御剑术” 和 “万剑诀” 都是借助他人使之实现的自信和能力**,只是后者相比而言规模更大,杠杆率更高。“万剑诀” 的重心在追求问题解决的覆盖面,而面临每个具体问题时就需要依赖每把剑的锋利度了。
 
 另外,“御”之一字更着重了一层控制的含义,而 “诀” 之一字在于影响多于操控,这里面的关键点就是:剑本身的成熟度。不够成熟的剑只能 “御” 之,足够成熟的剑方能 “诀” 之。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25457\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25457\350\256\262.md"
index 9f00b4969..daa2fb96a 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25457\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25457\350\256\262.md"
@@ -1,6 +1,6 @@
 # 57 三维度:专业、展现与连接
 
-曾经在和朋友探讨个人发展的问题时,讨论出一个 **PPC 理论** ,该理论粗略地把涉及个人发展的方向分成了三个维度,包括:
+曾经在和朋友探讨个人发展的问题时,讨论出一个 **PPC 理论**,该理论粗略地把涉及个人发展的方向分成了三个维度,包括:
 
 - 专业 Profession
 - 展现 Presentation
@@ -115,7 +115,7 @@
 
 自从有了微信公众号,100000+ 现在也是一个神奇的数字了;100000+ 的存在,体现了一个信息、观点与影响力的传递网络。
 
-五种连接圈层,第一层次 “10” 的连接是强连接;其他的都是弱连接,弱连接的价值在于获取、传递与交换信息。 **强连接交流情感,弱连接共享信息** 。
+五种连接圈层,第一层次 “10” 的连接是强连接;其他的都是弱连接,弱连接的价值在于获取、传递与交换信息。**强连接交流情感,弱连接共享信息** 。
 
 而建立连接的关键在于: **给予** 。也许并不需要物质上的给予,仅仅是心理上或是虚拟的给予。所以说为什么展现是扩大连接的基础,展现即创作表达,创作即给予。另外,建立连接得先提供价值,而且还得源源不断。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25458\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25458\350\256\262.md"
index f95f42c9c..9f9d75493 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25458\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25458\350\256\262.md"
@@ -34,7 +34,7 @@
 
 放下了乐器,未必是放弃了音乐,电影中的指挥,任何时候乐队中的任何一个乐器吹(拉、弹、打)错了一个音,他都能立刻分辨出来。这就是另外一条路的另一套技能,是为了得到更大规模的生产力和更震撼的演奏效果(品质)。
 
-除此之外, **前辈的另一个价值在于塑造环境,而环境决定了整体的平均水平线** ,在这个环境中的个体很少有能大幅偏离的。
+除此之外,**前辈的另一个价值在于塑造环境,而环境决定了整体的平均水平线**,在这个环境中的个体很少有能大幅偏离的。
 
 就以我的中学环境为例,当年我进入这所少数民族中学时,那一届高考最好的学生是考上了中央民族大学。六年后,到我参加高考时,学校师生都在为实现清华北大的零突破努力,虽然依然没能实现,但这届学生的最高水平已经可以考上除清华北大之外的任何大学了。
 
@@ -54,7 +54,7 @@
 
 中学是一个相对单一维度的领域,同辈同学间都是在忙于学习和考试;而到了职业和工作领域后,维度就丰富了很多,每一个同辈都可以拥有自己独特的领域,他们之间得以互相观察,并能相互沟通、交流与合作。
 
-那什么是领域?这听起来有点像是一个玄幻小说的术语,在一些玄幻小说中,拥有领域的人物都是超厉害的,在他们的领域中,都是近乎无敌的存在。 **领域,是一个你自己的世界,在这个世界中,你不断地提出问题并找到有趣或有效的解决方案** 。进入这个世界的人,碰到的任何问题,你都解决过或有解决方案,慢慢地人们就会认识到你在这个世界拥有某种领域,并识别出你的领域。然而,计算机专业毕业的程序员们,人人都拥有专业,但工作十年后,不是人人都能拥有领域。
+那什么是领域?这听起来有点像是一个玄幻小说的术语,在一些玄幻小说中,拥有领域的人物都是超厉害的,在他们的领域中,都是近乎无敌的存在。**领域,是一个你自己的世界,在这个世界中,你不断地提出问题并找到有趣或有效的解决方案** 。进入这个世界的人,碰到的任何问题,你都解决过或有解决方案,慢慢地人们就会认识到你在这个世界拥有某种领域,并识别出你的领域。然而,计算机专业毕业的程序员们,人人都拥有专业,但工作十年后,不是人人都能拥有领域。
 
 所以,在你前行的路上,碰到一个拥有领域的同行者,是一种幸运。所谓术业有专攻,每一个拥有领域的人,都有值得敬佩的地方,因为这都需要付出艰辛的努力。
 
@@ -66,7 +66,7 @@
 
 后辈,他们正沿着你走过的路直面而来。
 
-好些年前,工作没几年,带了两个刚毕业的学生。我把我的自留地分了一点让他们种,每隔两天我就去看看他们种的怎么样?每次看完,我都忍不住想去自己再犁一遍。后来我还是没忍住,最后又自己种了一遍。如今回想起来,虽然保障了当时的产能,却牺牲了人的成长速度。 **人,似乎不犯一些错,就成长不了,也许这就是成长的成本。**
+好些年前,工作没几年,带了两个刚毕业的学生。我把我的自留地分了一点让他们种,每隔两天我就去看看他们种的怎么样?每次看完,我都忍不住想去自己再犁一遍。后来我还是没忍住,最后又自己种了一遍。如今回想起来,虽然保障了当时的产能,却牺牲了人的成长速度。**人,似乎不犯一些错,就成长不了,也许这就是成长的成本。**
 
 如今,我再回头看这样的路径和例子,就会以成长思维去考虑,而不仅仅是产能视角。为了获得长期的产能效率,有时不得不承担一些短期的成本压力。而后辈们,既可能重复犯下曾经的错误,也可能走出更好的路径。通过观察他们的来路,我反省到了过去的错误,也看到了更好的路径。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25459\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25459\350\256\262.md"
index c60b64511..4d34b6411 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25459\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25459\350\256\262.md"
@@ -18,7 +18,7 @@
 
 那时我开始在团队承担起整体的系统设计工作,此时若再专注于局部代码优化其实是在驱动细节而非本质了。作为资深程序员出身的架构师,单兵作战能力都是极强的,就像《进击的巨人》中的利威尔兵长,具备单挑巨人的能力。可当面对成群结队的巨人来袭时,个人单挑能力的作用始终有限。
 
-这时, **从程序员到架构师不仅仅是一个名称的变化,它也意味着技能和视角的转变** 。在地上飞奔了七八年的程序员,在面对成群的巨人袭来时,深深地感觉到,杀光巨人不应是目的,真正的目的应是到达彼岸。所以,选择合适的路径,坚定地前行,清除或绕过挡道的巨人,到达目的地。
+这时,**从程序员到架构师不仅仅是一个名称的变化,它也意味着技能和视角的转变** 。在地上飞奔了七八年的程序员,在面对成群的巨人袭来时,深深地感觉到,杀光巨人不应是目的,真正的目的应是到达彼岸。所以,选择合适的路径,坚定地前行,清除或绕过挡道的巨人,到达目的地。
 
 是的,我是到了资深程序员阶段直接转向了架构师。而在路径图上还有另一条路,会经历另一个角色:技术主管,这是一个从程序员到架构师阶段的过渡角色。
 
@@ -36,7 +36,7 @@
 
 现实中,一个开发团队中最优秀的程序员容易被指定承担技术主管的角色,但优秀的程序员又很容易陷入到实现功能的细节中,满足于完美的实现,优雅简洁的代码。但实际上,这样优秀的程序员转入技术主管这个角色后,就很容易尝试控制设计和代码的实现,他们很难接受代码不按照他们希望的方式去编写,这个是他们作为优秀程序员一直以来的工作习惯,长此以往他们自身很容易变成整个开发团队的瓶颈,而团队里的其他成员也未能得到足够的锻炼和成长。
 
-所以技术主管实际相比团队里的其他程序员对系统的视角更开阔,以更有策略和长远的方式来考虑问题。 **他们即使拥有比团队里所有其他程序员更高超的开发实现技能,对所有开发任务拥有最强大的实现自信,也需要转变为另一种 “借助他人使之实现” 的能力和自信,因为技术主管是一个承担更广泛责任的角色** ,必然导致能够专注有效编码的时间会相比以前减少很多,而这一点正是优秀程序员转变为技术主管所面临的最大挑战之一。
+所以技术主管实际相比团队里的其他程序员对系统的视角更开阔,以更有策略和长远的方式来考虑问题。**他们即使拥有比团队里所有其他程序员更高超的开发实现技能,对所有开发任务拥有最强大的实现自信,也需要转变为另一种 “借助他人使之实现” 的能力和自信,因为技术主管是一个承担更广泛责任的角色**,必然导致能够专注有效编码的时间会相比以前减少很多,而这一点正是优秀程序员转变为技术主管所面临的最大挑战之一。
 
 最适合技术主管角色人,不一定是团队中编程能力最好的人,但必然是团队中编程、沟通和协作能力最综合平衡的人。而技术主管之所以是一个过渡,就在于继续往前走,如果偏向 “主管” 就会成为真正的管理者(经理),如果偏向 “技术” 就会走向架构师。
 
@@ -56,7 +56,7 @@
 
 看过了架构师的能力模型,我们再来试着分析下其对应的职责。技术主管的角色与架构师这一角色会产生一些职责上的重叠,事实上我认为在团队规模比较小的时候(十来人的规模),架构师和技术主管的职责几乎完全重叠,甚至技术主管还会代理一些团队主管的角色。
 
-随着软件系统复杂度和规模的提升,团队也相应变大,那么一个架构师此时所处的职责位置就开始和技术主管区别开来。 **如果把技术主管想成是站在楼顶看整个系统,那么架构师此时就是需要飞到天上去看整个系统了** 。
+随着软件系统复杂度和规模的提升,团队也相应变大,那么一个架构师此时所处的职责位置就开始和技术主管区别开来。**如果把技术主管想成是站在楼顶看整个系统,那么架构师此时就是需要飞到天上去看整个系统了** 。
 
 开发功能,解决 Bug,优化代码,这是一个高级或资深程序员的拿手技能,也是地面作战的基本技能。而一个架构师还需要掌握空中的技能,也许就像《进击的巨人》中的立体机动装置,让其能在需要时飞在空中看清全局,也能落地发起凌厉一击。
 
@@ -66,7 +66,7 @@
 
 这是相对技术主管更高维度的全局视角,另一方面依然有很多技术主管可能感觉没把握的技术决策和技术争端需要架构师的介入协调。之所以要找架构师来对一些技术争端和方案进行决策判断,很多情况在于程序员对架构师在技术领域内专业力和影响力的信任,而建立这种专业力和影响力是实际构建架构师非权威领导力的来源。
 
-何谓 “非权威领导力”?非权威自是相对权威而言,管理者的权威领导力来自于公司正式任命的职位和职权,而架构师在大部分公司基本连职位职责都没定义清楚,更没有职权一说,所以实际上就不会有任何权威领导力。所以, **架构师要发挥更大的作用和价值就需要去构建自己的非权威领导力,而这需要长期的专业力和影响力积累** 。
+何谓 “非权威领导力”?非权威自是相对权威而言,管理者的权威领导力来自于公司正式任命的职位和职权,而架构师在大部分公司基本连职位职责都没定义清楚,更没有职权一说,所以实际上就不会有任何权威领导力。所以,**架构师要发挥更大的作用和价值就需要去构建自己的非权威领导力,而这需要长期的专业力和影响力积累** 。
 
 除此之外,架构师还承担着在技术团队和非技术团队(例如:产品设计等团队)之间的接口作用,明确产品的边界,勾勒技术蓝图,协调不同技能的技术团队协作,完成最终的软件系统交付。这时架构师的角色就像服务化架构中的 API,定义了协作规范、交互协议和方式,但并不会聚焦在具体的实现上。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25460\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25460\350\256\262.md"
index 75647fa5b..1b4722722 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25460\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25460\350\256\262.md"
@@ -90,7 +90,7 @@
 
 即使是学习成长本身,也可以用工程模型来求解。这时你的学习维度就需要扩展一下,不仅仅局限于你当前的专业领域,还可以了解点神经科学,认知心理学之类的,并配合自己的现实情况、作息习惯,去建立你的学习模型,获得最佳学习效果。而学习效果,也是一个 “概率解”。虽然你不能知道确切的值,但我想你肯定能感觉出不同模型求解的效果好坏。
 
-简言之, **多维的路径,其实是从一个核心基础维度去扩散开的** 。
+简言之,**多维的路径,其实是从一个核心基础维度去扩散开的** 。
 
 最后,我们总结下,在求解成长的最优路径时,视角的不同,对求解的难度差别巨大。我分享了我的三个视角: **定位,时间视角;自省,自我视角;多维,空间视角** 。通过三个不同的视角,探讨了关于 “我” 与所在现实的时空关系,从中尝试提炼出一种方法,用于探索最适合自己的成长路径。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25461\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25461\350\256\262.md"
index 504cca97b..5ffb4c322 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25461\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25461\350\256\262.md"
@@ -75,7 +75,7 @@
 
 一个人的能力再强,也是有限的。当你想做更多、更大的事情时,就不可避免地要借助他人的力量,这时所面临的就将是大量的沟通了。
 
-沟通一般有两个目的:一是获取或同步信息;二是达成共识,得到承诺。前者需要的是清晰的表达和传递,后者就需要更深的技巧了。这些技巧说起来也很简单, **核心就是换位思考、同理心,外加对自身情绪的控制** ,但知易行难在沟通这件事上体现得尤其明显。
+沟通一般有两个目的:一是获取或同步信息;二是达成共识,得到承诺。前者需要的是清晰的表达和传递,后者就需要更深的技巧了。这些技巧说起来也很简单,**核心就是换位思考、同理心,外加对自身情绪的控制**,但知易行难在沟通这件事上体现得尤其明显。
 
 关于决策,如果都是在好或更好之间的话,那就真没什么纠结的问题了。而决策,是在优劣相当的情况下做出选择,更多的决策难点发生在取舍之间。程序员能碰到的大部分决策场景都是关于技术的,技术相对来说还有一些相对客观的标准来掂量,比如通过测试数据来验证技术决策的结果。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25462\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25462\350\256\262.md"
index cc8d355b8..8872093d3 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25462\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25462\350\256\262.md"
@@ -10,7 +10,7 @@
 
 在我的技术成长路上,我看到了三个方向,正好可以用三个字来表达: **“高”“精”“尖”** 。
 
-“高” 指的是 “高级(High-grade)”,“精” 代表 “精确(Precision)”,而 “尖” 则是 “尖端(Advanced)”。这是我所看到的技术人前进的三个主要方向,而这三个方向的走向往往还是互斥的。 **高级** ,说的不是更高级的技术,因为技术之间的横向比较没有高低级之分,比如操作系统、数据库、网络编程、机器学习等技术,没法比出个高下。这里的“高级”,如其英文是更高等级的意思,是职位和人的级别。而往高等级走的技术人,离 “精” 自然只能越来越远,毕竟站的高就只能看得广,但很难看得精确了。 **精确** ,就是把一门技术做到真正的精通。现在技术的分工越来越细,通常能精通一两个细分领域已实属不易。而要做到精,其实越往后付出越多,但感觉提升却变得越来越慢。都到 95 分了,再往后每提升 1 分都需要付出艰辛的努力。走到细微深处,也很难再看得远、看得广了。 **尖端** ,似乎听起来像 “精” 的极致,其实不然,这完全是另一条路。“高” 与 “精”,是工业界的实践之路,而 “尖” 是理论界的突破之路。只有能推进人类科技进步的技术才称得上尖端,就如 IT 界历史上著名的贝尔实验室里的科学家们做的工作。 **“高”“精”“尖”** 三个字,三个方向,三条路,各有各的机遇与风险。在三条路的岔路口,工作多年的你若止步不做选择,也许就止于一名普通的程序员或资深的技术人。若继续选择一个方向走下去,越往高处走,高处不胜寒,一旦落下,你知道再也回不去了;而走向精深之处,沿着技术的河流,溯根回源,密林幽幽,林声鸟不惊,一旦技术的潮流改了道,你知道你可能会迷失;而尖端之路,或者有朝一日一鸣惊人,青史留名,或者一生碌碌。人工智能的发展史上,曾有一段时间找错了路,让学界止步不前,而这一段时间就是走尖端之路的学者们二十年的岁月。 **“高” 是往宏观走,“精” 是往微观走,“尖” 是去突破边界。**
+“高” 指的是 “高级(High-grade)”,“精” 代表 “精确(Precision)”,而 “尖” 则是 “尖端(Advanced)”。这是我所看到的技术人前进的三个主要方向,而这三个方向的走向往往还是互斥的。**高级**,说的不是更高级的技术,因为技术之间的横向比较没有高低级之分,比如操作系统、数据库、网络编程、机器学习等技术,没法比出个高下。这里的“高级”,如其英文是更高等级的意思,是职位和人的级别。而往高等级走的技术人,离 “精” 自然只能越来越远,毕竟站的高就只能看得广,但很难看得精确了。**精确**,就是把一门技术做到真正的精通。现在技术的分工越来越细,通常能精通一两个细分领域已实属不易。而要做到精,其实越往后付出越多,但感觉提升却变得越来越慢。都到 95 分了,再往后每提升 1 分都需要付出艰辛的努力。走到细微深处,也很难再看得远、看得广了。**尖端**,似乎听起来像 “精” 的极致,其实不然,这完全是另一条路。“高” 与 “精”,是工业界的实践之路,而 “尖” 是理论界的突破之路。只有能推进人类科技进步的技术才称得上尖端,就如 IT 界历史上著名的贝尔实验室里的科学家们做的工作。**“高”“精”“尖”** 三个字,三个方向,三条路,各有各的机遇与风险。在三条路的岔路口,工作多年的你若止步不做选择,也许就止于一名普通的程序员或资深的技术人。若继续选择一个方向走下去,越往高处走,高处不胜寒,一旦落下,你知道再也回不去了;而走向精深之处,沿着技术的河流,溯根回源,密林幽幽,林声鸟不惊,一旦技术的潮流改了道,你知道你可能会迷失;而尖端之路,或者有朝一日一鸣惊人,青史留名,或者一生碌碌。人工智能的发展史上,曾有一段时间找错了路,让学界止步不前,而这一段时间就是走尖端之路的学者们二十年的岁月。**“高” 是往宏观走,“精” 是往微观走,“尖” 是去突破边界。**
 
 这三条路,“高” 和 “精” 的方向在业界更常见,而 “尖” 不是工业界常规的路,毕竟业界拥有类似贝尔实验室这样机构的公司太罕见,所以 “尖” 的路线更多在学术界。因而后面我们主要探讨 “高” 和 “精” 两个方向的路径断层与跨越方法。
 
@@ -57,7 +57,7 @@
 
 曾经,好些年前,我最早在公司的几个同事组成的小组内研究引入 Java NIO 的技术来编写网络程序,读了一些相关的书和开源框架代码(Mina、Netty),周围的几个同事就戏称我为 Java NIO 的专家。这就是用领域(Java NIO 是一个很细分的技术领域)加范围(局限于周围组内几个同事,他们要解决 NIO 的网络编程问题都绕不过我)定义专家的方式。
 
-因而,像前面说的爱因斯坦、牛顿、图灵,他们既是行业(学科维度)范围内的,也是世界(地理维度)范围内的专家。而公司内的专家职级定义,其范围无非就是与公司经营相关的某个领域,其大小无非就是公司组织架构的某一层级之内。 **走向专家之路,就是精确地找到、建立你的领域,并不断推高壁垒和扩大边界的过程。** 那么如何建立属于自己的、更大范围内且具备足够识别性的领域?这就是 “精” 的路径中的非连续性断层问题。曾经读过一篇吴军的文章,谈到了工程师成长中的类似问题,他用了一个公式来描述解法:
+因而,像前面说的爱因斯坦、牛顿、图灵,他们既是行业(学科维度)范围内的,也是世界(地理维度)范围内的专家。而公司内的专家职级定义,其范围无非就是与公司经营相关的某个领域,其大小无非就是公司组织架构的某一层级之内。**走向专家之路,就是精确地找到、建立你的领域,并不断推高壁垒和扩大边界的过程。** 那么如何建立属于自己的、更大范围内且具备足够识别性的领域?这就是 “精” 的路径中的非连续性断层问题。曾经读过一篇吴军的文章,谈到了工程师成长中的类似问题,他用了一个公式来描述解法:
 
 > 成就 = 成功率 x 事情的量级 x 做事的速度
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25463\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25463\350\256\262.md"
index e8bf377d5..49a121461 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25463\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25463\350\256\262.md"
@@ -51,13 +51,13 @@
 - 问题业务
 - 瘦狗业务
 
-**现金牛业务** ,比较形象地表达了就是产生现金的业务。比如谷歌的搜索业务、微软的 Windows 操作系统,都是它们的现金牛业务,有很高的市场占有率,但成长率相对就比较低了。
+**现金牛业务**,比较形象地表达了就是产生现金的业务。比如谷歌的搜索业务、微软的 Windows 操作系统,都是它们的现金牛业务,有很高的市场占有率,但成长率相对就比较低了。
 
-就个人来说,现金牛业务自然是一份稳定的工作,产生现金,维持个人生活的基本面,当然稳定之外越高薪越好。程序员这个职业就是很好的现金牛业务,行业繁荣,工作也比较稳定,专注于这个业务,不断提升薪资水平,这就是: **活在当下** 。 **明星业务** ,比较形象地表达了很有前景的新兴业务,已经走上了快速发展的轨道。比如:亚马逊的云计算(AWS)就是它的未来之星。而个人呢?如果你的现金牛业务(级别和薪资)已经进入行业正态分布的前 20%,那么再继续提升的难度就比较大了。
+就个人来说,现金牛业务自然是一份稳定的工作,产生现金,维持个人生活的基本面,当然稳定之外越高薪越好。程序员这个职业就是很好的现金牛业务,行业繁荣,工作也比较稳定,专注于这个业务,不断提升薪资水平,这就是: **活在当下** 。**明星业务**,比较形象地表达了很有前景的新兴业务,已经走上了快速发展的轨道。比如:亚马逊的云计算(AWS)就是它的未来之星。而个人呢?如果你的现金牛业务(级别和薪资)已经进入行业正态分布的前 20%,那么再继续提升的难度就比较大了。
 
-个人的明星业务是为未来 5 到 10 年准备的,就是现在还并不能带来稳定的现金流但感觉上了轨道的事。于我而言,是投资理财。人到中年,除了劳动性收入,资产性收益将作为很重要的补充收入来源,而当资本金足够大时,很可能就是未来的主要收入来源。当你开始在考虑未来的明星业务时,这就是: **活在未来** 。 **问题业务** ,比较形象地表达了还有比较多问题的业务领域,面临很多不确定性,也就是还没走上正轨。将来到底是死掉,还是成为新的明星业务,现在还看不清楚。比如谷歌的无人驾驶、机器人等业务领域都属于此类。
+个人的明星业务是为未来 5 到 10 年准备的,就是现在还并不能带来稳定的现金流但感觉上了轨道的事。于我而言,是投资理财。人到中年,除了劳动性收入,资产性收益将作为很重要的补充收入来源,而当资本金足够大时,很可能就是未来的主要收入来源。当你开始在考虑未来的明星业务时,这就是: **活在未来** 。**问题业务**,比较形象地表达了还有比较多问题的业务领域,面临很多不确定性,也就是还没走上正轨。将来到底是死掉,还是成为新的明星业务,现在还看不清楚。比如谷歌的无人驾驶、机器人等业务领域都属于此类。
 
-就个人而言,可能是一些自身的兴趣探索领域。于我来说,目前就是写作和英语,即使写作已经开了专栏,但并不算是稳定可靠的收入来源,主要还是以兴趣驱动,投入时间,不断探索,开拓新的维度,这就是: **活在多维** 。 **瘦狗业务** ,比较形象地表达了一些食之无味、弃之可惜的业务。瘦狗业务要么无法产生现金流,要么产生的现金流不断萎缩。今日之瘦狗,也许是昨日的明星或现金牛,比如像诺基亚的功能机。
+就个人而言,可能是一些自身的兴趣探索领域。于我来说,目前就是写作和英语,即使写作已经开了专栏,但并不算是稳定可靠的收入来源,主要还是以兴趣驱动,投入时间,不断探索,开拓新的维度,这就是: **活在多维** 。**瘦狗业务**,比较形象地表达了一些食之无味、弃之可惜的业务。瘦狗业务要么无法产生现金流,要么产生的现金流不断萎缩。今日之瘦狗,也许是昨日的明星或现金牛,比如像诺基亚的功能机。
 
 就个人而言,行业在发展,技术也在进化,曾经你赖以为生的 “现金牛” 技能,可能过几年后就会落后,逐渐变成了 “瘦狗”,无法果断地放弃旧技能、开发新技能,可能就如诺基亚一般在新的时代被淘汰。固守瘦狗业务,那就是: **活在过去** 。
 
diff --git "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25464\350\256\262.md" "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25464\350\256\262.md"
index a06687613..c74f38ba4 100644
--- "a/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25464\350\256\262.md"
+++ "b/docs/Code/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245/\347\254\25464\350\256\262.md"
@@ -20,7 +20,7 @@
 
 相比九十多年前孙中山先生的时代,今天是信息爆炸与过载的时代,知识与学问也淹没在这些爆炸的信息中,谁还能轻易堪称博学,我们只能说在信息的洪流中,保持永无止境地学习。如果能坚持学下去,那么今天的自己就比昨天的自己稍微博学一点,今年的自己也比去年的自己要博学一些。
 
-正因为信息过载,我们通过各式各样的大量阅读来接收信息,因此对这些信息进行 “审问、慎思、明辨” 就显得十分重要和关键了。“问、思、辨” 是对信息进行筛选、分析与处理,去其糟粕取其精华。经过降噪、筛选、分析处理后的信息再与我们自身已有的知识和经验结合形成属于自己的独立思考与观点,而这些独立的思考和观点才能用来指导我们的行动,也即 “笃行”。 **先有 “知”,方有 “行”。知,只是行的方法;行,才是知的目的。**
+正因为信息过载,我们通过各式各样的大量阅读来接收信息,因此对这些信息进行 “审问、慎思、明辨” 就显得十分重要和关键了。“问、思、辨” 是对信息进行筛选、分析与处理,去其糟粕取其精华。经过降噪、筛选、分析处理后的信息再与我们自身已有的知识和经验结合形成属于自己的独立思考与观点,而这些独立的思考和观点才能用来指导我们的行动,也即 “笃行”。**先有 “知”,方有 “行”。知,只是行的方法;行,才是知的目的。**
 
 ## 行
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25400\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25400\350\256\262.md"
index 3fe06b434..0c56cd3f1 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25400\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25400\350\256\262.md"	
@@ -27,7 +27,7 @@
 
 而分布式数据库,基本上是从底层开始,针对分布式场景设计出来的,因此从基础层面就可以解决传统数据库的一些棘手问题。虽然初期投入相对大一些,却可以保证后续技术体系的健康发展,在长期成本上具有显著优势。
 
-此外, **分布式数据库好比一个“百宝箱”,其中蕴含了独具特色的设计理念、千锤百炼的架构模式,以及取之不尽的算法细节** 。随着分布式数据库迅猛发展,越来越多的研发、产品和运维人员或多或少都会接触分布式数据库,因此学好分布式数据库,也会为你提升职场竞争优势带来帮助,成为你技术履历上的闪光点。
+此外,**分布式数据库好比一个“百宝箱”,其中蕴含了独具特色的设计理念、千锤百炼的架构模式,以及取之不尽的算法细节** 。随着分布式数据库迅猛发展,越来越多的研发、产品和运维人员或多或少都会接触分布式数据库,因此学好分布式数据库,也会为你提升职场竞争优势带来帮助,成为你技术履历上的闪光点。
 
 - 对于数据库工程师,除了日常使用,相关面试中常常会涉及设计数据库集群架构、保障数据库的横纵向扩展等内容,因此理解主流分布式数据库原理和相关案例,会帮助你完美应对。
 - 对于云产品经理,掌握目前商用与开源领域中主流的分布式数据库原理同样非常重要,这是规划和设计相关云产品的前置条件。
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25401\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25401\350\256\262.md"
index 6de426c8d..ae485a251 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25401\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25401\350\256\262.md"	
@@ -69,9 +69,9 @@ Oracle RAC 是典型的大型商业解决方案,且为软硬件一体化解决
 
 ![Drawing 4.png](assets/CgpVE2ABT4iAci6AAAE2nfoHLwM617.png)
 
-这是一次水平扩展与垂直扩展,通用经济设备与专用昂贵服务,开源与商业这几组概念的首次大规模碰撞。 **拉开了真正意义上分布式数据库的帷幕** 。
+这是一次水平扩展与垂直扩展,通用经济设备与专用昂贵服务,开源与商业这几组概念的首次大规模碰撞。**拉开了真正意义上分布式数据库的帷幕** 。
 
-当然从一般的观点出发,Hadoop 一类的大数据处理平台不应称为数据库。但是从前面我们归纳的两点特性看,它们又确实非常满足。因此我们可以将它们归纳为早期面向商业分析场景的分布式数据库。 **从此 OLAP 型数据库开始了自己独立演化的道路** 。
+当然从一般的观点出发,Hadoop 一类的大数据处理平台不应称为数据库。但是从前面我们归纳的两点特性看,它们又确实非常满足。因此我们可以将它们归纳为早期面向商业分析场景的分布式数据库。**从此 OLAP 型数据库开始了自己独立演化的道路** 。
 
 除了 Hadoop,另一种被称为 MPP(大规模并行处理)类型的数据库在此段时间也经历了高速的发展。MPP 数据库的架构图如下:
 
@@ -98,9 +98,9 @@ MPP 数据库的特点是首先支持 PB 级的数据处理,同时支持比较
 
 而与此同期,全球范围内又上演着 NoSQL 化浪潮,它与国内去 IOE 运动一起推动着数据库朝着横向分布的方向一路狂奔。关于 NoSQL 的内容,将会在下一讲详细介绍。
 
-与上一部分中提到的大数据技术类似,随着互联网的发展,去 IOE 运动将 OLTP 型数据库从原来的关系型数据库之中分离出来,但这里需要注意的是, **这种分离并不是从基础上构建一个完整的数据库,而是融合了旧有的开源型数据库,同时结合先进的分布式技术,共同构造了一种融合性的“准”数据库** 。它是面向具体的应用场景的,所以阉割掉了传统的 OLTP 数据库的一些特性,甚至是一些关键的特性,如子查询与 ACID 事务等。
+与上一部分中提到的大数据技术类似,随着互联网的发展,去 IOE 运动将 OLTP 型数据库从原来的关系型数据库之中分离出来,但这里需要注意的是,**这种分离并不是从基础上构建一个完整的数据库,而是融合了旧有的开源型数据库,同时结合先进的分布式技术,共同构造了一种融合性的“准”数据库** 。它是面向具体的应用场景的,所以阉割掉了传统的 OLTP 数据库的一些特性,甚至是一些关键的特性,如子查询与 ACID 事务等。
 
-而 NoSQL 数据库的重点是支持非结构化数据,如互联网索引,GIS 地理数据和时空数据等。这种数据在传统上会使用关系型数据库存储,但需要将此种数据强行转换为关系型结构,不仅设计烦琐,而且使用效率也比较低下。故 **NoSQL 数据库被认为是对整个数据库领域的补充** ,从而人们意识到数据库不应该仅仅支持一种数据模式。
+而 NoSQL 数据库的重点是支持非结构化数据,如互联网索引,GIS 地理数据和时空数据等。这种数据在传统上会使用关系型数据库存储,但需要将此种数据强行转换为关系型结构,不仅设计烦琐,而且使用效率也比较低下。故 **NoSQL 数据库被认为是对整个数据库领域的补充**,从而人们意识到数据库不应该仅仅支持一种数据模式。
 
 随着分布式数据库的发展,一种从基础上全新设计的分布式 OLTP 数据库变得越来越重要,而云计算更是为这种数据库注入新的灵魂,两者的结合将会给分布式数据库带来美妙的化学反应。
 
@@ -135,7 +135,7 @@ MPP 数据库的特点是首先支持 PB 级的数据处理,同时支持比较
 
 ## 总结
 
-用《三国演义》的第一句话来说:“天下大势,分久必合,合久必分。”而我们观察到的分布式数据库,乃至数据库本身的发展正暗合了这句话。 **分布式数据库发展就是一个由合到分,再到合的过程** :
+用《三国演义》的第一句话来说:“天下大势,分久必合,合久必分。”而我们观察到的分布式数据库,乃至数据库本身的发展正暗合了这句话。**分布式数据库发展就是一个由合到分,再到合的过程** :
 
 1. 早期的关系型商业数据库的分布式能力可以满足大部分用户的场景,因此产生了如 Oracle 等几种巨无霸数据库产品;
 1. OLAP 领域首先寻求突破,演化出了大数据技术与 MPP 类型数据库,提供功能更强的数据分析能力;
@@ -145,4 +145,4 @@ MPP 数据库的特点是首先支持 PB 级的数据处理,同时支持比较
 
 我们回顾历史,目的是更好地掌握未来。在本课程中,我们将详细分析现代分布式数据库、OLTP 型数据库的关键技术、使用场景和应用案例。使你在未来可以更好地评估和使用分布式数据库。
 
-而 **分布式数据库的历史同时体现了实用主义的特色** ,其演化是需求与技术博弈的结果,而不是精心设计出来的。我们的课程也会体现出实用主义的特点,让你学以致用,学有所获。
+而 **分布式数据库的历史同时体现了实用主义的特色**,其演化是需求与技术博弈的结果,而不是精心设计出来的。我们的课程也会体现出实用主义的特点,让你学以致用,学有所获。
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25402\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25402\350\256\262.md"
index 6fa8a9b4d..a99125ec5 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25402\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25402\350\256\262.md"	
@@ -18,7 +18,7 @@
 
 先来谈谈优点:由于 Schema(模式)的预定义,数据库获得存储相对紧凑,从而导致其性能较为优异;之后就是经典的 ACID 给业务带来了可控性,而基于标准化 SQL 的数据访问模式给企业级应用带来了更多的红利,因为“ **标准即是生产力** ”。
 
-它的缺点是:对前期设计要求高,因为后期修改 Schema 往往需要停机,没有考虑分布式场景,在扩展性和可用性层面缺乏支持;而分布式是 21 世纪应用必备的技能, **请你留意此处,这就是区分新老数据库的重要切入点** 。
+它的缺点是:对前期设计要求高,因为后期修改 Schema 往往需要停机,没有考虑分布式场景,在扩展性和可用性层面缺乏支持;而分布式是 21 世纪应用必备的技能,**请你留意此处,这就是区分新老数据库的重要切入点** 。
 
 自 20 世纪 70 年代末以来,SQL 和关系型数据库一直是行业标准。大多数流行的“企业”系统都是 System R 的直接后代,继承了 SQL 作为其查询语言。SQL 的意义是提供了一套结构化数据的访问标准,它是脱离特定厂商束缚的客观标准,虽然不同数据库都会对标准 SQL 进行扩充和改造,但是最为常用的部分还是与最初设计保持一致。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25405\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25405\350\256\262.md"
index 0882bf449..ee2f66df7 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25405\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25405\350\256\262.md"	
@@ -16,13 +16,13 @@
 
 ## CAP 理论与注意事项
 
-首先, **可用性是用于衡量系统能成功处理每个请求并作出响应的能力** 。可用性的定义是用户可以感知到的系统整体响应情况。但在实践中,我们希望组成系统的各个组件都可以保持可用性。
+首先,**可用性是用于衡量系统能成功处理每个请求并作出响应的能力** 。可用性的定义是用户可以感知到的系统整体响应情况。但在实践中,我们希望组成系统的各个组件都可以保持可用性。
 
-其次, **我们希望每个操作都保持一致性** 。一致性在此定义为原子一致性或线性化一致性。线性一致可以理解为:分布式系统内,对所有相同副本上的操作历史可以被看作一个日志,且它们在日志中操作的顺序都是相同的。线性化简化了系统可能状态的计算过程,并使分布式系统看起来像在单台计算机上运行一样。
+其次,**我们希望每个操作都保持一致性** 。一致性在此定义为原子一致性或线性化一致性。线性一致可以理解为:分布式系统内,对所有相同副本上的操作历史可以被看作一个日志,且它们在日志中操作的顺序都是相同的。线性化简化了系统可能状态的计算过程,并使分布式系统看起来像在单台计算机上运行一样。
 
-最后, **我们希望在容忍网络分区的同时实现一致性和可用性** 。网络是十分不稳定的,它经常会分为多个互相独立的子网络。在这些子网中,节点间无法相互通信。在这些被分区的节点之间发送的某些消息,将无法到达它的目的地。
+最后,**我们希望在容忍网络分区的同时实现一致性和可用性** 。网络是十分不稳定的,它经常会分为多个互相独立的子网络。在这些子网中,节点间无法相互通信。在这些被分区的节点之间发送的某些消息,将无法到达它的目的地。
 
-那么总结一下, **可用性要求任何无故障的节点都可以提供服务,而一致性要求结果需要线性一致** 。埃里克·布鲁尔(Eric Brewer)提出的 CAP 理论讨论了一致性、可用性和分区容错之间的抉择。
+那么总结一下,**可用性要求任何无故障的节点都可以提供服务,而一致性要求结果需要线性一致** 。埃里克·布鲁尔(Eric Brewer)提出的 CAP 理论讨论了一致性、可用性和分区容错之间的抉择。
 
 其中提到了,异步系统是无法满足可用性要求的,并且在存在网络分区的情况下,我们无法实现同时保证可用性和一致性的系统。不过我们可以构建出,在尽最大努力保证可用性的同时,也保证强一致性的系统;或者在尽最大努力保证一致性的同时,也保证可用性的系统。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25406\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25406\350\256\262.md"
index 837b433ff..eea2dae95 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25406\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25406\350\256\262.md"	
@@ -10,7 +10,7 @@
 
 通过第 1 讲的学习,我想你不仅了解了分布式数据库由合到分、再到合的发展历史,更重要的收获是知道了到底什么是分布式数据库,这个最根本的问题。
 
-从广义上讲,在不同主机或容器上运行的数据库就是分布式数据库,故我们能看到其丰富的产品列表。但是,正是由于其产品线过于丰富,我不可能面面俱到地去讲解所有知识点。同时由于数据库在狭义上可以被理解为 OLTP 型交易类数据库, **因此本课程更加聚焦于 DistributedSQL 与 NewSQL 的技术体系** ,也就是 OLTP 类分布式数据库。在后续的模块中我会着重介绍它们涉及的相关知识,这里给你一个预告。
+从广义上讲,在不同主机或容器上运行的数据库就是分布式数据库,故我们能看到其丰富的产品列表。但是,正是由于其产品线过于丰富,我不可能面面俱到地去讲解所有知识点。同时由于数据库在狭义上可以被理解为 OLTP 型交易类数据库,**因此本课程更加聚焦于 DistributedSQL 与 NewSQL 的技术体系**,也就是 OLTP 类分布式数据库。在后续的模块中我会着重介绍它们涉及的相关知识,这里给你一个预告。
 
 同时,这一模块也点出了 **分片与同步两种特性是分布式数据库的重要特性** 。
 
@@ -24,7 +24,7 @@ SQL 的重要性如我介绍的那样,这使得它的受众非常广泛。如
 
 我们会经常遇到一个问题:设计一套分库分片的结构,保证尽可能少地迁移数据库。其实这个需求本质上在分布式数据库语境下是毫无意义的,自动弹性的扩缩数据库节点应该是这种数据库必要特性。过分地使用分片算法来规避数据库迁移固然可以提高性能,但总归是一种不完整的技术方案,具有天然的缺陷。
 
-模块一的最后我们学习了同步数据的概念。 **同步其实是复制+一致性两个概念的综合** 。这两个概念互相配合造就了分布式数据库数据同步多样的表现形式。其中,复制是它的前提与必要条件,也就是说,如果一份数据不需要复制,也就没有所谓一致性的概念,那么同步技术也就不存在了。
+模块一的最后我们学习了同步数据的概念。**同步其实是复制+一致性两个概念的综合** 。这两个概念互相配合造就了分布式数据库数据同步多样的表现形式。其中,复制是它的前提与必要条件,也就是说,如果一份数据不需要复制,也就没有所谓一致性的概念,那么同步技术也就不存在了。
 
 在同步那一讲中,最先进入我们视野的是异步复制,这类似于没有一致性的参与,是一种单纯的、最简单的复制方式。后面说的其他的同步、半同步等复合技术,多少都有一致性概念的参与。而除了复制模式以外,我们还需要关注诸如复制协议、复制方式等技术细节。最后我们用 MySQL 复制技术的发展历程,总结了多种复制技术的特点,并点明了 **以一致性算法为核心的强一致性复制技术是未来的发展方式** 。
 
@@ -42,7 +42,7 @@ CAP 理论首先要明确,其中的 **C 指的是一致性模型中最强的
 
 分布式数据库,特别是 NoSQL 和 NewSQL 数据库,是目前主要的发展方向。同时,这两种数据库的品种也极为丰富。其中很多都是针对特定场景服务的,比如 NoSQL 中 Elasticsearch 针对的是搜索场景,Redis 针对缓存场景。而 NewSQL 更是百花齐放,如国内的滴滴、字节跳动等企业,都针对自己的业务特点实现了 NewSQL 数据库。更不要说如 BAT、Google 这样的大厂,他们都有自己的 NewSQL 类数据库。
 
-这背后的动力来源于 **内驱需求与外部环境** ,这两者共同叠加而产生了目前这种局面。
+这背后的动力来源于 **内驱需求与外部环境**,这两者共同叠加而产生了目前这种局面。
 
 内驱需求是,随着某种特定业务的产生并伴随其使用规模的扩大,从数据库这种底层解决该问题的需求逐步强烈。因为从数据库层面可以保证写入和查询满足某种一致性特性,而分布式数据库天然的服务化特性,又给使用者带来极大便利,从而可以加速这类业务快速发展。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25407\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25407\350\256\262.md"
index 5ac4e5f9f..b70bbff33 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25407\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25407\350\256\262.md"	
@@ -62,7 +62,7 @@
 
 行式存储会把每行的所有列存储在一起,从而形成数据文件。当需要把整行数据读取出来时,这种数据组织形式是比较合理且高效的。但是如果要读取多行中的某个列,这种模式的代价就很昂贵了,因为一些不需要的数据也会被读取出来。
 
-而列式存储与之相反,不同行的同一列数据会被就近存储在一个数据文件中。同时除了存储数据本身外,还需要存储该数据属于哪行。而行式存储由于列的顺序是固定的,不需要存储额外的信息来关联列与值之间的关系。 **列式存储非常适合处理分析聚合类型的任务** ,如计算数据趋势、平均值,等等。因为这些数据一般需要加载一列的所有行,而不关心的列数据不会被读取,从而获得了更高的性能。
+而列式存储与之相反,不同行的同一列数据会被就近存储在一个数据文件中。同时除了存储数据本身外,还需要存储该数据属于哪行。而行式存储由于列的顺序是固定的,不需要存储额外的信息来关联列与值之间的关系。**列式存储非常适合处理分析聚合类型的任务**,如计算数据趋势、平均值,等等。因为这些数据一般需要加载一列的所有行,而不关心的列数据不会被读取,从而获得了更高的性能。
 
 我们会发现 OLTP 数据库倾向于使用行式存储,而 OLAP 数据库更倾向于列式存储,正是这两种存储的物理特性导致了这种倾向性。而 HATP 数据库也是融合了两种存储模式的一种产物。
 
@@ -94,7 +94,7 @@
 
 ## 面向分布式的存储引擎特点
 
-以上内容为存储引擎的一些核心内容。那分布式数据库相比传统单机数据库,在存储引擎的架构上有什么不同呢?我总结了以下几点。 **内存型数据库会倾向于选择分布式模式来进行构建** 。原因也是显而易见的,由于单机内存容量相比磁盘来说是很小的,故需要构建分布式数据库来满足业务所需要的容量。 **列式存储也与分布式数据库存在天然的联系** 。你可以去研究一下,很多列式相关的开源项目都与 Hadoop 等平台有关系的。原因是针对 OLAP 的分析数据库,一个非常大的应用场景就是要分析所有数据。
+以上内容为存储引擎的一些核心内容。那分布式数据库相比传统单机数据库,在存储引擎的架构上有什么不同呢?我总结了以下几点。**内存型数据库会倾向于选择分布式模式来进行构建** 。原因也是显而易见的,由于单机内存容量相比磁盘来说是很小的,故需要构建分布式数据库来满足业务所需要的容量。**列式存储也与分布式数据库存在天然的联系** 。你可以去研究一下,很多列式相关的开源项目都与 Hadoop 等平台有关系的。原因是针对 OLAP 的分析数据库,一个非常大的应用场景就是要分析所有数据。
 
 而列式存储可以被认为是这种模式的一种优化,实现该模式的必要条件是要有分布式系统,因为一台机器的处理能力是有瓶颈的。如果希望处理超大规模数据,那么将数据分散到多个节点就成为必要的方式。所以说,列模式是由分析性分布式的优化需求所流行起来的。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25408\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25408\350\256\262.md"
index e9915a58f..77765db3c 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25408\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25408\350\256\262.md"	
@@ -126,7 +126,7 @@ Google 论文中的原始描述为:SSTable 用于 BigTable 内部数据存储
 
 二级索引一般都是稀疏索引,也就是索引与数据是分离的。索引的结果一般保存的是主键,而后根据主键去查找数据。这在分布式场景下有比较明显的性能问题,因为索引结果所在的节点很可能与数据不在一个节点上。
 
-以上问题的一个可行解决方案是以二级索引的结果(也就是主键)来分散索引数据,也就是在数据表创建时,同时创建二级索引。Apache Cassandra 的 SASI 在这方面就是一个很好的例子。它绑定在 SSTable 的生命周期上,在内存缓存刷新或是在数据合并时,二级索引就伴随着创建了。 **这一定程度上让稀疏的索引有了一定亲和性** 。
+以上问题的一个可行解决方案是以二级索引的结果(也就是主键)来分散索引数据,也就是在数据表创建时,同时创建二级索引。Apache Cassandra 的 SASI 在这方面就是一个很好的例子。它绑定在 SSTable 的生命周期上,在内存缓存刷新或是在数据合并时,二级索引就伴随着创建了。**这一定程度上让稀疏的索引有了一定亲和性** 。
 
 如果要使用键值对实现二级索引,那么索引结果会有如下几种组合方式。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25409\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25409\350\256\262.md"
index 6274c1102..04048a957 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25409\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25409\350\256\262.md"	
@@ -28,7 +28,7 @@ LSM 树包含内存驻留单元和磁盘驻留单元。首先数据会写入内
 
 双树中的两棵树分别指:内存驻留单元和磁盘驻留单元中分别有一棵树,你可以想象它们都是 B 树结构的。刷盘的时候,内存数据与磁盘上部分数据进行合并,而后写到磁盘这棵大树中的某个节点下面。成功后,合并前的内存数据与磁盘数据会被移除。
 
-可以看到双树操作是比较简单明了的,而且可以作为一种 B 树类的索引结构而存在。但实际上几乎没有存储引擎去使用它,主要原因是它的合并操作是同步的,也就是刷盘的时候要同步进行合并。而刷盘本身是个相对频繁的操作,这样会造成写放大,也就是会影响写入效率且会占用非常大的磁盘空间。 **多树结构是在双树的基础上提出的,内存数据刷盘时不进行合并操作** ,而是完全把内存数据写入到单独的文件中。那这个时候另外的问题就出现了:随着刷盘的持续进行,磁盘上的文件会快速增加。这时,读取操作就需要在很多文件中去寻找记录,这样读取数据的效率会直线下降。
+可以看到双树操作是比较简单明了的,而且可以作为一种 B 树类的索引结构而存在。但实际上几乎没有存储引擎去使用它,主要原因是它的合并操作是同步的,也就是刷盘的时候要同步进行合并。而刷盘本身是个相对频繁的操作,这样会造成写放大,也就是会影响写入效率且会占用非常大的磁盘空间。**多树结构是在双树的基础上提出的,内存数据刷盘时不进行合并操作**,而是完全把内存数据写入到单独的文件中。那这个时候另外的问题就出现了:随着刷盘的持续进行,磁盘上的文件会快速增加。这时,读取操作就需要在很多文件中去寻找记录,这样读取数据的效率会直线下降。
 
 为了解决这个问题,此种结构会引入合并操作(Compaction)。该操作是异步执行的,它从这众多文件中选择一部分出来,读取里面的内容而后进行合并,最后写入一个新文件中,而后老文件就被删除掉了。如下图所示,这就是典型的多树结构合并操作。而这种结构也是本讲介绍的主要结构。
 
@@ -112,9 +112,9 @@ LSM 树包含内存驻留单元和磁盘驻留单元。首先数据会写入内
 
 那么我们可以同时解决以上三种问题吗?根据 RUM 的假说,答案是不能。
 
-该假说总结了数据库系统优化的三个关键参数: **读取开销(Read)、更新开销(Update)和内存开销(Memory)** ,也就是 RUM。对应到上面三种放大,可以理解为 R 对应读放大、U 对应写放大,而 M 对应空间放大(Memory 可以理解为广义的存储,而不仅仅指代内存)。
+该假说总结了数据库系统优化的三个关键参数: **读取开销(Read)、更新开销(Update)和内存开销(Memory)**,也就是 RUM。对应到上面三种放大,可以理解为 R 对应读放大、U 对应写放大,而 M 对应空间放大(Memory 可以理解为广义的存储,而不仅仅指代内存)。
 
-该假说表明, **为了优化上述两项的开销必然带来第三项开销的上涨** ,可谓鱼与熊掌不可兼得。而 LSM 树是用牺牲读取性能来尽可能换取写入性能和空间利用率,上面我已经详细阐明其写入高效的原理,此处不做过多说明。
+该假说表明,**为了优化上述两项的开销必然带来第三项开销的上涨**,可谓鱼与熊掌不可兼得。而 LSM 树是用牺牲读取性能来尽可能换取写入性能和空间利用率,上面我已经详细阐明其写入高效的原理,此处不做过多说明。
 
 而有的同学会发现,合并操作会带来空间放大的问题,理论上应该会浪费空间。但是 LSM 树由于其不可变性,可以引入块压缩,来优化空间占用使用,且内存不需要做预留(B 树需要额外预留内存来进行树更新操作),从而使其可以很好地优化空间。
 
@@ -122,4 +122,4 @@ LSM 树包含内存驻留单元和磁盘驻留单元。首先数据会写入内
 
 ## 总结
 
-至此,我们学习了一个典型的面向分布式数据库所使用的存储引擎。从其特点可以看到, **它高速写入的特性对分布式数据库而言是有非常大吸引力的** ,同时其 **KV 结构更是分片所喜欢的一种数据格式,非常适合基于此构建分布式数据库** 。所以诸如 Apache Cassandra、ClickHouse 和 TiDB 等分布式数据库都选用 LSM 树或类似结构的存储引擎来构建分布式数据库。
+至此,我们学习了一个典型的面向分布式数据库所使用的存储引擎。从其特点可以看到,**它高速写入的特性对分布式数据库而言是有非常大吸引力的**,同时其 **KV 结构更是分片所喜欢的一种数据格式,非常适合基于此构建分布式数据库** 。所以诸如 Apache Cassandra、ClickHouse 和 TiDB 等分布式数据库都选用 LSM 树或类似结构的存储引擎来构建分布式数据库。
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25410\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25410\350\256\262.md"
index 37225f1e5..322d7d036 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25410\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25410\350\256\262.md"	
@@ -48,7 +48,7 @@
 
 缓存首先解决了内存与磁盘之间的速度差,同时可以在不改变算法的情况下优化数据库的性能。但是,内存毕竟有限,不可能将磁盘中的所有数据进行缓存。这时候就需要进行刷盘来释放缓存,刷盘操作一般是异步周期性执行的,这样做的好处是不会阻塞正常的写入和读取。
 
-刷盘时需要注意,脏页(被修改的页缓存)如果被其他对象引用,那么刷盘后不能马上释放空间,需要等到它没有引用的时候再从缓存中释放。 **刷盘操作同时需要与提交日志检查点进行配合,从而保证 D** ,也就是持久性。
+刷盘时需要注意,脏页(被修改的页缓存)如果被其他对象引用,那么刷盘后不能马上释放空间,需要等到它没有引用的时候再从缓存中释放。**刷盘操作同时需要与提交日志检查点进行配合,从而保证 D**,也就是持久性。
 
 当缓存到达一定阈值后,就不得不将有些旧的值从缓存中移除。这个时候就需要缓存淘汰算法来帮忙释放空间。这里有 FIFO、LRU、表盘(Clock)和 LFU 等算法,感兴趣的话你可以根据这几个关键词自行学习。
 
@@ -82,7 +82,7 @@
 
 而事务在提交的时候,一定要保证其日志已经写入提交日志中。也就是事务内容完全写入日志是事务完成的一个非常重要的标志。
 
-日志在理论上可以无限增长,但实际上没有意义。因为一旦数据从缓存中被刷入磁盘,该操作之前的日志就没有意义了,此时日志就可以被截断(Trim),从而释放空间。而这个被截断的点,我们一般称为检查点。 **检查点之前的页缓存中的脏页需要被完全刷入磁盘中** 。
+日志在理论上可以无限增长,但实际上没有意义。因为一旦数据从缓存中被刷入磁盘,该操作之前的日志就没有意义了,此时日志就可以被截断(Trim),从而释放空间。而这个被截断的点,我们一般称为检查点。**检查点之前的页缓存中的脏页需要被完全刷入磁盘中** 。
 
 日志在实现的时候,一般是由一组文件组成。日志在文件中顺序循环写入,如果一个文件中的数据都是检查点之前的旧数据,那么新日志就可以覆盖它们,从而避免新建文件的问题。同时,将不同文件放入不同磁盘,以提高日志系统的可用性。
 
@@ -101,7 +101,7 @@ steal 策略是说允许将事务中未提交的缓存数据写入数据库,
 
 force 策略是说事务提交的时候,需要将所有操作进行刷盘,而 no-force 则不需要。可以看到如果是 no-force,数据在磁盘上还是前镜像状态。这就需要 redo log 来配合,以备在系统出现故障后,从 redo log 里面恢复缓存中的数据,从而能转变为后镜像状态。
 
-从上可知, **当代数据库存储引擎大部分都有 undo log 和 redo log,那么它们就是 steal/no-force 策略的数据库** 。
+从上可知,**当代数据库存储引擎大部分都有 undo log 和 redo log,那么它们就是 steal/no-force 策略的数据库** 。
 
 下面再来说一个算法。
 
@@ -112,7 +112,7 @@ force 策略是说事务提交的时候,需要将所有操作进行刷盘,
 该算法同时使用 undo log 和 redo log 来完成数据库故障崩溃后的恢复工作,其处理流程分为如下三个步骤。
 
 1. 首先数据库重新启动后,进入分析模式。检查崩溃时数据库的脏页情况,用来识别需要从 redo 的什么位置开始恢复数据。同时搜集 undo 的信息去回滚未完成的事务。
-1. 进入执行 redo 的阶段。该过程通过 redo log 的回放,将在页缓存中但是没有持久化到磁盘的数据恢复出来。这里注意, **除了恢复了已提交的数据,一部分未提交的数据也恢复出来了** 。
+1. 进入执行 redo 的阶段。该过程通过 redo log 的回放,将在页缓存中但是没有持久化到磁盘的数据恢复出来。这里注意,**除了恢复了已提交的数据,一部分未提交的数据也恢复出来了** 。
 1. 进入执行 undo 的阶段。这个阶段会回滚所有在上一阶段被恢复的未提交事务。为了防止该阶段执行时数据库再次崩溃,存储引擎会记录下已执行的 undo 操作,防止它们重复被执行。
 
 ARIES 算法虽然被提出多年,但其概念和执行过程依然在现代存储引擎中扮演着重要作用。
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25411\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25411\350\256\262.md"
index f8bf1f603..1a8681114 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25411\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25411\350\256\262.md"	
@@ -28,7 +28,7 @@
 
 读到已提交在可重读基础上放弃了不可重读。与幻读类似,但不可重读针对的是一条数据。也就是只读取一条数据,而后在同一个事务内,再读取它数据就变化了。
 
-刚接触这个概念的同学可能会感觉匪夷所思,两者只相差一个数据量,就出现了两个隔离级别。这背后的原因是 **保证一条数据的难度要远远低于多条** ,也就是划分这两个级别,主要的考虑是背后的原理问题。而这个原理又牵扯出了性能与代价的问题。因此就像我在本专栏中反复阐述的一样,一些理论概念有其背后深刻的思考,你需要理解背后原理才能明白其中的奥义。不过不用担心,后面我会详细阐述它们之间实现的差别。
+刚接触这个概念的同学可能会感觉匪夷所思,两者只相差一个数据量,就出现了两个隔离级别。这背后的原因是 **保证一条数据的难度要远远低于多条**,也就是划分这两个级别,主要的考虑是背后的原理问题。而这个原理又牵扯出了性能与代价的问题。因此就像我在本专栏中反复阐述的一样,一些理论概念有其背后深刻的思考,你需要理解背后原理才能明白其中的奥义。不过不用担心,后面我会详细阐述它们之间实现的差别。
 
 而不可重读对应的是丢失更新,与写偏序类似,丢失更新是多个事务操作一条数据造成的。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25412\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25412\350\256\262.md"
index bedaa1b57..315e5bca4 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25412\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25412\350\256\262.md"	
@@ -76,7 +76,7 @@ WiredTiger 是 MongoDB 默认的存储引擎。它解决了原有 MongoDB 必须
 
 在缓存中每个树节点上,都配合一个更新缓冲,是用跳表实现的。当进行插入和更新操作时,这些数据写入缓冲内,而不直接修改节点。这样做的好处是,跳表这种结构不需要预留额外的空间,且并发性能较好。在刷盘时,跳表内的数据和节点页面一起被合并到磁盘上。
 
-由此可见, **WiredTiger 牺牲了一定的查询性能来换取空间利用率和写入性能** 。因为查询的时候出来读取页面数据外,还要合并跳表内的数据后才能获取最新的数据。
+由此可见,**WiredTiger 牺牲了一定的查询性能来换取空间利用率和写入性能** 。因为查询的时候出来读取页面数据外,还要合并跳表内的数据后才能获取最新的数据。
 
 ### BW-Tree
 
@@ -96,7 +96,7 @@ BW-Tree 为每个节点配置了一个页面 ID,而后该节点的所有操作
 
 ### 经典存储
 
-经典的 LSM 实现有 LeveledDB,和在其基础之上发展出来的 RocksDB。它们的特点我们之前有介绍过,也就是使用缓存来将随机写转换为顺序写,而后生成排序且不可变的数据。 **它对写入和空间友好,但是牺牲了读取性能** 。
+经典的 LSM 实现有 LeveledDB,和在其基础之上发展出来的 RocksDB。它们的特点我们之前有介绍过,也就是使用缓存来将随机写转换为顺序写,而后生成排序且不可变的数据。**它对写入和空间友好,但是牺牲了读取性能** 。
 
 ### Bitcask
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25413\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25413\350\256\262.md"
index b8af8d48a..a6a78842e 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25413\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25413\350\256\262.md"	
@@ -10,7 +10,7 @@
 
 分布式系统是由多个节点参与其中的,它们直接通过网络进行互联。每个节点会保存本地的状态,通过网络来互相同步这些状态;同时节点需要访问时间组件来获取当前时间。对于分布式系统来说,时间分为逻辑时间与物理时间。逻辑时间一般被实现为一个单调递增的计数器,而物理时间对应的是一个真实世界的时间,一般由操作系统提供。
 
-以上就是分布式系统所涉及的各种概念,看起很简单,实际上业界对分布式系统的共识就是上述所有环节没有一点是可靠的, **“不可靠”贯穿了分布式系统的整个生命周期** 。而总结这些不可靠就成为失败模型所解决的问题。
+以上就是分布式系统所涉及的各种概念,看起很简单,实际上业界对分布式系统的共识就是上述所有环节没有一点是可靠的,**“不可靠”贯穿了分布式系统的整个生命周期** 。而总结这些不可靠就成为失败模型所解决的问题。
 
 在介绍失败模型的具体内容之前,让我们打开思路,看看有哪些具体的原因引起了分布式系统的可靠性问题。
 
@@ -30,7 +30,7 @@
 
 这里需要注意的是,网络分区带来的问题难以解决,因为它是非常难发现的。这是由于网络环境复杂的拓扑和参与者众多共同左右而导致的。故我们需要设计复杂的算法,并使用诸如混沌工程的方式来解决此类问题。
 
-最后需要强调的一点是,一个单一读故障可能会引起大规模级联反映,从而放大故障的影响面,也就是著名的雪崩现象。这里你要注意,这种故障放大现象很可能来源于一个为了稳定系统而设计的机制。比如,当系统出现瓶颈后,一个新节点被加入进来,但它需要同步数据才能对外提供服务,而大规模同步数据很可能造成其他节点资源紧张,特别是网络带宽,从而导致整个系统都无法对外提供服务。 **解决级联故障的方式有退避算法和断路** 。退避算法大量应用在 API 的设计中,由于上文提到远程节点会存在暂时性故障,故需要进行重试来使访问尽可能成功地完成。而频繁地重试会造成远程节点资源耗尽而崩溃,退避算法正是依靠客户端来保证服务端高可用的一种手段。而从服务端角度进行直接保护的方式就是断路,如果对服务端的访问超过阈值,那么系统会中断该服务的请求,从而缓解系统压力。
+最后需要强调的一点是,一个单一读故障可能会引起大规模级联反映,从而放大故障的影响面,也就是著名的雪崩现象。这里你要注意,这种故障放大现象很可能来源于一个为了稳定系统而设计的机制。比如,当系统出现瓶颈后,一个新节点被加入进来,但它需要同步数据才能对外提供服务,而大规模同步数据很可能造成其他节点资源紧张,特别是网络带宽,从而导致整个系统都无法对外提供服务。**解决级联故障的方式有退避算法和断路** 。退避算法大量应用在 API 的设计中,由于上文提到远程节点会存在暂时性故障,故需要进行重试来使访问尽可能成功地完成。而频繁地重试会造成远程节点资源耗尽而崩溃,退避算法正是依靠客户端来保证服务端高可用的一种手段。而从服务端角度进行直接保护的方式就是断路,如果对服务端的访问超过阈值,那么系统会中断该服务的请求,从而缓解系统压力。
 
 以上就是分布式系统比较常见的故障。虽然你可能会觉得这些故障很直观,但是如果要去解决它们思路会比较分散。还好前人已经帮我们总结了一些模型来对这些故障进行分级,从而有的放矢地解决这些问题。接下来我就要为你介绍三种典型的失败模型。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25414\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25414\350\256\262.md"
index 031d93cac..31902bc43 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25414\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25414\350\256\262.md"	
@@ -84,7 +84,7 @@
 
 如图所示,节点 1 无法直接去判断节点 2 是否存活,这个时候它转而询问其相邻节点 3。由节点 3 去询问节点 2 的健康情况,最后将此信息由节点 3 返回给节点 1。
 
-这种算法的好处是不需要将心跳检测进行广播,而是通过有限的网络连接,就可以检测到集群中各个分组内的健康情况,从而得知整个集群的健康情况。此种方法由于使用了组内的多个节点进行检测,其算法的准确度相比于一个节点去检测提高了很多。同时我们可以并行进行检测,算法的收敛速度也是很快的。因此可以说, **间接检测法在准确度和效率上取得了比较好的平衡** 。
+这种算法的好处是不需要将心跳检测进行广播,而是通过有限的网络连接,就可以检测到集群中各个分组内的健康情况,从而得知整个集群的健康情况。此种方法由于使用了组内的多个节点进行检测,其算法的准确度相比于一个节点去检测提高了很多。同时我们可以并行进行检测,算法的收敛速度也是很快的。因此可以说,**间接检测法在准确度和效率上取得了比较好的平衡** 。
 
 但是在大规模分布式数据库中,心跳检测法会面临效率上的挑战,那么何种算法比较好处理这种挑战呢?下面我要为你介绍 Gossip 协议检测法。
 
@@ -94,7 +94,7 @@
 
 算法的细节是每个节点都有一份全局节点列表,从中选择一些节点进行检测。如果成功就增加成功计数器,同时记录最近一次的检测时间;而后该节点把自己的检测列表的周期性同步给邻居节点,邻居节点获得这份列表后会与自己本地的列表进行合并;最终系统内所有节点都会知道整个系统的健康状态。
 
-如果某些节点没有进行正确响应,那么它们就会被标记为失败,从而进行后续的处理。 **这里注意,要设置合适的阈值来防止将正常的节点标记为错误** 。Gossip 算法广泛应用在无主的分布式系统中,比较著名的 Cassandra 就是采用了这种检测手法。
+如果某些节点没有进行正确响应,那么它们就会被标记为失败,从而进行后续的处理。**这里注意,要设置合适的阈值来防止将正常的节点标记为错误** 。Gossip 算法广泛应用在无主的分布式系统中,比较著名的 Cassandra 就是采用了这种检测手法。
 
 我们会发现,这种检测方法吸收了上文提到的间接检测方法的一些优势。每个节点是否应该被认为失败,是由多个节点判断的结果推导出的,并不是由单一节点做出的判断,这大大提高了系统的稳定性。但是,此种检测方法会极大增加系统内消息数量,故选择合适的数据包成为优化该模式的关键。这个问题我会在“17 | 数据可靠传播:反熵理论如何帮助数据库可靠工作”中详细介绍 Gossip 协议时给出答案。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25419\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25419\350\256\262.md"
index 850cf0976..35f644768 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25419\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25419\350\256\262.md"	
@@ -55,7 +55,7 @@ Spanner 不仅仅有 Google Cloud 的一种商业产品可供大家选择,同
 
 Spanner 引入了很多新技术去改善分布式事务的性能,但我们发现其流程整体还是传统的二阶段提交,并没有在结构上发生重大的改变,而 Calvin 却充满了颠覆性。让我们来看看它是怎么处理分布式事务的。
 
-首先,传统分布式事务处理使用到了锁来保证并发竞争的事务满足隔离级别的约束。比如,序列化级别保证了事务是一个接一个运行的。而每个副本的执行顺序是无法预测的,但结果是可以预测的。Calvin 的方案是让事务在每个副本上的执行顺序达到一致,那么执行结果也肯定是一致的。这样做的好处是避免了众多事务之间的锁竞争,从而大大提高了高并发度事务的吞吐量。同时,节点崩溃不影响事务的执行。因为事务执行步骤已经分配,节点恢复后从失败处接着运行该事务即可, **这种模式使分布式事务的可用性也大大提高** 。目前实现了 Calvin 事务模式的数据库是 FaunaDB。
+首先,传统分布式事务处理使用到了锁来保证并发竞争的事务满足隔离级别的约束。比如,序列化级别保证了事务是一个接一个运行的。而每个副本的执行顺序是无法预测的,但结果是可以预测的。Calvin 的方案是让事务在每个副本上的执行顺序达到一致,那么执行结果也肯定是一致的。这样做的好处是避免了众多事务之间的锁竞争,从而大大提高了高并发度事务的吞吐量。同时,节点崩溃不影响事务的执行。因为事务执行步骤已经分配,节点恢复后从失败处接着运行该事务即可,**这种模式使分布式事务的可用性也大大提高** 。目前实现了 Calvin 事务模式的数据库是 FaunaDB。
 
 其次,将事务进行排序的组件被称为 sequencer。它搜集事务信息,而后将它们拆解为较小的 epoch,这样做的目的是减小锁竞争,并提高并行度。一旦事务被准备好,sequencer 会将它们发送给 scheduler。scheduler 根据 sequencer 处理的结果,适时地并行执行部分事务步骤,同时也保证顺序执行的步骤不会被并行。因为这些步骤已经排好了顺序,scheduler 执行的时候不需要与 sequencer 进行交互,从而提高了执行效率。Calvin 事务的处理组件如下图所示。
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25422\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25422\350\256\262.md"
index ff9183119..78f487890 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25422\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25422\350\256\262.md"	
@@ -97,7 +97,7 @@ Apache ShardingShpere 的 Proxy 模式架构图
 2. **事务** 。由于传统数据库都需要复用原有的存储节点,故事务方案大多都是我们介绍过的两阶段提交这类原子提交协议。学习过模块三中分布式事务的同学都清楚,传统两阶段在性能和规模上都有很大的限制,必须采用新的事务模式才能突破这层天花板。而传统数据库的底层被锁死,很难在这个领域有更好的表现。
 3. **OLAP** 。传统数据库在转为分布式之前能很好地支持 OLAP。但其 Sharding 后,该过程变得越来越困难。同时随着大数据技术的崛起,它们有主动放弃该领域的趋势。而新一代的 HTAP 架构无一例外都是 NewSQL 和云原生数据库的天下,这个领域是从传统数据库发展而来的分布式数据库无法企及的。
 
-以上我们谈的传统数据库在分布式领域的局限其实总结为一点就是, **它们的底层存储引擎限制了其上层分布式功能的拓展** 。只有如 NewSQL 类数据库一般,使用创新的存储引擎,才能在整体上打造出功能与性能匹配的现代分布式数据库。但是,此类数据库由于发展多年,在稳定性、维护性上有不可动摇的优势,即使存在一些局限性,但其对单机版本的用户依然有很强的吸引力。
+以上我们谈的传统数据库在分布式领域的局限其实总结为一点就是,**它们的底层存储引擎限制了其上层分布式功能的拓展** 。只有如 NewSQL 类数据库一般,使用创新的存储引擎,才能在整体上打造出功能与性能匹配的现代分布式数据库。但是,此类数据库由于发展多年,在稳定性、维护性上有不可动摇的优势,即使存在一些局限性,但其对单机版本的用户依然有很强的吸引力。
 
 ## 总结
 
diff --git "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25423\350\256\262.md" "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25423\350\256\262.md"
index 23bfeeab7..6d565f2c3 100644
--- "a/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25423\350\256\262.md"	
+++ "b/docs/Database/24 \350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223/\347\254\25423\350\256\262.md"	
@@ -122,7 +122,7 @@ Saga 支持向前和向后恢复。
 
 显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务最终总会成功,或补偿事务难以定义或不可能,向前恢复会更符合你的需求。理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机、网络可能会失败,甚至数据中心也可能会停电,这时需要提供故障恢复后回退的机制,比如人工干预。
 
-总的来说, **TCC 是以应用服务的层次进行分布式事务的处理,而 XA、Bed、Saga 则是以数据库为层次进行分布式处理,故中间件一般倾向于采用后者来实现更细粒度的控制** 。
+总的来说,**TCC 是以应用服务的层次进行分布式事务的处理,而 XA、Bed、Saga 则是以数据库为层次进行分布式处理,故中间件一般倾向于采用后者来实现更细粒度的控制** 。
 
 ## Apache ShardingShpere 的分布式事务变迁
 
diff --git "a/docs/Database/300 \345\210\206\351\222\237\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230/\347\254\25400\350\256\262.md" "b/docs/Database/300 \345\210\206\351\222\237\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230/\347\254\25400\350\256\262.md"
index 92f57d2f9..e7614dc57 100644
--- "a/docs/Database/300 \345\210\206\351\222\237\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230/\347\254\25400\350\256\262.md"	
+++ "b/docs/Database/300 \345\210\206\351\222\237\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230/\347\254\25400\350\256\262.md"	
@@ -12,7 +12,7 @@
 
 所有这些数据都是靠良好的架构和不断改进的缓存体系来支撑的。
 
-其实,作为互联网公司,只要有直接面对用户的业务,要想持续确保系统的访问性能和可用性,都需要使用缓存。因此,缓存也是后端工程师面试中一个非常重要的考察点,面试官通常会通过应聘者对缓存相关知识的理解深入程度,来判断其开发经验和学习能力。可以说,对缓存的掌握程度,在某种意义上决定了后端开发者的职业高度。 **想学好缓存,需要掌握哪些知识呢?**
+其实,作为互联网公司,只要有直接面对用户的业务,要想持续确保系统的访问性能和可用性,都需要使用缓存。因此,缓存也是后端工程师面试中一个非常重要的考察点,面试官通常会通过应聘者对缓存相关知识的理解深入程度,来判断其开发经验和学习能力。可以说,对缓存的掌握程度,在某种意义上决定了后端开发者的职业高度。**想学好缓存,需要掌握哪些知识呢?**
 
 可以看一下这张“缓存知识点全景图”。
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25401\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25401\350\256\262.md"
index dd33cf143..3c987cc77 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25401\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25401\350\256\262.md"	
@@ -58,7 +58,7 @@ Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库。
 
 但是 Lucene 仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中。 更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理。Lucene 非常 复杂。
 
-Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单, **通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API** 。
+Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单,**通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API** 。
 
 然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容:
 
@@ -89,7 +89,7 @@ Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引
 > 我们还需对比结构化数据库,看看ES的基础概念,为我们后面学习作铺垫。
 
 - **Near Realtime(NRT)** 近实时。数据提交索引后,立马就可以搜索到。
-- **Cluster 集群** ,一个集群由一个唯一的名字标识,默认为“elasticsearch”。集群名称非常重要,具有相同集群名的节点才会组成一个集群。集群名称可以在配置文件中指定。
+- **Cluster 集群**,一个集群由一个唯一的名字标识,默认为“elasticsearch”。集群名称非常重要,具有相同集群名的节点才会组成一个集群。集群名称可以在配置文件中指定。
 - **Node 节点** :存储集群的数据,参与集群的索引和搜索功能。像集群有名字,节点也有自己的名称,默认在启动时会以一个随机的UUID的前七个字符作为节点的名字,你可以为其指定任意的名字。通过集群名在网络中发现同伴组成集群。一个节点也可是集群。
 - **Index 索引** : 一个索引是一个文档的集合(等同于solr中的集合)。每个索引有唯一的名字,通过这个名字来操作它。一个集群中可以有任意多个索引。
 - **Type 类型** :指在一个索引中,可以索引不同类型的文档,如用户数据、博客数据。从6.0.0 版本起已废弃,一个索引中只存放一类数据。
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25402\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25402\350\256\262.md"
index 09f665977..9ad8ddcf5 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25402\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25402\350\256\262.md"	
@@ -20,7 +20,7 @@ Beats是一个面向 **轻量型采集器** 的平台,这些采集器可以从
 
 ### Logstash
 
-Logstash是 **动态数据收集管道** ,拥有可扩展的插件生态系统,支持从不同来源采集数据,转换数据,并将数据发送到不同的存储库中。其能够与ElasticSearch产生强大的协同作用,后被Elastic公司在2013年收购。
+Logstash是 **动态数据收集管道**,拥有可扩展的插件生态系统,支持从不同来源采集数据,转换数据,并将数据发送到不同的存储库中。其能够与ElasticSearch产生强大的协同作用,后被Elastic公司在2013年收购。
 
 它具有如下特性:
 
@@ -34,7 +34,7 @@ Logstash是 **动态数据收集管道** ,拥有可扩展的插件生态系统
 
 ### ElasticSearch
 
-ElasticSearch对数据进行 **搜索、分析和存储** ,其是基于JSON的分布式搜索和分析引擎,专门为实现水平可扩展性、高可靠性和管理便捷性而设计的。
+ElasticSearch对数据进行 **搜索、分析和存储**,其是基于JSON的分布式搜索和分析引擎,专门为实现水平可扩展性、高可靠性和管理便捷性而设计的。
 
 它的实现原理主要分为以下几个步骤:
 
@@ -46,7 +46,7 @@ ElasticSearch对数据进行 **搜索、分析和存储** ,其是基于JSON的
 
 ### Kibana
 
-Kibana实现 **数据可视化** ,其作用就是在ElasticSearch中进行民航。Kibana能够以图表的形式呈现数据,并且具有可扩展的用户界面,可以全方位的配置和管理ElasticSearch。
+Kibana实现 **数据可视化**,其作用就是在ElasticSearch中进行民航。Kibana能够以图表的形式呈现数据,并且具有可扩展的用户界面,可以全方位的配置和管理ElasticSearch。
 
 Kibana最早的时候是基于Logstash创建的工具,后被Elastic公司在2013年收购。
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25404\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25404\350\256\262.md"
index b2af4d108..9fc56ab90 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25404\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25404\350\256\262.md"	
@@ -220,7 +220,7 @@ GET /bank/_search
 
 ![img](assets/es-usage-8.png)
 
-两者都可以写查询条件,而且语法也类似。区别在于, **query 上下文的条件是用来给文档打分的,匹配越好 _score 越高;filter 的条件只产生两种结果:符合与不符合,后者被过滤掉** 。
+两者都可以写查询条件,而且语法也类似。区别在于,**query 上下文的条件是用来给文档打分的,匹配越好 _score 越高;filter 的条件只产生两种结果:符合与不符合,后者被过滤掉** 。
 
 所以,我们进一步看只包含filter的查询
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25405\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25405\350\256\262.md"
index f013b0745..a886ab2c3 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25405\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25405\350\256\262.md"	
@@ -33,7 +33,7 @@ PUT /customer/_doc/1
 }
 ```
 
-那么如果我们需要对这个建立索引的过程做更多的控制:比如想要确保这个索引有数量适中的主分片,并且在我们索引任何数据之前,分析器和映射已经被建立好。那么就会引入两点:第一个 **禁止自动创建索引** ,第二个是 **手动创建索引** 。
+那么如果我们需要对这个建立索引的过程做更多的控制:比如想要确保这个索引有数量适中的主分片,并且在我们索引任何数据之前,分析器和映射已经被建立好。那么就会引入两点:第一个 **禁止自动创建索引**,第二个是 **手动创建索引** 。
 
 - 禁止自动创建索引
 
@@ -162,7 +162,7 @@ green open test-index-users                          LSaIB57XSC6uVtGQHoPYxQ 1 1
 
 ### 打开/关闭索引
 
-- **关闭索引** 一旦索引被关闭,那么这个索引只能显示元数据信息, **不能够进行读写操作** 。
+- **关闭索引** 一旦索引被关闭,那么这个索引只能显示元数据信息,**不能够进行读写操作** 。
 
 ![img](assets/es-index-manage-7.png)
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25407\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25407\350\256\262.md"
index 73eb1704d..6cdf121c5 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25407\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25407\350\256\262.md"	
@@ -22,7 +22,7 @@ GET /bank/_search
 }
 ```
 
-这种查询就是本文要介绍的 **复合查询** ,并且bool查询只是复合查询一种。
+这种查询就是本文要介绍的 **复合查询**,并且bool查询只是复合查询一种。
 
 ## bool query(布尔查询)
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25408\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25408\350\256\262.md"
index 761164f41..d1e5c14f9 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25408\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25408\350\256\262.md"	
@@ -8,19 +8,19 @@
 
 一些理解:
 
-- 第一点: **全局观** ,即我们现在学习内容在整个体系的哪个位置?
+- 第一点: **全局观**,即我们现在学习内容在整个体系的哪个位置?
 
 如下图,可以很方便的帮助你构筑这种体系
 
 ![img](assets/es-dsl-full-text-3.png)
 
-- 第二点: **分类别** ,从上层理解,而不是本身
+- 第二点: **分类别**,从上层理解,而不是本身
 
 比如Full text Query中,我们只需要把如下的那么多点分为3大类,你的体系能力会大大提升
 
 ![img](assets/es-dsl-full-text-1.png)
 
-- 第三点: **知识点还是API** ? API类型的是可以查询的,只需要知道大致有哪些功能就可以了。
+- 第三点: **知识点还是API**? API类型的是可以查询的,只需要知道大致有哪些功能就可以了。
 
 ![img](assets/es-dsl-full-text-2.png)
 
@@ -77,7 +77,7 @@ Elasticsearch 执行上面这个 match 查询的步骤是:
 用 term 查询在倒排索引中查找 quick 然后获取一组包含该项的文档,本例的结果是文档:1、2 和 3 。
 1. **为每个文档评分** 。
 用 term 查询计算每个文档相关度评分 \_score ,这是种将词频(term frequency,即词 quick 在相关文档的 title 字段中出现的频率)和反向文档频率(inverse document frequency,即词 quick 在所有文档的 title 字段中出现的频率),以及字段的长度(即字段越短相关度越高)相结合的计算方式。
-* **验证结果** ![img](assets/es-dsl-full-text-4.png)
+* **验证结果**![img](assets/es-dsl-full-text-4.png)
 ### match多个词深入
 我们在上文中复合查询中已经使用了match多个词,比如“Quick pets”; 这里我们通过例子带你更深入理解match多个词
 * **match多个词的本质** 查询多个词"BROWN DOG!"
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25412\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25412\350\256\262.md"
index 62b98286b..351afd5ba 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25412\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25412\350\256\262.md"	
@@ -36,7 +36,7 @@
 
 ![img](assets/es-agg-pipeline-1.png)
 
-> 第一个维度:管道聚合有很多不同 **类型** ,每种类型都与其他聚合计算不同的信息,但是可以将这些类型分为两类:
+> 第一个维度:管道聚合有很多不同 **类型**,每种类型都与其他聚合计算不同的信息,但是可以将这些类型分为两类:
 
 - **父级** 父级聚合的输出提供了一组管道聚合,它可以计算新的存储桶或新的聚合以添加到现有存储桶中。
 - **兄弟** 同级聚合的输出提供的管道聚合,并且能够计算与该同级聚合处于同一级别的新聚合。
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25414\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25414\350\256\262.md"
index dc4ad9b78..47b18359b 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25414\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25414\350\256\262.md"	
@@ -47,7 +47,7 @@
 
 ## 补充:ElasticSearch分析器
 
-> 上图中很重要的一项是 **语法分析/语言处理** , 所以我们还需要补充ElasticSearch分析器知识点。
+> 上图中很重要的一项是 **语法分析/语言处理**, 所以我们还需要补充ElasticSearch分析器知识点。
 
 分析 包含下面的过程:
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25415\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25415\350\256\262.md"
index 86dbbd80f..2173a57bf 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25415\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25415\350\256\262.md"	
@@ -88,7 +88,7 @@ Elasticsearch通过在后台进行Merge Segment来解决这个问题。小的段
 
 一旦合并结束,老的段被删除:
 
-1. 新的段被刷新(flush)到了磁盘。 ** 写入一个包含新段且排除旧的和较小的段的新提交点。
+1. 新的段被刷新(flush)到了磁盘。** 写入一个包含新段且排除旧的和较小的段的新提交点。
 1. 新的段被打开用来搜索。
 1. 老的段被删除。
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25416\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25416\350\256\262.md"
index f3ebdfb4a..4d0a3b94b 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25416\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25416\350\256\262.md"	
@@ -90,7 +90,7 @@ Elasticsearch中通过分区实现分布式,数据写入的时候根据_routin
 
 这里有一个问题就是请求膨胀,用户的一个搜索请求在Elasticsearch内部会变成Shard个请求,这里有个优化点,虽然是Shard个请求,但是这个Shard个数不一定要是当前Index中的Shard个数,只要是当前查询相关的Shard即可,这个需要基于业务和请求内容优化,通过这种方式可以优化请求膨胀数。
 
-Elasticsearch中的查询主要分为两类, **Get请求** :通过ID查询特定Doc; **Search请求** :通过Query查询匹配Doc。
+Elasticsearch中的查询主要分为两类,**Get请求** :通过ID查询特定Doc; **Search请求** :通过Query查询匹配Doc。
 
 ![img](assets/es-th-3-9.jpeg)
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25417\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25417\350\256\262.md"
index 876cf9f4f..ae647beff 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25417\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25417\350\256\262.md"	
@@ -73,9 +73,9 @@ G1对于集群正常运作的情况下减轻G1停顿对服务时延的影响还
 
 **这个简单的更改可以带来显著的影响。仅仅是使用正确的调度程序,我们看到了 500 倍的写入能力提升** 。
 
-如果你使用旋转介质(如机械硬盘),尝试获取尽可能快的硬盘(高性能服务器硬盘,15k RPM 驱动器)。 **使用 RAID0 是提高硬盘速度的有效途径,对机械硬盘和 SSD 来说都是如此** 。没有必要使用镜像或其它 RAID 变体,因为 Elasticsearch 在自身层面通过副本,已经提供了备份的功能,所以不需要利用磁盘的备份功能,同时如果使用磁盘备份功能的话,对写入速度有较大的影响。
+如果你使用旋转介质(如机械硬盘),尝试获取尽可能快的硬盘(高性能服务器硬盘,15k RPM 驱动器)。**使用 RAID0 是提高硬盘速度的有效途径,对机械硬盘和 SSD 来说都是如此** 。没有必要使用镜像或其它 RAID 变体,因为 Elasticsearch 在自身层面通过副本,已经提供了备份的功能,所以不需要利用磁盘的备份功能,同时如果使用磁盘备份功能的话,对写入速度有较大的影响。
 
-最后, **避免使用网络附加存储(NAS)** 。人们常声称他们的 NAS 解决方案比本地驱动器更快更可靠。除却这些声称,我们从没看到 NAS 能配得上它的大肆宣传。NAS 常常很慢,显露出更大的延时和更宽的平均延时方差,而且它是单点故障的。
+最后,**避免使用网络附加存储(NAS)** 。人们常声称他们的 NAS 解决方案比本地驱动器更快更可靠。除却这些声称,我们从没看到 NAS 能配得上它的大肆宣传。NAS 常常很慢,显露出更大的延时和更宽的平均延时方差,而且它是单点故障的。
 
 ## 索引优化设置
 
@@ -367,7 +367,7 @@ hot 节点主要是索引节点(写节点),同时会保存近期的一些
 node.attr.box_type: hot
 ```
 
-如果是针对指定的 index 操作,可以通过 settings 设置 index.routing.allocation.require.box_type: hot 将索引写入 hot 节点。 **warm 节点** :
+如果是针对指定的 index 操作,可以通过 settings 设置 index.routing.allocation.require.box_type: hot 将索引写入 hot 节点。**warm 节点** :
 
 这种类型的节点是为了处理大量的,而且不经常访问的只读索引而设计的。由于这些索引是只读的,warm 节点倾向于挂载大量磁盘(普通磁盘)来替代 SSD。内存、CPU 的配置跟 hot 节点保持一致即可;节点数量一般也是大于等于 3 个。
 
@@ -379,7 +379,7 @@ node.attr.box_type: warm
 
 同时,也可以在 elasticsearch.yml 中设置 index.codec:best_compression 保证 warm 节点的压缩配置。
 
-当索引不再被频繁查询时,可通过 index.routing.allocation.require.box_type:warm,将索引标记为 warm,从而保证索引不写入 hot 节点,以便将 SSD 磁盘资源用在刀刃上。一旦设置这个属性,ES 会自动将索引合并到 warm 节点。 **协调(coordinating)节点**
+当索引不再被频繁查询时,可通过 index.routing.allocation.require.box_type:warm,将索引标记为 warm,从而保证索引不写入 hot 节点,以便将 SSD 磁盘资源用在刀刃上。一旦设置这个属性,ES 会自动将索引合并到 warm 节点。**协调(coordinating)节点**
 
 协调节点用于做分布式里的协调,将各分片或节点返回的数据整合后返回。该节点不会被选作主节点,也不会存储任何索引数据。该服务器主要用于查询负载均衡。在查询的时候,通常会涉及到从多个 node 服务器上查询数据,并将请求分发到多个指定的 node 服务器,并对各个 node 服务器返回的结果进行一个汇总处理,最终返回给客户端。在 ES 集群中,所有的节点都有可能是协调节点,但是,可以通过设置 node.master、node.data、node.ingest 都为 false 来设置专门的协调节点。需要较好的 CPU 和较高的内存。
 
diff --git "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25418\350\256\262.md" "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25418\350\256\262.md"
index 08fc317e2..c9b288211 100644
--- "a/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25418\350\256\262.md"	
+++ "b/docs/Database/ElasticSearch \347\237\245\350\257\206\344\275\223\347\263\273\350\257\246\350\247\243/\347\254\25418\350\256\262.md"	
@@ -210,7 +210,7 @@ HBase 2.0 版本中也实现了 off-heap,在堆外建立了 cache,脱离系
 
 ### 扩展性优化
 
-接下来是最后一块内核优化内容, **扩展性优化** 。
+接下来是最后一块内核优化内容,**扩展性优化** 。
 
 ![img](assets/es-tencent-y-25.jpeg)
 
diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25400\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25400\350\256\262.md"
index facc47763..964d216fc 100644
--- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25400\350\256\262.md"	
+++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25400\350\256\262.md"	
@@ -24,7 +24,7 @@
 
 因此,我希望这个专栏能够激发开发者对数据库原理的探索欲,从而更好地理解工作中遇到的问题,更能知道背后的为什么。所以 **我会选那些平时使用数据库时高频出现的知识,如事务、索引、锁等内容构成专栏的主线** 。这些主线上是一个个的知识点。每个点就是一个概念、一个机制或者一个原理说明。在每个说明之后,我会和你讨论一个实践相关的问题。
 
-希望能以这样的方式,让你对 MySQL 的几条主线有一个整体的认识,并且了解基本概念。在之后的实践篇中,我会引用到这些主线的知识背景,并着力说明它们是怎样指导实践的。这样, **你可以从点到线,再到面,形成自己的 MySQL 知识网络。**
+希望能以这样的方式,让你对 MySQL 的几条主线有一个整体的认识,并且了解基本概念。在之后的实践篇中,我会引用到这些主线的知识背景,并着力说明它们是怎样指导实践的。这样,**你可以从点到线,再到面,形成自己的 MySQL 知识网络。**
 
 在这里,有一份目录,你也可以先了解下整个专栏的知识结构。
 
diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25401\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25401\350\256\262.md"
index a637aceaa..e211588c4 100644
--- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25401\350\256\262.md"	
+++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25401\350\256\262.md"	
@@ -30,7 +30,7 @@ Server 层包括连接器、查询缓存、分析器、优化器、执行器等
 
 也就是说,你执行 create table 建表的时候,如果不指定引擎类型,默认使用的就是 InnoDB。不过,你也可以通过指定存储引擎的类型来选择别的引擎,比如在 create table 语句中使用 engine=memory, 来指定使用内存引擎创建表。不同存储引擎的表数据存取方式不同,支持的功能也不同,在后面的文章中,我们会讨论到引擎的选择。
 
-从图中不难看出,不同的存储引擎共用一个 **Server层** ,也就是从连接器到执行器的部分。你可以先对每个组件的名字有个印象,接下来我会结合开头提到的那条 SQL 语句,带你走一遍整个执行流程,依次看下每个组件的作用。
+从图中不难看出,不同的存储引擎共用一个 **Server层**,也就是从连接器到执行器的部分。你可以先对每个组件的名字有个印象,接下来我会结合开头提到的那条 SQL 语句,带你走一遍整个执行流程,依次看下每个组件的作用。
 
 ## 连接器
 
diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25403\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25403\350\256\262.md"
index 4085d65e2..2874e78af 100644
--- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25403\350\256\262.md"	
+++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25403\350\256\262.md"	
@@ -54,7 +54,7 @@ mysql> show variables like 'transaction_isolation';
 +-----------------------+----------------+
 ```
 
-总结来说,存在即合理,哪个隔离级别都有它自己的使用场景,你要根据自己的业务情况来定。我想 **你可能会问那什么时候需要“可重复读”的场景呢** ?我们来看一个数据校对逻辑的案例。
+总结来说,存在即合理,哪个隔离级别都有它自己的使用场景,你要根据自己的业务情况来定。我想 **你可能会问那什么时候需要“可重复读”的场景呢**?我们来看一个数据校对逻辑的案例。
 
 假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。
 
diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25404\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25404\350\256\262.md"
index 0ef2597ea..a3be5212f 100644
--- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25404\350\256\262.md"	
+++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25404\350\256\262.md"	
@@ -29,7 +29,7 @@
 
 你可以设想下,如果你现在要找身份证号在 \[ID_card_X, ID_card_Y\] 这个区间的所有用户,就必须全部扫描一遍了。
 
-所以, **哈希表这种结构适用于只有等值查询的场景** ,比如 Memcached 及其他一些 NoSQL 引擎。
+所以,**哈希表这种结构适用于只有等值查询的场景**,比如 Memcached 及其他一些 NoSQL 引擎。
 
 而 **有序数组在等值查询和范围查询场景中的性能就都非常优秀** 。还是上面这个根据身份证号查名字的例子,如果我们使用有序数组来实现的话,示意图如下所示:
 
@@ -44,7 +44,7 @@
 
 如果仅仅看查询效率,有序数组就是最好的数据结构了。但是,在需要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高。
 
-所以, **有序数组索引只适用于静态存储引擎** ,比如你要保存的是 2017 年某个城市的所有人口信息,这类不会再修改的数据。
+所以,**有序数组索引只适用于静态存储引擎**,比如你要保存的是 2017 年某个城市的所有人口信息,这类不会再修改的数据。
 
 二叉搜索树也是课本里的经典数据结构了。还是上面根据身份证号查名字的例子,如果我们用二叉搜索树来实现的话,示意图如下所示:
 
diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25405\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25405\350\256\262.md"
index 2f32faa4c..dd2426d3d 100644
--- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25405\350\256\262.md"	
+++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25405\350\256\262.md"	
@@ -30,13 +30,13 @@ insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,
 4. 再回到 ID 索引树查到 ID=500 对应的 R4;
 5. 在 k 索引树取下一个值 k=6,不满足条件,循环结束。
 
-在这个过程中, **回到主键索引树搜索的过程,我们称为回表** 。可以看到,这个查询过程读了 k 索引树的 3 条记录(步骤 1、3 和 5),回表了两次(步骤 2 和 4)。
+在这个过程中,**回到主键索引树搜索的过程,我们称为回表** 。可以看到,这个查询过程读了 k 索引树的 3 条记录(步骤 1、3 和 5),回表了两次(步骤 2 和 4)。
 
 在这个例子中,由于查询结果所需要的数据只在主键索引上有,所以不得不回表。那么,有没有可能经过索引优化,避免回表过程呢?
 
 ## 覆盖索引
 
-如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。 **由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。** 需要注意的是,在引擎内部使用覆盖索引在索引 k 上其实读了三个记录,R3~R5(对应的索引 k 上的记录项),但是对于 MySQL 的 Server 层来说,它就是找引擎拿到了两条记录,因此 MySQL 认为扫描行数是 2。
+如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。**由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。** 需要注意的是,在引擎内部使用覆盖索引在索引 k 上其实读了三个记录,R3~R5(对应的索引 k 上的记录项),但是对于 MySQL 的 Server 层来说,它就是找引擎拿到了两条记录,因此 MySQL 认为扫描行数是 2。
 
 !!! Note "备注"
     关于如何查看扫描行数的问题,我将会在 [第 17 讲《如何正确地显示随机消息?》](../第17讲)中,和你详细讨论。
@@ -66,7 +66,7 @@ CREATE TABLE `tuser` (
 
 看到这里你一定有一个疑问,如果为每一种查询都设计一个索引,索引是不是太多了。如果我现在要按照市民的身份证号去查他的家庭地址呢?虽然这个查询需求在业务中出现的概率不高,但总不能让它走全表扫描吧?反过来说,单独为一个不频繁的请求创建一个(身份证号,地址)的索引又感觉有点浪费。应该怎么做呢?
 
-这里,我先和你说结论吧。 **B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。** 为了直观地说明这个概念,我们用(name,age)这个联合索引来分析。
+这里,我先和你说结论吧。**B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。** 为了直观地说明这个概念,我们用(name,age)这个联合索引来分析。
 
 
图2 (name,age)索引示意图 @@ -81,7 +81,7 @@ CREATE TABLE `tuser` ( 可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。 -基于上面对最左前缀索引的说明,我们来讨论一个问题: **在建立联合索引的时候,如何安排索引内的字段顺序。** 这里我们的评估标准是,索引的复用能力。因为可以支持最左前缀,所以当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。因此, **第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。** 所以现在你知道了,这段开头的问题里,我们要为高频请求创建 (身份证号,姓名)这个联合索引,并用这个索引支持“根据身份证号查询地址”的需求。 +基于上面对最左前缀索引的说明,我们来讨论一个问题: **在建立联合索引的时候,如何安排索引内的字段顺序。** 这里我们的评估标准是,索引的复用能力。因为可以支持最左前缀,所以当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。因此,**第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。** 所以现在你知道了,这段开头的问题里,我们要为高频请求创建 (身份证号,姓名)这个联合索引,并用这个索引支持“根据身份证号查询地址”的需求。 那么,如果既有联合查询,又有基于 a、b 各自的查询呢?查询条件里面只有 b 的语句,是无法使用 (a,b) 这个联合索引的,这时候你不得不维护另外一个索引,也就是说你需要同时维护 (a,b)、(b) 这两个索引。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25406\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25406\350\256\262.md" index 39c21345a..f4536d3dd 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25406\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25406\350\256\262.md" @@ -8,7 +8,7 @@ ## 全局锁 -顾名思义,全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。 **全局锁的典型使用场景是,做全库逻辑备份。** 也就是把整库每个表都 select 出来存成文本。 +顾名思义,全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。**全局锁的典型使用场景是,做全库逻辑备份。** 也就是把整库每个表都 select 出来存成文本。 以前有一种做法,是通过 FTWRL 确保不会有其他线程对数据库做更新,然后对整个库做备份。注意,在备份过程中整个库完全处于只读状态。 @@ -45,11 +45,11 @@ 官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。 -你一定在疑惑,有了这个功能,为什么还需要 FTWRL 呢? **一致性读是好,但前提是引擎要支持这个隔离级别。** 比如,对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用 FTWRL 命令了。 +你一定在疑惑,有了这个功能,为什么还需要 FTWRL 呢?**一致性读是好,但前提是引擎要支持这个隔离级别。** 比如,对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用 FTWRL 命令了。 -所以, **single-transaction 方法只适用于所有的表使用事务引擎的库。** 如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。 +所以,**single-transaction 方法只适用于所有的表使用事务引擎的库。** 如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。 -你也许会问, **既然要全库只读,为什么不使用 set global readonly=true 的方式呢** ?确实 readonly 方式也可以让全库进入只读状态,但我还是会建议你用 FTWRL 方式,主要有两个原因: +你也许会问,**既然要全库只读,为什么不使用 set global readonly=true 的方式呢**?确实 readonly 方式也可以让全库进入只读状态,但我还是会建议你用 FTWRL 方式,主要有两个原因: - 一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。 - 二是,在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。 @@ -66,7 +66,7 @@ MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁 举个例子, 如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。 -在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。而对于 InnoDB 这种支持行锁的引擎,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大。 **另一类表级的锁是 MDL(metadata lock)。** MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。 +在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。而对于 InnoDB 这种支持行锁的引擎,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大。**另一类表级的锁是 MDL(metadata lock)。** MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。 因此,在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。 @@ -94,7 +94,7 @@ MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁 你现在应该知道了,事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。 -基于上面的分析,我们来讨论一个问题, **如何安全地给小表加字段?** +基于上面的分析,我们来讨论一个问题,**如何安全地给小表加字段?** 首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25407\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25407\350\256\262.md" index 32c946a6b..c8d7b7562 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25407\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25407\350\256\262.md" @@ -22,7 +22,7 @@ MySQL 的行锁是在引擎层由各个引擎自己实现的。但并不是所 知道了这个答案,你一定知道了事务 A 持有的两个记录的行锁,都是在 commit 的时候才释放的。 -也就是说, **在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。** +也就是说,**在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。** 知道了这个设定,对我们使用事务有什么帮助呢?那就是,如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。我给你举个例子。 @@ -77,7 +77,7 @@ MySQL 的行锁是在引擎层由各个引擎自己实现的。但并不是所 因此,这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;如果你的团队有能修改 MySQL 源码的人,也可以做在 MySQL 里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。 -可能你会问, **如果团队里暂时没有数据库方面的专家,不能实现这样的方案,能不能从设计上优化这个问题呢?** +可能你会问,**如果团队里暂时没有数据库方面的专家,不能实现这样的方案,能不能从设计上优化这个问题呢?** 你可以考虑通过将一行改成逻辑上的多行来减少锁冲突。还是以影院账户为例,可以考虑放在多条记录上,比如 10 个记录,影院的账户总额等于这 10 个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的 1/10,可以减少锁等待个数,也就减少了死锁检测的 CPU 消耗。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25408\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25408\350\256\262.md" index 27313f0ef..30ed9cc21 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25408\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25408\350\256\262.md" @@ -64,7 +64,7 @@ InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。 图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。 -你可能会问,前面的文章不是说,语句更新会生成 undo log(回滚日志)吗?那么, **undo log 在哪呢?** 实际上,图 2 中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。 +你可能会问,前面的文章不是说,语句更新会生成 undo log(回滚日志)吗?那么,**undo log 在哪呢?** 实际上,图 2 中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。 明白了多版本和 row trx_id 的概念后,我们再来想一下,InnoDB 是怎么定义那个“100G”的快照的。 @@ -99,7 +99,7 @@ InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。 你看,有了这个声明后,系统里面随后发生的更新,是不是就跟这个事务看到的内容无关了呢?因为之后的更新,生成的版本一定属于上面的 2 或者 3(a) 的情况,而对它来说,这些新的数据版本是不存在的,所以这个事务的快照,就是“静态”的了。 -所以你现在知道了, **InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。** +所以你现在知道了,**InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。** 接下来,我们继续看一下图 1 中的三个事务,分析下事务 A 的语句返回的结果,为什么是 k=1。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25409\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25409\350\256\262.md" index d611ce4ce..1bd31e9f1 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25409\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25409\350\256\262.md" @@ -62,13 +62,13 @@ select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz'; 显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。 -那么, **什么条件下可以使用 change buffer 呢?** 对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。 +那么,**什么条件下可以使用 change buffer 呢?** 对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。 因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。 change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。 -现在,你已经理解了 change buffer 的机制,那么我们再一起来看看 **如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。** 第一种情况是, **这个记录要更新的目标页在内存中** 。这时,InnoDB 的处理流程如下: +现在,你已经理解了 change buffer 的机制,那么我们再一起来看看 **如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。** 第一种情况是,**这个记录要更新的目标页在内存中** 。这时,InnoDB 的处理流程如下: - 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束; - 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。 @@ -77,7 +77,7 @@ change buffer 用的是 buffer pool 里的内存,因此不能无限增大。ch 但,这不是我们关注的重点。 -第二种情况是, **这个记录要更新的目标页不在内存中** 。这时,InnoDB 的处理流程如下: +第二种情况是,**这个记录要更新的目标页不在内存中** 。这时,InnoDB 的处理流程如下: - 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束; - 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25410\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25410\350\256\262.md" index 61a6aeda8..25a5715a0 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25410\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25410\350\256\262.md" @@ -112,7 +112,7 @@ select * from t force index(a) where a between 10000 and 20000;/*Q2*/
图4 表 t 的 show index 结果
-那么, **MySQL 是怎样得到索引的基数的呢?** 这里,我给你简单介绍一下 MySQL 采样统计的方法。 +那么,**MySQL 是怎样得到索引的基数的呢?** 这里,我给你简单介绍一下 MySQL 采样统计的方法。 为什么要采样统计呢?因为把整张表取出来一行行统计,虽然可以得到精确的结果,但是代价太高了,所以只能选择“采样统计”。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25411\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25411\350\256\262.md" index 203e90a7a..ba5efbafe 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25411\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25411\350\256\262.md" @@ -146,7 +146,7 @@ select id,name,email from SUser where email='xxx'; mysql> select field_list from t where id_card = reverse('input_id_card_string'); ``` -由于身份证号的最后 6 位没有地址码这样的重复逻辑,所以最后这 6 位很可能就提供了足够的区分度。当然了,实践中你不要忘记使用 count(distinct) 方法去做个验证。 **第二种方式是使用 hash 字段。** 你可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。 +由于身份证号的最后 6 位没有地址码这样的重复逻辑,所以最后这 6 位很可能就提供了足够的区分度。当然了,实践中你不要忘记使用 count(distinct) 方法去做个验证。**第二种方式是使用 hash 字段。** 你可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。 ```sql mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc); diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25412\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25412\350\256\262.md" index d8d42caf6..7d91ff9fb 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25412\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25412\350\256\262.md" @@ -47,11 +47,11 @@ checkpoint 可不是随便往前修改一下位置就可以的。比如图 2 中 **第四种场景是,** 年底了咸亨酒店要关门几天,需要把账结清一下。这时候掌柜要把所有账都记到账本上,这样过完年重新开张的时候,就能就着账本明确账目情况了。 这种场景,对应的就是 MySQL 正常关闭的情况。这时候,MySQL 会把内存的脏页都 flush 到磁盘上,这样下次 MySQL 启动的时候,就可以直接从磁盘上读数据,启动速度会很快。 -接下来, **你可以分析一下上面四种场景对性能的影响。** 其中,第三种情况是属于 MySQL 空闲时的操作,这时系统没什么压力,而第四种场景是数据库本来就要关闭了。这两种情况下,你不会太关注“性能”问题。所以这里,我们主要来分析一下前两种场景下的性能问题。 +接下来,**你可以分析一下上面四种场景对性能的影响。** 其中,第三种情况是属于 MySQL 空闲时的操作,这时系统没什么压力,而第四种场景是数据库本来就要关闭了。这两种情况下,你不会太关注“性能”问题。所以这里,我们主要来分析一下前两种场景下的性能问题。 第一种是“redo log 写满了,要 flush 脏页”,这种情况是 InnoDB 要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。如果你从监控上看,这时候更新数会跌为 0。 -第二种是“内存不够用了,要先将脏页写到磁盘”,这种情况其实是常态。 **InnoDB 用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:** +第二种是“内存不够用了,要先将脏页写到磁盘”,这种情况其实是常态。**InnoDB 用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:** - 第一种是,还没有使用的; - 第二种是,使用了并且是干净页; @@ -86,7 +86,7 @@ InnoDB 的策略是尽量使用内存,因此对于一个长时间运行的库 虽然我们现在已经定义了“全力刷脏页”的行为,但平时总不能一直是全力刷吧?毕竟磁盘能力不能只用来刷脏页,还需要服务用户请求。所以接下来,我们就一起看看 InnoDB 怎么控制引擎按照“全力”的百分比来刷脏页。 -根据我前面提到的知识点,试想一下, **如果你来设计策略控制刷脏页的速度,会参考哪些因素呢?** +根据我前面提到的知识点,试想一下,**如果你来设计策略控制刷脏页的速度,会参考哪些因素呢?** 这个问题可以这么想,如果刷太慢,会出现什么情况?首先是内存脏页太多,其次是 redo log 写满。 @@ -107,7 +107,7 @@ F1(M) InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,我们假设为 N。InnoDB 会根据这个 N 算出一个范围在 0 到 100 之间的数字,这个计算公式可以记为 F2(N)。F2(N) 算法比较复杂,你只要知道 N 越大,算出来的值越大就好了。 -然后, **根据上述算得的 F1(M) 和 F2(N) 两个值,取其中较大的值记为 R,之后引擎就可以按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度。** 上述的计算流程比较抽象,不容易理解,所以我画了一个简单的流程图。图中的 F1、F2 就是上面我们通过脏页比例和 redo log 写入速度算出来的两个值。 +然后,**根据上述算得的 F1(M) 和 F2(N) 两个值,取其中较大的值记为 R,之后引擎就可以按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度。** 上述的计算流程比较抽象,不容易理解,所以我画了一个简单的流程图。图中的 F1、F2 就是上面我们通过脏页比例和 redo log 写入速度算出来的两个值。
图3 InnoDB 刷脏页速度策略 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25413\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25413\350\256\262.md" index f002f34d7..8d45d4e59 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25413\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25413\350\256\262.md" @@ -19,7 +19,7 @@ 我建议你不论使用 MySQL 的哪个版本,都将这个值设置为 ON。因为,一个表单独存储为一个文件更容易管理,而且在你不需要这个表的时候,通过 drop table 命令,系统就会直接删除这个文件。而如果是放在共享表空间中,即使表删掉了,空间也是不会回收的。 -所以, **将 innodb_file_per_table 设置为 ON,是推荐做法,我们接下来的讨论都是基于这个设置展开的。** 我们在删除整个表的时候,可以使用 drop table 命令回收表空间。但是,我们遇到的更多的删除数据的场景是删除某些行,这时就遇到了我们文章开头的问题:表中的数据被删除了,但是表空间却没有被回收。 +所以,**将 innodb_file_per_table 设置为 ON,是推荐做法,我们接下来的讨论都是基于这个设置展开的。** 我们在删除整个表的时候,可以使用 drop table 命令回收表空间。但是,我们遇到的更多的删除数据的场景是删除某些行,这时就遇到了我们文章开头的问题:表中的数据被删除了,但是表空间却没有被回收。 我们要彻底搞明白这个问题的话,就要从数据删除流程说起了。 @@ -38,7 +38,7 @@ 答案是,整个数据页就可以被复用了。 -但是, **数据页的复用跟记录的复用是不同的。** 记录的复用,只限于符合范围条件的数据。比如上面的这个例子,R4 这条记录被删除后,如果插入一个 ID 是 400 的行,可以直接复用这个空间。但如果插入的是一个 ID 是 800 的行,就不能复用这个位置了。 +但是,**数据页的复用跟记录的复用是不同的。** 记录的复用,只限于符合范围条件的数据。比如上面的这个例子,R4 这条记录被删除后,如果插入一个 ID 是 400 的行,可以直接复用这个空间。但如果插入的是一个 ID 是 800 的行,就不能复用这个位置了。 而当整个页从 B+ 树里面摘掉以后,可以复用到任何位置。以图 1 为例,如果将数据页 page A 上的所有记录删除以后,page A 会被标记为可复用。这时候如果要插入一条 ID=50 的记录需要使用新页的时候,page A 是可以被复用的。 @@ -48,7 +48,7 @@ 你现在知道了,delete 命令其实只是把记录的位置,或者数据页标记为了“可复用”,但磁盘文件的大小是不会变的。也就是说,通过 delete 命令是不能回收表空间的。这些可以复用,而没有被使用的空间,看起来就像是“空洞”。 -实际上, **不止是删除数据会造成空洞,插入数据也会。** 如果数据是按照索引递增顺序插入的,那么索引是紧凑的。但如果数据是随机插入的,就可能造成索引的数据页分裂。 +实际上,**不止是删除数据会造成空洞,插入数据也会。** 如果数据是按照索引递增顺序插入的,那么索引是紧凑的。但如果数据是随机插入的,就可能造成索引的数据页分裂。 假设图 1 中 page A 已经满了,这时我要再插入一行数据,会怎样呢? diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25414\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25414\350\256\262.md" index 71bec826b..bfe0829f1 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25414\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25414\350\256\262.md" @@ -43,11 +43,11 @@ 当然,现在这个看上去笨笨的 MySQL,在执行 count(\*) 操作的时候还是做了优化的。 -你知道的,InnoDB 是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。对于 count(\*) 这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL 优化器会找到最小的那棵树来遍历。 **在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。** +你知道的,InnoDB 是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。对于 count(\*) 这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL 优化器会找到最小的那棵树来遍历。**在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。** 如果你用过 show table status 命令的话,就会发现这个命令的输出结果里面也有一个 TABLE_ROWS 用于显示这个表当前有多少行,这个命令执行挺快的,那这个 TABLE_ROWS 能代替 count(\*) 吗? -你可能还记得在 [第 10 讲《MySQL 为什么有时候会选错索引?》](../第10讲) 中我提到过,索引统计的值是通过采样来估算的。实际上,TABLE_ROWS 就是从这个采样估算得来的,因此它也很不准。有多不准呢,官方文档说误差可能达到 40% 到 50%。 **所以,show table status 命令显示的行数也不能直接使用。** +你可能还记得在 [第 10 讲《MySQL 为什么有时候会选错索引?》](../第10讲) 中我提到过,索引统计的值是通过采样来估算的。实际上,TABLE_ROWS 就是从这个采样估算得来的,因此它也很不准。有多不准呢,官方文档说误差可能达到 40% 到 50%。**所以,show table status 命令显示的行数也不能直接使用。** 到这里我们小结一下: @@ -73,7 +73,7 @@ Redis 的数据不能永久地留在内存里,所以你会找一个地方把 当然了,这还是有解的。比如,Redis 异常重启以后,到数据库里面单独执行一次 count(\*) 获取真实的行数,再把这个值写回到 Redis 里就可以了。异常重启毕竟不是经常出现的情况,这一次全表扫描的成本,还是可以接受的。 -但实际上, **将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使 Redis 正常工作,这个值还是逻辑上不精确的。** 你可以设想一下有这么一个页面,要显示操作记录的总数,同时还要显示最近操作的 100 条记录。那么,这个页面的逻辑就需要先到 Redis 里面取出计数,再到数据表里面取数据记录。 +但实际上,**将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使 Redis 正常工作,这个值还是逻辑上不精确的。** 你可以设想一下有这么一个页面,要显示操作记录的总数,同时还要显示最近操作的 100 条记录。那么,这个页面的逻辑就需要先到 Redis 里面取出计数,再到数据表里面取数据记录。 我们是这么定义不精确的: @@ -106,7 +106,7 @@ Redis 的数据不能永久地留在内存里,所以你会找一个地方把 ## 在数据库保存计数 -根据上面的分析,用缓存系统保存计数有丢失数据和计数不精确的问题。那么, **如果我们把这个计数直接放到数据库里单独的一张计数表 C 中,又会怎么样呢?** +根据上面的分析,用缓存系统保存计数有丢失数据和计数不精确的问题。那么,**如果我们把这个计数直接放到数据库里单独的一张计数表 C 中,又会怎么样呢?** 首先,这解决了崩溃丢失的问题,InnoDB 是支持崩溃恢复不丢数据的。 @@ -150,9 +150,9 @@ Redis 的数据不能永久地留在内存里,所以你会找一个地方把 这是什么意思呢?接下来,我们就一个个地来看看。 -**对于 count(主键 id) 来说** ,InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。 +**对于 count(主键 id) 来说**,InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。 -**对于 count(1) 来说** ,InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。 +**对于 count(1) 来说**,InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。 单看这两个用法的差别的话,你能对比出来,count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。 @@ -163,7 +163,7 @@ Redis 的数据不能永久地留在内存里,所以你会找一个地方把 也就是前面的第一条原则,server 层要什么字段,InnoDB 就返回什么字段。 -**但是 count(*) 是例外** ,并不会把全部字段取出来,而是专门做了优化,不取值。count(\*) 肯定不是 null,按行累加。 +**但是 count(*) 是例外**,并不会把全部字段取出来,而是专门做了优化,不取值。count(\*) 肯定不是 null,按行累加。 看到这里,你一定会说,优化器就不能自己判断一下吗,主键 id 肯定非空啊,为什么不能按照 count(\*) 来处理,多么简单的优化啊。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25416\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25416\350\256\262.md" index 98b2918c4..7ba165838 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25416\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25416\350\256\262.md" @@ -91,7 +91,7 @@ select @b-@a;
图4 全排序的 OPTIMIZER_TRACE 部分结果
-number_of_tmp_files 表示的是,排序过程中使用的临时文件数。你一定奇怪,为什么需要 12 个文件?内存放不下时,就需要使用外部排序,外部排序一般使用归并排序算法。可以这么简单理解, **MySQL 将需要排序的数据分成 12 份,每一份单独排序后存在这些临时文件中。然后把这 12 个有序文件再合并成一个有序的大文件。** 如果 sort_buffer_size 超过了需要排序的数据量的大小,number_of_tmp_files 就是 0,表示排序可以直接在内存中完成。 +number_of_tmp_files 表示的是,排序过程中使用的临时文件数。你一定奇怪,为什么需要 12 个文件?内存放不下时,就需要使用外部排序,外部排序一般使用归并排序算法。可以这么简单理解,**MySQL 将需要排序的数据分成 12 份,每一份单独排序后存在这些临时文件中。然后把这 12 个有序文件再合并成一个有序的大文件。** 如果 sort_buffer_size 超过了需要排序的数据量的大小,number_of_tmp_files 就是 0,表示排序可以直接在内存中完成。 否则就需要放在临时文件中排序。sort_buffer_size 越小,需要分成的份数越多,number_of_tmp_files 的值就越大。 @@ -113,7 +113,7 @@ sort_mode 里面的 packed_additional_fields 的意思是,排序过程对字 所以如果单行很大,这个方法效率不够好。 -那么, **如果 MySQL 认为排序的单行长度太大会怎么做呢?** +那么,**如果 MySQL 认为排序的单行长度太大会怎么做呢?** 接下来,我来修改一个参数,让 MySQL 采用另外一种算法。 @@ -180,7 +180,7 @@ city、name、age 这三个字段的定义总长度是 36,我把 max_length_fo 看到这里,你就了解了,MySQL 做排序是一个成本比较高的操作。那么你会问,是不是所有的 order by 都需要排序操作呢?如果不排序就能得到正确的结果,那对系统的消耗会小很多,语句的执行时间也会变得更短。 -其实,并不是所有的 order by 语句,都需要排序操作的。从上面分析的执行过程,我们可以看到,MySQL 之所以需要生成临时表,并且在临时表上做排序操作, **其原因是原来的数据都是无序的。** 你可以设想下,如果能够保证从 city 这个索引上取出来的行,天然就是按照 name 递增排序的话,是不是就可以不用再排序了呢? +其实,并不是所有的 order by 语句,都需要排序操作的。从上面分析的执行过程,我们可以看到,MySQL 之所以需要生成临时表,并且在临时表上做排序操作,**其原因是原来的数据都是无序的。** 你可以设想下,如果能够保证从 city 这个索引上取出来的行,天然就是按照 name 递增排序的话,是不是就可以不用再排序了呢? 确实是这样的。 @@ -220,9 +220,9 @@ alter table t add index city_user(city, name); 从图中可以看到,Extra 字段中没有 Using filesort 了,也就是不需要排序了。而且由于 (city,name) 这个联合索引本身有序,所以这个查询也不用把 4000 行全都读一遍,只要找到满足条件的前 1000 条记录就可以退出了。也就是说,在我们这个例子里,只需要扫描 1000 次。 -既然说到这里了,我们再往前讨论, **这个语句的执行流程有没有可能进一步简化呢?** 不知道你还记不记得,我在篇文章 [第 05 讲《 深入浅出索引(下)》](../第05讲) 中,和你介绍的覆盖索引。 +既然说到这里了,我们再往前讨论,**这个语句的执行流程有没有可能进一步简化呢?** 不知道你还记不记得,我在篇文章 [第 05 讲《 深入浅出索引(下)》](../第05讲) 中,和你介绍的覆盖索引。 -这里我们可以再稍微复习一下。 **覆盖索引是指,索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。** +这里我们可以再稍微复习一下。**覆盖索引是指,索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。** 按照覆盖索引的概念,我们可以再优化一下这个查询语句的执行流程。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25417\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25417\350\256\262.md" index fa39bb284..6b97d5f35 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25417\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25417\350\256\262.md" @@ -63,9 +63,9 @@ Extra 字段显示 Using temporary,表示的是需要使用临时表;Using f
图3 rowid 排序
-然后,我再问你一个问题,你觉得对于临时内存表的排序来说,它会选择哪一种算法呢?回顾一下上一篇文章的一个结论: **对于 InnoDB 表来说** ,执行全字段排序会减少磁盘访问,因此会被优先选择。 +然后,我再问你一个问题,你觉得对于临时内存表的排序来说,它会选择哪一种算法呢?回顾一下上一篇文章的一个结论: **对于 InnoDB 表来说**,执行全字段排序会减少磁盘访问,因此会被优先选择。 -我强调了“InnoDB 表”,你肯定想到了, **对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘** 。优化器没有了这一层顾虑,那么它会优先考虑的,就是用于排序的行越少越好了,所以,MySQL 这时就会选择 rowid 排序。 +我强调了“InnoDB 表”,你肯定想到了,**对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘** 。优化器没有了这一层顾虑,那么它会优先考虑的,就是用于排序的行越少越好了,所以,MySQL 这时就会选择 rowid 排序。 理解了这个算法选择的逻辑,我们再来看看语句的执行流程。同时,通过今天的这个例子,我们来尝试分析一下语句的扫描行数。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25418\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25418\350\256\262.md" index edf81b12c..b5e60ba9d 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25418\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25418\350\256\262.md" @@ -45,7 +45,7 @@ mysql> select count(*) from tradelog where month(t_modified)=7; 但是,如果计算 month() 函数的话,你会看到传入 7 的时候,在树的第一层就不知道该怎么办了。 -也就是说, **对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。** +也就是说,**对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。** 需要注意的是,优化器并不是要放弃使用这个索引。 @@ -220,7 +220,7 @@ CONVERT() 函数,在这里的意思是把输入的字符串转成 utf8mb4 字 这就再次触发了我们上面说到的原则:对索引字段做函数操作,优化器会放弃走树搜索功能。 -到这里,你终于明确了,字符集不同只是条件之一, **连接过程中要求在被驱动表的索引字段上加函数操作** ,是直接导致对被驱动表做全表扫描的原因。 +到这里,你终于明确了,字符集不同只是条件之一,**连接过程中要求在被驱动表的索引字段上加函数操作**,是直接导致对被驱动表做全表扫描的原因。 作为对比验证,我给你提另外一个需求,“查找 trade_detail 表里 id=4 的操作,对应的操作者是谁”,再来看下这个语句和它的执行计划。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25419\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25419\350\256\262.md" index 8e2c513b0..a463cce38 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25419\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25419\350\256\262.md" @@ -112,7 +112,7 @@ flush tables with read lock; 所以,出现 Waiting for table flush 状态的可能情况是:有一个 flush tables 命令被别的语句堵住了,然后它又堵住了我们的 select 语句。 -现在,我们一起来复现一下这种情况, **复现步骤** 如图 6 所示: +现在,我们一起来复现一下这种情况,**复现步骤** 如图 6 所示:
img diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25420\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25420\350\256\262.md" index c477938ba..8844d91a9 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25420\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25420\350\256\262.md" @@ -79,7 +79,7 @@ session B 的第二条语句 update t set c=5 where id=0,语义是“我把 id 由于在 T1 时刻,session A 还只是给 id=5 这一行加了行锁, 并没有给 id=0 这行加上锁。因此,session B 在 T2 时刻,是可以执行这两条 update 语句的。这样,就破坏了 session A 里 Q1 语句要锁住所有 d=5 的行的加锁声明。 -session C 也是一样的道理,对 id=1 这一行的修改,也是破坏了 Q1 的加锁声明。 **其次,是数据一致性的问题。** +session C 也是一样的道理,对 id=1 这一行的修改,也是破坏了 Q1 的加锁声明。**其次,是数据一致性的问题。** 我们知道,锁的设计是为了保证数据的一致性。而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。 @@ -119,7 +119,7 @@ update t set d=100 where d=5;/* 所有 d=5 的行,d 改成 100*/ 也就是说,id=0 和 id=1 这两行,发生了数据不一致。这个问题很严重,是不行的。 -到这里,我们再回顾一下, **这个数据不一致到底是怎么引入的?** +到这里,我们再回顾一下,**这个数据不一致到底是怎么引入的?** 我们分析一下可以知道,这是我们假设“select * from t where d=5 for update 这条语句只给 d=5 这一行,也就是 id=5 的这一行加锁”导致的。 @@ -182,7 +182,7 @@ update t set c=5 where id=0; /*(0,5,5)*/ 也就是说,跟行锁有冲突关系的是“另外一个行锁”。 -但是间隙锁不一样, **跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。** 间隙锁之间都不存在冲突关系。 +但是间隙锁不一样,**跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。** 间隙锁之间都不存在冲突关系。 这句话不太好理解,我给你举个例子: @@ -238,7 +238,7 @@ commit; 至此,两个 session 进入互相等待状态,形成死锁。当然,InnoDB 的死锁检测马上就发现了这对死锁关系,让 session A 的 insert 语句报错返回了。 -你现在知道了, **间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的** 。其实,这还只是一个简单的例子,在下一篇文章中我们还会碰到更多、更复杂的例子。 +你现在知道了,**间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的** 。其实,这还只是一个简单的例子,在下一篇文章中我们还会碰到更多、更复杂的例子。 你可能会说,为了解决幻读的问题,我们引入了这么一大串内容,有没有更简单一点的处理方法呢。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25421\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25421\350\256\262.md" index fd749ec77..b6f15383a 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25421\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25421\350\256\262.md" @@ -69,7 +69,7 @@ insert into t values(0,0,0),(5,5,5), 1. 根据原则 1,加锁单位是 next-key lock,因此会给 (0,5\] 加上 next-key lock。 2. 要注意 c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的,需要向右遍历,查到 c=10 才放弃。根据原则 2,访问到的都要加锁,因此要给 (5,10\] 加 next-key lock。 3. 但是同时这个符合优化 2:等值判断,向右遍历,最后一个值不满足 c=5 这个等值条件,因此退化成间隙锁 (5,10)。 -4. 根据原则 2 , **只有访问到的对象才会加锁** ,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成。 +4. 根据原则 2 ,**只有访问到的对象才会加锁**,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成。 但 session C 要插入一个 (7,7,7) 的记录,就会被 session A 的间隙锁 (5,10) 锁住。 @@ -205,7 +205,7 @@ mysql> insert into t values(30,10,30); 可以看到,(c=10,id=30)之后的这个间隙并没有在加锁范围里,因此 insert 语句插入 c=12 是可以执行成功的。 -这个例子对我们实践的指导意义就是, **在删除数据的时候尽量加 limit** 。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。 +这个例子对我们实践的指导意义就是,**在删除数据的时候尽量加 limit** 。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。 ## 案例八:一个死锁的例子 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25422\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25422\350\256\262.md" index 0d7d4d3dd..86d73b6a7 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25422\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25422\350\256\262.md" @@ -77,7 +77,7 @@ 2. SQL 语句没写好; 3. MySQL 选错了索引。 -接下来,我们就具体分析一下这三种可能,以及对应的解决方案。 **导致慢查询的第一种可能是,索引没有设计好。** 这种场景一般就是通过紧急创建索引来解决。MySQL 5.6 版本以后,创建索引都支持 Online DDL 了,对于那种高峰期数据库已经被这个语句打挂了的情况,最高效的做法就是直接执行 alter table 语句。 +接下来,我们就具体分析一下这三种可能,以及对应的解决方案。**导致慢查询的第一种可能是,索引没有设计好。** 这种场景一般就是通过紧急创建索引来解决。MySQL 5.6 版本以后,创建索引都支持 Online DDL 了,对于那种高峰期数据库已经被这个语句打挂了的情况,最高效的做法就是直接执行 alter table 语句。 比较理想的是能够在备库先执行。假设你现在的服务是一主一备,主库 A、备库 B,这个方案的大致流程是这样的: diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25425\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25425\350\256\262.md" index b7fc505fe..fe9276a73 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25425\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25425\350\256\262.md" @@ -78,7 +78,7 @@ seconds_behind_master 的计算方法是这样的: 比如,一些归档类的数据,平时没有注意删除历史数据,等到空间快满了,业务开发人员要一次性地删掉大量历史数据。同时,又因为要避免在高峰期操作会影响业务(至少有这个意识还是很不错的),所以会在晚上执行这些大量数据的删除操作。 -结果,负责的 DBA 同学半夜就会收到延迟报警。然后,DBA 团队就要求你后续再删除数据的时候,要控制每个事务删除的数据量,分成多次删除。 **另一种典型的大事务场景,就是大表 DDL。** 这个场景,我在前面的文章中介绍过。处理方案就是,计划内的 DDL,建议使用 gh-ost 方案(这里,你可以再回顾下第 13 篇文章\[《为什么表数据删掉一半,表文件大小不变?》\]中的相关内容)。 +结果,负责的 DBA 同学半夜就会收到延迟报警。然后,DBA 团队就要求你后续再删除数据的时候,要控制每个事务删除的数据量,分成多次删除。**另一种典型的大事务场景,就是大表 DDL。** 这个场景,我在前面的文章中介绍过。处理方案就是,计划内的 DDL,建议使用 gh-ost 方案(这里,你可以再回顾下第 13 篇文章\[《为什么表数据删掉一半,表文件大小不变?》\]中的相关内容)。 ### 追问 3:如果主库上也不做大事务了,还有什么原因会导致主备延迟吗? @@ -157,7 +157,7 @@ insert into t(c) values(5); 最后的结果就是,主库 A 和备库 B 上出现了两行不一致的数据。可以看到,这个数据不一致,是由可用性优先流程导致的。 -那么,如果我还是用 **可用性优先策略,但设置 binlog_format=row** ,情况又会怎样呢? +那么,如果我还是用 **可用性优先策略,但设置 binlog_format=row**,情况又会怎样呢? 因为 row 格式在记录 binlog 的时候,会记录新插入的行的所有字段值,所以最后只会有一行不一致。而且,两边的主备同步的应用线程会报错 duplicate key error 并停止。也就是说,这种情况下,备库 B 的 (5,4) 和主库 A 的 (5,5) 这两行数据,都不会被对方执行。 @@ -173,7 +173,7 @@ insert into t(c) values(5); 1. 使用 row 格式的 binlog 时,数据不一致的问题更容易被发现。而使用 mixed 或者 statement 格式的 binlog 时,数据很可能悄悄地就不一致了。如果你过了很久才发现数据不一致的问题,很可能这时的数据不一致已经不可查,或者连带造成了更多的数据逻辑不一致。 2. 主备切换的可用性优先策略会导致数据不一致。因此,大多数情况下,我都建议你使用可靠性优先策略。毕竟对数据服务来说的话,数据的可靠性一般还是要优于可用性的。 -但事无绝对, **有没有哪种情况数据的可用性优先级更高呢?** +但事无绝对,**有没有哪种情况数据的可用性优先级更高呢?** 答案是,有的。 @@ -188,7 +188,7 @@ insert into t(c) values(5); 这样的话,这种场景就又可以使用可靠性优先策略了。 -接下来我们再看看, **按照可靠性优先的思路,异常切换会是什么效果?** +接下来我们再看看,**按照可靠性优先的思路,异常切换会是什么效果?** 假设,主库 A 和备库 B 间的主备延迟是 30 分钟,这时候主库 A 掉电了,HA 系统要切换 B 作为主库。我们在主动切换的时候,可以等到主备延迟小于 5 秒的时候再启动切换,但这时候已经别无选择了。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25426\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25426\350\256\262.md" index 50f12957f..f5cb80001 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25426\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25426\350\256\262.md" @@ -129,7 +129,7 @@ insert into t1 values(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5); 2. key=hash_func(db1+t1+“a”+2), value=1,表示会影响到这个表 a=2 的行。 3. key=hash_func(db1+t1+“a”+1), value=1,表示会影响到这个表 a=1 的行。 -可见, **相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。** 你可能也发现了,这两个方案其实都有一些约束条件: +可见,**相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。** 你可能也发现了,这两个方案其实都有一些约束条件: 1. 要能够从 binlog 里面解析出表名、主键值和唯一索引的值。也就是说,主库的 binlog 格式必须是 row; 2. 表必须有主键; diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25427\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25427\350\256\262.md" index 637896816..69b0b1657 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25427\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25427\350\256\262.md" @@ -89,7 +89,7 @@ mysqlbinlog File --stop-datetime=T --start-datetime=T 这时候,从库 B 的同步线程就会报告 Duplicate entry ‘id_of_R’ for key ‘PRIMARY’ 错误,提示出现了主键冲突,然后停止同步。 -所以, **通常情况下,我们在切换任务的时候,要先主动跳过这些错误,有两种常用的方法。** **一种做法是** ,主动跳过一个事务。跳过命令的写法是: +所以,**通常情况下,我们在切换任务的时候,要先主动跳过这些错误,有两种常用的方法。** **一种做法是**,主动跳过一个事务。跳过命令的写法是: ```sql set global sql_slave_skip_counter=1; diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25429\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25429\350\256\262.md" index 1eee2c7ac..60caa5e66 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25429\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25429\350\256\262.md" @@ -43,7 +43,7 @@ CREATE TABLE `t` ( 然后,你可能还会想起我们在 [第 07 讲](../第07讲) 中讲到的热点更新和死锁检测的时候,如果把 innodb_thread_concurrency 设置为 128 的话,那么出现同一行热点更新的问题时,是不是很快就把 128 消耗完了,这样整个系统是不是就挂了呢? -实际上, **在线程进入锁等待以后,并发线程的计数会减一** ,也就是说等行锁(也包括间隙锁)的线程是不算在 128 里面的。 +实际上,**在线程进入锁等待以后,并发线程的计数会减一**,也就是说等行锁(也包括间隙锁)的线程是不算在 128 里面的。 MySQL 这样设计是非常有意义的。因为,进入锁等待的线程已经不吃 CPU 了;更重要的是,必须这么设计,才能避免整个系统锁死。 @@ -114,7 +114,7 @@ insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on du 更新判断是一个相对比较常用的方案了,不过依然存在一些问题。其中,“判定慢”一直是让 DBA 头疼的问题。 -你一定会疑惑, **更新语句,如果失败或者超时,就可以发起主备切换了,为什么还会有判定慢的问题呢?** +你一定会疑惑,**更新语句,如果失败或者超时,就可以发起主备切换了,为什么还会有判定慢的问题呢?** 其实,这里涉及到的是服务器 IO 资源分配的问题。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25431\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25431\350\256\262.md" index 98879ddd4..1959446b3 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25431\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25431\350\256\262.md" @@ -51,7 +51,7 @@ Flashback 恢复数据的原理,是修改 binlog 的内容,拿回原库重 这是因为,一个在执行线上逻辑的主库,数据状态的变更往往是有关联的。可能由于发现数据问题的时间晚了一点儿,就导致已经在之前误操作的基础上,业务代码逻辑又继续修改了其他数据。所以,如果这时候单独恢复这几行数据,而又未经确认的话,就可能会出现对数据的二次破坏。 -当然, **我们不止要说误删数据的事后处理办法,更重要是要做到事前预防** 。 +当然,**我们不止要说误删数据的事后处理办法,更重要是要做到事前预防** 。 我有以下两个建议: diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25432\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25432\350\256\262.md" index ee363110f..9d059d807 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25432\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25432\350\256\262.md" @@ -47,7 +47,7 @@ 到这里你就知道了,原来不是“说停就停的”。 -接下来,我们 **再看一个 kill 不掉的例子** ,也就是我们在前面 [第 29 讲](../第29讲) 中提到的 innodb_thread_concurrency 不够用的例子。 +接下来,我们 **再看一个 kill 不掉的例子**,也就是我们在前面 [第 29 讲](../第29讲) 中提到的 innodb_thread_concurrency 不够用的例子。 首先,执行 set global innodb_thread_concurrency=2,将 InnoDB 的并发线程上限数设置为 2;然后,执行下面的序列: @@ -143,7 +143,7 @@ 在这些操作中,最花时间的就是第三步在本地构建哈希表的操作。所以,当一个库中的表个数非常多的时候,这一步就会花比较长的时间。 -也就是说, **我们感知到的连接过程慢,其实并不是连接慢,也不是服务端慢,而是客户端慢。** +也就是说,**我们感知到的连接过程慢,其实并不是连接慢,也不是服务端慢,而是客户端慢。** 图中的提示也说了,如果在连接命令中加上 -A,就可以关掉这个自动补全的功能,然后客户端就可以快速返回了。 @@ -198,7 +198,7 @@ MySQL 客户端默认采用第一种方式,而如果加上–quick 参数, 我在上一篇文章末尾,给你留下的问题是,希望你分享一下误删数据的处理经验。 -**@苍茫 同学提到了一个例子** ,我觉得值得跟大家分享一下。运维的同学直接拷贝文本去执行,SQL 语句截断,导致数据库执行出错。 +**@苍茫 同学提到了一个例子**,我觉得值得跟大家分享一下。运维的同学直接拷贝文本去执行,SQL 语句截断,导致数据库执行出错。 从浏览器拷贝文本执行,是一个非常不规范的操作。除了这个例子里面说的 SQL 语句截断问题,还可能存在乱码问题。 @@ -212,6 +212,6 @@ MySQL 客户端默认采用第一种方式,而如果加上–quick 参数, 不过,这个方案最大的敌人是这样的思想:这是个小操作,不需要这么严格。 -**@Knight²º¹⁸ 给了一个保护文件的方法** ,我之前没有用过这种方法,不过这确实是一个不错的思路。 +**@Knight²º¹⁸ 给了一个保护文件的方法**,我之前没有用过这种方法,不过这确实是一个不错的思路。 为了数据安全和服务稳定,多做点预防方案的设计讨论,总好过故障处理和事后复盘。方案设计讨论会和故障复盘会,这两种会议的会议室气氛完全不一样。经历过的同学一定懂的。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25433\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25433\350\256\262.md" index 648ba57fc..7c3aeff39 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25433\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25433\350\256\262.md" @@ -37,7 +37,7 @@ mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t"> $target_file 1. 一个查询在发送过程中,占用的 MySQL 内部的内存最大就是 net_buffer_length 这么大,并不会达到 200G; 2. socket send buffer 也不可能达到 200G(默认定义 /proc/sys/net/core/wmem_default),如果 socket send buffer 被写满,就会暂停读数据的流程。 -也就是说, **MySQL 是“边读边发的”** ,这个概念很重要。这就意味着,如果客户端接收得慢,会导致 MySQL 服务端由于结果发不出去,这个事务的执行时间变长。 +也就是说,**MySQL 是“边读边发的”**,这个概念很重要。这就意味着,如果客户端接收得慢,会导致 MySQL 服务端由于结果发不出去,这个事务的执行时间变长。 比如下面这个状态,就是我故意让客户端不去读 socket receive buffer 中的内容,然后在服务端 show processlist 看到的结果。 @@ -46,11 +46,11 @@ mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t"> $target_file
图2 服务端发送阻塞
-如果你看到 State 的值一直处于 **“Sending to client”** ,就表示服务器端的网络栈写满了。 +如果你看到 State 的值一直处于 **“Sending to client”**,就表示服务器端的网络栈写满了。 我在上一篇文章中曾提到,如果客户端使用–quick 参数,会使用 mysql_use_result 方法。这个方法是读一行处理一行。你可以想象一下,假设有一个业务的逻辑比较复杂,每读一行数据以后要处理的逻辑如果很慢,就会导致客户端要过很久才会去取下一行数据,可能就会出现如图 2 所示的这种情况。 -因此, **对于正常的线上业务来说,如果一个查询的返回结果不会很多的话,我都建议你使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存。** +因此,**对于正常的线上业务来说,如果一个查询的返回结果不会很多的话,我都建议你使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存。** 当然前提是查询返回结果不多。在 [第 30 讲](../第30讲) 评论区,有同学说到自己因为执行了一个大查询导致客户端占用内存近 20G,这种情况下就需要改用 mysql_use_result 接口了。 @@ -58,7 +58,7 @@ mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t"> $target_file 而如果要快速减少处于这个状态的线程的话,将 net_buffer_length 参数设置为一个更大的值是一个可选方案。 -与“Sending to client”长相很类似的一个状态是 **“Sending data”** ,这是一个经常被误会的问题。有同学问我说,在自己维护的实例上看到很多查询语句的状态是“Sending data”,但查看网络也没什么问题啊,为什么 Sending data 要这么久? +与“Sending to client”长相很类似的一个状态是 **“Sending data”**,这是一个经常被误会的问题。有同学问我说,在自己维护的实例上看到很多查询语句的状态是“Sending data”,但查看网络也没什么问题啊,为什么 Sending data 要这么久? 实际上,一个查询语句的状态变化是这样的(注意:这里,我略去了其他无关的状态): diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25434\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25434\350\256\262.md" index b28db2f00..e666ea509 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25434\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25434\350\256\262.md" @@ -238,7 +238,7 @@ select * from t1 straight_join t2 on (t1.a=t2.b); 所以,这个问题的结论就是,总是应该使用小表做驱动表。 -当然了,这里我需要说明下, **什么叫作“小表”** 。 +当然了,这里我需要说明下,**什么叫作“小表”** 。 我们前面的例子是没有加条件的。如果我在语句的 where 条件加上 t2.id\<=50 这个限定条件,再来看下这两条语句: @@ -265,7 +265,7 @@ select t1.b,t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100; 这里,我们应该选择表 t1 作为驱动表。也就是说在这个例子里,“只需要一列参与 join 的表 t1”是那个相对小的表。 -所以,更准确地说, **在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。** +所以,更准确地说,**在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。** ## 小结 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25435\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25435\350\256\262.md" index baff044d1..91a6c319d 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25435\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25435\350\256\262.md" @@ -84,7 +84,7 @@ select * from t1 where a>=1 and a<=100; 从图 3 的 explain 结果中,我们可以看到 Extra 字段多了 Using MRR,表示的是用上了 MRR 优化。而且,由于我们在 read_rnd_buffer 中按照 id 做了排序,所以最后得到的结果集也是按照主键 id 递增顺序的,也就是与图 1 结果集中行的顺序相反。 -到这里,我们小结一下。 **MRR 能够提升性能的核心** 在于,这条查询语句在索引 a 上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键 id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势。 +到这里,我们小结一下。**MRR 能够提升性能的核心** 在于,这条查询语句在索引 a 上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键 id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势。 ## Batched Key Access diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25436\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25436\350\256\262.md" index e8566e74a..73cdc7afb 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25436\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25436\350\256\262.md" @@ -39,7 +39,7 @@ select * from t1 join temp_t on (t1.b=temp_t.b); 4. session A 内有同名的临时表和普通表的时候,show create 语句,以及增删改查语句访问的是临时表。 5. show tables 命令不显示临时表。 -由于临时表只能被创建它的 session 访问,所以在这个 session 结束的时候,会自动删除临时表。也正是由于这个特性, **临时表就特别适合我们文章开头的 join 优化这种场景** 。 +由于临时表只能被创建它的 session 访问,所以在这个 session 结束的时候,会自动删除临时表。也正是由于这个特性,**临时表就特别适合我们文章开头的 join 优化这种场景** 。 为什么呢?原因主要包括以下两个方面: @@ -201,7 +201,7 @@ DROP TABLE `t_normal` /* generated by server */ 所以,drop table 命令记录 binlog 的时候,就必须对语句做改写。“/\* generated by server \*/”说明了这是一个被服务端改写过的命令。 -说到主备复制, **还有另外一个问题需要解决** :主库上不同的线程创建同名的临时表是没关系的,但是传到备库执行是怎么处理的呢? +说到主备复制,**还有另外一个问题需要解决** :主库上不同的线程创建同名的临时表是没关系的,但是传到备库执行是怎么处理的呢? 现在,我给你举个例子,下面的序列中实例 S 是 M 的备库。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25438\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25438\350\256\262.md" index 491ea19af..be4954e3f 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25438\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25438\350\256\262.md" @@ -177,7 +177,7 @@ alter table t1 add index a_btree_index using btree (id); 1. 如果你的表更新量大,那么并发度是一个很重要的参考指标,InnoDB 支持行锁,并发度比内存表好; 2. 能放到内存表的数据量都不大。如果你考虑的是读的性能,一个读 QPS 很高并且数据量不大的表,即使是使用 InnoDB,数据也是都会缓存在 InnoDB Buffer Pool 里的。因此,使用 InnoDB 表的读性能也不会差。 -所以, **我建议你把普通内存表都用 InnoDB 表来代替。** 但是,有一个场景却是例外的。 +所以,**我建议你把普通内存表都用 InnoDB 表来代替。** 但是,有一个场景却是例外的。 这个场景就是,我们在 [第 35 讲](../第35讲) 和 [第 36 讲](../第36讲) 说到的用户临时表。在数据量可控,不会耗费过多内存的情况下,你可以考虑使用内存表。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25439\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25439\350\256\262.md" index 24eef2ae4..95f75a56e 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25439\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25439\350\256\262.md" @@ -29,7 +29,7 @@ CREATE TABLE `t` ( 可以看到,表定义里面出现了一个 AUTO_INCREMENT=2,表示下一次插入数据时,如果需要自动生成自增值,会生成 id=2。 -其实,这个输出结果容易引起这样的误解:自增值是保存在表结构定义里的。实际上, **表的结构定义存放在后缀名为.frm 的文件中,但是并不会保存自增值。** +其实,这个输出结果容易引起这样的误解:自增值是保存在表结构定义里的。实际上,**表的结构定义存放在后缀名为.frm 的文件中,但是并不会保存自增值。** 不同的引擎对于自增值的保存策略不同。 @@ -104,7 +104,7 @@ insert into t values(null, 1, 1); 可以看到,这个操作序列复现了一个自增主键 id 不连续的现场 (没有 id=2 的行)。 -可见, **唯一键冲突是导致自增主键 id 不连续的第一种原因。** +可见,**唯一键冲突是导致自增主键 id 不连续的第一种原因。** 同样地,事务 **回滚也会产生类似的现象,这就是第二种原因。** @@ -191,7 +191,7 @@ MySQL 5.1.22 版本引入了一个新策略,新增参数 innodb_autoinc_lock_m 1. 一种思路是,让原库的批量插入数据语句,固定生成连续的 id 值。所以,自增锁直到语句执行结束才释放,就是为了达到这个目的。 2. 另一种思路是,在 binlog 里面把插入数据的操作都如实记录进来,到备库执行的时候,不再依赖于自增主键去生成。这种情况,其实就是 innodb_autoinc_lock_mode 设置为 2,同时 binlog_format 设置为 row。 -因此, **在生产上,尤其是有 insert … select 这种批量插入数据的场景时,从并发插入数据性能的角度考虑,我建议你这样设置:innodb_autoinc_lock_mode=2 ,并且 binlog_format=row** +因此,**在生产上,尤其是有 insert … select 这种批量插入数据的场景时,从并发插入数据性能的角度考虑,我建议你这样设置:innodb_autoinc_lock_mode=2 ,并且 binlog_format=row** 这样做,既能提升并发性,又不会出现数据一致性问题。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25441\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25441\350\256\262.md" index 286092eb5..7e97671fa 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25441\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25441\350\256\262.md" @@ -103,7 +103,7 @@ load data infile '/server_tmp/t.csv' into table db2.t; - 若相同,则构造成一行,调用 InnoDB 引擎接口,写入到表中。 4. 重复步骤 3,直到 /server_tmp/t.csv 整个文件读入完成,提交事务。 -你可能有一个疑问, **如果 binlog_format=statement,这个 load 语句记录到 binlog 里以后,怎么在备库重放呢?** +你可能有一个疑问,**如果 binlog_format=statement,这个 load 语句记录到 binlog 里以后,怎么在备库重放呢?** 由于 /server_tmp/t.csv 文件只保存在主库所在的主机上,如果只是把这条语句原文写到 binlog 中,在备库执行的时候,备库的本地机器上没有这个文件,就会导致主备同步停止。 @@ -123,12 +123,12 @@ load data infile '/server_tmp/t.csv' into table db2.t; 注意,这里备库执行的 load data 语句里面,多了一个“local”。它的意思是“将执行这条命令的客户端所在机器的本地文件 /tmp/SQL_LOAD_MB-1-0 的内容,加载到目标表 db2.t 中”。 -也就是说, **load data 命令有两种用法** : +也就是说,**load data 命令有两种用法** : 1. 不加“local”,是读取服务端的文件,这个文件必须在 secure_file_priv 指定的目录或子目录下; 2. 加上“local”,读取的是客户端的文件,只要 mysql 客户端有访问这个文件的权限即可。这时候,MySQL 客户端会先把本地文件传给服务端,然后执行上述的 load data 流程。 -另外需要注意的是, **select …into outfile 方法不会生成表结构文件** , 所以我们导数据时还需要单独的命令得到表结构定义。mysqldump 提供了一个–tab 参数,可以同时导出表结构定义文件和 csv 数据文件。这条命令的使用方法如下: +另外需要注意的是,**select …into outfile 方法不会生成表结构文件**, 所以我们导数据时还需要单独的命令得到表结构定义。mysqldump 提供了一个–tab 参数,可以同时导出表结构定义文件和 csv 数据文件。这条命令的使用方法如下: ```sql mysqldump -hhost -Pport -uuser ---single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --tab=secure_file_priv diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25442\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25442\350\256\262.md" index 23fec8f66..4902cac78 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25442\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25442\350\256\262.md" @@ -48,7 +48,7 @@ grant all privileges on *.* to 'ua'@'%' with grant option; 1. grant 命令对于全局权限,同时更新了磁盘和内存。命令完成后即时生效,接下来新创建的连接会使用新的权限。 2. 对于一个已经存在的连接,它的全局权限不受 grant 命令的影响。 -需要说明的是, **一般在生产环境上要合理控制用户权限的范围** 。 +需要说明的是,**一般在生产环境上要合理控制用户权限的范围** 。 我们上面用到的这个 grant 语句就是一个典型的错误示范。如果一个用户有所有权限,一般就不应该设置为所有 IP 地址都可以访问。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25444\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25444\350\256\262.md" index b02335654..09818ff24 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25444\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25444\350\256\262.md" @@ -93,7 +93,7 @@ select * from a left join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q2*/ 因此,语句 Q2的执行流程是这样的:顺序扫描表 b,每一行用b.f1到表 a 中去查,匹配到记录后判断a.f2=b.f2 是否满足,满足条件的话就作为结果集的一部分返回。 -那么, **为什么语句 Q1 和 Q2 这两个查询的执行流程会差距这么大呢?** 其实,这是因为优化器基于 Q2 这个查询的语义做了优化。 +那么,**为什么语句 Q1 和 Q2 这两个查询的执行流程会差距这么大呢?** 其实,这是因为优化器基于 Q2 这个查询的语义做了优化。 为了理解这个问题,我需要再和你交代一个背景知识点:在 MySQL 里,NULL 跟任何值执行等值判断和不等值判断的结果,都是 NULL。这里包括, select NULL = NULL 的结果,也是返回 NULL。 @@ -108,9 +108,9 @@ select * from a left join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q2*/
图5 Q2 的改写结果
-这个例子说明,即使我们在 SQL 语句中写成 left join,执行过程还是有可能不是从左到右连接的。也就是说, **使用 left join 时,左边的表不一定是驱动表。** +这个例子说明,即使我们在 SQL 语句中写成 left join,执行过程还是有可能不是从左到右连接的。也就是说,**使用 left join 时,左边的表不一定是驱动表。** -这样看来, **如果需要 left join 的语义,就不能把被驱动表的字段放在 where 条件里面做等值判断或不等值判断,必须都写在 on 里面。** 那如果是 join 语句呢? +这样看来,**如果需要 left join 的语义,就不能把被驱动表的字段放在 where 条件里面做等值判断或不等值判断,必须都写在 on 里面。** 那如果是 join 语句呢? 这时候,我们再看看这两条语句: diff --git "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25445\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25445\350\256\262.md" index af505cc5b..48287aa45 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25445\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230 45 \350\256\262/\347\254\25445\350\256\262.md" @@ -120,14 +120,14 @@ session B 里,我从 innodb_trx 表里查出的这两个字段,第二个字 1. update 和 delete 语句除了事务本身,还涉及到标记删除旧数据,也就是要把数据放到 purge 队列里等待后续物理删除,这个操作也会把 max_trx_id+1, 因此在一个事务中至少加 2; 2. InnoDB 的后台操作,比如表的索引信息统计这类操作,也是会启动内部事务的,因此你可能看到,trx_id 值并不是按照加 1 递增的。 -那么, **T2 时刻查到的这个很大的数字是怎么来的呢?** 其实,这个数字是每次查询的时候由系统临时计算出来的。它的算法是:把当前事务的 trx 变量的指针地址转成整数,再加上 248。使用这个算法,就可以保证以下两点: +那么,**T2 时刻查到的这个很大的数字是怎么来的呢?** 其实,这个数字是每次查询的时候由系统临时计算出来的。它的算法是:把当前事务的 trx 变量的指针地址转成整数,再加上 248。使用这个算法,就可以保证以下两点: 1. 因为同一个只读事务在执行期间,它的指针地址是不会变的,所以不论是在 innodb_trx 还是在 innodb_locks 表里,同一个只读事务查出来的 trx_id 就会是一样的。 2. 如果有并行的多个只读事务,每个事务的 trx 变量的指针地址肯定不同。这样,不同的并发只读事务,查出来的 trx_id 就是不同的。 -那么, **为什么还要再加上 248呢?** 在显示值里面加上 248,目的是要保证只读事务显示的 trx_id 值比较大,正常情况下就会区别于读写事务的 id。但是,trx_id 跟 row_id 的逻辑类似,定义长度也是 8 个字节。因此,在理论上还是可能出现一个读写事务与一个只读事务显示的 trx_id 相同的情况。不过这个概率很低,并且也没有什么实质危害,可以不管它。 +那么,**为什么还要再加上 248呢?** 在显示值里面加上 248,目的是要保证只读事务显示的 trx_id 值比较大,正常情况下就会区别于读写事务的 id。但是,trx_id 跟 row_id 的逻辑类似,定义长度也是 8 个字节。因此,在理论上还是可能出现一个读写事务与一个只读事务显示的 trx_id 相同的情况。不过这个概率很低,并且也没有什么实质危害,可以不管它。 -另一个问题是, **只读事务不分配 trx_id,有什么好处呢?** +另一个问题是,**只读事务不分配 trx_id,有什么好处呢?** - 一个好处是,这样做可以减小事务视图里面活跃事务数组的大小。因为当前正在运行的只读事务,是不影响数据的可见性判断的。所以,在创建事务的一致性视图时,InnoDB 就只需要拷贝读写事务的 trx_id。 - 另一个好处是,可以减少 trx_id 的申请次数。在 InnoDB 里,即使你只是执行一个普通的 select 语句,在执行过程中,也是要对应一个只读事务的。所以只读事务优化后,普通的查询语句不需要申请 trx_id,就大大减少了并发事务申请 trx_id 的锁冲突。 @@ -157,7 +157,7 @@ session B 里,我从 innodb_trx 表里查出的这两个字段,第二个字 并且,MySQL 重启时 max_trx_id 也不会清 0,也就是说重启 MySQL,这个 bug 仍然存在。 -那么, **这个 bug 也是只存在于理论上吗?** 假设一个 MySQL 实例的 TPS 是每秒 50 万,持续这个压力的话,在 17.8 年后,就会出现这个情况。如果 TPS 更高,这个年限自然也就更短了。但是,从 MySQL 的真正开始流行到现在,恐怕都还没有实例跑到过这个上限。不过,这个 bug 是只要 MySQL 实例服务时间够长,就会必然出现的。 +那么,**这个 bug 也是只存在于理论上吗?** 假设一个 MySQL 实例的 TPS 是每秒 50 万,持续这个压力的话,在 17.8 年后,就会出现这个情况。如果 TPS 更高,这个年限自然也就更短了。但是,从 MySQL 的真正开始流行到现在,恐怕都还没有实例跑到过这个上限。不过,这个 bug 是只要 MySQL 实例服务时间够长,就会必然出现的。 当然,这个例子更现实的意义是,可以加深我们对低水位和数据可见性的理解。你也可以借此机会再回顾下 [第 08 讲《事务到底是隔离的还是不隔离的?》](../第08讲) 中的相关内容。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25400\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25400\350\256\262.md" index 5b99a1e7a..076650089 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25400\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25400\350\256\262.md" @@ -8,7 +8,7 @@ ### 我与 MySQL 相伴相随 -在久游工作时,我负责全国最为热火的网游劲舞团,那时只要说你是负责劲舞团的 DBA,身上都闪着光芒,但谁又能想到,我曾遇到过连续 72 小时的加班回档全服游戏数据。为了避免再次发生类似情况, **早在 2008 年我就在久游设计了多实例高可用架构** ,并结合 LVM 快照功能,防止下一次游戏升级可能导致的业务数据错乱等情况。 +在久游工作时,我负责全国最为热火的网游劲舞团,那时只要说你是负责劲舞团的 DBA,身上都闪着光芒,但谁又能想到,我曾遇到过连续 72 小时的加班回档全服游戏数据。为了避免再次发生类似情况,**早在 2008 年我就在久游设计了多实例高可用架构**,并结合 LVM 快照功能,防止下一次游戏升级可能导致的业务数据错乱等情况。 我可以说是 **国内最早从事 MySQL 内核工作的 DBA** 。那时随着海量数据的不断发展,业务对于 MySQL 数据库的要求变得更为“苛刻”,不但要能够使用 MySQL,还要能对内核进行额外的开发。为此,我深入 MySQL 内核设计领域,为迎合 SSD 技术的发展,独立开发了 SBP(Secondary Buffer Pool)架构,并在久游、网易等业务中大规模使用。 @@ -21,7 +21,7 @@ - 收入不断攀升,比起其他种类数据库,MySQL 收入显然优势突出。目前,一线城市的数据库从业人员要达到 50 万是很轻松的一件事情,若去互联网公司,薪资可以说上不封顶。 - 作为一份职业的成就感,MySQL 带给我太多的“感动”。伴随着互联网的崛起,MySQL 已经成为互联网公司数据库的标准配置。看到自己运维开发的数据库能够支撑数以万计的用户,这种感觉真的是好极了。 -我时常思考,如何将自己这么多年在 MySQL 方面的知识沉淀形成方法论进行输出, **希望能有更多的同学享受到 MySQL 发展的红利。** ### 怎么用好 MySQL 呢 +我时常思考,如何将自己这么多年在 MySQL 方面的知识沉淀形成方法论进行输出,**希望能有更多的同学享受到 MySQL 发展的红利。** ### 怎么用好 MySQL 呢 虽然这些年先后出版过 **《MySQL技术内幕》《MySQL内核》** 系列三本书,但相对理论,每本书的方向都较为专一,未能有效地 **从整个业务的全链路角度去分享一个互联网海量 MySQL 架构的实现** 。 @@ -35,16 +35,16 @@ 非常有幸,收到拉勾教育平台的邀请,给了我再次进行 MySQL 创作的动力,拉勾网上有许多技术同学,他们有着一颗对知识渴望的心,希望学习对其工作真正有帮助的课程。 -所以我将着力打造好这门课程,结合现阶段你学习 MySQL 时存在的痛点, **如对于 MySQL 8.0 新特性与业务结合、金融级数据库高可用设计、分布式架构设计能力等,** 用自己超过 15 年的一线 MySQL 工作经验,帮助你从业务全流程的角度看待数据库系统,设计出一个基于 MySQL 的海量并发系统。同时,也希望你能在学完这门课程后,形成自己的数据库架构方法论,并积极交流与探讨,不断成长。 +所以我将着力打造好这门课程,结合现阶段你学习 MySQL 时存在的痛点,**如对于 MySQL 8.0 新特性与业务结合、金融级数据库高可用设计、分布式架构设计能力等,** 用自己超过 15 年的一线 MySQL 工作经验,帮助你从业务全流程的角度看待数据库系统,设计出一个基于 MySQL 的海量并发系统。同时,也希望你能在学完这门课程后,形成自己的数据库架构方法论,并积极交流与探讨,不断成长。 ### 课程设计 -总的来说,我通过表结构设计、索引设计、高可用架构设计、分布式架构设计,由浅入深、循序渐进地与你一起 **打造出一个能支撑海量的并发访问的分布式 MySQL 架构。** **模块一:表结构设计** ,该模块中我会以实际的业务为案例分析,先带你分析不同字段类型的选型,然后再学习 MySQL 中表的设计,比如表结构设计、访问设计、物理存储设计。通过模块一解决你表结构设计的痛点问题,让你打好架构设计最为基础的工作。 +总的来说,我通过表结构设计、索引设计、高可用架构设计、分布式架构设计,由浅入深、循序渐进地与你一起 **打造出一个能支撑海量的并发访问的分布式 MySQL 架构。** **模块一:表结构设计**,该模块中我会以实际的业务为案例分析,先带你分析不同字段类型的选型,然后再学习 MySQL 中表的设计,比如表结构设计、访问设计、物理存储设计。通过模块一解决你表结构设计的痛点问题,让你打好架构设计最为基础的工作。 -又因为单表的设计不足以支撑业务上线,所以在学完“表结构设计”后, **模块二就是索引的设计** 。在该模块中,我会通过讲述索引的基本原理,层层推进到索引的创建和优化,最后触达复杂 SQL 索引的设计与调优,比如多表 JOIN、子查询、分区表的问题。希望学完这部分内容之后,你能解决线上所有的 SQL 问题,不论是 OLTP 业务,还是复杂的 OLAP 业务。 +又因为单表的设计不足以支撑业务上线,所以在学完“表结构设计”后,**模块二就是索引的设计** 。在该模块中,我会通过讲述索引的基本原理,层层推进到索引的创建和优化,最后触达复杂 SQL 索引的设计与调优,比如多表 JOIN、子查询、分区表的问题。希望学完这部分内容之后,你能解决线上所有的 SQL 问题,不论是 OLTP 业务,还是复杂的 OLAP 业务。 那么在讲完表结构与索引设计之后,业务上线必不可少的就是高可用的环节,而 MySQL 作为一个开源的数据库,虽然提供了大量的高可用解决方案,但或多或少存在不少问题。所以 **模块三高可用的架构设计** 中,我会层层递进,手把手教你搭建一个完整的、可靠的、符合各种业务类型的高可用解决方案。 -除此之外,海量的业务还会涉及分布式架构的设计,这其实对当前业务与 DBA 同学来说,是非常具有挑战性的技术难点。而在 **模块四分布式架构** 中,我将会从分布式架构概述、分布式表结构设计、分布式索引设计、分布式事务等角度展开。相信我,学完这部分内容,你会觉得分布式并不是一个很难的架构,对于各种分布式架构中的难题,可以做到信手拈来。 **模块五偏向拓展** ,是对一些数据库设计中热门话题的分析,当你学完前四部分的内容后,进阶学习这些问题,能从更宏观、更上层的角度去设计出一个更好的架构,解决对应的问题,比如热点更新问题、数据迁移等问题。 +除此之外,海量的业务还会涉及分布式架构的设计,这其实对当前业务与 DBA 同学来说,是非常具有挑战性的技术难点。而在 **模块四分布式架构** 中,我将会从分布式架构概述、分布式表结构设计、分布式索引设计、分布式事务等角度展开。相信我,学完这部分内容,你会觉得分布式并不是一个很难的架构,对于各种分布式架构中的难题,可以做到信手拈来。**模块五偏向拓展**,是对一些数据库设计中热门话题的分析,当你学完前四部分的内容后,进阶学习这些问题,能从更宏观、更上层的角度去设计出一个更好的架构,解决对应的问题,比如热点更新问题、数据迁移等问题。 总的来说,这门课值得你期待,也是我所认为最具有架构实战的 MySQL课程,所以我希望你能认真钻研、学透这门课程,早日成为一名真正合格的数据库架构师。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25401\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25401\350\256\262.md" index 1b86e96cf..0404f8de9 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25401\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25401\350\256\262.md" @@ -93,7 +93,7 @@ ORDER BY sale_date; 除了整型类型,数字类型常用的还有浮点和高精度类型。 -MySQL 之前的版本中存在浮点类型 Float 和 Double,但这些类型因为不是高精度,也不是 SQL 标准的类型, **所以在真实的生产环境中不推荐使用** ,否则在计算时,由于精度类型问题,会导致最终的计算结果出错。 +MySQL 之前的版本中存在浮点类型 Float 和 Double,但这些类型因为不是高精度,也不是 SQL 标准的类型,**所以在真实的生产环境中不推荐使用**,否则在计算时,由于精度类型问题,会导致最终的计算结果出错。 更重要的是,从 MySQL 8.0.17 版本开始,当创建表用到类型 Float 或 Double 时,会抛出下面的警告:MySQL 提醒用户不该用上述浮点类型,甚至提醒将在之后版本中废弃浮点类型。 @@ -117,7 +117,7 @@ salary DECIMAL(8,2) 在真实业务场景中,整型类型最常见的就是在业务中用来表示某件物品的数量。例如上述表的销售数量,或电商中的库存数量、购买次数等。在业务中,整型类型的另一个常见且重要的使用用法是作为表的主键,即用来唯一标识一行数据。 -整型结合属性 auto_increment,可以实现 **自增功能** ,但在表结构设计时用自增做主键,希望你特别要注意以下两点,若不注意,可能会对业务造成灾难性的打击: +整型结合属性 auto_increment,可以实现 **自增功能**,但在表结构设计时用自增做主键,希望你特别要注意以下两点,若不注意,可能会对业务造成灾难性的打击: - 用 BIGINT 做主键,而不是 INT; - 自增值并不持久化,可能会有回溯现象(MySQL 8.0 版本前)。 @@ -140,7 +140,7 @@ ERROR 1062 (23000): Duplicate entry '2147483647' for key 't.PRIMARY' 可以看到,当达到 INT 上限后,再次进行自增插入时,会报重复错误,MySQL 数据库并不会自动将其重置为 1。 -第二个特别要注意的问题是,(敲黑板 2) **MySQL 8.0 版本前,自增不持久化,自增值可能会存在回溯问题** ! +第二个特别要注意的问题是,(敲黑板 2) **MySQL 8.0 版本前,自增不持久化,自增值可能会存在回溯问题**! ```sql mysql> SELECT * FROM t; @@ -225,6 +225,6 @@ CREATE TABLE User ( - 不推荐使用整型类型的属性 Unsigned,若非要使用,参数 sql_mode 务必额外添加上选项 NO_UNSIGNED_SUBTRACTION; - 自增整型类型做主键,务必使用类型 BIGINT,而非 INT,后期表结构调整代价巨大; -- MySQL 8.0 版本前,自增整型会有回溯问题, **做业务开发的你一定要了解这个问题;** - 当达到自增整型类型的上限值时,再次自增插入,MySQL 数据库会报重复错误; +- MySQL 8.0 版本前,自增整型会有回溯问题,**做业务开发的你一定要了解这个问题;** - 当达到自增整型类型的上限值时,再次自增插入,MySQL 数据库会报重复错误; - 不要再使用浮点类型 Float、Double,MySQL 后续版本将不再支持上述两种类型; - 账户余额字段,设计是用整型类型,而不是 DECIMAL 类型,这样性能更好,存储更紧凑。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25402\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25402\350\256\262.md" index 4da0c2315..bacad762d 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25402\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25402\350\256\262.md" @@ -8,15 +8,15 @@ MySQL 数据库的字符串类型有 CHAR、VARCHAR、BINARY、BLOB、TEXT、ENU ### CHAR 和 VARCHAR 的定义 -CHAR(N) 用来保存固定长度的字符,N 的范围是 0 ~ 255, **请牢记,N 表示的是字符,而不是字节** 。VARCHAR(N) 用来保存变长字符,N 的范围为 0 ~ 65536, N 表示字符。 +CHAR(N) 用来保存固定长度的字符,N 的范围是 0 ~ 255,**请牢记,N 表示的是字符,而不是字节** 。VARCHAR(N) 用来保存变长字符,N 的范围为 0 ~ 65536, N 表示字符。 在超出 65536 个字符的情况下,可以考虑使用更大的字符类型 TEXT 或 BLOB,两者最大存储长度为 4G,其区别是 BLOB 没有字符集属性,纯属二进制存储。 -和 Oracle、Microsoft SQL Server 等传统关系型数据库不同的是,MySQL 数据库的 VARCHAR 字符类型,最大能够存储 65536 个字符, **所以在 MySQL 数据库下,绝大部分场景使用类型 VARCHAR 就足够了。** #### 字符集 +和 Oracle、Microsoft SQL Server 等传统关系型数据库不同的是,MySQL 数据库的 VARCHAR 字符类型,最大能够存储 65536 个字符,**所以在 MySQL 数据库下,绝大部分场景使用类型 VARCHAR 就足够了。** #### 字符集 在表结构设计中,除了将列定义为 CHAR 和 VARCHAR 用以存储字符以外,还需要额外定义字符对应的字符集,因为每种字符在不同字符集编码下,对应着不同的二进制值。常见的字符集有 GBK、UTF8,通常推荐把默认字符集设置为 UTF8。 -而且随着移动互联网的飞速发展, **推荐把 MySQL 的默认字符集设置为 UTF8MB4** ,否则,某些 emoji 表情字符无法在 UTF8 字符集下存储,比如 emoji 笑脸表情,对应的字符编码为 0xF09F988E: +而且随着移动互联网的飞速发展,**推荐把 MySQL 的默认字符集设置为 UTF8MB4**,否则,某些 emoji 表情字符无法在 UTF8 字符集下存储,比如 emoji 笑脸表情,对应的字符编码为 0xF09F988E: ![](assets/CioPOWCLjaOANGrbAAD3LJzwYeU752.png) @@ -41,13 +41,13 @@ character-set-server = utf8mb4 ... ``` -另外,不同的字符集,CHAR(N)、VARCHAR(N) 对应最长的字节也不相同。比如 GBK 字符集,1 个字符最大存储 2 个字节,UTF8MB4 字符集 1 个字符最大存储 4 个字节。所以从底层存储内核看, **在多字节字符集下,CHAR 和 VARCHAR 底层的实现完全相同,都是变长存储** ! +另外,不同的字符集,CHAR(N)、VARCHAR(N) 对应最长的字节也不相同。比如 GBK 字符集,1 个字符最大存储 2 个字节,UTF8MB4 字符集 1 个字符最大存储 4 个字节。所以从底层存储内核看,**在多字节字符集下,CHAR 和 VARCHAR 底层的实现完全相同,都是变长存储**! ![1.png](assets/CioPOWCLjb-ADQ97AADmWCr4bpE672.png) -从上面的例子可以看到,CHAR(1) 既可以存储 1 个 'a' 字节,也可以存储 4 个字节的 emoji 笑脸表情,因此 CHAR 本质也是变长的。 **鉴于目前默认字符集推荐设置为 UTF8MB4,所以在表结构设计时,可以把 CHAR 全部用 VARCHAR 替换,底层存储的本质实现一模一样** 。 +从上面的例子可以看到,CHAR(1) 既可以存储 1 个 'a' 字节,也可以存储 4 个字节的 emoji 笑脸表情,因此 CHAR 本质也是变长的。**鉴于目前默认字符集推荐设置为 UTF8MB4,所以在表结构设计时,可以把 CHAR 全部用 VARCHAR 替换,底层存储的本质实现一模一样** 。 -#### 排序规则 **排序规则(Collation)是比较和排序字符串的一种规则** ,每个字符集都会有默认的排序规则,你可以用命令 SHOW CHARSET 来查看 +#### 排序规则 **排序规则(Collation)是比较和排序字符串的一种规则**,每个字符集都会有默认的排序规则,你可以用命令 SHOW CHARSET 来查看 ```plaintext mysql> SHOW CHARSET LIKE 'utf8%'; @@ -89,7 +89,7 @@ mysql> SELECT CAST('a' as char) COLLATE utf8mb4_0900_as_cs = CAST('A' as CHAR) C 1 row in set (0.00 sec) ``` -**牢记,绝大部分业务的表结构设计无须设置排序规则为大小写敏感** !除非你能明白你的业务真正需要。 +**牢记,绝大部分业务的表结构设计无须设置排序规则为大小写敏感**!除非你能明白你的业务真正需要。 ### 正确修改字符集 @@ -99,7 +99,7 @@ mysql> SELECT CAST('a' as char) COLLATE utf8mb4_0900_as_cs = CAST('A' as CHAR) C ALTER TABLE emoji_test CHARSET utf8mb4; ``` -其实,上述修改只是将表的字符集修改为 UTF8MB4,下次新增列时,若不显式地指定字符集,新列的字符集会变更为 UTF8MB4, **但对于已经存在的列,其默认字符集并不做修改** ,你可以通过命令 SHOW CREATE TABLE 确认: +其实,上述修改只是将表的字符集修改为 UTF8MB4,下次新增列时,若不显式地指定字符集,新列的字符集会变更为 UTF8MB4,**但对于已经存在的列,其默认字符集并不做修改**,你可以通过命令 SHOW CREATE TABLE 确认: ```sql mysql> SHOW CREATE TABLE emoji_test\\G **** **** **** **** **** **** ***1. row** **** **** **** **** **** **** *Table: emoji_test @@ -282,7 +282,7 @@ regDate: 2020-09-07 17:28:00 字符串是使用最为广泛的数据类型之一,但也是设计最初容易犯错的部分,后期业务跑起来再进行修改,代价将会非常巨大。希望你能反复细读本讲的内容,从而在表结构设计伊始,业务就做好最为充分的准备。我总结下本节的重点内容: -* CHAR 和 VARCHAR 虽然分别用于存储定长和变长字符,但对于变长字符集(如 GBK、UTF8MB4),其本质是一样的,都是变长, **设计时完全可以用 VARCHAR 替代 CHAR;** * 推荐 MySQL 字符集默认设置为 UTF8MB4,可以用于存储 emoji 等扩展字符; +* CHAR 和 VARCHAR 虽然分别用于存储定长和变长字符,但对于变长字符集(如 GBK、UTF8MB4),其本质是一样的,都是变长,**设计时完全可以用 VARCHAR 替代 CHAR;** * 推荐 MySQL 字符集默认设置为 UTF8MB4,可以用于存储 emoji 等扩展字符; * 排序规则很重要,用于字符的比较和排序,但大部分场景不需要用区分大小写的排序规则; * 修改表中已有列的字符集,使用命令 ALTER TABLE ... CONVERT TO ....; * 用户性别,运行状态等有限值的列,MySQL 8.0.16 版本直接使用 CHECK 约束机制,之前的版本可使用 ENUM 枚举字符串类型,外加 SQL\_MODE 的严格模式; diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25403\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25403\350\256\262.md" index eadf20cd1..c8c7f9b9b 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25403\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25403\350\256\262.md" @@ -74,7 +74,7 @@ mysql> SELECT name,money,last_modify_date FROM User WHERE name = 'David'; 除了 DATETIME,日期类型中还有一种 TIMESTAMP 的时间戳类型,其实际存储的内容为‘1970-01-01 00:00:00’到现在的毫秒数。在 MySQL 中,由于类型 TIMESTAMP 占用 4 个字节,因此其存储的时间上限只能到‘2038-01-19 03:14:07’。 -同类型 DATETIME 一样,从 MySQL 5.6 版本开始,类型 TIMESTAMP 也能支持毫秒。与 DATETIME 不同的是, **若带有毫秒时,类型 TIMESTAMP 占用 7 个字节,而 DATETIME 无论是否存储毫秒信息,都占用 8 个字节** 。 +同类型 DATETIME 一样,从 MySQL 5.6 版本开始,类型 TIMESTAMP 也能支持毫秒。与 DATETIME 不同的是,**若带有毫秒时,类型 TIMESTAMP 占用 7 个字节,而 DATETIME 无论是否存储毫秒信息,都占用 8 个字节** 。 类型 TIMESTAMP 最大的优点是可以带有时区属性,因为它本质上是从毫秒转化而来。如果你的业务需要对应不同的国家时区,那么类型 TIMESTAMP 是一种不错的选择。比如新闻类的业务,通常用户想知道这篇新闻发布时对应的自己国家时间,那么 TIMESTAMP 是一种选择。 @@ -184,7 +184,7 @@ mysqlslap -uroot --number-of-queries=1000000 --concurrency=100 --query='SELECT N #### 表结构设计规范:每条记录都要有一个时间字段 -在做表结构设计规范时, **强烈建议你每张业务核心表都增加一个 DATETIME 类型的 last_modify_date 字段,并设置修改自动更新机制,** 即便标识每条记录最后修改的时间。 +在做表结构设计规范时,**强烈建议你每张业务核心表都增加一个 DATETIME 类型的 last_modify_date 字段,并设置修改自动更新机制,** 即便标识每条记录最后修改的时间。 例如,在前面的表 User 中的字段 last_modify_date,就是用于表示最后一次的修改时间: @@ -202,7 +202,7 @@ CREATE TABLE User ( ); ``` -通过字段 last_modify_date 定义的 ON UPDATE CURRENT_TIMESTAMP(6),那么每次这条记录,则都会自动更新 last_modify_date 为当前时间。 **这样设计的好处是:** 用户可以知道每个用户最近一次记录更新的时间,以便做后续的处理。比如在电商的订单表中,可以方便对支付超时的订单做处理;在金融业务中,可以根据用户资金最后的修改时间做相应的资金核对等。 +通过字段 last_modify_date 定义的 ON UPDATE CURRENT_TIMESTAMP(6),那么每次这条记录,则都会自动更新 last_modify_date 为当前时间。**这样设计的好处是:** 用户可以知道每个用户最近一次记录更新的时间,以便做后续的处理。比如在电商的订单表中,可以方便对支付超时的订单做处理;在金融业务中,可以根据用户资金最后的修改时间做相应的资金核对等。 在后面的内容中,我们也会谈到 MySQL 数据库的主从逻辑数据核对的设计实现,也会利用到last_modify_date 字段。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25404\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25404\350\256\262.md" index c97dd3fe8..c860d0938 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25404\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25404\350\256\262.md" @@ -4,7 +4,7 @@ 关系型的结构化存储存在一定的弊端,因为它需要预先定义好所有的列以及列对应的类型。但是业务在发展过程中,或许需要扩展单个列的描述功能,这时,如果能用好 JSON 数据类型,那就能打通关系型和非关系型数据的存储之间的界限,为业务提供更好的架构选择。 -当然,很多同学在用 JSON 数据类型时会遇到各种各样的问题, **其中最容易犯的误区就是将类型 JSON 简单理解成字符串类型。** 但当你学完今天的内容之后,会真正认识到 JSON 数据类型的威力,从而在实际工作中更好地存储非结构化的数据。 +当然,很多同学在用 JSON 数据类型时会遇到各种各样的问题,**其中最容易犯的误区就是将类型 JSON 简单理解成字符串类型。** 但当你学完今天的内容之后,会真正认识到 JSON 数据类型的威力,从而在实际工作中更好地存储非结构化的数据。 ### JSON 数据类型 @@ -59,7 +59,7 @@ JSON对象除了支持字符串、整型、日期类型,JSON 内嵌的字段 上面的示例演示的是一个 JSON 数组,其中有 2 个 JSON 对象。 -到目前为止,可能很多同学会把 JSON 当作一个很大的字段串类型,从表面上来看,没有错。但本质上,JSON 是一种新的类型,有自己的存储格式,还能在每个对应的字段上创建索引,做特定的优化,这是传统字段串无法实现的。JSON 类型的另一个好处是 **无须预定义字段** ,字段可以无限扩展。而传统关系型数据库的列都需预先定义,想要扩展需要执行 ALTER TABLE ... ADD COLUMN ... 这样比较重的操作。 +到目前为止,可能很多同学会把 JSON 当作一个很大的字段串类型,从表面上来看,没有错。但本质上,JSON 是一种新的类型,有自己的存储格式,还能在每个对应的字段上创建索引,做特定的优化,这是传统字段串无法实现的。JSON 类型的另一个好处是 **无须预定义字段**,字段可以无限扩展。而传统关系型数据库的列都需预先定义,想要扩展需要执行 ALTER TABLE ... ADD COLUMN ... 这样比较重的操作。 需要注意是,JSON 类型是从 MySQL 5.7 版本开始支持的功能,而 8.0 版本解决了更新 JSON 的日志性能瓶颈。如果要在生产环境中使用 JSON 数据类型,强烈推荐使用 MySQL 8.0 版本。 @@ -69,7 +69,7 @@ JSON对象除了支持字符串、整型、日期类型,JSON 内嵌的字段 #### 用户登录设计 -在数据库中, **JSON 类型比较适合存储一些修改较少、相对静态的数据** ,比如用户登录信息的存储如下: +在数据库中,**JSON 类型比较适合存储一些修改较少、相对静态的数据**,比如用户登录信息的存储如下: ```sql DROP TABLE IF EXISTS UserLogin; diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25405\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25405\350\256\262.md" index 3a2f0829e..ea14d0056 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25405\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25405\350\256\262.md" @@ -10,13 +10,13 @@ 范式设计是非常重要的理论,是通过数学集合概念来推导范式的过程,在理论上,要求表结构设计必须至少满足三范式的要求。 -由于完全是数据推导过程,范式理论非常枯燥, **但你只要记住几个要点就能抓住其中的精髓:** +由于完全是数据推导过程,范式理论非常枯燥,**但你只要记住几个要点就能抓住其中的精髓:** - 一范式要求所有属性都是不可分的基本数据项; - 二范式解决部分依赖; - 三范式解决传递依赖。 -虽然我已经提炼了范式设计的精髓,但要想真正理解范式设计,就要抛弃纯理论的范式设计准则, **从业务角度出发,设计出符合范式准则要求的表结构。** +虽然我已经提炼了范式设计的精髓,但要想真正理解范式设计,就要抛弃纯理论的范式设计准则,**从业务角度出发,设计出符合范式准则要求的表结构。** ### 工程上的表结构设计实战 @@ -31,11 +31,11 @@ 主键用于唯一标识一行数据,所以一张表有主键,就已经直接满足一范式的要求了。在 01 讲的整型类型中,我提及可以使用 BIGINT 的自增类型作为主键,同时由于整型的自增性,数据库插入也是顺序的,性能较好。 -但你要注意,使用 BIGINT 的自增类型作为主键的设计仅仅适合 **非核心业务表** ,比如告警表、日志表等。 **真正的核心业务表,一定不要用自增键做主键** ,主要有 6 个原因: +但你要注意,使用 BIGINT 的自增类型作为主键的设计仅仅适合 **非核心业务表**,比如告警表、日志表等。**真正的核心业务表,一定不要用自增键做主键**,主要有 6 个原因: - 自增存在回溯问题; - 自增值在服务器端产生,存在并发性能问题; -- 自增值做主键,只能在当前实例中保证唯一, **不能保证全局唯一** ; +- 自增值做主键,只能在当前实例中保证唯一,**不能保证全局唯一** ; - 公开数据值,容易引发安全问题,例如知道地址[http://www.example.com/User/10/](http://www.example.com/customers/10/?fileGuid=xxQTRXtVcqtHK6j8),很容猜出 User 有 11、12 依次类推的值,容易引发数据泄露; - MGR(MySQL Group Replication) 可能引起的性能问题; - 分布式架构设计问题。 @@ -57,7 +57,7 @@ INSERT INTO ... VALUES (NULL,...),(NULL,...),(NULL,...); 如果参数 innodb_autoinc_lock_mode 设置为2,自增锁需要持有 3 次,每插入一条记录获取一次自增锁。 - **这样设计好处是:** 当前插入不影响其他自增主键的插入,可以获得最大的自增并发插入性能。 -- **缺点是:** 一条 SQL 插入的多条记录并不是连续的,如结果可能是 1、3、5 这样单调递增但非连续的情况。 **所以,如果你想获得自增值的最大并发性能,把参数 innodb_autoinc_lock_mode 设置为2。** 虽然,我们可以调整参数 innodb_autoinc_lock_mode获得自增的最大性能,但是由于其还存在上述 5 个问题。因此,在互联网海量并发架构实战中, **我更推荐 UUID 做主键或业务自定义生成主键。** #### UUID主键设计 +- **缺点是:** 一条 SQL 插入的多条记录并不是连续的,如结果可能是 1、3、5 这样单调递增但非连续的情况。**所以,如果你想获得自增值的最大并发性能,把参数 innodb_autoinc_lock_mode 设置为2。** 虽然,我们可以调整参数 innodb_autoinc_lock_mode获得自增的最大性能,但是由于其还存在上述 5 个问题。因此,在互联网海量并发架构实战中,**我更推荐 UUID 做主键或业务自定义生成主键。** #### UUID主键设计 UUID(Universally Unique Identifier)代表全局唯一标识 ID。显然,由于全局唯一性,你可以把它用来作为数据库的主键。 @@ -168,7 +168,7 @@ INSERT INTO User VALUES (UUID_TO_BIN(UUID(),TRUE),......); #### 业务自定义生成主键 -当然了,UUID 虽好,但是在分布式数据库场景下,主键还需要加入一些额外的信息,这样才能保证后续二级索引的查询效率(具体这部分内容将在后面的分布式章节中进行介绍)。 **现在你只需要牢记:分布式数据库架构,仅用 UUID 做主键依然是不够的。** 所以,对于分布式架构的核心业务表,我推荐类似如下的设计,比如: +当然了,UUID 虽好,但是在分布式数据库场景下,主键还需要加入一些额外的信息,这样才能保证后续二级索引的查询效率(具体这部分内容将在后面的分布式章节中进行介绍)。**现在你只需要牢记:分布式数据库架构,仅用 UUID 做主键依然是不够的。** 所以,对于分布式架构的核心业务表,我推荐类似如下的设计,比如: ```plaintext PK = 时间字段 + 随机码(可选) + 业务信息1 + 业务信息2 ...... @@ -251,7 +251,7 @@ SELECT * FROM UserTag; ### 总结 -总的来说,范式是偏数据库理论范畴的表结构设计准则,在实际的工程实践上没有必要严格遵循三范式要求, **在 MySQL 海量并发的工程实践上,表结构设计应遵循这样几个规范:** +总的来说,范式是偏数据库理论范畴的表结构设计准则,在实际的工程实践上没有必要严格遵循三范式要求,**在 MySQL 海量并发的工程实践上,表结构设计应遵循这样几个规范:** - 每张表一定要有一个主键; - 自增主键只推荐用在非核心业务表,甚至应避免使用; diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25406\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25406\350\256\262.md" index b554aca51..34d7af883 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25406\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25406\350\256\262.md" @@ -4,17 +4,17 @@ 据我观察,很多同学不会在表结构设计之初就考虑存储的设计,只有当业务发展到一定规模才会意识到问题的严重性。而物理存储主要是考虑是否要启用表的压缩功能,默认情况下,所有表都是非压缩的。 -但一些同学一听到压缩,总会下意识地认为压缩会导致 MySQL 数据库的性能下降。 **这个观点说对也不对,需要根据不同场景进行区分。** 这一讲,我们就来看一看表的物理存储设计:不同场景下,表压缩功能的使用。 +但一些同学一听到压缩,总会下意识地认为压缩会导致 MySQL 数据库的性能下降。**这个观点说对也不对,需要根据不同场景进行区分。** 这一讲,我们就来看一看表的物理存储设计:不同场景下,表压缩功能的使用。 ### 表压缩 数据库中的表是由一行行记录(rows)所组成,每行记录被存储在一个页中,在 MySQL 中,一个页的大小默认为 16K,一个个页又组成了每张表的表空间。 -通常我们认为, **如果一个页中存放的记录数越多,数据库的性能越高** 。这是因为数据库表空间中的页是存放在磁盘上,MySQL 数据库先要将磁盘中的页读取到内存缓冲池,然后以页为单位来读取和管理记录。 +通常我们认为,**如果一个页中存放的记录数越多,数据库的性能越高** 。这是因为数据库表空间中的页是存放在磁盘上,MySQL 数据库先要将磁盘中的页读取到内存缓冲池,然后以页为单位来读取和管理记录。 一个页中存放的记录越多,内存中能存放的记录数也就越多,那么存取效率也就越高。若想将一个页中存放的记录数变多,可以启用压缩功能。此外,启用压缩后,存储空间占用也变小了,同样单位的存储能存放的数据也变多了。 -若要启用压缩技术,数据库可以根据记录、页、表空间进行压缩,不过在实际工程中,我们普遍使用页压缩技术, **这是为什么呢?** +若要启用压缩技术,数据库可以根据记录、页、表空间进行压缩,不过在实际工程中,我们普遍使用页压缩技术,**这是为什么呢?** - **压缩每条记录:** 因为每次读写都要压缩和解压,过于依赖 CPU 的计算能力,性能会明显下降;另外,因为单条记录大小不会特别大,一般小于 1K,压缩效率也并不会特别好。 - **压缩表空间:** 压缩效率非常不错,但要求表空间文件静态不增长,这对基于磁盘的关系型数据库来说,很难实现。 @@ -31,7 +31,7 @@ #### COMPRESS 页压缩 -COMPRESS 页压缩是 MySQL 5.7 版本之前提供的页压缩功能。只要在创建表时指定ROW_FORMAT=COMPRESS,并设置通过选项 KEY_BLOCK_SIZE 设置压缩的比例。 **需要牢记的是,** 虽然是通过选项 ROW_FORMAT 启用压缩功能,但这并不是记录级压缩,依然是根据页的维度进行压缩。 +COMPRESS 页压缩是 MySQL 5.7 版本之前提供的页压缩功能。只要在创建表时指定ROW_FORMAT=COMPRESS,并设置通过选项 KEY_BLOCK_SIZE 设置压缩的比例。**需要牢记的是,** 虽然是通过选项 ROW_FORMAT 启用压缩功能,但这并不是记录级压缩,依然是根据页的维度进行压缩。 下面这是一张日志表,ROW_FROMAT 设置为 COMPRESS,表示启用 COMPRESS 页压缩功能,KEY_BLOCK_SIZE 设置为 8,表示将一个 16K 的页压缩为 8K。 @@ -108,7 +108,7 @@ TPC 页压缩 所以,用户对流水表有性能需求。此外,流水又非常大,启用压缩功能可更为有效地存储数据。 -若对压缩产生的性能抖动有所担心, **我的建议** :由于流水表通常是按月或天进行存储,对当前正在使用的流水表不要启用 TPC 功能,对已经成为历史的流水表启用 TPC 压缩功能,如下所示: +若对压缩产生的性能抖动有所担心,**我的建议** :由于流水表通常是按月或天进行存储,对当前正在使用的流水表不要启用 TPC 功能,对已经成为历史的流水表启用 TPC 压缩功能,如下所示: ![图片2.png](assets/Cgp9HWCbdRyAO8QQAAIDb8I7ubs097.png) diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25407\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25407\350\256\262.md" index c5ef50d0c..a2f149890 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25407\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25407\350\256\262.md" @@ -20,7 +20,7 @@ MySQL 8.0 版本前,有不少同学会吐槽 MySQL 对于 SQL 标准的支持 通常来说,MySQL 数据库用于 OLTP 的在线系统中,不用特别复杂的 SQL 语法支持。但 MySQL 8.0 完备的 SQL 支持意味着 MySQL 未来将逐渐补齐在 OLAP 业务方面的短板,让我们一起拭目以待。 -当然,通过 SQL 访问表,你肯定并不陌生,这也不是本讲的重点。 **接下来我重点带你了解 MySQL 怎么通过 NoSQL 的方式访问表中的数据。** +当然,通过 SQL 访问表,你肯定并不陌生,这也不是本讲的重点。**接下来我重点带你了解 MySQL 怎么通过 NoSQL 的方式访问表中的数据。** 我们先来看看当前 MySQL 版本中支持的不同表的访问方式: @@ -87,7 +87,7 @@ VALUES ('User','test','user_id','user_id|cellphone|last_login','0','0','0','PRIA ![Drawing 5.png](assets/Cgp9HWCeMtOAFN_LAAJjGPv67qw823.png) -从测试结果可以看到, **基于 Memcached 的 KV 访问方式比传统的 SQL 方式要快54.33%** ,而且CPU 的开销反而还要低20%。 +从测试结果可以看到,**基于 Memcached 的 KV 访问方式比传统的 SQL 方式要快54.33%**,而且CPU 的开销反而还要低20%。 当然了,上述操作只是将表 User 作为 KV 访问,如果想将其他表通过 KV 的方式访问,可以继续在表 Containers 中进行配置。但是在使用时,务必先通过 GET 命令指定要访问的表: diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25408\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25408\350\256\262.md" index 76948e419..a34ccf509 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25408\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25408\350\256\262.md" @@ -1,6 +1,6 @@ # 08 索引:排序的艺术 -在模块一中,我们学习了怎么根据合适的类型正确地创建一张表,但创建的表不能立刻用在真正的业务系统上。因为表结构设计只是设计数据库最初的环节之一,我们还缺少数据库设计中最为重要的一个环节——索引设计, **只有正确设计索引,业务才能达到上线的初步标准** 。 +在模块一中,我们学习了怎么根据合适的类型正确地创建一张表,但创建的表不能立刻用在真正的业务系统上。因为表结构设计只是设计数据库最初的环节之一,我们还缺少数据库设计中最为重要的一个环节——索引设计,**只有正确设计索引,业务才能达到上线的初步标准** 。 所以模块二我会讲索引的设计、业务应用与调优等案例。今天我们先来学习关系型数据库最核心的概念——索引,对索引做一个初步的概述,让你对数据库中的索引有一个体系的认知,并用好 B+ 树索引。 @@ -10,19 +10,19 @@ 索引之所以能提升查询速度,在于它在插入时对数据进行了排序(显而易见,它的缺点是影响插入或者更新的性能)。 -所以,索引是一门排序的艺术,有效地设计并创建索引,会提升数据库系统的整体性能。在目前的 MySQL 8.0 版本中,InnoDB 存储引擎支持的索引有 B+ 树索引、全文索引、R 树索引。 **这一讲我们就先关注使用最为广泛的 B+ 树索引** 。 +所以,索引是一门排序的艺术,有效地设计并创建索引,会提升数据库系统的整体性能。在目前的 MySQL 8.0 版本中,InnoDB 存储引擎支持的索引有 B+ 树索引、全文索引、R 树索引。**这一讲我们就先关注使用最为广泛的 B+ 树索引** 。 ### B+树索引结构 B+ 树索引是数据库系统中最为常见的一种索引数据结构,几乎所有的关系型数据库都支持它。 -**那为什么关系型数据库都热衷支持 B+树索引呢** ?因为它是目前为止排序最有效率的数据结构。像二叉树,哈希索引、红黑树、SkipList,在海量数据基于磁盘存储效率方面远不如 B+ 树索引高效。 +**那为什么关系型数据库都热衷支持 B+树索引呢**?因为它是目前为止排序最有效率的数据结构。像二叉树,哈希索引、红黑树、SkipList,在海量数据基于磁盘存储效率方面远不如 B+ 树索引高效。 -所以,上述的数据结构一般仅用于内存对象,基于磁盘的数据排序与存储,最有效的依然是 B+ 树索引。 **B+树索引的特点是:** 基于磁盘的平衡树,但树非常矮,通常为 3~4 层,能存放千万到上亿的排序数据。树矮意味着访问效率高,从千万或上亿数据里查询一条数据,只用 3、4 次 I/O。 +所以,上述的数据结构一般仅用于内存对象,基于磁盘的数据排序与存储,最有效的依然是 B+ 树索引。**B+树索引的特点是:** 基于磁盘的平衡树,但树非常矮,通常为 3~4 层,能存放千万到上亿的排序数据。树矮意味着访问效率高,从千万或上亿数据里查询一条数据,只用 3、4 次 I/O。 又因为现在的固态硬盘每秒能执行至少 10000 次 I/O ,所以查询一条数据,哪怕全部在磁盘上,也只需要 0.003 ~ 0.004 秒。另外,因为 B+ 树矮,在做排序时,也只需要比较 3~4 次就能定位数据需要插入的位置,排序效率非常不错。 -B+ 树索引由根节点(root node)、中间节点(non leaf node)、叶子节点(leaf node)组成,其中叶子节点存放所有排序后的数据。 **当然也存在一种比较特殊的情况** ,比如高度为 1 的B+ 树索引: +B+ 树索引由根节点(root node)、中间节点(non leaf node)、叶子节点(leaf node)组成,其中叶子节点存放所有排序后的数据。**当然也存在一种比较特殊的情况**,比如高度为 1 的B+ 树索引: ![Drawing 1.png](assets/CioPOWCk3P2AETnSAADbd7NkkIw226.png) @@ -38,7 +38,7 @@ CREATE TABLE User ( ) ``` -所有 B+ 树都是从高度为 1 的树开始,然后根据数据的插入,慢慢增加树的高度。 **你要牢记:索引是对记录进行排序,** 高度为 1 的 B+ 树索引中,存放的记录都已经排序好了,若要在一个叶子节点内再进行查询,只进行二叉查找,就能快速定位数据。 +所有 B+ 树都是从高度为 1 的树开始,然后根据数据的插入,慢慢增加树的高度。**你要牢记:索引是对记录进行排序,** 高度为 1 的 B+ 树索引中,存放的记录都已经排序好了,若要在一个叶子节点内再进行查询,只进行二叉查找,就能快速定位数据。 可随着插入 B+ 树索引的记录变多,1个页(16K)无法存放这么多数据,所以会发生 B+ 树的分裂,B+ 树的高度变为 2,当 B+ 树的高度大于等于 2 时,根节点和中间节点存放的是索引键对,由(索引键、指针)组成。 @@ -80,7 +80,7 @@ CREATE TABLE User ( 总记录数 = 1100(根节点) * 1100(中间节点) * 32 = 38,720,000 ``` -讲到这儿,你会发现,高度为 3 的 B+ 树索引竟然能存放 3800W 条记录。在 3800W 条记录中定位一条记录,只需要查询 3 个页。 **那么 B+ 树索引的优势是否逐步体现出来了呢** ? +讲到这儿,你会发现,高度为 3 的 B+ 树索引竟然能存放 3800W 条记录。在 3800W 条记录中定位一条记录,只需要查询 3 个页。**那么 B+ 树索引的优势是否逐步体现出来了呢**? 不过,在真实环境中,每个页其实利用率并没有这么高,还会存在一些碎片的情况,我们假设每个页的使用率为60%,则: @@ -128,11 +128,11 @@ possible_keys: NULL Extra: Using where ``` -讲到这儿,你应该了解了 B+ 树索引的组织形式,以及为什么在上亿的数据中可以通过B+树索引快速定位查询的记录。 **但 B+ 树的查询高效是要付出代价的,就是我们前面说的插入性能问题** ,接下去咱们就来讨论一下。 +讲到这儿,你应该了解了 B+ 树索引的组织形式,以及为什么在上亿的数据中可以通过B+树索引快速定位查询的记录。**但 B+ 树的查询高效是要付出代价的,就是我们前面说的插入性能问题**,接下去咱们就来讨论一下。 ### 优化 B+ 树索引的插入性能 -B+ 树在插入时就对要对数据进行排序,但排序的开销其实并没有你想象得那么大,因为排序是 CPU 操作(当前一个时钟周期 CPU 能处理上亿指令)。 **真正的开销在于 B+ 树索引的维护,保证数据排序,这里存在两种不同数据类型的插入情况** 。 +B+ 树在插入时就对要对数据进行排序,但排序的开销其实并没有你想象得那么大,因为排序是 CPU 操作(当前一个时钟周期 CPU 能处理上亿指令)。**真正的开销在于 B+ 树索引的维护,保证数据排序,这里存在两种不同数据类型的插入情况** 。 - **数据顺序(或逆序)插入:** B+ 树索引的维护代价非常小,叶子节点都是从左往右进行插入,比较典型的是自增 ID 的插入、时间的插入(若在自增 ID 上创建索引,时间列上创建索引,则 B+ 树插入通常是比较快的)。 - **数据无序插入:** B+ 树为了维护排序,需要对页进行分裂、旋转等开销较大的操作,另外,即便对于固态硬盘,随机写的性能也不如顺序写,所以磁盘性能也会收到较大影响。比较典型的是用户昵称,每个用户注册时,昵称是随意取的,若在昵称上创建索引,插入是无序的,索引维护需要的开销会比较大。 @@ -145,7 +145,7 @@ B+ 树在插入时就对要对数据进行排序,但排序的开销其实并 ![Drawing 8.png](assets/CioPOWCk3TuAchxwAACr8OD6suQ711.png) -可以看到,UUID 由于是无序值,所以在插入时性能比起顺序值自增 ID 和排序 UUID,性能上差距比较明显。 **所以,我再次强调:** 在表结构设计时,主键的设计一定要尽可能地使用顺序值,这样才能保证在海量并发业务场景下的性能。 +可以看到,UUID 由于是无序值,所以在插入时性能比起顺序值自增 ID 和排序 UUID,性能上差距比较明显。**所以,我再次强调:** 在表结构设计时,主键的设计一定要尽可能地使用顺序值,这样才能保证在海量并发业务场景下的性能。 以上就是索引查询和插入的知识,接下来我们就分析怎么在 MySQL 数据库中查看 B+ 树索引。 @@ -170,7 +170,7 @@ WHERE table_name = 'orders' and index_name = 'PRIMARY'; 从上面的结果中可以看到,表 orders 中的主键索引,大约有 5778522 条记录,其中叶子节点一共有 48867 个页,索引所有页的数量为 49024。根据上面的介绍,你可以推理出非叶节点的数量为 49024 ~ 48867,等于 157 个页。 -另外,我看见网上一些所谓的 MySQL“军规”中写道“一张表的索引不能超过 5 个”。 **根本没有这样的说法,完全是无稽之谈。** 在我看来,如果业务的确需要很多不同维度进行查询,那么就该创建对应多索引,这是没有任何值得商讨的地方。 **真正在业务上遇到的问题是:** 由于业务开发同学对数据库不熟悉,创建 N 多索引,但实际这些索引从创建之初到现在根本就没有使用过!因为优化器并不会选择这些低效的索引,这些无效索引占用了空间,又影响了插入的性能。 **那你怎么知道哪些 B+树索引未被使用过呢** ?在 MySQL 数据库中,可以通过查询表sys.schema_unused_indexes,查看有哪些索引一直未被使用过,可以被废弃: +另外,我看见网上一些所谓的 MySQL“军规”中写道“一张表的索引不能超过 5 个”。**根本没有这样的说法,完全是无稽之谈。** 在我看来,如果业务的确需要很多不同维度进行查询,那么就该创建对应多索引,这是没有任何值得商讨的地方。**真正在业务上遇到的问题是:** 由于业务开发同学对数据库不熟悉,创建 N 多索引,但实际这些索引从创建之初到现在根本就没有使用过!因为优化器并不会选择这些低效的索引,这些无效索引占用了空间,又影响了插入的性能。**那你怎么知道哪些 B+树索引未被使用过呢**?在 MySQL 数据库中,可以通过查询表sys.schema_unused_indexes,查看有哪些索引一直未被使用过,可以被废弃: ```sql SELECT * FROM schema_unused_indexes diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25409\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25409\350\256\262.md" index 47b3efb09..6f85cc2db 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25409\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25409\350\256\262.md" @@ -14,7 +14,7 @@ InnoDB 存储引擎是 MySQL 数据库中使用最为广泛的引擎,在海量 ![1.png](assets/Cgp9HWCscj-AMF2xAAGCKX1ziAQ251.png) -从图中你能看到,堆表的组织结构中,数据和索引分开存储。索引是排序后的数据,而堆表中的数据是无序的,索引的叶子节点存放了数据在堆表中的地址,当堆表的数据发生改变,且位置发生了变更,所有索引中的地址都要更新,这非常影响性能,特别是对于 OLTP 业务。 **而索引组织表,数据根据主键排序存放在索引中** ,主键索引也叫聚集索引(Clustered Index)。在索引组织表中,数据即索引,索引即数据。 +从图中你能看到,堆表的组织结构中,数据和索引分开存储。索引是排序后的数据,而堆表中的数据是无序的,索引的叶子节点存放了数据在堆表中的地址,当堆表的数据发生改变,且位置发生了变更,所有索引中的地址都要更新,这非常影响性能,特别是对于 OLTP 业务。**而索引组织表,数据根据主键排序存放在索引中**,主键索引也叫聚集索引(Clustered Index)。在索引组织表中,数据即索引,索引即数据。 MySQL InnoDB 存储引擎就是这样的数据组织方式;Oracle、Microsoft SQL Server 后期也推出了支持索引组织表的存储方式。 @@ -30,7 +30,7 @@ MySQL InnoDB 存储引擎就是这样的数据组织方式;Oracle、Microsoft ### 二级索引 -InnoDB 存储引擎的数据是根据主键索引排序存储的,除了主键索引外, **其他的索引都称之为二级索引(Secondeary Index),** 或非聚集索引(None Clustered Index)。 +InnoDB 存储引擎的数据是根据主键索引排序存储的,除了主键索引外,**其他的索引都称之为二级索引(Secondeary Index),** 或非聚集索引(None Clustered Index)。 二级索引也是一颗 B+ 树索引,但它和主键索引不同的是叶子节点存放的是索引键值、主键值。对于 08 讲创建的表 User,假设在列 name 上还创建了索引 idx_name,该索引就是二级索引: @@ -52,7 +52,7 @@ CREATE TABLE User ( SELECT * FROM User WHERE name = 'David', ``` -通过二级索引 idx_name 只能定位主键值,需要额外再通过主键索引进行查询,才能得到最终的结果。 **这种“二级索引通过主键索引进行再一次查询”的操作叫作“回表”** ,你可以通过下图理解二级索引的查询: +通过二级索引 idx_name 只能定位主键值,需要额外再通过主键索引进行查询,才能得到最终的结果。**这种“二级索引通过主键索引进行再一次查询”的操作叫作“回表”**,你可以通过下图理解二级索引的查询: ![3.png](assets/CioPOWCsciqAXCpWAAEzcba-N8Q504.png) @@ -116,7 +116,7 @@ CREATE TABLE check_idx_name ( ) -- 唯一约束 ``` -讲到这儿,你应该理解了吧? **在索引组织表中,万物皆索引,索引就是数据,数据就是索引** 。 **最后,为了加深你对于索引组织表的理解** ,我们再来回顾一下堆表的实现。 +讲到这儿,你应该理解了吧?**在索引组织表中,万物皆索引,索引就是数据,数据就是索引** 。**最后,为了加深你对于索引组织表的理解**,我们再来回顾一下堆表的实现。 堆表中的索引都是二级索引,哪怕是主键索引也是二级索引,也就是说它没有聚集索引,每次索引查询都要回表。同时,堆表中的记录全部存放在数据文件中,并且无序存放,这对互联网海量并发的 OLTP 业务来说,堆表的实现的确“过时”了。 @@ -124,11 +124,11 @@ CREATE TABLE check_idx_name ( 有的同学会提问:感觉二级索引与主键索引并没有太大的差别,所以为了进一步理解二级索引的开销,接下来我们一起学习二级索引的性能评估。 -希望学完这部分内容之后,你能明白, **为什么通常二级索引会比主键索引慢一些** 。 +希望学完这部分内容之后,你能明白,**为什么通常二级索引会比主键索引慢一些** 。 ### 二级索引的性能评估 -主键在设计时可以选择比较顺序的方式,比如自增整型,自增的 UUID 等,所以主键索引的排序效率和插入性能相对较高。二级索引就不一样了,它可能是比较顺序插入,也可能是完全随机的插入, **具体如何呢?来看一下比较接近业务的表 User:** +主键在设计时可以选择比较顺序的方式,比如自增整型,自增的 UUID 等,所以主键索引的排序效率和插入性能相对较高。二级索引就不一样了,它可能是比较顺序插入,也可能是完全随机的插入,**具体如何呢?来看一下比较接近业务的表 User:** ```sql CREATE TABLE User ( @@ -153,16 +153,16 @@ KEY idx_last_modify_date(last_modify_date) ); ``` -可以看到,表 User 有三个二级索引 idx\_name、idx\_register\_date、idx\_last\_modify\_date。 **通常业务是无法要求用户注册的昵称是顺序的,所以索引 idx_name 的插入是随机的,** 性能开销相对较大;另外用户昵称通常可更新,但业务为了性能考虑,可以限制单个用户每天、甚至是每年昵称更新的次数,比如每天更新一次,每年更新三次。 **而用户注册时间是比较顺序的,所以索引 idx_register_date 的性能开销相对较小,** 另外用户注册时间一旦插入后也不会更新,只是用于标识一个注册时间。 **而关于 idx_last_modify_date ,** 我在 03 讲就强调过,在真实业务的表结构设计中,你必须对每个核心业务表创建一个列 last\_modify\_date,标识每条记录的修改时间。 +可以看到,表 User 有三个二级索引 idx\_name、idx\_register\_date、idx\_last\_modify\_date。**通常业务是无法要求用户注册的昵称是顺序的,所以索引 idx_name 的插入是随机的,** 性能开销相对较大;另外用户昵称通常可更新,但业务为了性能考虑,可以限制单个用户每天、甚至是每年昵称更新的次数,比如每天更新一次,每年更新三次。**而用户注册时间是比较顺序的,所以索引 idx_register_date 的性能开销相对较小,** 另外用户注册时间一旦插入后也不会更新,只是用于标识一个注册时间。**而关于 idx_last_modify_date ,** 我在 03 讲就强调过,在真实业务的表结构设计中,你必须对每个核心业务表创建一个列 last\_modify\_date,标识每条记录的修改时间。 这时索引 idx\_last\_modify\_date 的插入和 idx\_register\_date 类似,是比较顺序的,但不同的是,索引 idx\_last\_modify\_date 会存在比较频繁的更新操作,比如用户消费导致余额修改、money 字段更新,这会导致二级索引的更新。 -由于每个二级索引都包含了主键值,查询通过主键值进行回表,所以在设计表结构时让主键值尽可能的紧凑,为的就是能提升二级索引的性能, **我在 05 讲推荐过 16 字节顺序 UUID 的列设计,这是性能和存储的最佳实践** 。 +由于每个二级索引都包含了主键值,查询通过主键值进行回表,所以在设计表结构时让主键值尽可能的紧凑,为的就是能提升二级索引的性能,**我在 05 讲推荐过 16 字节顺序 UUID 的列设计,这是性能和存储的最佳实践** 。 除此之外,在实际核心业务中,开发同学还有很大可能会设计带有业务属性的主键,但请牢记以下两点设计原则: * 要比较顺序,对聚集索引性能友好; * 尽可能紧凑,对二级索引的性能和存储友好; ### 函数索引 到目前为止,我们的索引都是创建在列上,从 MySQL 5.7 版本开始,MySQL 就开始支持创建函数索引 **(即索引键是一个函数表达式)。** 函数索引有两大用处: * 优化业务 SQL 性能; -* 配合虚拟列(Generated Column)。 **先来看第一个好处,优化业务 SQL 性能。** 我们知道,不是每个开发人员都能比较深入地了解索引的原理,有时他们的表结构设计和编写 SQL 语句会存在“错误”,比如对于上面的表 User,要查询 2021 年1 月注册的用户,有些开发同学会错误地写成如下所示的 SQL: +* 配合虚拟列(Generated Column)。**先来看第一个好处,优化业务 SQL 性能。** 我们知道,不是每个开发人员都能比较深入地了解索引的原理,有时他们的表结构设计和编写 SQL 语句会存在“错误”,比如对于上面的表 User,要查询 2021 年1 月注册的用户,有些开发同学会错误地写成如下所示的 SQL: ```sql SELECT * FROM User @@ -170,7 +170,7 @@ SELECT * FROM User WHERE DATE_FORMAT(register_date,'%Y-%m') = '2021-01' ``` -或许开发同学认为在 register\_date 创建了索引,所以所有的 SQL 都可以使用该索引。 **但索引的本质是排序,** 索引 idx\_register\_date 只对 register\_date 的数据排序,又没有对DATE\_FORMAT(register\_date) 排序,因此上述 SQL 无法使用二级索引idx\_register\_date。 **数据库规范要求查询条件中函数写在等式右边,而不能写在左边** ,就是这个原因。 +或许开发同学认为在 register\_date 创建了索引,所以所有的 SQL 都可以使用该索引。**但索引的本质是排序,** 索引 idx\_register\_date 只对 register\_date 的数据排序,又没有对DATE\_FORMAT(register\_date) 排序,因此上述 SQL 无法使用二级索引idx\_register\_date。**数据库规范要求查询条件中函数写在等式右边,而不能写在左边**,就是这个原因。 我们通过命令 EXPLAIN 查看上述 SQL 的执行计划,会更为直观地发现索引 idx\_register\_date没有被使用到: ```sql @@ -245,7 +245,7 @@ possible_keys: idx_register_date ```plaintext -如果线上业务真的没有按正确的 SQL 编写,那么可能造成数据库存在很多慢查询 SQL,导致业务缓慢甚至发生雪崩的场景。 **要尽快解决这个问题,可以使用函数索引,** 创建一个DATE\_FORMAT(register\_date) 的索引,这样就能利用排序数据快速定位了: +如果线上业务真的没有按正确的 SQL 编写,那么可能造成数据库存在很多慢查询 SQL,导致业务缓慢甚至发生雪崩的场景。**要尽快解决这个问题,可以使用函数索引,** 创建一个DATE\_FORMAT(register\_date) 的索引,这样就能利用排序数据快速定位了: ``` ALTER TABLE User @@ -290,7 +290,7 @@ possible_keys: idx_func_register_date ```plaintext -上述创建的函数索引可以解决业务线上的燃眉之急,但强烈建议业务开发同学在下一个版本中优化 SQL,否则这会导致对同一份数据做了两份索引,索引需要排序,排序多了就会影响性能。 **函数索引第二大用处是结合虚拟列使用。** 在前面的 JSON 小节中,我们已经创建了表 UserLogin: +上述创建的函数索引可以解决业务线上的燃眉之急,但强烈建议业务开发同学在下一个版本中优化 SQL,否则这会导致对同一份数据做了两份索引,索引需要排序,排序多了就会影响性能。**函数索引第二大用处是结合虚拟列使用。** 在前面的 JSON 小节中,我们已经创建了表 UserLogin: ``` CREATE TABLE UserLogin ( diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25410\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25410\350\256\262.md" index 7dd04f34c..3913d880a 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25410\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25410\350\256\262.md" @@ -47,7 +47,7 @@ SELECT * FROM table WHERE a = ? ORDER BY b DESC SELECT * FROM table WHERE b = ? ORDER BY a DESC ``` -讲到这儿,我已经带你学习了组合索引的基本内容, **接下来我们就看一看怎么在业务实战中正确地设计组合索引?** +讲到这儿,我已经带你学习了组合索引的基本内容,**接下来我们就看一看怎么在业务实战中正确地设计组合索引?** ### 业务索引设计实战 @@ -128,7 +128,7 @@ WHERE o_custkey = 147601 ORDER BY o_orderdate DESC **** **** **** **** **** 可以看到,上述 SQL 的执行计划显示进行 Index lookup 索引查询,然后进行 Sort 排序,最终得到结果。 -由于已对列 o_custky 创建索引,因此上述 SQL 语句并不会执行得特别慢,但是在海量的并发业务访问下,每次 SQL 执行都需要排序就会对业务的性能产生非常明显的影响,比如 CPU 负载变高,QPS 降低。 **要解决这个问题,最好的方法是:在取出结果时已经根据字段 o_orderdate 排序,这样就不用额外的排序了。** +由于已对列 o_custky 创建索引,因此上述 SQL 语句并不会执行得特别慢,但是在海量的并发业务访问下,每次 SQL 执行都需要排序就会对业务的性能产生非常明显的影响,比如 CPU 负载变高,QPS 降低。**要解决这个问题,最好的方法是:在取出结果时已经根据字段 o_orderdate 排序,这样就不用额外的排序了。** 为此,我们在表 orders 上创建新的组合索引 idx_custkey_orderdate,对字段(o_custkey,o_orderdate)进行索引: @@ -252,7 +252,7 @@ WHERE o_custkey = 147601; 19 rows in set (0.00 sec) ``` -可以看到,执行一共返回 19 条记录。 **这意味着在未使用索引覆盖技术前,这条 SQL 需要总共回表 19 次,** 每次从二级索引读取到数据,就需要通过主键去获取字段 o_totalprice。 +可以看到,执行一共返回 19 条记录。**这意味着在未使用索引覆盖技术前,这条 SQL 需要总共回表 19 次,** 每次从二级索引读取到数据,就需要通过主键去获取字段 o_totalprice。 在使用索引覆盖技术后,无需回表,减少了 19 次的回表开销, diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25411\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25411\350\256\262.md" index 552163ee6..a9b04b4b0 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25411\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25411\350\256\262.md" @@ -33,7 +33,7 @@ ) ENGINE=InnoDB ``` -在查询字段 o_custkey 时,理论上可以使用三个相关的索引:ORDERS_FK1、idx_custkey_orderdate、idx_custkey_orderdate_totalprice。 **那 MySQL 优化器是怎么从这三个索引中进行选择的呢?** 在关系型数据库中,B+ 树索引只是存储的一种数据结构,具体怎么使用,还要依赖数据库的优化器,优化器决定了具体某一索引的选择,也就是常说的执行计划。 **而优化器的选择是基于成本(cost),哪个索引的成本越低,优先使用哪个索引。** +在查询字段 o_custkey 时,理论上可以使用三个相关的索引:ORDERS_FK1、idx_custkey_orderdate、idx_custkey_orderdate_totalprice。**那 MySQL 优化器是怎么从这三个索引中进行选择的呢?** 在关系型数据库中,B+ 树索引只是存储的一种数据结构,具体怎么使用,还要依赖数据库的优化器,优化器决定了具体某一索引的选择,也就是常说的执行计划。**而优化器的选择是基于成本(cost),哪个索引的成本越低,优先使用哪个索引。** ![image](assets/Cgp9HWCvUQGAR_xXAACOy0gME7Q765.png) @@ -46,7 +46,7 @@ MySQL 执行过程 SQL 优化器会分析所有可能的执行计划,选择成本最低的执行,这种优化器称之为:CBO(Cost-based Optimizer,基于成本的优化器)。 -而在 MySQL中, **一条 SQL 的计算成本计算如下所示:** +而在 MySQL中,**一条 SQL 的计算成本计算如下所示:** ```plaintext Cost = Server Cost + Engine Cost @@ -182,7 +182,7 @@ possible_keys: idx_orderdate Extra: Using where ``` -从上述执行计划中可以发现,优化器已经通过 possible_keys 识别出可以使用索引 idx_orderdate, **但最终却使用全表扫描的方式取出结果。** 最为根本的原因在于:优化器认为使用通过主键进行全表扫描的成本比通过二级索引 idx_orderdate 的成本要低,可以通过 FORMAT=tree 观察得到: +从上述执行计划中可以发现,优化器已经通过 possible_keys 识别出可以使用索引 idx_orderdate,**但最终却使用全表扫描的方式取出结果。** 最为根本的原因在于:优化器认为使用通过主键进行全表扫描的成本比通过二级索引 idx_orderdate 的成本要低,可以通过 FORMAT=tree 观察得到: ```sql EXPLAIN FORMAT=tree @@ -199,7 +199,7 @@ AND o_orderdate < '1994-12-31'\G **** **** **** **** **** **** ***1. row** 可以看到,MySQL 认为全表扫描,然后再通过 WHERE 条件过滤的成本为 592267.11,对比强制使用二级索引 idx_orderdate 的成本为 844351.87。 -成本上看,全表扫描低于使用二级索引。故,MySQL 优化器没有使用二级索引 idx_orderdate。 **为什么全表扫描比二级索引查询快呢?** 因为二级索引需要回表,当回表的记录数非常大时,成本就会比直接扫描要慢,因此这取决于回表的记录数。 +成本上看,全表扫描低于使用二级索引。故,MySQL 优化器没有使用二级索引 idx_orderdate。**为什么全表扫描比二级索引查询快呢?** 因为二级索引需要回表,当回表的记录数非常大时,成本就会比直接扫描要慢,因此这取决于回表的记录数。 所以,第二条 SQL 语句,只是时间范围发生了变化,但是 MySQL 优化器就会自动使用二级索引 idx_orderdate了,这时我们再观察执行计划: @@ -220,7 +220,7 @@ possible_keys: idx_orderdate Extra: Using index condition ``` -再次强调,并不是 MySQL 选择索引出错, **而是 MySQL 会根据成本计算得到最优的执行计划,** 根据不同条件选择最优执行计划,而不是同一类型一成不变的执行过程,这才是优秀的优化器该有的样子。 +再次强调,并不是 MySQL 选择索引出错,**而是 MySQL 会根据成本计算得到最优的执行计划,** 根据不同条件选择最优执行计划,而不是同一类型一成不变的执行过程,这才是优秀的优化器该有的样子。 #### 案例2:索引创建在有限状态上 @@ -234,7 +234,7 @@ B+ 树索引通常要建立在高选择性的字段或字段组合上,如性 但字段 o_orderstatus 的状态是有限的,一般仅为已完成、支付中、超时已关闭这几种。 -通常订单状态绝大部分都是已完成,只有绝少部分因为系统故障原因,会在 15 分钟后还没有完成订单, **因此订单状态是存在数据倾斜的。** +通常订单状态绝大部分都是已完成,只有绝少部分因为系统故障原因,会在 15 分钟后还没有完成订单,**因此订单状态是存在数据倾斜的。** 这时,虽然订单状态是低选择性的,但是由于其有数据倾斜,且我们只是从索引查询少量数据,因此可以对订单状态创建索引: diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25412\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25412\350\256\262.md" index bbe1d8f55..ab8d11510 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25412\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25412\350\256\262.md" @@ -2,7 +2,7 @@ 前面几讲,我带你学习了索引和优化器的工作原理,相信你已经可以对单表的 SQL 语句进行索引的设计和调优工作。但除了单表的 SQL 语句,还有两大类相对复杂的 SQL,多表 JOIN 和子查询语句,这就要在多张表上创建索引,难度相对提升不少。 -而很多开发人员下意识地认为 JOIN 会降低 SQL 的性能效率,所以就将一条多表 SQL 拆成单表的一条条查询,但这样反而会影响 SQL 执行的效率。 **究其原因,在于开发人员不了解 JOIN 的实现过程。** +而很多开发人员下意识地认为 JOIN 会降低 SQL 的性能效率,所以就将一条多表 SQL 拆成单表的一条条查询,但这样反而会影响 SQL 执行的效率。**究其原因,在于开发人员不了解 JOIN 的实现过程。** 那接下来,我们就来关注 JOIN 的工作原理,再在此基础上了解 JOIN 实现的算法和应用场景,从而让你放心大胆地使用 JOIN。 @@ -42,7 +42,7 @@ SELECT ... FROM R INNER JOIN S ON R.x = S.x WEHRE ... 对于上述 Left Join 来说,驱动表就是左表 R;Right Join中,驱动表就是右表 S。这是 JOIN 类型决定左表或右表的数据一定要进行查询。但对于 INNER JOIN,驱动表可能是表 R,也可能是表 S。 -在这种场景下, **谁需要查询的数据量越少,谁就是驱动表。** 我们来看下面的例子: +在这种场景下,**谁需要查询的数据量越少,谁就是驱动表。** 我们来看下面的例子: ```sql SELECT ... FROM R INNER JOIN S @@ -56,7 +56,7 @@ WHERE R.y = ? AND S.z = ? 优化器一般认为,通过索引进行查询的效率都一样,所以 Nested Loop Join 算法主要要求驱动表的数量要尽可能少。 -所以,如果 WHERE R.y = ?过滤出的数据少,那么这条 SQL 语句会先使用表 R 上列 y 上的索引,筛选出数据,然后再使用表 S 上列 x 的索引进行关联,最后再通过 WHERE S.z = ?过滤出最后数据。 **为了深入理解优化器驱动表的选择** ,咱们先来看下面这条 SQL: +所以,如果 WHERE R.y = ?过滤出的数据少,那么这条 SQL 语句会先使用表 R 上列 y 上的索引,筛选出数据,然后再使用表 S 上列 x 的索引进行关联,最后再通过 WHERE S.z = ?过滤出最后数据。**为了深入理解优化器驱动表的选择**,咱们先来看下面这条 SQL: ```sql SELECT COUNT(1) @@ -185,7 +185,7 @@ WHERE 选择 1 就是 MySQL 8.0 不支持 Hash Join 时优化器的处理方式,缺点是:如关联的数据量非常大,创建索引需要时间;其次可能需要回表,优化器大概率会选择直接扫描内表。 -选择 2 只对大约 5 条记录的表 region 创建哈希索引,时间几乎可以忽略不计,其次直接选择对内表扫描,没有回表的问题。 **很明显,MySQL 8.0 会选择Hash Join。** +选择 2 只对大约 5 条记录的表 region 创建哈希索引,时间几乎可以忽略不计,其次直接选择对内表扫描,没有回表的问题。**很明显,MySQL 8.0 会选择Hash Join。** 了解完优化器的选择后,最后看一下命令 EXPLAIN FORMAT=tree 执行计划的最终结果: @@ -253,8 +253,8 @@ EXPLAIN: -> Limit: 30 row(s) (cost=27.76 rows=30) 由于驱动表的数据是固定 30 条,因此不论表 orders、lineitem、part 的数据量有多大,哪怕是百亿条记录,由于都是通过主键进行关联,上述 SQL 的执行速度几乎不变。 -**所以,OLTP 业务完全可以大胆放心地写 JOIN,但是要确保 JOIN 的索引都已添加** , DBA 们在业务上线之前一定要做 SQL Review,确保预期内的索引都已创建。 +**所以,OLTP 业务完全可以大胆放心地写 JOIN,但是要确保 JOIN 的索引都已添加**, DBA 们在业务上线之前一定要做 SQL Review,确保预期内的索引都已创建。 ### 总结 -MySQL 数据库中支持 JOIN 连接的算法有 Nested Loop Join 和 Hash Join 两种,前者通常用于 OLTP 业务,后者用于 OLAP 业务。在 OLTP 可以写 JOIN,优化器会自动选择最优的执行计划。但若使用 JOIN,要确保 SQL 的执行计划使用了正确的索引以及索引覆盖, **因此索引设计显得尤为重要,这也是DBA在架构设计方面的重要工作之一。** \ No newline at end of file +MySQL 数据库中支持 JOIN 连接的算法有 Nested Loop Join 和 Hash Join 两种,前者通常用于 OLTP 业务,后者用于 OLAP 业务。在 OLTP 可以写 JOIN,优化器会自动选择最优的执行计划。但若使用 JOIN,要确保 SQL 的执行计划使用了正确的索引以及索引覆盖,**因此索引设计显得尤为重要,这也是DBA在架构设计方面的重要工作之一。** \ No newline at end of file diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25413\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25413\350\256\262.md" index 191bf4b4b..da95e8398 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25413\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25413\350\256\262.md" @@ -6,7 +6,7 @@ 除了多表连接之外,开发同学还会大量用子查询语句(subquery)。但是因为之前版本的MySQL 数据库对子查询优化有限,所以很多 OLTP 业务场合下,我们都要求在线业务尽可能不用子查询。 -然而,MySQL 8.0 版本中,子查询的优化得到大幅提升。所以从现在开始, **放心大胆地在MySQL 中使用子查询吧!** ### 为什么开发同学这么喜欢写子查询? +然而,MySQL 8.0 版本中,子查询的优化得到大幅提升。所以从现在开始,**放心大胆地在MySQL 中使用子查询吧!** ### 为什么开发同学这么喜欢写子查询? 我工作这么多年,发现相当多的开发同学喜欢写子查询,而不是传统的 JOIN 语句。举一个简单的例子,如果让开发同学“找出1993年,没有下过订单的客户数量”,大部分同学会用子查询来写这个需求,比如: @@ -43,7 +43,7 @@ WHERE o_custkey IS NULL; ``` -可以发现,虽然 LEFT JOIN 也能完成上述需求,但不容易理解, **因为 LEFT JOIN 是一个代数关系,而子查询更偏向于人类的思维角度进行理解。** 所以,大部分人都更倾向写子查询,即便是天天与数据库打交道的 DBA 。 +可以发现,虽然 LEFT JOIN 也能完成上述需求,但不容易理解,**因为 LEFT JOIN 是一个代数关系,而子查询更偏向于人类的思维角度进行理解。** 所以,大部分人都更倾向写子查询,即便是天天与数据库打交道的 DBA 。 不过从优化器的角度看,LEFT JOIN 更易于理解,能进行传统 JOIN 的两表连接,而子查询则要求优化器聪明地将其转换为最优的 JOIN 连接。 @@ -88,7 +88,7 @@ WHERE ### 依赖子查询的优化 -在 MySQL 8.0 版本之前,MySQL 对于子查询的优化并不充分。所以在子查询的执行计划中会看到 DEPENDENT SUBQUERY 的提示,这表示是一个依赖子查询,子查询需要依赖外部表的关联。 **如果你看到这样的提示,就要警惕,** 因为 DEPENDENT SUBQUERY 执行速度可能非常慢,大部分时候需要你手动把它转化成两张表之间的连接。 +在 MySQL 8.0 版本之前,MySQL 对于子查询的优化并不充分。所以在子查询的执行计划中会看到 DEPENDENT SUBQUERY 的提示,这表示是一个依赖子查询,子查询需要依赖外部表的关联。**如果你看到这样的提示,就要警惕,** 因为 DEPENDENT SUBQUERY 执行速度可能非常慢,大部分时候需要你手动把它转化成两张表之间的连接。 我们以下面这条 SQL 为例: @@ -113,7 +113,7 @@ WHERE 通过命令 EXPLAIN FORMAT=tree 输出执行计划,你可以看到,第 3 行有这样的提示:_ **Select #2 (subquery in condition; run only once)** _。这表示子查询只执行了一次,然后把最终的结果保存起来了。 -执行计划的第 6 行 **Index lookup on ** ,表示对表 orders 和子查询结果所得到的表进行 JOIN 连接,最后返回结果。 +执行计划的第 6 行 **Index lookup on **,表示对表 orders 和子查询结果所得到的表进行 JOIN 连接,最后返回结果。 所以,当前这个执行计划是对表 orders 做2次扫描,每次扫描约 5587618 条记录: @@ -140,7 +140,7 @@ MySQL 8.0 版本执行过程 可以发现,第 3 行的执行技术输出是:Select #2 (subquery in condition; dependent),并不像先前的执行计划,提示只执行一次。另外,通过第 1 行也可以发现,这条 SQL 变成了 exists 子查询,每次和子查询进行关联。 -所以,上述执行计划其实表示:先查询每个员工的订单信息,接着对每条记录进行内部的子查询进行依赖判断。也就是说,先进行外表扫描,接着做依赖子查询的判断。 **所以,子查询执行了5587618,而不是1次!!!** 所以,两者的执行计划,扫描次数的对比如下所示: +所以,上述执行计划其实表示:先查询每个员工的订单信息,接着对每条记录进行内部的子查询进行依赖判断。也就是说,先进行外表扫描,接着做依赖子查询的判断。**所以,子查询执行了5587618,而不是1次!!!** 所以,两者的执行计划,扫描次数的对比如下所示: ![图片1.png](assets/CioPOWC9iHGAGvHhAADKi9rGjL8095.png) @@ -170,7 +170,7 @@ WHERE ![Drawing 15.png](assets/CioPOWC4sAqAETsfAAA-Ui3vTHk812.png) -可以看到,经过 SQL 重写后,派生表的执行速度几乎与独立子查询一样。所以, **若看到依赖子查询的执行计划,记得先进行 SQL 重写优化哦。** ### 总结 +可以看到,经过 SQL 重写后,派生表的执行速度几乎与独立子查询一样。所以,**若看到依赖子查询的执行计划,记得先进行 SQL 重写优化哦。** ### 总结 这一讲,我们学习了 MySQL 子查询的优势、新版本 MySQL 8.0 对子查询的优化,以及老版本MySQL 下如何对子查询进行优化。希望你在学完今天的内容之后,可以不再受子查询编写的困惑,而是在各种场景下用好子查询。 @@ -178,5 +178,5 @@ WHERE 1. 子查询相比 JOIN 更易于人类理解,所以受众更广,使用更多; 1. 当前 MySQL 8.0 版本可以“毫无顾忌”地写子查询,对于子查询的优化已经相当完备; -1. 对于老版本的 MySQL, **请 Review 所有子查询的SQL执行计划,** 对于出现 DEPENDENT SUBQUERY 的提示,请务必即使进行优化,否则对业务将造成重大的性能影响; +1. 对于老版本的 MySQL,**请 Review 所有子查询的SQL执行计划,** 对于出现 DEPENDENT SUBQUERY 的提示,请务必即使进行优化,否则对业务将造成重大的性能影响; 1. DEPENDENT SUBQUERY 的优化,一般是重写为派生表进行表连接。表连接的优化就是我们12讲所讲述的内容。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25414\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25414\350\256\262.md" index 01d516520..e39bed644 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25414\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25414\350\256\262.md" @@ -98,7 +98,7 @@ SELECT * FROM t; ### 分区表的误区:性能提升 -很多同学会认为,分区表是把一张大表拆分成了多张小表,所以这样 MySQL 数据库的性能会有大幅提升。 **这是错误的认识** !如果你寄希望于通过分区表提升性能,那么我不建议你使用分区,因为做不到。 **分区表技术不是用于提升 MySQL 数据库的性能,而是方便数据的管理** 。 +很多同学会认为,分区表是把一张大表拆分成了多张小表,所以这样 MySQL 数据库的性能会有大幅提升。**这是错误的认识**!如果你寄希望于通过分区表提升性能,那么我不建议你使用分区,因为做不到。**分区表技术不是用于提升 MySQL 数据库的性能,而是方便数据的管理** 。 我们再回顾下 08 讲中提及的“B+树高度与数据存储量之间的关系”: @@ -129,7 +129,7 @@ possible_keys: NULL **通过执行计划我们可以看到** :上述 SQL 需要访问 4 个分区,假设每个分区需要 3 次 I/O,则这条 SQL 总共要 12 次 I/O。但是,如果使用普通表,记录数再多,也就 4 次的 I/O 的时间。 -所以,分区表设计时,务必明白你的查询条件都带有分区字段,否则会扫描所有分区的数据或索引。 **所以,分区表设计不解决性能问题,更多的是解决数据迁移和备份的问题。** +所以,分区表设计时,务必明白你的查询条件都带有分区字段,否则会扫描所有分区的数据或索引。**所以,分区表设计不解决性能问题,更多的是解决数据迁移和备份的问题。** 而为了让你更好理解分区表的使用,我们继续看一个真实业务的分区表设计。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25415\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25415\350\256\262.md" index d80db55a4..3d83a513e 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25415\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25415\350\256\262.md" @@ -38,7 +38,7 @@ Query OK, 2482 rows affected (0.07 sec) ![Drawing 2.png](assets/CioPOWDDJ8qAUjIpAAIxj14V0cE562.png) -在有二进制日志的基础上,MySQL 数据库就可以通过数据复制技术实现数据同步了。而数据复制的本质就是把一台 MySQL 数据库上的变更同步到另一台 MySQL 数据库上。 **下面这张图显示了当前 MySQL 数据库的复制架构** : +在有二进制日志的基础上,MySQL 数据库就可以通过数据复制技术实现数据同步了。而数据复制的本质就是把一台 MySQL 数据库上的变更同步到另一台 MySQL 数据库上。**下面这张图显示了当前 MySQL 数据库的复制架构** : ![Drawing 3.png](assets/CioPOWDDJ8-ALENTAADSwBdKoIk997.png) @@ -78,7 +78,7 @@ relay_log_info_repository = TABLE 上述设置都是用于保证 crash safe,即无论 Master 还是 Slave 宕机,当它们恢复后,连上主机后,主从数据依然一致,不会产生任何不一致的问题。 -我经常听有同学反馈:MySQL会存在主从数据不一致的情况, **请确认上述参数都已配置,否则任何的不一致都不是 MySQL 的问题,而是你使用 MySQL 的错误姿势所致** 。 +我经常听有同学反馈:MySQL会存在主从数据不一致的情况,**请确认上述参数都已配置,否则任何的不一致都不是 MySQL 的问题,而是你使用 MySQL 的错误姿势所致** 。 了解完复制的配置后,我们接下来看一下 MySQL 支持的复制类型。 @@ -117,7 +117,7 @@ rpl_semi_sync_master_wait_no_slave = 1 - 第 2、3 行表示分别启用半同步 Master 和半同步 Slave 插件; - 第 4 行表示半同步复制过程中,提交的事务必须至少有一个 Slave 接收到二进制日志。 -在半同步复制中,有损半同步复制是 MySQL 5.7 版本前的半同步复制机制,这种半同步复制在Master 发生宕机时, **Slave 会丢失最后一批提交的数据** ,若这时 Slave 提升(Failover)为Master,可能会发生已经提交的事情不见了,发生了回滚的情况。 +在半同步复制中,有损半同步复制是 MySQL 5.7 版本前的半同步复制机制,这种半同步复制在Master 发生宕机时,**Slave 会丢失最后一批提交的数据**,若这时 Slave 提升(Failover)为Master,可能会发生已经提交的事情不见了,发生了回滚的情况。 有损半同步复制原理如下图所示: @@ -170,7 +170,7 @@ CHANGE MASTER TO master_delay = 3600 1. 二进制日志记录了所有对于 MySQL 变更的操作; 1. 可以通过命令 SHOW BINLOG EVENTS IN ... FROM ... 查看二进制日志的基本信息; 1. 可以通过工具 mysqlbinlog 查看二进制日志的详细内容; -1. **复制搭建虽然简单,但别忘记配置 crash safe 相关参数** ,否则可能导致主从数据不一致; +1. **复制搭建虽然简单,但别忘记配置 crash safe 相关参数**,否则可能导致主从数据不一致; 1. 异步复制用于非核心业务场景,不要求数据一致性; 1. 无损半同步复制用于核心业务场景,如银行、保险、证券等核心业务,需要严格保障数据一致性; 1. 多源复制可将多个 Master 数据汇总到一个数据库示例进行分析; diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25416\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25416\350\256\262.md" index 08c60e80b..88f265b58 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25416\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25416\350\256\262.md" @@ -33,7 +33,7 @@ WHEREE time between ... and ... LIMIT 1000; ``` -上面的 SQL 就是把一个大的 DELETE 操作拆分成了每次删除 1000 条记录的小操作。而小事务的另一个优势是:可以进行多线程的并发操作,进一步提升删除效率。 **MySQL 数据库中,大事务除了会导致提交速度变慢,还会导致主从复制延迟** 。 +上面的 SQL 就是把一个大的 DELETE 操作拆分成了每次删除 1000 条记录的小操作。而小事务的另一个优势是:可以进行多线程的并发操作,进一步提升删除效率。**MySQL 数据库中,大事务除了会导致提交速度变慢,还会导致主从复制延迟** 。 试想一下,一个大事务在主服务器上运行了 30 分钟,那么在从服务器上也需要运行 30 分钟。在从机回放这个大事务的过程中,主从服务器之间的数据就产生了延迟;产生大事务的另一种可能性是主服务上没有创建索引,导致一个简单的操作时间变得非常长。这样在从机回放时,也会需要很长的时间从而导致主从的复制延迟。 @@ -52,7 +52,7 @@ MySQL 的从机并行复制有两种模式。 COMMIT ORDER 模式的从机并行复制,从机完全根据主服务的并行度进行回放。理论上来说,主从延迟极小。但如果主服务器上并行度非常小,事务并不小,比如单线程每次插入 1000 条记录,则从机单线程回放,也会存在一些复制延迟的情况。 -而 WRITESET 模式是基于每个事务并行,如果事务间更新的记录不冲突,就可以并行。还是以“单线程每次插入 1000 条记录”为例,如果插入的记录没有冲突,比如唯一索引冲突, **那么虽然主机是单线程,但从机可以是多线程并行回放!!!** 所以在 WRITESET 模式下,主从复制几乎没有延迟。那么要启用 WRITESET 复制模式,你需要做这样的配置: +而 WRITESET 模式是基于每个事务并行,如果事务间更新的记录不冲突,就可以并行。还是以“单线程每次插入 1000 条记录”为例,如果插入的记录没有冲突,比如唯一索引冲突,**那么虽然主机是单线程,但从机可以是多线程并行回放!!!** 所以在 WRITESET 模式下,主从复制几乎没有延迟。那么要启用 WRITESET 复制模式,你需要做这样的配置: ```plaintext binlog_transaction_dependency_tracking = WRITESET @@ -71,7 +71,7 @@ slave-parallel-workers = 16 ![Drawing 0.png](assets/CioPOWDDKNSAVgwsAAFw4VDRM9U648.png) -但是, **Seconds_Behind_Master 不准确!用于严格判断主从延迟的问题并不合适,** 有这样三个原因。 +但是,**Seconds_Behind_Master 不准确!用于严格判断主从延迟的问题并不合适,** 有这样三个原因。 1. 它计算规则是(当前回放二进制时间 - 二进制日志中的时间),如果 I/O 线程有延迟,那么 Second_Behind_Master 为 0,这时可能已经落后非常多了,例如存在有大事务的情况下; 1. 对于级联复制,最下游的从服务器延迟是不准确的,因为它只表示和上一级主服务器之间的延迟; @@ -122,7 +122,7 @@ END 上图引入了 Load Balance 负载均衡的组件,这样 Server 对于数据库的请求不用关心后面有多少个从机,对于业务来说也就是透明的,只需访问 Load Balance 服务器的 IP 或域名就可以。 -通过配置 Load Balance 服务,还能将读取请求平均或按照权重平均分布到不同的从服务器。这可以根据架构的需要做灵活的设计。 **请记住:读写分离设计的前提是从机不能落后主机很多,最好是能准实时数据同步,务必一定要开始并行复制,并确保线上已经将大事务拆成小事务** 。 +通过配置 Load Balance 服务,还能将读取请求平均或按照权重平均分布到不同的从服务器。这可以根据架构的需要做灵活的设计。**请记住:读写分离设计的前提是从机不能落后主机很多,最好是能准实时数据同步,务必一定要开始并行复制,并确保线上已经将大事务拆成小事务** 。 当然,若是一些报表类的查询,只要不影响最终结果,业务是能够容忍一些延迟的。但无论如何,请一定要在线上数据库环境中做好主从复制延迟的监控。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25417\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25417\350\256\262.md" index c01727e53..03df9ce6b 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25417\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25417\350\256\262.md" @@ -122,7 +122,7 @@ INSERT INTO Orders(o_orderkey, ... ) VALUES (...) 上图中,将不同编号的订单根据不同的数据库进行存放,比如服务器编号为 1 的订单存放在数据库 DB1 中,服务器编号为 2 的订单存放在数据库 DB2 中。 -此外,这里也用到了 MySQL 复制中的部分复制技术,即左上角的主服务器仅将 DB1 中的数据同步到右上角的服务器。同理,右上角的主服务器仅将 DB2 中的数据同步到左上角的服务器。下面的两台从服务器不变,依然从原来的 MySQL 实例中同步数据。 **这样做得好处是:** +此外,这里也用到了 MySQL 复制中的部分复制技术,即左上角的主服务器仅将 DB1 中的数据同步到右上角的服务器。同理,右上角的主服务器仅将 DB2 中的数据同步到左上角的服务器。下面的两台从服务器不变,依然从原来的 MySQL 实例中同步数据。**这样做得好处是:** - 在常态情况下,上面两台 MySQL 数据库是双活的,都可以有数据的写入,业务的性能得到极大提升。 - 订单数据是完整的,服务器编号为 1 和 2 的数据都在一个 MySQL 实例上。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25418\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25418\350\256\262.md" index 609e7c8b6..be4fba15b 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25418\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25418\350\256\262.md" @@ -4,7 +4,7 @@ 在这些架构中,仅仅解决了业务连续性的问题:也就是当服务器因为各种原因,发生宕机,导致MySQL 数据库不可用之后,快速恢复业务。但对有状态的数据库服务来说,在一些核心业务系统中,比如电商、金融等,还要保证数据一致性。 -这里的“数据一致性”是指在任何灾难场景下, **一条数据都不允许丢失** (一般也把这种数据复制方式叫作“强同步”)。 +这里的“数据一致性”是指在任何灾难场景下,**一条数据都不允许丢失** (一般也把这种数据复制方式叫作“强同步”)。 今天我们就来看一看,怎么在这种最高要求(数据一致性)的业务场景中,设计 MySQL 的高可用架构。 @@ -69,7 +69,7 @@ ![5.png](assets/Cgp9HWDTCQyAFtFCAAF7qJpeUuM426.png) -从图中可以看到,我们加入两个异步复制的节点,用于业务实现读写分离,另外再从机房 3 的备机中,引入一个异步复制的延迟备机,用于做数据误删除操作的恢复。 **当设计成类似上述的架构时,你才能认为自己的同城容灾架构是合格的!** 另一个重要的点:因为机房 1 中的主服务器要向四个从服务器发送日志,这时网卡有成为瓶颈的可能,所以请务必配置万兆网卡。 **在明白三园区架构后,要实现跨城容灾也就非常简单了,** 只要把三个机房放在不同城市就行。但这样的设计,当主服务器发生宕机时,数据库就会切到跨城,而跨城之间的网络延迟超过了25 ms。所以,跨城容灾一般设计成“三地五中心”的架构,如下图所示: +从图中可以看到,我们加入两个异步复制的节点,用于业务实现读写分离,另外再从机房 3 的备机中,引入一个异步复制的延迟备机,用于做数据误删除操作的恢复。**当设计成类似上述的架构时,你才能认为自己的同城容灾架构是合格的!** 另一个重要的点:因为机房 1 中的主服务器要向四个从服务器发送日志,这时网卡有成为瓶颈的可能,所以请务必配置万兆网卡。**在明白三园区架构后,要实现跨城容灾也就非常简单了,** 只要把三个机房放在不同城市就行。但这样的设计,当主服务器发生宕机时,数据库就会切到跨城,而跨城之间的网络延迟超过了25 ms。所以,跨城容灾一般设计成“三地五中心”的架构,如下图所示: ![6.png](assets/CioPOWDTCROAOWeTAAF4gz8w6PY448.png) @@ -91,7 +91,7 @@ 所以,除了高可用的容灾架构设计,我们还要做一层兜底服务,用于判断数据的一致性。这里要引入数据核对,用来解决以下两方面的问题。 - **数据在业务逻辑上一致:** 这个保障业务是对的; -- **主从服务器之间的数据一致:** 这个保障从服务器的数据是安全的、可切的。 **业务逻辑核对由业务的同学负责编写,** 从整个业务逻辑调度看账平不平。例如“今天库存的消耗”是否等于“订单明细表中的总和”,“在途快递” + “已收快递”是否等于“已下快递总和”。总之,这是个业务逻辑,用于对账。 **主从服务器之间的核对,是由数据库团队负责的。** 需要额外写一个主从核对服务,用于保障主从数据的一致性。这个核对不依赖复制本身,也是一种逻辑核对。思路是:将最近一段时间内主服务器上变更过的记录与从服务器核对,从逻辑上验证是否一致。其实现如图所示: +- **主从服务器之间的数据一致:** 这个保障从服务器的数据是安全的、可切的。**业务逻辑核对由业务的同学负责编写,** 从整个业务逻辑调度看账平不平。例如“今天库存的消耗”是否等于“订单明细表中的总和”,“在途快递” + “已收快递”是否等于“已下快递总和”。总之,这是个业务逻辑,用于对账。**主从服务器之间的核对,是由数据库团队负责的。** 需要额外写一个主从核对服务,用于保障主从数据的一致性。这个核对不依赖复制本身,也是一种逻辑核对。思路是:将最近一段时间内主服务器上变更过的记录与从服务器核对,从逻辑上验证是否一致。其实现如图所示: ![7.png](assets/CioPOWDTCR2AcmV6AAC1N5tCM7E109.png) diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25420\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25420\350\256\262.md" index 71f1e0c78..47d19aa80 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25420\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25420\350\256\262.md" @@ -70,7 +70,7 @@ UPDATE User set money = money + 300 WHERE id = 1; 在上面的例子中,若节点 2 上的事务先提交,则节点 1 提交时会失败,事务会进行回滚。 -所以,如果要发挥多主模式的优势,就要避免写入时有冲突。 **最好的做法是:每个节点写各自的数据库,比如节点 1 写 DB1,节点 2 写 DB2,节点 3 写 DB3,这样集群的写入性能就能线性提升了。** 不过这要求我们在架构设计时,就做好这样的考虑,否则多主不一定能带来预期中的性能提升。 +所以,如果要发挥多主模式的优势,就要避免写入时有冲突。**最好的做法是:每个节点写各自的数据库,比如节点 1 写 DB1,节点 2 写 DB2,节点 3 写 DB3,这样集群的写入性能就能线性提升了。** 不过这要求我们在架构设计时,就做好这样的考虑,否则多主不一定能带来预期中的性能提升。 #### 自增处理 @@ -90,7 +90,7 @@ group_replication_auto_increment_increment = 7 可以看到,由于是多主模式,允许多个节点并发的产生自增值。所以自增的产生结果为1、8、16、17、22,自增值不一定是严格连续的,而仅仅是单调递增的,这与单实例 MySQL 有着很大的不同。 -在 05 讲表结构设计中,我也强调过:尽量不要使用自增值做主键,在 MGR 存在问题,在后续分布式架构中也一样存在类似的自增问题。 **所以,对于核心业务表,还是使用有序 UUID 的方式更为可靠,性能也会更好。** 总之,使用 MGR 技术后,所有高可用事情都由数据库自动完成。那么,业务该如何利用 MGR的能力,是否还需要 VIP、DNS 等机制保证业务的透明性呢?接下来,我们就来看一下, **业务如何利用 MGR 的特性构建高可用解决方案。** +在 05 讲表结构设计中,我也强调过:尽量不要使用自增值做主键,在 MGR 存在问题,在后续分布式架构中也一样存在类似的自增问题。**所以,对于核心业务表,还是使用有序 UUID 的方式更为可靠,性能也会更好。** 总之,使用 MGR 技术后,所有高可用事情都由数据库自动完成。那么,业务该如何利用 MGR的能力,是否还需要 VIP、DNS 等机制保证业务的透明性呢?接下来,我们就来看一下,**业务如何利用 MGR 的特性构建高可用解决方案。** ### InnoDB Cluster diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25421\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25421\350\256\262.md" index cbb47fac7..ce3b0dcd4 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25421\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25421\350\256\262.md" @@ -22,7 +22,7 @@ #### 全量备份 -指备份当前时间点数据库中的所有数据,根据备份内容的不同, **全量备份可以分为逻辑备份、物理备份两种方式。** +指备份当前时间点数据库中的所有数据,根据备份内容的不同,**全量备份可以分为逻辑备份、物理备份两种方式。** - **逻辑备份** @@ -159,11 +159,11 @@ IDENTIFIED BY 'password' 对于 MySQL 8.0 之前的版本,我们可以使用第三方开源工具 Xtrabackup,官方网址:[https://github.com/percona/percona-xtrabackup](https://github.com/percona/percona-xtrabackup)。 -不过,物理备份实现机制较逻辑备份复制很多,需要深入了解 MySQL 数据库内核的实现, **我强烈建议使用 MySQL 官方的物理备份工具,开源第三方物理备份工具只作为一些场景的辅助手段。** #### 增量备份 +不过,物理备份实现机制较逻辑备份复制很多,需要深入了解 MySQL 数据库内核的实现,**我强烈建议使用 MySQL 官方的物理备份工具,开源第三方物理备份工具只作为一些场景的辅助手段。** #### 增量备份 前面我们学习的逻辑备份、物理备份都是全量备份,也就是对整个数据库进行备份。然而,数据库中的数据不断变化,我们不可能每时每分对数据库进行增量的备份。 -所以,我们需要通过“全量备份 + 增量备份”的方式,构建完整的备份策略。 **增量备份就是对日志文件进行备份,在 MySQL 数据库中就是二进制日志文件。** 因为二进制日志保存了对数据库所有变更的修改,所以“全量备份 + 增量备份”,就可以实现基于时间点的恢复(point in time recovery),也就是“通过全量 + 增量备份”可以恢复到任意时间点。 +所以,我们需要通过“全量备份 + 增量备份”的方式,构建完整的备份策略。**增量备份就是对日志文件进行备份,在 MySQL 数据库中就是二进制日志文件。** 因为二进制日志保存了对数据库所有变更的修改,所以“全量备份 + 增量备份”,就可以实现基于时间点的恢复(point in time recovery),也就是“通过全量 + 增量备份”可以恢复到任意时间点。 全量备份时会记录这个备份对应的时间点位,一般是某个 GTID 位置,增量备份可以在这个点位后重放日志,这样就能实现基于时间点的恢复。 @@ -203,7 +203,7 @@ mysqlbinlog binlog.000001 binlog.000002 | mysql -u root -p 在 18 讲中,我们讲到线上主从复制的高可用架构,还需要进行主从之间的数据核对,用来确保数据是真实一致的。 -同样,对于备份文件,也需要进行校验,才能确保备份文件的正确的,当真的发生灾难时,可通过备份文件进行恢复。 **因此,备份系统还需要一个备份文件的校验功能** 。 +同样,对于备份文件,也需要进行校验,才能确保备份文件的正确的,当真的发生灾难时,可通过备份文件进行恢复。**因此,备份系统还需要一个备份文件的校验功能** 。 备份文件校验的大致逻辑是恢复全部文件,接着通过增量备份进行恢复,然后将恢复的 MySQL实例连上线上的 MySQL 服务器作为从服务器,然后再次进行数据核对。 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25423\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25423\350\256\262.md" index e14a1a3ee..4239f38f4 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25423\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25423\350\256\262.md" @@ -45,7 +45,7 @@ CREATE TABLE `orders` ( 从图中我们可以看到,采用 RANGE 算法进行分片后,表 orders 中,1992 年的订单数据存放在分片 1 中、1993 年的订单数据存放在分片 2 中、1994 年的订单数据存放在分片 3中,依次类推,如果要存放新年份的订单数据,追加新的分片即可。 -**不过,RANGE 分片算法在分布式数据库架构中,是一种非常糟糕的算法** ,因为对于分布式架构,通常希望能解决传统单实例数据库两个痛点: +**不过,RANGE 分片算法在分布式数据库架构中,是一种非常糟糕的算法**,因为对于分布式架构,通常希望能解决传统单实例数据库两个痛点: - 性能可扩展,通过增加分片节点,性能可以线性提升; - 存储容量可扩展,通过增加分片节点,解决单点存储容量的数据瓶颈。 @@ -126,7 +126,7 @@ ORDER BY o_orderdate DESC LIMIT 10 如果根据第 4 种标准的分库分表规范,那么分布式 MySQL 数据库的架构可以是这样: ![image](assets/Cgp9HWDv4qeAeLM7AAGBfl0Kr4I115.jpg) -**有没有发现,按上面这样的分布式设计,数据分片完成后,所有的库表依然是在同一个 MySQL实例上!!!** 牢记,分布式数据库并不一定要求有很多个实例,最基本的要求是将数据进行打散分片。接着,用户可以根据自己的需要,进行扩缩容,以此实现数据库性能和容量的伸缩性。 **这才是分布式数据库真正的魅力所在** 。 +**有没有发现,按上面这样的分布式设计,数据分片完成后,所有的库表依然是在同一个 MySQL实例上!!!** 牢记,分布式数据库并不一定要求有很多个实例,最基本的要求是将数据进行打散分片。接着,用户可以根据自己的需要,进行扩缩容,以此实现数据库性能和容量的伸缩性。**这才是分布式数据库真正的魅力所在** 。 对于上述的分布式数据库架构,一开始我们将 4 个分片数据存储在一个 MySQL 实例上,但是如果遇到一些大促活动,可以对其进行扩容,比如把 4 个分片扩容到 4 个MySQL实例上: ![06.jpg](assets/CioPOWDv4-uACx5SAAF0mbv_uqE895.jpg) @@ -144,7 +144,7 @@ ORDER BY o_orderdate DESC LIMIT 10 如果到了 1000 个分片依然无法满足业务的需求,这时能不能拆成 2000 个分片呢?从理论上来说是可以的,但是这意味着需要对一张表中的数据进行逻辑拆分,这个工作非常复杂,通常不推荐。 -所以, **一开始一定要设计足够多的分片** 。在实际工作中,我遇到很多次业务将分片数量从 32、64 拆成 256、512。每次这样的工作,都是扒一层皮,太不值得。所以,做好分布式数据库设计的工作有多重要! +所以,**一开始一定要设计足够多的分片** 。在实际工作中,我遇到很多次业务将分片数量从 32、64 拆成 256、512。每次这样的工作,都是扒一层皮,太不值得。所以,做好分布式数据库设计的工作有多重要! 那么扩容在 MySQL 数据库中如何操作呢?其实,本质是搭建一个复制架构,然后通过设置过滤复制,仅回放分片所在的数据库就行,这个数据库配置在从服务器上大致进行如下配置: diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25424\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25424\350\256\262.md" index c0c4be0a1..cfe1bdeee 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25424\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25424\350\256\262.md" @@ -35,11 +35,11 @@ CREATE TABLE `orders` ( ![MySQL24.jpg](assets/CioPOWD5SNqAQ1OnAAEeCSHRa5U020.jpg) -**所以,在分布式数据库架构下,尽量不要用自增作为表的主键** ,这也是我们在第一模块“表结构设计”中强调过的:自增性能很差、安全性不高、不适用于分布式架构。 +**所以,在分布式数据库架构下,尽量不要用自增作为表的主键**,这也是我们在第一模块“表结构设计”中强调过的:自增性能很差、安全性不高、不适用于分布式架构。 讲到这儿,我们已经说明白了“自增主键”的所有问题,那么该如何设计主键呢?依然还是用全局唯一的键作为主键,比如 MySQL 自动生成的有序 UUID;业务生成的全局唯一键(比如发号器);或者是开源的 UUID 生成算法,比如雪花算法(但是存在时间回溯的问题)。 -总之, **用有序的全局唯一替代自增,是这个时代数据库主键的主流设计标准** ,如果你还停留在用自增做主键,或许代表你已经落后于时代发展了。 +总之,**用有序的全局唯一替代自增,是这个时代数据库主键的主流设计标准**,如果你还停留在用自增做主键,或许代表你已经落后于时代发展了。 ### 索引设计 diff --git "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25426\350\256\262.md" "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25426\350\256\262.md" index 6154de5ef..8bf8136fc 100644 --- "a/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25426\350\256\262.md" +++ "b/docs/Database/MySQL \345\256\236\346\210\230\345\256\235\345\205\270/\347\254\25426\350\256\262.md" @@ -8,7 +8,7 @@ 条带化是存储的一种技术,将磁盘进行条带化后,可以把连续的数据分割成相同大小的数据块,简单的说,条带化就是把每段数据分别写入阵列中不同磁盘上的方法。 -可以看到,条带化的本质是通过将数据打散到多个磁盘,从而提升存储的整体性能, **这与分布式数据库的分片理念是不是非常类似呢** ?下图显示了 RAID0 的条带化存储: +可以看到,条带化的本质是通过将数据打散到多个磁盘,从而提升存储的整体性能,**这与分布式数据库的分片理念是不是非常类似呢**?下图显示了 RAID0 的条带化存储: ![图片11](assets/CioPOWELjNmASogwAAI2vtt5o7o641.png) @@ -34,7 +34,7 @@ ![图片3](assets/CioPOWELjSCAGulnAAFceRV75sg680.png) -可以看到,订单服务可以根据字段 o_custkey 访问不同分片的数据,这也是大部分业务会进行的设计(由于服务层通常是无状态的,因此这里不考虑高可用的情况)。 **但是,这样的设计不符合全链路的条带化设计思想** 。 +可以看到,订单服务可以根据字段 o_custkey 访问不同分片的数据,这也是大部分业务会进行的设计(由于服务层通常是无状态的,因此这里不考虑高可用的情况)。**但是,这样的设计不符合全链路的条带化设计思想** 。 全链路的设计思想,要将上层服务也作为条带的一部分进行处理,也就是说,订单服务也要跟着分片进行分布式架构的改造。 @@ -50,7 +50,7 @@ - 上层服务跟着数据分片进行条带化部署,业务性能更好; - 上层服务跟着数据分片进行条带化部署,可用性更好; -第1点通常比较好理解,但是 2、3点 就不怎么好理解了。 **为什么性能也会更好呢** ?这里请你考虑一下业务的部署情况,也就是,经常听说的多活架构设计。 +第1点通常比较好理解,但是 2、3点 就不怎么好理解了。**为什么性能也会更好呢**?这里请你考虑一下业务的部署情况,也就是,经常听说的多活架构设计。 ### 多活架构 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" index 372438094..a58b9770f 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" @@ -40,7 +40,7 @@ I/O 多路复用机制如下图所示: ![IO多路复用.png](assets/2020-02-24 - 与服务器端以 socket 和 I/O 多路复用的技术建立链接; - 将命令转换为 Redis 通讯协议,再将这些协议发送至缓冲区。 -**步骤三:服务器端接收到命令** 服务器会先去输入缓冲中读取数据,然后判断数据的大小是否超过了系统设置的值(默认是 1GB),如果大于此值就会返回错误信息,并关闭客户端连接。 默认大小如下图所示: ![redis-run-max_query_buffer.png](assets/2020-02-24-122547.png) 当数据大小验证通过之后,服务器端会对输入缓冲区中的请求命令进行分析,提取命令请求中包含的命令参数,存储在 client 对象(服务器端会为每个链接创建一个 Client 对象)的属性中。 **步骤四:执行前准备** ① 判断是否为退出命令,如果是则直接返回; +**步骤三:服务器端接收到命令** 服务器会先去输入缓冲中读取数据,然后判断数据的大小是否超过了系统设置的值(默认是 1GB),如果大于此值就会返回错误信息,并关闭客户端连接。 默认大小如下图所示: ![redis-run-max_query_buffer.png](assets/2020-02-24-122547.png) 当数据大小验证通过之后,服务器端会对输入缓冲区中的请求命令进行分析,提取命令请求中包含的命令参数,存储在 client 对象(服务器端会为每个链接创建一个 Client 对象)的属性中。**步骤四:执行前准备** ① 判断是否为退出命令,如果是则直接返回; ② 非 null 判断,检查 client 对象是否为 null,如果是返回错误信息; @@ -70,7 +70,7 @@ I/O 多路复用机制如下图所示: ![IO多路复用.png](assets/2020-02-24 ⑮ 监视器 (monitor) 判断,如果服务器打开了监视器功能,那么服务器也会把执行命令和相关参数发送给监视器 (监视器是用于监控服务器运行状态的)。 -当服务器经过以上操作之后,就可以执行真正的操作命令了。 **步骤五:执行最终命令,调用 redisCommand 中的 proc 函数执行命令。** **步骤六:执行完后相关记录和统计** ① 检查慢查询是否开启,如果开启会记录慢查询日志; ② 检查统计信息是否开启,如果开启会记录一些统计信息,例如执行命令所耗费时长和计数器(calls)加1; ③ 检查持久化功能是否开启,如果开启则会记录持久化信息; ④ 如果有其它从服务器正在复制当前服务器,则会将刚刚执行的命令传播给其他从服务器。 **步骤七:返回结果给客户端** 命令执行完之后,服务器会通过 socket 的方式把执行结果发送给客户端,客户端再把结果展示给用户,至此一条命令的执行就结束了。 +当服务器经过以上操作之后,就可以执行真正的操作命令了。**步骤五:执行最终命令,调用 redisCommand 中的 proc 函数执行命令。** **步骤六:执行完后相关记录和统计** ① 检查慢查询是否开启,如果开启会记录慢查询日志; ② 检查统计信息是否开启,如果开启会记录一些统计信息,例如执行命令所耗费时长和计数器(calls)加1; ③ 检查持久化功能是否开启,如果开启则会记录持久化信息; ④ 如果有其它从服务器正在复制当前服务器,则会将刚刚执行的命令传播给其他从服务器。**步骤七:返回结果给客户端** 命令执行完之后,服务器会通过 socket 的方式把执行结果发送给客户端,客户端再把结果展示给用户,至此一条命令的执行就结束了。 ### 小结 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" index f3979db42..cf9ce822b 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" @@ -10,7 +10,7 @@ Redis 持久化拥有以下三种方式: - **快照方式** (RDB, Redis DataBase)将某一个时刻的内存数据,以二进制的方式写入磁盘; - **文件追加方式** (AOF, Append Only File),记录所有的操作命令,并以文本的形式追加到文件中; -- **混合持久化方式** ,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。 +- **混合持久化方式**,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。 因为每种持久化方案,都有特定的使用场景,让我们先从 RDB 持久化说起吧。 @@ -86,7 +86,7 @@ dir ./ - save 300 10:表示 300 秒内如果至少有 10 个 key 值变化,则把数据持久化到硬盘; - save 60 10000:表示 60 秒内如果至少有 10000 个 key 值变化,则把数据持久化到硬盘。 -**② rdbcompression 参数** 它的默认值是 `yes` 表示开启 RDB 文件压缩,Redis 会采用 LZF 算法进行压缩。如果不想消耗 CPU 性能来进行文件压缩的话,可以设置为关闭此功能,这样的缺点是需要更多的磁盘空间来保存文件。 **③ rdbchecksum 参数** 它的默认值为 `yes` 表示写入文件和读取文件时是否开启 RDB 文件检查,检查是否有无损坏,如果在启动是检查发现损坏,则停止启动。 +**② rdbcompression 参数** 它的默认值是 `yes` 表示开启 RDB 文件压缩,Redis 会采用 LZF 算法进行压缩。如果不想消耗 CPU 性能来进行文件压缩的话,可以设置为关闭此功能,这样的缺点是需要更多的磁盘空间来保存文件。**③ rdbchecksum 参数** 它的默认值为 `yes` 表示写入文件和读取文件时是否开启 RDB 文件检查,检查是否有无损坏,如果在启动是检查发现损坏,则停止启动。 ### 5 配置查询 @@ -105,7 +105,7 @@ Redis 中可以使用命令查询当前配置参数。查询命令的格式为 ### 7 RDB 文件恢复 -当 Redis 服务器启动时,如果 Redis 根目录存在 RDB 文件 dump.rdb,Redis 就会自动加载 RDB 文件恢复持久化数据。 如果根目录没有 dump.rdb 文件,请先将 dump.rdb 文件移动到 Redis 的根目录。 **验证 RDB 文件是否被加载** Redis 在启动时有日志信息,会显示是否加载了 RDB 文件,我们执行 Redis 启动命令:`src/redis-server redis.conf` ,如下图所示: ![image.png](assets/2020-02-24-122634.png) 从日志上可以看出, Redis 服务在启动时已经正常加载了 RDB 文件。 +当 Redis 服务器启动时,如果 Redis 根目录存在 RDB 文件 dump.rdb,Redis 就会自动加载 RDB 文件恢复持久化数据。 如果根目录没有 dump.rdb 文件,请先将 dump.rdb 文件移动到 Redis 的根目录。**验证 RDB 文件是否被加载** Redis 在启动时有日志信息,会显示是否加载了 RDB 文件,我们执行 Redis 启动命令:`src/redis-server redis.conf` ,如下图所示: ![image.png](assets/2020-02-24-122634.png) 从日志上可以看出, Redis 服务在启动时已经正常加载了 RDB 文件。 > 小贴士:Redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" index c9607e6e1..7bf9cbde2 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" @@ -35,7 +35,7 @@ Redis 默认是关闭 AOF 持久化的,想要开启 AOF 持久化,有以下 ##### ② 配置文件启动 AOF -Redis 的配置文件在它的根路径下的 redis.conf 文件中,获取 Redis 的根目录可以使用命令 `config get dir` 获取,如下图所示: ![image.png](assets/2020-02-24-122528.png) 只需要在配置文件中设置 `appendonly yes` 即可,默认 `appendonly no` 表示关闭 AOF 持久化。 **配置文件启动 AOF 的优缺点** :修改配置文件的缺点是每次修改配置文件都要重启 Redis 服务才能生效,优点是无论重启多少次 Redis 服务,配置文件中设置的配置信息都不会失效。 +Redis 的配置文件在它的根路径下的 redis.conf 文件中,获取 Redis 的根目录可以使用命令 `config get dir` 获取,如下图所示: ![image.png](assets/2020-02-24-122528.png) 只需要在配置文件中设置 `appendonly yes` 即可,默认 `appendonly no` 表示关闭 AOF 持久化。**配置文件启动 AOF 的优缺点** :修改配置文件的缺点是每次修改配置文件都要重启 Redis 服务才能生效,优点是无论重启多少次 Redis 服务,配置文件中设置的配置信息都不会失效。 ### 3 触发持久化 @@ -121,7 +121,7 @@ aof-load-truncated yes 正常情况下,只要开启了 AOF 持久化,并且提供了正常的 appendonly.aof 文件,在 Redis 启动时就会自定加载 AOF 文件并启动,执行如下图所示: ![image.png](assets/2020-02-24-122532.png) 其中 `DB loaded from append only file......` 表示 Redis 服务器在启动时,先去加载了 AOF 持久化文件。 -> 小贴士:默认情况下 appendonly.aof 文件保存在 Redis 的根目录下。 **持久化文件加载规则** +> 小贴士:默认情况下 appendonly.aof 文件保存在 Redis 的根目录下。**持久化文件加载规则** - 如果只开启了 AOF 持久化,Redis 启动时只会加载 AOF 文件(appendonly.aof),进行数据恢复; - 如果只开启了 RDB 持久化,Redis 启动时只会加载 RDB 文件(dump.rdb),进行数据恢复; diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25408\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25408\350\256\262.md" index b71ea0311..b0907deab 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25408\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25408\350\256\262.md" @@ -28,7 +28,7 @@ "v4" ``` -如果 **尝试插入已存在的键** ,不会改变原来的值,示例如下: +如果 **尝试插入已存在的键**,不会改变原来的值,示例如下: ```shell 127.0.0.1:6379> hsetnx myhash k4 val4 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" index a1bd780c5..0b0d69423 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" @@ -158,7 +158,7 @@ public class SetExample { "intset" ``` -从上面代码可以看出, **当所有元素都为整数时,集合会以 intset 结构进行(数据)存储** 。 当发生以下两种情况时,会导致集合类型使用 hashtable 而非 intset 存储: 1)当元素的个数超过一定数量时,默认是 512 个,该值可通过命令 `set-max-intset-entries xxx` 来配置。 2)当元素为非整数时,集合将会使用 hashtable 来存储,如下代码所示: +从上面代码可以看出,**当所有元素都为整数时,集合会以 intset 结构进行(数据)存储** 。 当发生以下两种情况时,会导致集合类型使用 hashtable 而非 intset 存储: 1)当元素的个数超过一定数量时,默认是 512 个,该值可通过命令 `set-max-intset-entries xxx` 来配置。 2)当元素为非整数时,集合将会使用 hashtable 来存储,如下代码所示: ```shell 127.0.0.1:6379> sadd myht "redis" "db" @@ -167,7 +167,7 @@ public class SetExample { "hashtable" ``` -从上面代码可以看出, **当元素为非整数时,集合会使用 hashtable 进行存储** 。 +从上面代码可以看出,**当元素为非整数时,集合会使用 hashtable 进行存储** 。 ### 4 源码解析 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" index 0b7458def..cb0e2c964 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" @@ -212,7 +212,7 @@ public class TTLTest { ``` -**更多过期操作方法** ,如下列表: +**更多过期操作方法**,如下列表: * pexpire(String key, long milliseconds):设置 n 毫秒后过期; * expireAt(String key, long unixTime):设置某个时间戳后过期(精确到秒); * pexpireAt(String key, long millisecondsTimestamp):设置某个时间戳后过期(精确到毫秒); @@ -282,7 +282,7 @@ public class TTLTest { 上面我们讲了过期键在 Redis 正常运行中一些使用案例,接下来,我们来看 Redis 在持久化的过程中是如何处理过期键的。 Redis 持久化文件有两种格式:RDB(Redis Database)和 AOF(Append Only File),下面我们分别来看过期键在这两种格式中的呈现状态。 -#### **RDB 中的过期键** RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。 **1. RDB 文件生成** 从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键不会被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。 **2. RDB 文件加载** +#### **RDB 中的过期键** RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。**1. RDB 文件生成** 从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键不会被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。**2. RDB 文件加载** RDB 加载分为以下两种情况: @@ -322,7 +322,7 @@ if (server.masterhost == NULL && expiretime != -1 && expiretime < now) { } ``` -#### **AOF 中的过期键** **1. AOF 文件写入** 当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。 **2. AOF 重写** +#### **AOF 中的过期键** **1. AOF 文件写入** 当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。**2. AOF 重写** 执行 AOF 重写时,会对 Redis 中的键值对进行检查已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" index b0b912d9f..02d1c4334 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" @@ -114,7 +114,7 @@ long long getExpire(redisDb *db, robj *key) { Redis 默认每秒进行 10 次过期扫描,此配置可通过 Redis 的配置文件 redis.conf 进行配置,配置键为 hz 它的默认值是 `hz 10`。 -需要注意的是:Redis 每次扫描并不是遍历过期字典中的所有键,而是采用随机抽取判断并删除过期键的形式执行的。 **定期删除流程** +需要注意的是:Redis 每次扫描并不是遍历过期字典中的所有键,而是采用随机抽取判断并删除过期键的形式执行的。**定期删除流程** 1. 从过期字典中随机取出 20 个键; 1. 删除这 20 个键中过期的键; diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" index 476a0272b..255f2c5a8 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" @@ -119,7 +119,7 @@ unit 参数表示统计单位,它可以设置以下值: georadius key longitude latitude radius m|km|ft|mi \[WITHCOORD\] \[WITHDIST\] \[WITHHASH\] \[COUNT count\] \[ASC|DESC\] ``` -可选参数说明如下。 **1. WITHCOORD** 说明:返回满足条件位置的经纬度信息。 +可选参数说明如下。**1. WITHCOORD** 说明:返回满足条件位置的经纬度信息。 示例代码: @@ -202,7 +202,7 @@ georadius key longitude latitude radius m|km|ft|mi \[WITHCOORD\] \[WITHDIST\] \[ geohash key member [member ...] ``` -此命令支持查询一个或多个地址的哈希值。 **6. 删除地理位置** +此命令支持查询一个或多个地址的哈希值。**6. 删除地理位置** ```shell 127.0.0.1:6379> zrem site xiaoming diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" index c794715bc..f02ec068c 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" @@ -65,7 +65,7 @@ 从内测淘汰策略分类上,我们可以得知,除了随机删除和不删除之外,主要有两种淘汰算法:LRU 算法和 LFU 算法。 -#### **LRU 算法** LRU 全称是 Least Recently Used 译为最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 **1. LRU 算法实现** LRU 算法需要基于链表结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可。 **2. 近 LRU 算法** Redis 使用的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是给现有的数据结构添加一个额外的字段,用于记录此键值的最后一次访问时间,Redis 内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。 **3. LRU 算法缺点** LRU 算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么它就不会被淘汰,即使它是使用次数最少的缓存,那它也不会被淘汰,因此在 Redis 4.0 之后引入了 LFU 算法,下面我们一起来看 +#### **LRU 算法** LRU 全称是 Least Recently Used 译为最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。**1. LRU 算法实现** LRU 算法需要基于链表结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可。**2. 近 LRU 算法** Redis 使用的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是给现有的数据结构添加一个额外的字段,用于记录此键值的最后一次访问时间,Redis 内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。**3. LRU 算法缺点** LRU 算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么它就不会被淘汰,即使它是使用次数最少的缓存,那它也不会被淘汰,因此在 Redis 4.0 之后引入了 LFU 算法,下面我们一起来看 #### **LFU 算法** diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" index 537f70048..d28a0efcd 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" @@ -18,7 +18,7 @@ cd redisbloom make # 编译redisbloom ``` -编译正常执行完,会在根目录生成一个 redisbloom.so 文件。 **2. 启动 Redis 服务器** +编译正常执行完,会在根目录生成一个 redisbloom.so 文件。**2. 启动 Redis 服务器** ```plaintext > ./src/redis-server redis.conf --loadmodule ./src/modules/RedisBloom-master/redisbloom.so diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" index c6dd7b223..684377934 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" @@ -77,13 +77,13 @@ public class DelayTest { 2. 代码实现比较简单 **缺点** 1. 不支持持久化保存 2. 不支持分布式系统 -#### **MQ 实现方式** RabbitMQ 本身并不支持延迟队列,但可以通过添加插件 rabbitmq-delayed-message-exchange 来实现延迟队列。 **优点** 1. 支持分布式 +#### **MQ 实现方式** RabbitMQ 本身并不支持延迟队列,但可以通过添加插件 rabbitmq-delayed-message-exchange 来实现延迟队列。**优点** 1. 支持分布式 2. 支持持久化 **缺点** 框架比较重,需要搭建和配置 MQ。 -#### **Redis 实现方式** Redis 是通过有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。 **优点** 1. 灵活方便,Redis 是互联网公司的标配,无序额外搭建相关环境; +#### **Redis 实现方式** Redis 是通过有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。**优点** 1. 灵活方便,Redis 是互联网公司的标配,无序额外搭建相关环境; 2. 可进行消息持久化,大大提高了延迟队列的可靠性; 3. 分布式支持,不像 JDK 自身的 DelayQueue; -4. 高可用性,利用 Redis 本身高可用方案,增加了系统健壮性。 **缺点** 需要使用无限循环的方式来执行任务检查,会消耗少量的系统资源。 +4. 高可用性,利用 Redis 本身高可用方案,增加了系统健壮性。**缺点** 需要使用无限循环的方式来执行任务检查,会消耗少量的系统资源。 结合以上优缺点,我们决定使用 Redis 来实现延迟队列,具体实现代码如下。 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" index ad88476f9..b97c57ea6 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" @@ -2,7 +2,7 @@ 我在开发的时候曾经遇到了这样一个问题,产品要求给每个在线预约看病的患者,距离预约时间的前一天发送一条提醒推送,以防止患者错过看病的时间。这个时候就要求我们给每个人设置一个定时任务,用前面文章说的延迟队列也可以实现,但延迟队列的实现方式需要开启一个无限循环任务,那有没有其他的实现方式呢? -答案是肯定的,接下来我们就用 Keyspace Notifications(键空间通知)来实现定时任务, **定时任务指的是指定一个时间来执行某个任务,就叫做定时任务** 。 +答案是肯定的,接下来我们就用 Keyspace Notifications(键空间通知)来实现定时任务,**定时任务指的是指定一个时间来执行某个任务,就叫做定时任务** 。 ### 开启键空间通知 @@ -30,7 +30,7 @@ OK * 这种方式设置的配置信息是存储在内存中的,重启 Redis 服务之后,配置项会丢失。 -#### **配置文件设置方式** 找到 Redis 的配置文件 redis.conf,设置配置项 `notify-keyspace-events Ex`,然后重启 Redis 服务器。 **优点:** +#### **配置文件设置方式** 找到 Redis 的配置文件 redis.conf,设置配置项 `notify-keyspace-events Ex`,然后重启 Redis 服务器。**优点:** * 无论 Redis 服务器重启多少次,配置都不会丢失。 @@ -56,7 +56,7 @@ OK * e:驱逐(evict)事件,每当有键因为 maxmemory 政策而被删除时发送 * A:参数 g$lshzxe 的别名 -以上配置项可以自由组合,例如我们订阅列表事件就是 El,但需要注意的是, **如果 notify-keyspace-event 的值设置为空,则表示不开启任何通知,有值则表示开启通知** 。 +以上配置项可以自由组合,例如我们订阅列表事件就是 El,但需要注意的是,**如果 notify-keyspace-event 的值设置为空,则表示不开启任何通知,有值则表示开启通知** 。 ### 功能实现 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" index cfe8fdd8c..d38da3262 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" @@ -35,7 +35,7 @@ docker run -p 6379:6379 redislabs/redisearch:latest ![RediSearch安装成功.png](assets/29f9b8e0-7b3b-11ea-8940-6df1558f5aa2) -安装完成之后 **使用 redis-cli 来检查 RediSearch 模块是否加载成功** ,使用 Docker 启动 redis-cli,命令如下: +安装完成之后 **使用 redis-cli 来检查 RediSearch 模块是否加载成功**,使用 Docker 启动 redis-cli,命令如下: ```bash docker exec -it myredis redis-cli @@ -112,7 +112,7 @@ OK OK ``` -注意: **这里必须要设置语言编码为中文,也就是“language "chinese"”** ,默认是英文编码,如果不设置则无法支持中文查询(无法查出结果)。 +注意: **这里必须要设置语言编码为中文,也就是“language "chinese"”**,默认是英文编码,如果不设置则无法支持中文查询(无法查出结果)。 我们使用之前的查询方式,命令如下: @@ -400,7 +400,7 @@ public class RediSearchExample { ### 小结 -本文我们使用 Docker 和 源码编译的方式成功的启动了 RediSearch 功能,要使用 RediSearch 的全文搜索功能,必须先要创建一个索引,然后再索引中添加数据,再使用 ft.search 命令进行全文搜索,如果要查询中文内容的话,需要在添加数据时设置中文编码,并且在查询时也要设置中文编码,指定“language "chinese"”。 **参考 & 鸣谢** +本文我们使用 Docker 和 源码编译的方式成功的启动了 RediSearch 功能,要使用 RediSearch 的全文搜索功能,必须先要创建一个索引,然后再索引中添加数据,再使用 ft.search 命令进行全文搜索,如果要查询中文内容的话,需要在添加数据时设置中文编码,并且在查询时也要设置中文编码,指定“language "chinese"”。**参考 & 鸣谢** 官网地址: diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" index 0d762dd18..232efa30e 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" @@ -233,7 +233,7 @@ Redis 过期键值删除使用的是贪心策略,它每秒会进行 10 次过 ### 限制 Redis 内存大小 -在 64 位操作系统中 Redis 的内存大小是没有限制的,也就是配置项 `maxmemory ` 是被注释掉的,这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操心系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此我们需要限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内存淘汰策略, **内存淘汰策略在 Redis 4.0 之后有 8 种** : +在 64 位操作系统中 Redis 的内存大小是没有限制的,也就是配置项 `maxmemory ` 是被注释掉的,这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操心系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此我们需要限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内存淘汰策略,**内存淘汰策略在 Redis 4.0 之后有 8 种** : - **noeviction** :不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略; - **allkeys-lru** :淘汰整个键值中最久未使用的键值; diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" index 871efa62e..c8534ba9e 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" @@ -34,7 +34,7 @@ OK OK ``` -**1. 执行流程** 在执行完 replicaof 命令之后,从服务器的数据会被清空,主服务会把它的数据副本同步给从服务器。 **2. 测试同步功能** 主从服务器设置完同步之后,我们来测试一下主从数据同步,首先我们先在主服务器上执行保存数据操作,再去从服务器查询。 +**1. 执行流程** 在执行完 replicaof 命令之后,从服务器的数据会被清空,主服务会把它的数据副本同步给从服务器。**2. 测试同步功能** 主从服务器设置完同步之后,我们来测试一下主从数据同步,首先我们先在主服务器上执行保存数据操作,再去从服务器查询。 主服务器执行命令: diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" index 8ed2b392e..6b05cd8ef 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" @@ -129,7 +129,7 @@ Stopping 30006 ![image.png](assets/f7eb6e40-8579-11ea-a9ff-29e53f17413b) -接下来我们进行配置并启动 Redis 集群。 **1. 设置配置文件** 我们需要修改每个节点内的 redis.conf 文件,设置 `cluster-enabled yes` 表示开启集群模式,并且修改各自的端口,我们继续使用 30001 到 30006,通过 `port 3000X` 设置。 **2. 启动各个节点** redis.conf 配置好之后,我们就可以启动所有的节点了,命令如下: +接下来我们进行配置并启动 Redis 集群。**1. 设置配置文件** 我们需要修改每个节点内的 redis.conf 文件,设置 `cluster-enabled yes` 表示开启集群模式,并且修改各自的端口,我们继续使用 30001 到 30006,通过 `port 3000X` 设置。**2. 启动各个节点** redis.conf 配置好之后,我们就可以启动所有的节点了,命令如下: ```shell cd /usr/local/soft/mycluster/node1 @@ -251,7 +251,7 @@ abec9f98f9c01208ba77346959bc35e8e274b6a3 127.0.0.1:[email protected] slave 8873 1a324d828430f61be6eaca7eb2a90728dd5049de 127.0.0.1:[email protected] slave f5958382af41d4e1f5b0217c1413fe19f390b55f 0 1585142916000 4 connected ``` -可以看出端口为 30007 的节点并加入到集群中,并设置成了主节点。 **添加方式二:add-node** 使用 `redis-cli --cluster add-node 添加节点ip:port 集群某节点ip:port` 也可以把一个节点添加到集群中,执行命令如下: +可以看出端口为 30007 的节点并加入到集群中,并设置成了主节点。**添加方式二:add-node** 使用 `redis-cli --cluster add-node 添加节点ip:port 集群某节点ip:port` 也可以把一个节点添加到集群中,执行命令如下: ```shell redis-cli --cluster add-node 127.0.0.1:30008 127.0.0.1:30001 diff --git "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" index 00df3ccc9..8e9ebf702 100644 --- "a/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" +++ "b/docs/Database/Redis \346\240\270\345\277\203\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" @@ -4,25 +4,25 @@ ### RedisClient -**是否收费** :免费。 **项目介绍** :Java 编写的 Redis 连接客户端,功能丰富,并且是免费的。 **支持平台** :Windows。 **项目地址** :[https://github.com/caoxinyu/RedisClient](https://github.com/caoxinyu/RedisClient) +**是否收费** :免费。**项目介绍** :Java 编写的 Redis 连接客户端,功能丰富,并且是免费的。**支持平台** :Windows。**项目地址** :[https://github.com/caoxinyu/RedisClient](https://github.com/caoxinyu/RedisClient) 使用截图: ![image.png](assets/0e749a90-8ee1-11ea-9776-f5261045ba7d) -### Redis Desktop Manager **是否收费** :收费。 **项目介绍** :一款基于 Qt5 的跨平台 Redis 桌面管理软件。 **支持平台** :Windows、macOS、Linux。 **项目地址** :[https://github.com/uglide/RedisDesktopManager](https://github.com/uglide/RedisDesktopManager) +### Redis Desktop Manager **是否收费** :收费。**项目介绍** :一款基于 Qt5 的跨平台 Redis 桌面管理软件。**支持平台** :Windows、macOS、Linux。**项目地址** :[https://github.com/uglide/RedisDesktopManager](https://github.com/uglide/RedisDesktopManager) 使用截图: ![image.png](assets/39fb7cb0-8ee1-11ea-861a-9398d62a6944) -### RedisStudio **是否收费** :免费。 **项目介绍** :一款 C++ 编写的 Redis 管理工具,比较老,好久没更新了。 **支持平台** :Windows。 **项目地址** :[https://github.com/cinience/RedisStudio](https://github.com/cinience/RedisStudio) +### RedisStudio **是否收费** :免费。**项目介绍** :一款 C++ 编写的 Redis 管理工具,比较老,好久没更新了。**支持平台** :Windows。**项目地址** :[https://github.com/cinience/RedisStudio](https://github.com/cinience/RedisStudio) 使用截图: ![image.png](assets/4c11eb50-8ee1-11ea-bf74-150f7ff6235d) -### AnotherRedisDesktopManager **是否收费** :免费。 **项目介绍** :一款基于 Node.js 开发的 Redis 桌面管理器,它的特点就是相对来说比较稳定,在数据量比较大的时候不会崩溃。 **支持平台** :Windows、macOS、Linux。 **项目地址** :[https://github.com/qishibo/AnotherRedisDesktopManager](https://github.com/qishibo/AnotherRedisDesktopManager) +### AnotherRedisDesktopManager **是否收费** :免费。**项目介绍** :一款基于 Node.js 开发的 Redis 桌面管理器,它的特点就是相对来说比较稳定,在数据量比较大的时候不会崩溃。**支持平台** :Windows、macOS、Linux。**项目地址** :[https://github.com/qishibo/AnotherRedisDesktopManager](https://github.com/qishibo/AnotherRedisDesktopManager) 使用截图: diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25400\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25400\350\256\262.md" index b5897ce15..75c81fbf7 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25400\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25400\350\256\262.md" @@ -17,7 +17,7 @@ MyBatis 是 Java 生态中非常著名的一款 ORM 框架,也是我们此次 结合工作实践来讲,MyBatis 所具备的亮点可总结为如下三个方面。 -**第一,MyBatis 本身就是一款设计非常精良、架构设计非常清晰的持久层框架,并且 MyBatis 中还使用到了很多经典的设计模式,例如,工厂方法模式、适配器模式、装饰器模式、代理模式等。** 在阅读 MyBatis 代码的时候,你也许会惊奇地发现:原来大师设计出来的代码真的是一种艺术。所以,从这个层面来讲,深入研究 MyBatis 原理,甚至阅读它的源码,不仅可以帮助你快速解决工作中遇到的 MyBatis 相关问题,还可以提高你的设计思维。 **第二,MyBatis 提供了很多扩展点,例如,MyBatis 的插件机制、对第三方日志框架和第三方数据源的兼容等。** 正由于这种可扩展的能力,让 MyBatis 的生命力非常旺盛,这也是很多 Java 开发人员将 MyBatis 作为自己首选 Java 持久化框架的原因之一,反过来促进了 MyBatis 用户的不断壮大。 **第三,开发人员使用 MyBatis 上手会非常快,具有很强的易用性和可靠性** 。这也是 MyBatis 流行的一个很重要的原因。当你具备了 MySQL 和 JDBC 的基础知识之后,学习 MyBatis 的难度远远小于 Hibernate 等持久化框架。 +**第一,MyBatis 本身就是一款设计非常精良、架构设计非常清晰的持久层框架,并且 MyBatis 中还使用到了很多经典的设计模式,例如,工厂方法模式、适配器模式、装饰器模式、代理模式等。** 在阅读 MyBatis 代码的时候,你也许会惊奇地发现:原来大师设计出来的代码真的是一种艺术。所以,从这个层面来讲,深入研究 MyBatis 原理,甚至阅读它的源码,不仅可以帮助你快速解决工作中遇到的 MyBatis 相关问题,还可以提高你的设计思维。**第二,MyBatis 提供了很多扩展点,例如,MyBatis 的插件机制、对第三方日志框架和第三方数据源的兼容等。** 正由于这种可扩展的能力,让 MyBatis 的生命力非常旺盛,这也是很多 Java 开发人员将 MyBatis 作为自己首选 Java 持久化框架的原因之一,反过来促进了 MyBatis 用户的不断壮大。**第三,开发人员使用 MyBatis 上手会非常快,具有很强的易用性和可靠性** 。这也是 MyBatis 流行的一个很重要的原因。当你具备了 MySQL 和 JDBC 的基础知识之后,学习 MyBatis 的难度远远小于 Hibernate 等持久化框架。 例如,你在 MyBatis 中编写的是原生的 SQL 语句,随着业务发展和变化,SQL 语句也会变得复杂,拆分和优化 SQL 是非常重要的提高系统性能的手段,这个时候你只要了解 SQL 本身的优化即可;而使用 Hibernate、EclipseLink 等框架的时候,还需要了解 HQL、JPQL 以及 Criteria API 生成原生 SQL 的机制。相较之下,MyBatis 会更加容易一些。这一优势对于很多互联网公司和软件企业来说,是非常有诱惑力的,毕竟企业可以在保证软件质量的前提下,快速培养出能够在一线工作的员工。 @@ -33,7 +33,7 @@ MyBatis 是 Java 生态中非常著名的一款 ORM 框架,也是我们此次 (职位信息来源:拉勾网) -作为一名 Java 工程师, **深入掌握一款持久化框架已经是一项必备技能** ,并且成为个人职场竞争力的关键项。拉勾网显示,研发工程师、架构师等高薪岗位,都要求你熟悉并曾经深入使用过某种持久化框架,其中以 MyBatis 居多,“熟悉 MyBatis” 或是“精通 MyBatis” 等字眼更是频繁出现在岗位职责中。 +作为一名 Java 工程师,**深入掌握一款持久化框架已经是一项必备技能**,并且成为个人职场竞争力的关键项。拉勾网显示,研发工程师、架构师等高薪岗位,都要求你熟悉并曾经深入使用过某种持久化框架,其中以 MyBatis 居多,“熟悉 MyBatis” 或是“精通 MyBatis” 等字眼更是频繁出现在岗位职责中。 所以说,如果你想要进入一线大厂,能够熟练使用 MyBatis 开发已经是一项非常基本的技能,同时大厂也更希望自己的开发人员深入了解 MyBatis 框架的原理和核心实现。 @@ -50,7 +50,7 @@ MyBatis 是 Java 生态中非常著名的一款 ORM 框架,也是我们此次 另外,结合我自己多年的工作经验来看,如果你跟面试官聊到 MyBatis 的内容,一般来说,面试官大概率不会期待听到“如何搭建 MyBatis 开发环境”“如何写配置文件”或是“如何调用 MyBatis 的API”这些很琐碎的话题。面试官其实更想听的是面试者“对 MyBatis 运行原理的理解”“对 MyBatis 整个框架的把握”“在实践过程中踩到的坑以及如何通过对 MyBatis 的理解解决问题”等话题,这些话题才能体现出个人的技术深度。 -从这个角度看, **阅读 MyBatis 源码、理解 MyBatis 原理,已经成为你掌握 MyBatis 精髓和提高职场竞争力的关键,也是进入一线大厂的必备技能** 。 +从这个角度看,**阅读 MyBatis 源码、理解 MyBatis 原理,已经成为你掌握 MyBatis 精髓和提高职场竞争力的关键,也是进入一线大厂的必备技能** 。 ### 为什么会有这门课 @@ -76,14 +76,14 @@ MyBatis 是 Java 生态中非常著名的一款 ORM 框架,也是我们此次 ### 这门课的核心内容是什么 -正是因为深刻了解到很多开发人员在学习过程中可能会碰到资料不全、无人指路、架构经验各不相同等一系列问题,再加上我曾经分享过各种开源项目的源码分析资料,并且收到大家的一致好评,所以我决定和“拉勾教育”合作,开设一个系列课程,根据自己丰富的开源项目分析经验,来带你一起 **分析 MyBatis 源码** 、 **拆解 MyBatis 架构** ,希望 **帮你理清 MyBatis 的底层原理、深刻理解 MyBatis 的架构设计** 。 +正是因为深刻了解到很多开发人员在学习过程中可能会碰到资料不全、无人指路、架构经验各不相同等一系列问题,再加上我曾经分享过各种开源项目的源码分析资料,并且收到大家的一致好评,所以我决定和“拉勾教育”合作,开设一个系列课程,根据自己丰富的开源项目分析经验,来带你一起 **分析 MyBatis 源码** 、 **拆解 MyBatis 架构**,希望 **帮你理清 MyBatis 的底层原理、深刻理解 MyBatis 的架构设计** 。 具体来说,我是从以下四个层面来设计这门课程的。 -- 从基础知识开始,通过 **一个订票系统持久层的 Demo 演示** ,手把手带你快速上手 MyBatis 的基础使用。之后在此基础上,再带你了解 MyBatis 框架的整体三层架构,并介绍 MyBatis 中各个模块的核心功能,为后面的分析打好基础。 -- 带你 **自底向上剖析 MyBatis 的核心源码实现** ,深入理解 MyBatis 基础模块的工作原理及核心实现,让你不再停留在简单使用 MyBatis 的阶段,做到知其然,也知其所以然。 +- 从基础知识开始,通过 **一个订票系统持久层的 Demo 演示**,手把手带你快速上手 MyBatis 的基础使用。之后在此基础上,再带你了解 MyBatis 框架的整体三层架构,并介绍 MyBatis 中各个模块的核心功能,为后面的分析打好基础。 +- 带你 **自底向上剖析 MyBatis 的核心源码实现**,深入理解 MyBatis 基础模块的工作原理及核心实现,让你不再停留在简单使用 MyBatis 的阶段,做到知其然,也知其所以然。 - 在介绍源码实现的过程中,还会 **穿插设计模式** 的相关知识点,带领你了解设计模式的优秀实践方式,让你深刻 **体会优秀架构设计的美感** 。这样在你进行架构设计以及代码编写的时候,就可以真正使用这些设计模式,进而让你的代码扩展性更强、可维护性更好。 -- 还会带领你 **了解 MyBatis 周边的扩展** ,帮助你打开视野,让你不仅能够学到 MyBatis 本身的原理和设计,还会了解到 MyBatis 与 Spring 集成的底层原理、MyBatis 插件扩展的精髓,以及 MyBatis 衍生生态的魅力。 +- 还会带领你 **了解 MyBatis 周边的扩展**,帮助你打开视野,让你不仅能够学到 MyBatis 本身的原理和设计,还会了解到 MyBatis 与 Spring 集成的底层原理、MyBatis 插件扩展的精髓,以及 MyBatis 衍生生态的魅力。 这里需要说明的是,本课程涉及的 MyBatis 源码以及示例代码,我将会在 GitHub 上随课程推进不断更新,链接是:[https://github.com/xxxlxy2008/mybatis](https://github.com/xxxlxy2008/mybatis)。 diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25401\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25401\350\256\262.md" index 37c234f07..5c5a754d7 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25401\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25401\350\256\262.md" @@ -1,6 +1,6 @@ # 01 常见持久层框架赏析,到底是什么让你选择 MyBatis? -**在绝大多数在线应用场景中,数据是存储在关系型数据库中的** ,当然,有特殊要求的场景中,我们也会将其他持久化存储(如 ElasticSearch、HBase、MongoDB 等)作为辅助存储。但不可否认的是,关系型数据库凭借几十年的发展、生态积累、众多成功的案例,依然是互联网企业的核心存储。 +**在绝大多数在线应用场景中,数据是存储在关系型数据库中的**,当然,有特殊要求的场景中,我们也会将其他持久化存储(如 ElasticSearch、HBase、MongoDB 等)作为辅助存储。但不可否认的是,关系型数据库凭借几十年的发展、生态积累、众多成功的案例,依然是互联网企业的核心存储。 作为一个 Java 开发者,几乎天天与关系型数据库打交道,在生产环境中常用的关系型数据库产品有 SQL Server、MySQL、Oracle 等。在使用这些数据库产品的时候,基本上是如下思路: @@ -21,7 +21,7 @@ 1. 遍历 ResultSet,从结果集中读取数据,并将每一行数据库记录转换成一个 JavaBean 对象; 1. 关闭 ResultSet 结果集、Statement 对象及数据库 Connection,从而释放这些对象占用的底层资源。 -无论是执行查询操作,还是执行其他 DML 操作,1、2、3、4、6 这些步骤都会重复出现。为了简化重复逻辑,提高代码的可维护性,可以 **将上述重复逻辑封装到一个类似 DBUtils 的工具类中** ,在使用时只需要调用 DBUtils 工具类中的方法即可。当然,我们也可以 **使用“反射+配置”的方式,将步骤 5 中关系模型到对象模型的转换进行封装** ,但是这种封装要做到通用化且兼顾灵活性,就需要一定的编程功底。 +无论是执行查询操作,还是执行其他 DML 操作,1、2、3、4、6 这些步骤都会重复出现。为了简化重复逻辑,提高代码的可维护性,可以 **将上述重复逻辑封装到一个类似 DBUtils 的工具类中**,在使用时只需要调用 DBUtils 工具类中的方法即可。当然,我们也可以 **使用“反射+配置”的方式,将步骤 5 中关系模型到对象模型的转换进行封装**,但是这种封装要做到通用化且兼顾灵活性,就需要一定的编程功底。 为了处理上述代码重复的问题以及后续的维护问题,我们在实践中会进行一系列评估,选择一款适合项目需求、符合人员能力的 ORM(Object Relational Mapping,对象-关系映射)框架来封装 1~6 步的重复性代码,实现对象模型、关系模型之间的转换。这正是 **ORM 框架的核心功能:根据配置(配置文件或是注解)实现对象模型、关系模型两者之间无感知的映射** (如下图)。 @@ -45,7 +45,7 @@ Hibernate 是 Java 生态中著名的 ORM 框架之一。Hibernate 现在也在 这里我们要重点讲解的是 Hibernate ORM 的相关内容,截至 2020 年底,Hibernate ORM 的最新版本是 5.4 版本,6.0 版本还正在开发中。作为一个老牌的 ORM 框架,Hibernate 经受住了 Java EE 企业级应用的考验,一度成为 Java ORM 领域的首选框架。 -在使用 Hibernate 的时候,Java 开发可以使用映射文件或是注解定义 Java 语言中的类与数据库中的表之间的各种映射关系,这里使用到的映射文件后缀为“.hbm.xml”。hbm.xml 映射文件将一张数据库表与一个 Java 类进行关联之后,该数据库表中的每一行记录都可以被转换成对应的一个 Java 对象。 **正是由于 Hibernate 映射的存在,Java 开发只需要使用面向对象思维就可以完成数据库表的设计。** 在 Java 这种纯面向对象的语言中,两个 Java 对象之间可能存在一对一、一对多或多对多等复杂关联关系。Hibernate 中的映射文件也必须要能够表达这种复杂关联关系才能够满足我们的需求,同时,还要能够将这种关联关系与数据库中的关联表、外键等一系列关系模型中的概念进行映射,这也就是 ORM 框架中常提到的“ **关联映射** ”。 +在使用 Hibernate 的时候,Java 开发可以使用映射文件或是注解定义 Java 语言中的类与数据库中的表之间的各种映射关系,这里使用到的映射文件后缀为“.hbm.xml”。hbm.xml 映射文件将一张数据库表与一个 Java 类进行关联之后,该数据库表中的每一行记录都可以被转换成对应的一个 Java 对象。**正是由于 Hibernate 映射的存在,Java 开发只需要使用面向对象思维就可以完成数据库表的设计。** 在 Java 这种纯面向对象的语言中,两个 Java 对象之间可能存在一对一、一对多或多对多等复杂关联关系。Hibernate 中的映射文件也必须要能够表达这种复杂关联关系才能够满足我们的需求,同时,还要能够将这种关联关系与数据库中的关联表、外键等一系列关系模型中的概念进行映射,这也就是 ORM 框架中常提到的“ **关联映射** ”。 下面我们就来结合示例介绍“一对多”关联关系。例如,一个顾客(Customer)可以创建多个订单(Order),而一个订单(Order)只属于一个顾客(Customer),两者之间存在一对多的关系。在 Java 程序中,可以在 Customer 类中添加一个 List 类型的字段来维护这种一对多的关系;在数据库中,可以在订单表(t_order)中添加一个 customer_id 列作为外键,指向顾客表(t_customer)的主键 id,从而维护这种一对多的关系,如下图所示: @@ -91,9 +91,9 @@ Hibernate 是 Java 生态中著名的 ORM 框架之一。Hibernate 现在也在 ``` -一对一、多对多等关联映射在 Hibernate 映射文件中,都定义了相应的 XML 标签,原理与“一对多”基本一致,只是使用方式和场景略有不同,这里就不再展开介绍,你若感兴趣的话可以参考 [Hibernate 的官方文档](https://hibernate.org/orm/documentation/5.4/)进行学习。 **除了能够完成面向对象模型与数据库中关系模型的映射,Hibernate 还可以帮助我们屏蔽不同数据库产品中 SQL 语句的差异。** 我们知道,虽然目前有 SQL 标准,但是不同的关系型数据库产品对 SQL 标准的支持有细微不同,这就会出现一些非常尴尬的情况,例如,一条 SQL 语句在 MySQL 上可以正常执行,而在 Oracle 数据库上执行会报错。 +一对一、多对多等关联映射在 Hibernate 映射文件中,都定义了相应的 XML 标签,原理与“一对多”基本一致,只是使用方式和场景略有不同,这里就不再展开介绍,你若感兴趣的话可以参考 [Hibernate 的官方文档](https://hibernate.org/orm/documentation/5.4/)进行学习。**除了能够完成面向对象模型与数据库中关系模型的映射,Hibernate 还可以帮助我们屏蔽不同数据库产品中 SQL 语句的差异。** 我们知道,虽然目前有 SQL 标准,但是不同的关系型数据库产品对 SQL 标准的支持有细微不同,这就会出现一些非常尴尬的情况,例如,一条 SQL 语句在 MySQL 上可以正常执行,而在 Oracle 数据库上执行会报错。 -Hibernate **封装了数据库层面的全部操作** ,Java 程序员不再需要直接编写 SQL 语句,只需要使用 Hibernate 提供的 API 即可完成数据库操作。 +Hibernate **封装了数据库层面的全部操作**,Java 程序员不再需要直接编写 SQL 语句,只需要使用 Hibernate 提供的 API 即可完成数据库操作。 例如,Hibernate 为用户提供的 Criteria 是一套灵活的、可扩展的数据操纵 API,最重要的是 Criteria 是一套面向对象的 API,使用它操作数据库的时候,Java 开发者只需要关注 Criteria 这套 API 以及返回的 Java 对象,不需要考虑数据库底层如何实现、SQL 语句如何编写,等等。 @@ -110,7 +110,7 @@ List list = criteria.add(Restrictions.like("name","yang%")) 除了 Criteria API 之外,Hibernate 还提供了一套面向对象的查询语言—— HQL(Hibernate Query Language)。从语句的结构上来看,HQL 语句与 SQL 语句十分类似,但这二者也是有区别的: **HQL 是面向对象的查询语言,而 SQL 是面向关系型的查询语言** 。 -在实现复杂数据库操作的时候,我们可以使用 HQL 这种面向对象的查询语句来实现,Hibernate 的 HQL 引擎会根据底层使用的数据库产品,将 HQL 语句转换成合法的 SQL 语句。 **Hibernate 通过其简洁的 API 以及统一的 HQL 语句,帮助上层程序屏蔽掉底层数据库的差异,增强了程序的可移植性。** +在实现复杂数据库操作的时候,我们可以使用 HQL 这种面向对象的查询语句来实现,Hibernate 的 HQL 引擎会根据底层使用的数据库产品,将 HQL 语句转换成合法的 SQL 语句。**Hibernate 通过其简洁的 API 以及统一的 HQL 语句,帮助上层程序屏蔽掉底层数据库的差异,增强了程序的可移植性。** 另外,Hibernate 还具有如下的一些其他优点: @@ -119,7 +119,7 @@ List list = criteria.add(Restrictions.like("name","yang%")) - Hibernate 提供了延迟加载的功能,可以避免无效查询; - Hibernate 还提供了由对象模型自动生成数据库表的逆向操作。 -**但需要注意的是,Hibernate 并不是一颗“银弹”** ,我们无法在面向对象模型中找到数据库中所有概念的映射,例如,索引、函数、存储过程等。在享受 Hibernate 带来便捷的同时,我们还需要忍受它的一些缺点。例如,索引对提升数据库查询性能有很大帮助,我们建立索引并适当优化 SQL 语句,就会让数据库使用合适的索引提高整个查询的速度。但是,我们很难修改 Hibernate 生成的 SQL 语句。为什么这么说呢?因为在一些场景中,数据库设计非常复杂,表与表之间的关系错综复杂,Hibernate 引擎生成的 SQL 语句会非常难以理解,要让生成的 SQL 语句使用正确的索引更是难上加难,这就很容易生成慢查询 SQL。 +**但需要注意的是,Hibernate 并不是一颗“银弹”**,我们无法在面向对象模型中找到数据库中所有概念的映射,例如,索引、函数、存储过程等。在享受 Hibernate 带来便捷的同时,我们还需要忍受它的一些缺点。例如,索引对提升数据库查询性能有很大帮助,我们建立索引并适当优化 SQL 语句,就会让数据库使用合适的索引提高整个查询的速度。但是,我们很难修改 Hibernate 生成的 SQL 语句。为什么这么说呢?因为在一些场景中,数据库设计非常复杂,表与表之间的关系错综复杂,Hibernate 引擎生成的 SQL 语句会非常难以理解,要让生成的 SQL 语句使用正确的索引更是难上加难,这就很容易生成慢查询 SQL。 另外,在一些大数据量、高并发、低延迟的场景中,Hibernate 在性能方面带来的损失就会逐渐显现出来。 @@ -129,7 +129,7 @@ List list = criteria.add(Restrictions.like("name","yang%")) 在开始介绍 Spring Data JPA 之前,我们先要来介绍一下 JPA(Java Persistence API)规范。 -JPA 是在 JDK 5.0 后提出的 Java 持久化规范(JSR 338)。 **JPA 规范本身是为了整合市面上已有的 ORM 框架** ,结束 Hibernate、EclipseLink、JDO 等 ORM 框架各自为战的割裂局面,简化 Java 持久层开发。 +JPA 是在 JDK 5.0 后提出的 Java 持久化规范(JSR 338)。**JPA 规范本身是为了整合市面上已有的 ORM 框架**,结束 Hibernate、EclipseLink、JDO 等 ORM 框架各自为战的割裂局面,简化 Java 持久层开发。 JPA 规范从现有的 ORM 框架中借鉴了很多优点,例如,Gavin King 作为 Hibernate 创始人,同时也参与了 JPA 规范的编写,所以在 JPA 规范中可以看到很多与 Hibernate 类似的概念和设计。 @@ -141,13 +141,13 @@ JPA 生态图 JPA 有三个核心部分:ORM 映射元数据、操作实体对象 API 和面向对象的查询语言(JPQL)。这与 Hibernate 的核心功能基本类似,就不再重复讲述。 -Java 开发者应该都知道“Spring 全家桶”的强大,Spring 目前已经成为事实上的标准了,很少有企业会完全离开 Spring 来开发 Java 程序。现在的 Spring 已经不仅仅是最早的 IoC 容器了,而是整个 Spring 生态,例如,Spring Cloud、Spring Boot、Spring Security 等,其中就包含了 Spring Data。 **Spring Data 是 Spring 在持久化方面做的一系列扩展和整合** ,下图就展示了 Spring Data 中的子项目: +Java 开发者应该都知道“Spring 全家桶”的强大,Spring 目前已经成为事实上的标准了,很少有企业会完全离开 Spring 来开发 Java 程序。现在的 Spring 已经不仅仅是最早的 IoC 容器了,而是整个 Spring 生态,例如,Spring Cloud、Spring Boot、Spring Security 等,其中就包含了 Spring Data。**Spring Data 是 Spring 在持久化方面做的一系列扩展和整合**,下图就展示了 Spring Data 中的子项目: ![Drawing 3.png](assets/CgqCHmAJKq6AZEwjAAHdDlc6RI0325.png) Spring Data 生态图 -Spring Data 中的每个子项目都对应一个持久化存储,通过不断的整合接入各种持久化存储的能力,Spring 的生态又向前迈进了一大步,其中最常被大家用到的应该就是 Spring Data JPA。 **Spring Data JPA 是符合 JPA 规范的一个 Repository 层的实现** ,其所在的位置如下图所示: +Spring Data 中的每个子项目都对应一个持久化存储,通过不断的整合接入各种持久化存储的能力,Spring 的生态又向前迈进了一大步,其中最常被大家用到的应该就是 Spring Data JPA。**Spring Data JPA 是符合 JPA 规范的一个 Repository 层的实现**,其所在的位置如下图所示: ![Drawing 4.png](assets/CgqCHmAJKraAbIoyAAEm9GmJgx4010.png) @@ -159,9 +159,9 @@ Spring Data JPA 生态图 在这一讲的最后,结合上述两个 ORM 框架的知识点,我们再来介绍一下本课程的主角—— MyBatis。 -Apache 基金会中的 iBatis 项目是 MyBatis 的前身。iBatis 项目由于各种原因,在 Apache 基金会并没有得到很好的发展,最终于 2010 年脱离 Apache,并更名为 MyBatis。三年后,也就是 2013 年,MyBatis 将源代码迁移到了 [GitHub](https://github.com/mybatis)。 **MyBatis 中一个重要的功能就是可以帮助 Java 开发封装重复性的 JDBC 代码** ,这与前文分析的 Spring Data JPA 、Hibernate 等 ORM 框架一样。MyBatis 封装重复性代码的方式是通过 Mapper 映射配置文件以及相关注解,将 ResultSet 结果映射为 Java 对象,在具体的映射规则中可以嵌套其他映射规则和必要的子查询,这样就可以轻松实现复杂映射的逻辑,当然,也能够实现一对一、一对多、多对多关系映射以及相应的双向关系映射。 +Apache 基金会中的 iBatis 项目是 MyBatis 的前身。iBatis 项目由于各种原因,在 Apache 基金会并没有得到很好的发展,最终于 2010 年脱离 Apache,并更名为 MyBatis。三年后,也就是 2013 年,MyBatis 将源代码迁移到了 [GitHub](https://github.com/mybatis)。**MyBatis 中一个重要的功能就是可以帮助 Java 开发封装重复性的 JDBC 代码**,这与前文分析的 Spring Data JPA 、Hibernate 等 ORM 框架一样。MyBatis 封装重复性代码的方式是通过 Mapper 映射配置文件以及相关注解,将 ResultSet 结果映射为 Java 对象,在具体的映射规则中可以嵌套其他映射规则和必要的子查询,这样就可以轻松实现复杂映射的逻辑,当然,也能够实现一对一、一对多、多对多关系映射以及相应的双向关系映射。 -很多人会将 Hibernate 和 MyBatis 做比较,认为 Hibernate 是全自动 ORM 框架,而 MyBatis 只是半自动的 ORM 框架或是一个 SQL 模板引擎。其实,这些比较都无法完全说明一个框架比另一个框架先进,关键还是看应用场景。 **MyBatis 相较于 Hibernate 和各类 JPA 实现框架更加灵活、更加轻量级、更加可控** 。 +很多人会将 Hibernate 和 MyBatis 做比较,认为 Hibernate 是全自动 ORM 框架,而 MyBatis 只是半自动的 ORM 框架或是一个 SQL 模板引擎。其实,这些比较都无法完全说明一个框架比另一个框架先进,关键还是看应用场景。**MyBatis 相较于 Hibernate 和各类 JPA 实现框架更加灵活、更加轻量级、更加可控** 。 - 我们可以在 MyBatis 的 Mapper 映射文件中,直接编写原生的 SQL 语句,应用底层数据库产品的方言,这就给了我们直接优化 SQL 语句的机会; - 我们还可以按照数据库的使用规则,让原生 SQL 语句选择我们期望的索引,从而保证服务的性能,这就 **特别适合大数据量、高并发等需要将 SQL 优化到极致的场景** ; @@ -169,7 +169,7 @@ Apache 基金会中的 iBatis 项目是 MyBatis 的前身。iBatis 项目由于 在实际业务中,对同一数据集的查询条件可能是动态变化的,如果你有使用 JDBC 或其他类似框架的经历应该能体会到,拼接 SQL 语句字符串是一件非常麻烦的事情,尤其是条件复杂的场景中,拼接过程要特别小心,要确保在合适的位置添加“where”“and”“in”等 SQL 语句的关键字以及空格、逗号、等号等分隔符,而且这个拼接过程非常枯燥、没有技术含量,可能经过反复调试才能得到一个可执行的 SQL 语句。 -**MyBatis 提供了强大的动态 SQL 功能来帮助我们开发者摆脱这种重复劳动** ,我们只需要在映射配置文件中编写好动态 SQL 语句,MyBatis 就可以根据执行时传入的实际参数值拼凑出完整的、可执行的 SQL 语句。 +**MyBatis 提供了强大的动态 SQL 功能来帮助我们开发者摆脱这种重复劳动**,我们只需要在映射配置文件中编写好动态 SQL 语句,MyBatis 就可以根据执行时传入的实际参数值拼凑出完整的、可执行的 SQL 语句。 ### 总结 diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25402\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25402\350\256\262.md" index 62d30432f..f580ac321 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25402\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25402\350\256\262.md" @@ -9,7 +9,7 @@ - 随后介绍订单系统的 DAO 接口层,DAO 接口层是操作数据的最小化单元,也是读写数据库的地基; - 最后再简单提供了一个 Service 层和测试用例,用来检测前面的代码实现是否能正常工作。 -现在 **几乎所有的 Java 工程都会使用 Maven 来管理 jar 包依赖** ,所以我们首先创建一个 Maven 项目,然后在 pom.xml 中添加如下 jar 依赖,这些 jar 包都是订单示例系统必不可少的依赖: +现在 **几乎所有的 Java 工程都会使用 Maven 来管理 jar 包依赖**,所以我们首先创建一个 Maven 项目,然后在 pom.xml 中添加如下 jar 依赖,这些 jar 包都是订单示例系统必不可少的依赖: ```java @@ -43,7 +43,7 @@ ### domain 设计 -在业务系统的开发中, **domain 层的主要目的就是将业务上的概念抽象成面向对象模型中的类** ,这些类是业务系统运作的基础。在我们的简易订单系统中,有用户、地址、订单、订单条目和商品这五个核心的概念。 +在业务系统的开发中,**domain 层的主要目的就是将业务上的概念抽象成面向对象模型中的类**,这些类是业务系统运作的基础。在我们的简易订单系统中,有用户、地址、订单、订单条目和商品这五个核心的概念。 订单系统中 domain 层的设计,如下图所示: @@ -51,11 +51,11 @@ 简易订单系统 domain 层设计图 -在上图中, **Customer 类抽象的是电商平台中的用户** ,其中记录了用户的唯一标识(id 字段)、姓名(name 字段)以及手机号(phone 字段),另外,还记录了当前用户添加的全部送货地址。 +在上图中,**Customer 类抽象的是电商平台中的用户**,其中记录了用户的唯一标识(id 字段)、姓名(name 字段)以及手机号(phone 字段),另外,还记录了当前用户添加的全部送货地址。 -**Address 类抽象了用户的送货地址** ,其中记录了街道(street 字段)、城市(city 字段)、国家(country 字段)等信息,还维护了一个 Customer 类型的引用,指向所属的用户。 **Order 类抽象的是电商平台中的订单** ,记录了订单的唯一标识(id 字段)、订单创建时间(createTime 字段),其中通过 customer 字段(Customer 类型)指向了订单关联的用户,通过 deliveryAddress 字段(Address 类型)指向了该订单的送货地址。另外,还可以通过 orderItems 集合(List 集合)记录订单内的具体条目。 **OrderItem 类抽象了订单中的购物条目** ,记录了购物条目的唯一标识(id 字段),其中 product 字段(Product 类型)指向了该购物条目中具体购买的商品,amount 字段记录购买商品的个数,price 字段则是该 OrderItem 的总金额(即 Product.price * amount),Order 订单的总价格(totalPrice 字段)则是由其中全部 OrderItem 的 price 累加得到的。注意,这里的 OrderItem 总金额以及 Order 总金额,都不会持久化到数据,而是实时计算得到的。 +**Address 类抽象了用户的送货地址**,其中记录了街道(street 字段)、城市(city 字段)、国家(country 字段)等信息,还维护了一个 Customer 类型的引用,指向所属的用户。**Order 类抽象的是电商平台中的订单**,记录了订单的唯一标识(id 字段)、订单创建时间(createTime 字段),其中通过 customer 字段(Customer 类型)指向了订单关联的用户,通过 deliveryAddress 字段(Address 类型)指向了该订单的送货地址。另外,还可以通过 orderItems 集合(List 集合)记录订单内的具体条目。**OrderItem 类抽象了订单中的购物条目**,记录了购物条目的唯一标识(id 字段),其中 product 字段(Product 类型)指向了该购物条目中具体购买的商品,amount 字段记录购买商品的个数,price 字段则是该 OrderItem 的总金额(即 Product.price * amount),Order 订单的总价格(totalPrice 字段)则是由其中全部 OrderItem 的 price 累加得到的。注意,这里的 OrderItem 总金额以及 Order 总金额,都不会持久化到数据,而是实时计算得到的。 -**Product 类抽象了电商平台中商品的概念** ,其中记录了商品的唯一标识(id 字段)、商品名称(name 字段)、商品描述(description 字段)以及商品价格(price 字段)。 +**Product 类抽象了电商平台中商品的概念**,其中记录了商品的唯一标识(id 字段)、商品名称(name 字段)、商品描述(description 字段)以及商品价格(price 字段)。 结合前面的介绍以及类图分析,你可以看到: @@ -84,7 +84,7 @@ ### DAO 层 -**DAO 层主要是负责与持久化存储进行交互,完成数据持久化的相关工作** ,这里我们就介绍一下 如何使用 MyBatis 来开发 Java 应用中的持久层。 +**DAO 层主要是负责与持久化存储进行交互,完成数据持久化的相关工作**,这里我们就介绍一下 如何使用 MyBatis 来开发 Java 应用中的持久层。 在 DAO 层中,需要先根据需求确定 DAO 层的基本能力,一般情况下,是针对每个 domain 类提供最基础的 CRUD 操作,之后在 DAO 层之上的 Service 层,就可以直接使用 DAO 层的接口,而无须关心底层使用的是数据库还是其他存储,也无须关心读写数据使用的是 SQL 语句还是其他查询语句,这就能够实现业务逻辑和存储的解耦。 @@ -201,7 +201,7 @@ AddressMapper 接口对应的 AddressMapper.xml 配置文件中,同样定义 ``` -下面来看 ProductMapper 接口,其中 **除了根据 id 查询 Product 之外,还可以通过 name 进行模糊查询** ,具体定义如下: +下面来看 ProductMapper 接口,其中 **除了根据 id 查询 Product 之外,还可以通过 name 进行模糊查询**,具体定义如下: ```plaintext public interface ProductMapper { @@ -324,7 +324,7 @@ public interface OrderMapper { #### 2. DaoUtils 工具类 -在 DAO 层中,除了定义上述接口和相关实现之外,还需要管理数据库连接和事务。在订单系统中,我们 **使用 DaoUtils 工具类来完成 MyBatis 中 SqlSession 以及事务的相关操作** ,这个实现非常简单,在实践中,一般会使用专门的事务管理器来管理事务。 +在 DAO 层中,除了定义上述接口和相关实现之外,还需要管理数据库连接和事务。在订单系统中,我们 **使用 DaoUtils 工具类来完成 MyBatis 中 SqlSession 以及事务的相关操作**,这个实现非常简单,在实践中,一般会使用专门的事务管理器来管理事务。 下面是 DaoUtils 工具类的核心实现: @@ -366,7 +366,7 @@ public class DaoUtils { } ``` -在 DaoUtils 中加载的 mybatis-config.xml 配置文件位于 /resource 目录下, **是 MyBatis 框架配置的入口** ,其中配置了要连接的数据库地址、Mapper.xml 文件的位置以及一些自定义变量和别名,具体定义如下所示: +在 DaoUtils 中加载的 mybatis-config.xml 配置文件位于 /resource 目录下,**是 MyBatis 框架配置的入口**,其中配置了要连接的数据库地址、Mapper.xml 文件的位置以及一些自定义变量和别名,具体定义如下所示: ```plaintext @@ -414,7 +414,7 @@ public class DaoUtils { 介绍完 DAO 层之后,我们接下来再来聊聊 Service 层。 -**Service 层的核心职责是实现业务逻辑** 。在 Service 层实现的业务逻辑一般要依赖到前面介绍的 DAO 层的能力, **将业务逻辑封装到 Service 层可以更方便地复用业务逻辑实现,代码会显得非常简洁,系统也会更加稳定** 。 +**Service 层的核心职责是实现业务逻辑** 。在 Service 层实现的业务逻辑一般要依赖到前面介绍的 DAO 层的能力,**将业务逻辑封装到 Service 层可以更方便地复用业务逻辑实现,代码会显得非常简洁,系统也会更加稳定** 。 我们先来看 CustomerService 实现,其中提供了注册用户、添加送货地址、查询用户基本信息、查询用户全部送货地址等基本功能,具体实现如下所示: diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25403\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25403\350\256\262.md" index b649113f2..8ee8b9257 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25403\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25403\350\256\262.md" @@ -181,17 +181,17 @@ MyBatis 的源码结构图 完成 MyBatis 源码环境搭建之后,我再来带你分析一下 MyBatis 的架构。 -MyBatis 分为三层架构,分别是 **基础支撑层、核心处理层** 和 **接口层** ,如下图所示: +MyBatis 分为三层架构,分别是 **基础支撑层、核心处理层** 和 **接口层**,如下图所示: ![Lark20210129-194050.png](assets/CgpVE2AT9G2AXu4RAAM4svUMBPc909.png) MyBatis 三层架构图 -#### 1. 基础支撑层 **基础支撑层是整个 MyBatis 框架的地基,为整个 MyBatis 框架提供了非常基础的功能** ,其中每个模块都提供了一个内聚的、单一的能力,MyBatis 基础支撑层按照这些单一的能力可以划分为上图所示的九个基础模块 +#### 1. 基础支撑层 **基础支撑层是整个 MyBatis 框架的地基,为整个 MyBatis 框架提供了非常基础的功能**,其中每个模块都提供了一个内聚的、单一的能力,MyBatis 基础支撑层按照这些单一的能力可以划分为上图所示的九个基础模块 -由于资源加载模块的功能非常简单,使用频率也不高,这里我就不介绍了,你若感兴趣可以自行查阅相关资料去了解和学习。下面我就来简单描述这剩下的八个模块的基本功能,在本课程第二个模块,我还会带你详细分析这些基础模块的具体实现。 **第一个,类型转换模块。** 在上一讲展示的订单系统实现中,我们可以在 mybatis-config.xml 配置文件中通过 `` 标签为一个类定义一个别名,这里用到的“别名机制”就是由 MyBatis 基础支撑层中的类型转换模块实现的。 +由于资源加载模块的功能非常简单,使用频率也不高,这里我就不介绍了,你若感兴趣可以自行查阅相关资料去了解和学习。下面我就来简单描述这剩下的八个模块的基本功能,在本课程第二个模块,我还会带你详细分析这些基础模块的具体实现。**第一个,类型转换模块。** 在上一讲展示的订单系统实现中,我们可以在 mybatis-config.xml 配置文件中通过 `` 标签为一个类定义一个别名,这里用到的“别名机制”就是由 MyBatis 基础支撑层中的类型转换模块实现的。 -除了“别名机制”,类型转换模块还 **实现了 MyBatis 中 JDBC 类型与 Java 类型之间的相互转换** ,这一功能在绑定实参、映射 ResultSet 场景中都有所体现: +除了“别名机制”,类型转换模块还 **实现了 MyBatis 中 JDBC 类型与 Java 类型之间的相互转换**,这一功能在绑定实参、映射 ResultSet 场景中都有所体现: - 在 SQL 模板绑定用户传入实参的场景中,类型转换模块会将 Java 类型数据转换成 JDBC 类型数据; - 在将 ResultSet 映射成结果对象的时候,类型转换模块会将 JDBC 类型数据转换成 Java 类型数据。 @@ -202,21 +202,21 @@ MyBatis 三层架构图 类型转换基本功能示意图 -**第二个,日志模块。** 日志是我们生产实践中排查问题、定位 Bug、锁定性能瓶颈的主要线索来源,在任何一个成熟系统中都会有级别合理、信息翔实的日志模块,MyBatis 也不例外。MyBatis 提供了日志模块来集成 Java 生态中的第三方日志框架,该模块目前可以集成 Log4j、Log4j2、slf4j 等优秀的日志框架。 **第三个,反射工具模块。** Java 中的反射功能非常强大,许多开源框架都会依赖反射实现一些相对灵活的需求,但是大多数 Java 程序员在实际工作中很少会直接使用到反射技术。MyBatis 的反射工具箱是在 Java 反射的基础之上进行的一层封装,为上层使用方提供更加灵活、方便的 API 接口,同时缓存 Java 的原生反射相关的元数据,提升了反射代码执行的效率,优化了反射操作的性能。 **第四个,Binding 模块。** 在上一讲介绍的订单系统示例中,我们可以通过 SqlSession 获取 Mapper 接口的代理,然后通过这个代理执行关联 Mapper.xml 文件中的数据库操作。通过这种方式,可以将一些错误提前到编译期,该功能就是通过 Binding 模块完成的。 +**第二个,日志模块。** 日志是我们生产实践中排查问题、定位 Bug、锁定性能瓶颈的主要线索来源,在任何一个成熟系统中都会有级别合理、信息翔实的日志模块,MyBatis 也不例外。MyBatis 提供了日志模块来集成 Java 生态中的第三方日志框架,该模块目前可以集成 Log4j、Log4j2、slf4j 等优秀的日志框架。**第三个,反射工具模块。** Java 中的反射功能非常强大,许多开源框架都会依赖反射实现一些相对灵活的需求,但是大多数 Java 程序员在实际工作中很少会直接使用到反射技术。MyBatis 的反射工具箱是在 Java 反射的基础之上进行的一层封装,为上层使用方提供更加灵活、方便的 API 接口,同时缓存 Java 的原生反射相关的元数据,提升了反射代码执行的效率,优化了反射操作的性能。**第四个,Binding 模块。** 在上一讲介绍的订单系统示例中,我们可以通过 SqlSession 获取 Mapper 接口的代理,然后通过这个代理执行关联 Mapper.xml 文件中的数据库操作。通过这种方式,可以将一些错误提前到编译期,该功能就是通过 Binding 模块完成的。 -这里特别说明的是,在使用 MyBatis 的时候,我们无须编写 Mapper 接口的具体实现,而是利用 Binding 模块自动生成 Mapper 接口的动态代理对象。有些简单的数据操作,我们还可以直接在 Mapper 接口中使用注解完成,连 Mapper.xml 配置文件都无须编写,但如果 ResultSet 映射以及动态 SQL 非常复杂,还是建议在 Mapper.xml 配置文件中维护会比较方便。 **第五个,数据源模块。** 持久层框架核心组件之一就是数据源,一款性能出众的数据源可以成倍提升系统的性能。MyBatis 自身提供了一套不错的数据源实现,也是 MyBatis 的默认实现。另外,在 Java 生态中,就有很多优异开源的数据源可供选择,MyBatis 的数据源模块中也提供了与第三方数据源集成的相关接口,这也为用户提供了更多的选择空间,提升了数据源切换的灵活性。 **第六个,缓存模块。** 数据库是实践生成中非常核心的存储,很多业务数据都会落地到数据库,所以数据库性能的优劣直接影响了上层业务系统的优劣。我们很多线上业务都是读多写少的场景,在数据库遇到瓶颈时,缓存是最有效、最常用的手段之一(如下图所示),正确使用缓存可以将一部分数据库请求拦截在缓存这一层,这就能够减少一部分数据库的压力,提高系统性能。 +这里特别说明的是,在使用 MyBatis 的时候,我们无须编写 Mapper 接口的具体实现,而是利用 Binding 模块自动生成 Mapper 接口的动态代理对象。有些简单的数据操作,我们还可以直接在 Mapper 接口中使用注解完成,连 Mapper.xml 配置文件都无须编写,但如果 ResultSet 映射以及动态 SQL 非常复杂,还是建议在 Mapper.xml 配置文件中维护会比较方便。**第五个,数据源模块。** 持久层框架核心组件之一就是数据源,一款性能出众的数据源可以成倍提升系统的性能。MyBatis 自身提供了一套不错的数据源实现,也是 MyBatis 的默认实现。另外,在 Java 生态中,就有很多优异开源的数据源可供选择,MyBatis 的数据源模块中也提供了与第三方数据源集成的相关接口,这也为用户提供了更多的选择空间,提升了数据源切换的灵活性。**第六个,缓存模块。** 数据库是实践生成中非常核心的存储,很多业务数据都会落地到数据库,所以数据库性能的优劣直接影响了上层业务系统的优劣。我们很多线上业务都是读多写少的场景,在数据库遇到瓶颈时,缓存是最有效、最常用的手段之一(如下图所示),正确使用缓存可以将一部分数据库请求拦截在缓存这一层,这就能够减少一部分数据库的压力,提高系统性能。 ![Lark20210129-194055.png](assets/Cip5yGAT9ICAItLcAAHSeuL0ugo137.png) 缓存模块结构图 -除了使用 Redis、Memcached 等外置的第三方缓存以外,持久化框架一般也会自带内置的缓存,例如,MyBatis 就提供了一级缓存和二级缓存,具体实现位于基础支撑层的缓存模块中。 **第七个,解析器模块** 。在上一讲的订单系统示例中,我们可以看到 MyBatis 中有两大部分配置文件需要解析,一个是 mybatis-config.xml 配置文件,另一个是 Mapper.xml 配置文件。这两个文件都是由 MyBatis 的解析器模块进行解析的,其中主要是依赖 XPath 实现 XML 配置文件以及各类表达式的高效解析。 **第八个,事务管理模块。** 持久层框架一般都会提供一套事务管理机制实现数据库的事务控制,MyBatis 对数据库中的事务进行了一层简单的抽象,提供了简单易用的事务接口和实现。一般情况下,Java 项目都会集成 Spring,并由 Spring 框架管理事务。在后面的课程中,我还会深入讲解 MyBatis 与 Spring 集成的原理,其中就包括事务管理相关的集成。 +除了使用 Redis、Memcached 等外置的第三方缓存以外,持久化框架一般也会自带内置的缓存,例如,MyBatis 就提供了一级缓存和二级缓存,具体实现位于基础支撑层的缓存模块中。**第七个,解析器模块** 。在上一讲的订单系统示例中,我们可以看到 MyBatis 中有两大部分配置文件需要解析,一个是 mybatis-config.xml 配置文件,另一个是 Mapper.xml 配置文件。这两个文件都是由 MyBatis 的解析器模块进行解析的,其中主要是依赖 XPath 实现 XML 配置文件以及各类表达式的高效解析。**第八个,事务管理模块。** 持久层框架一般都会提供一套事务管理机制实现数据库的事务控制,MyBatis 对数据库中的事务进行了一层简单的抽象,提供了简单易用的事务接口和实现。一般情况下,Java 项目都会集成 Spring,并由 Spring 框架管理事务。在后面的课程中,我还会深入讲解 MyBatis 与 Spring 集成的原理,其中就包括事务管理相关的集成。 #### 2. 核心处理层 -介绍完 MyBatis 的基础支撑层之后,我们再来分析 MyBatis 的核心处理层。 **核心处理层是 MyBatis 核心实现所在,其中涉及 MyBatis 的初始化以及执行一条 SQL 语句的全流程** 。下面我就针对核心处理层中的各部分实现进行介绍。 **第一个,配置解析。** 我们知道,MyBatis 有三处可以添加配置信息的地方,分别是:mybatis-config.xml 配置文件、Mapper.xml 配置文件以及 Mapper 接口中的注解信息。在 MyBatis 初始化过程中,会加载这些配置信息,并将解析之后得到的配置对象保存到 Configuration 对象中。 +介绍完 MyBatis 的基础支撑层之后,我们再来分析 MyBatis 的核心处理层。**核心处理层是 MyBatis 核心实现所在,其中涉及 MyBatis 的初始化以及执行一条 SQL 语句的全流程** 。下面我就针对核心处理层中的各部分实现进行介绍。**第一个,配置解析。** 我们知道,MyBatis 有三处可以添加配置信息的地方,分别是:mybatis-config.xml 配置文件、Mapper.xml 配置文件以及 Mapper 接口中的注解信息。在 MyBatis 初始化过程中,会加载这些配置信息,并将解析之后得到的配置对象保存到 Configuration 对象中。 -例如,在订单系统示例中使用的 `` 标签(也就是自定义的查询结果集映射规则)会被解析成 ResultMap 对象。我们可以利用得到的 Configuration 对象创建 SqlSessionFactory 对象(也就是创建 SqlSession 对象的工厂对象),之后即可创建 SqlSession 对象执行数据库操作了。 **第二个,SQL 解析与 scripting 模块。** MyBatis 的最大亮点应该要数其动态 SQL 功能了,只需要通过 MyBatis 提供的标签即可根据实际的运行条件动态生成实际执行的 SQL 语句。MyBatis 提供的动态 SQL 标签非常丰富,包括 `` 标签、`` 标签、`` 标签、`` 标签等。 **MyBatis 中的 scripting 模块就是负责动态生成 SQL 的核心模块** 。它会根据运行时用户传入的实参,解析动态 SQL 中的标签,并形成 SQL 模板,然后处理 SQL 模板中的占位符,用运行时的实参填充占位符,得到数据库真正可执行的 SQL 语句。 **第三个,SQL 执行。** 在 MyBatis 中,要执行一条 SQL 语句,会涉及非常多的组件,比较核心的有:Executor、StatementHandler、ParameterHandler 和 ResultSetHandler。 +例如,在订单系统示例中使用的 `` 标签(也就是自定义的查询结果集映射规则)会被解析成 ResultMap 对象。我们可以利用得到的 Configuration 对象创建 SqlSessionFactory 对象(也就是创建 SqlSession 对象的工厂对象),之后即可创建 SqlSession 对象执行数据库操作了。**第二个,SQL 解析与 scripting 模块。** MyBatis 的最大亮点应该要数其动态 SQL 功能了,只需要通过 MyBatis 提供的标签即可根据实际的运行条件动态生成实际执行的 SQL 语句。MyBatis 提供的动态 SQL 标签非常丰富,包括 `` 标签、`` 标签、`` 标签、`` 标签等。**MyBatis 中的 scripting 模块就是负责动态生成 SQL 的核心模块** 。它会根据运行时用户传入的实参,解析动态 SQL 中的标签,并形成 SQL 模板,然后处理 SQL 模板中的占位符,用运行时的实参填充占位符,得到数据库真正可执行的 SQL 语句。**第三个,SQL 执行。** 在 MyBatis 中,要执行一条 SQL 语句,会涉及非常多的组件,比较核心的有:Executor、StatementHandler、ParameterHandler 和 ResultSetHandler。 其中,Executor 会调用事务管理模块实现事务的相关控制,同时会通过缓存模块管理一级缓存和二级缓存。SQL 语句的真正执行将会由 StatementHandler 实现。那具体是怎么完成的呢?StatementHandler 会先依赖 ParameterHandler 进行 SQL 模板的实参绑定,然后由 java.sql.Statement 对象将 SQL 语句以及绑定好的实参传到数据库执行,从数据库中拿到 ResultSet,最后,由 ResultSetHandler 将 ResultSet 映射成 Java 对象返回给调用方,这就是 SQL 执行模块的核心。 @@ -228,7 +228,7 @@ MyBatis 三层架构图 与此同时,在实际应用的时候,你也可以通过自定义插件来扩展 MyBatis,或者改变 MyBatis 的默认行为。因为插件会影响 MyBatis 内核的行为,所以在自定义插件之前,你必须非常了解 MyBatis 内部的运行原理,以避免写出不符合预期的插件,引入一些诡异的功能 Bug 或性能问题。 -#### 3. 接口层 **接口层是 MyBatis 暴露给调用的接口集合** ,这些接口都是使用 MyBatis 时最常用的一些接口,例如,SqlSession 接口、SqlSessionFactory 接口等。其中,最核心的是 SqlSession 接口,你可以通过它实现很多功能,例如,获取 Mapper 代理、执行 SQL 语句、控制事务开关等 +#### 3. 接口层 **接口层是 MyBatis 暴露给调用的接口集合**,这些接口都是使用 MyBatis 时最常用的一些接口,例如,SqlSession 接口、SqlSessionFactory 接口等。其中,最核心的是 SqlSession 接口,你可以通过它实现很多功能,例如,获取 Mapper 代理、执行 SQL 语句、控制事务开关等 ### 总结 diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25404\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25404\350\256\262.md" index f9101579c..8c770f9cf 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25404\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25404\350\256\262.md" @@ -4,7 +4,7 @@ 有人说反射打破了类的封装性,破坏了我们的面向对象思维,我倒不这么认为。我觉得正是由于 Java 的反射机制,解决了很多面向对象无法解决的问题,才受到众多 Java 开源框架的青睐,也出现了有很多惊艳的反射实践,当然,这也包括 MyBatis 中的反射工具箱。 -凡事都有两面性,越是灵活、越是强大的工具,用起来的门槛就越高,反射亦如此。这也是写业务代码时,很少用到反射的原因。反过来说,如果必须要用反射解决业务问题的时候,就需要停下来思考我们的系统设计是不是有问题了。 **为了降低反射使用门槛,MyBatis 内部封装了一个反射工具箱** ,其中包含了 MyBatis 自身常用的反射操作,MyBatis 其他模块只需要调用反射工具箱暴露的简洁 API 即可实现想要的反射功能。 +凡事都有两面性,越是灵活、越是强大的工具,用起来的门槛就越高,反射亦如此。这也是写业务代码时,很少用到反射的原因。反过来说,如果必须要用反射解决业务问题的时候,就需要停下来思考我们的系统设计是不是有问题了。**为了降低反射使用门槛,MyBatis 内部封装了一个反射工具箱**,其中包含了 MyBatis 自身常用的反射操作,MyBatis 其他模块只需要调用反射工具箱暴露的简洁 API 即可实现想要的反射功能。 反射工具箱的具体代码实现位于 org.apache.ibatis.reflection 包中,下面我就带你一起深入分析该模块的核心实现。 @@ -21,7 +21,7 @@ - defaultConstructor(Constructor\ 类型):默认构造方法。 - caseInsensitivePropertyMap(Map\ 类型):所有属性名称的集合,记录到这个集合中的属性名称都是大写的。 -**在我们构造一个 Reflector 对象的时候,传入一个 Class 对象,通过解析这个 Class 对象,即可填充上述核心字段** ,整个核心流程大致可描述为如下。 +**在我们构造一个 Reflector 对象的时候,传入一个 Class 对象,通过解析这个 Class 对象,即可填充上述核心字段**,整个核心流程大致可描述为如下。 1. 用 type 字段记录传入的 Class 对象。 1. 通过反射拿到 Class 类的全部构造方法,并进行遍历,过滤得到唯一的无参构造方法来初始化 defaultConstructor 字段。这部分逻辑在 addDefaultConstructor() 方法中实现。 @@ -35,7 +35,7 @@ 了解了初始化的核心流程之后,我们再继续深入分析其中涉及的方法,这些方法也是 Reflector 的核心方法。 -首先来看 addGetMethods() 方法和 addSetMethods() 方法,它们分别用来解析传入 Class 类中的 getter方法和 setter() 方法,两者的逻辑十分相似。这里,我们就以 addGetMethods() 方法为例深入分析,其主要包括如下三个核心步骤。 **第一步,获取方法信息。** 这里会调用 getClassMethods() 方法获取当前 Class 类的所有方法的唯一签名(注意一下,这里同时包含继承自父类以及接口的方法),以及每个方法对应的 Method 对象。 +首先来看 addGetMethods() 方法和 addSetMethods() 方法,它们分别用来解析传入 Class 类中的 getter方法和 setter() 方法,两者的逻辑十分相似。这里,我们就以 addGetMethods() 方法为例深入分析,其主要包括如下三个核心步骤。**第一步,获取方法信息。** 这里会调用 getClassMethods() 方法获取当前 Class 类的所有方法的唯一签名(注意一下,这里同时包含继承自父类以及接口的方法),以及每个方法对应的 Method 对象。 在递归扫描父类以及父接口的过程中,会使用 Map\ 集合记录遍历到的方法,实现去重的效果,其中 Key 是对应的方法签名,Value 为方法对应的 Method 对象。生成的方法签名的格式如下: @@ -49,9 +49,9 @@ java.lang.String#addGetMethods:java.lang.Class ``` -可见, **这里生成的方法签名是包含返回值的,可以作为该方法全局唯一的标识** 。 **第二步,按照 Java 的规范,从上一步返回的 Method 数组中查找 getter 方法,将其记录到 conflictingGetters 集合中** 。这里的 conflictingGetters 集合(HashMap\()类型)中的 Key 为属性名称,Value 是该属性对应的 getter 方法集合。 +可见,**这里生成的方法签名是包含返回值的,可以作为该方法全局唯一的标识** 。**第二步,按照 Java 的规范,从上一步返回的 Method 数组中查找 getter 方法,将其记录到 conflictingGetters 集合中** 。这里的 conflictingGetters 集合(HashMap\()类型)中的 Key 为属性名称,Value 是该属性对应的 getter 方法集合。 -为什么一个属性会查找到多个 getter 方法呢?这主要是由于类间继承导致的,在子类中我们可以覆盖父类的方法,覆盖不仅可以修改方法的具体实现,还可以修改方法的返回值,getter 方法也不例外,这就导致在第一步中产生了两个签名不同的方法。 **第三步,解决方法签名冲突。** 这里会调用 resolveGetterConflicts() 方法对这种 getter 方法的冲突进行处理, **处理冲突的核心逻辑其实就是比较 getter 方法的返回值,优先选择返回值为子类的 getter 方法** ,例如: +为什么一个属性会查找到多个 getter 方法呢?这主要是由于类间继承导致的,在子类中我们可以覆盖父类的方法,覆盖不仅可以修改方法的具体实现,还可以修改方法的返回值,getter 方法也不例外,这就导致在第一步中产生了两个签名不同的方法。**第三步,解决方法签名冲突。** 这里会调用 resolveGetterConflicts() 方法对这种 getter 方法的冲突进行处理,**处理冲突的核心逻辑其实就是比较 getter 方法的返回值,优先选择返回值为子类的 getter 方法**,例如: ```plaintext // 该方法定义在SuperClazz类中 @@ -92,13 +92,13 @@ Invoker 接口继承关系图 #### 4. ReflectorFactory -通过上面的分析我们知道,Reflector 初始化过程会有一系列的反射操作, **为了提升 Reflector 的初始化速度,MyBatis 提供了 ReflectorFactory 这个工厂接口对 Reflector 对象进行缓存** ,其中最核心的方法是用来获取 Reflector 对象的 findForClass() 方法。 +通过上面的分析我们知道,Reflector 初始化过程会有一系列的反射操作,**为了提升 Reflector 的初始化速度,MyBatis 提供了 ReflectorFactory 这个工厂接口对 Reflector 对象进行缓存**,其中最核心的方法是用来获取 Reflector 对象的 findForClass() 方法。 DefaultReflectorFactory 是 ReflectorFactory 接口的默认实现,它默认会在内存中维护一个 ConcurrentHashMap\, Reflector> 集合(reflectorMap 字段)缓存其创建的所有 Reflector 对象。 在其 findForClass() 方法实现中,首先会根据传入的 Class 类查询 reflectorMap 缓存,如果查找到对应的 Reflector 对象,则直接返回;否则创建相应的 Reflector 对象,并记录到 reflectorMap 中缓存,等待下次使用。 -### 默认对象工厂 **ObjectFactory 是 MyBatis 中的反射工厂** ,其中提供了两个 create() 方法的重载,我们可以通过两个 create() 方法创建指定类型的对象 +### 默认对象工厂 **ObjectFactory 是 MyBatis 中的反射工厂**,其中提供了两个 create() 方法的重载,我们可以通过两个 create() 方法创建指定类型的对象 DefaultObjectFactory 是 ObjectFactory 接口的默认实现,其 create() 方法底层是通过调用 instantiateClass() 方法创建对象的。instantiateClass() 方法会通过反射的方式根据传入的参数列表,选择合适的构造函数实例化对象。 @@ -108,7 +108,7 @@ DefaultObjectFactory 是 ObjectFactory 接口的默认实现,其 create() 方 在前面《02 | 订单系统持久层示例分析,20 分钟带你快速上手 MyBatis》介绍的订单系统示例中,我们在 orderMap 这个 ResultMap 映射中,如果要配置 Order 与 OrderItem 的一对多关系,可以使用 `` 标签进行配置;如果 OrderItem 个数明确,可以直接使用数组下标索引方式(即 ordersItems\[0\])填充 orderItems 集合。 -这里的 “.” 导航以及数组下标的解析,也都是在反射工具箱中完成的。下面我们就来介绍 reflection.property 包下的 **三个属性解析相关的工具类** ,在后面的 MetaClass、MetaObject 等工具类中,也都需要属性解析能力。 +这里的 “.” 导航以及数组下标的解析,也都是在反射工具箱中完成的。下面我们就来介绍 reflection.property 包下的 **三个属性解析相关的工具类**,在后面的 MetaClass、MetaObject 等工具类中,也都需要属性解析能力。 - PropertyTokenizer 工具类负责解析由“.”和“\[\]”构成的表达式。PropertyTokenizer 继承了 Iterator 接口,可以迭代处理嵌套多层表达式。 - PropertyCopier 是一个属性拷贝的工具类,提供了与 Spring 中 BeanUtils.copyProperties() 类似的功能,实现相同类型的两个对象之间的属性值拷贝,其核心方法是 copyBeanProperties() 方法。 @@ -116,7 +116,7 @@ DefaultObjectFactory 是 ObjectFactory 接口的默认实现,其 create() 方 ### MetaClass -**MetaClass 提供了获取类中属性描述信息的功能,底层依赖前面介绍的 Reflector** ,在 MetaClass 的构造方法中会将传入的 Class 封装成一个 Reflector 对象,并记录到 reflector 字段中,MetaClass 的后续属性查找都会使用到该 Reflector 对象。 +**MetaClass 提供了获取类中属性描述信息的功能,底层依赖前面介绍的 Reflector**,在 MetaClass 的构造方法中会将传入的 Class 封装成一个 Reflector 对象,并记录到 reflector 字段中,MetaClass 的后续属性查找都会使用到该 Reflector 对象。 MetaClass 中的 findProperty() 方法是实现属性查找的核心方法,它主要处理了“.”导航的属性查找,该方法会用前文介绍的 PropertyTokenizer 解析传入的 name 表达式,该表达式可能通过“.”导航多层,例如,order.deliveryAddress.customer.name。 @@ -132,11 +132,11 @@ ObjectWrapper 的实现类如下图所示: ![Drawing 1.png](assets/CioPOWAaP22Aea6TAAB1kkkDx98845.png) -ObjectWrapper 继承关系图 **BaseWrapper 是 ObjectWrapper 接口的抽象实现** ,其中只有一个 MetaObject 类型的字段。BaseWrapper 为子类实现了 resolveCollection()、getCollectionValue() 和 setCollectionValue() 三个针对集合对象的处理方法。其中,resolveCollection() 方法会将指定属性作为集合对象返回,底层依赖 MetaObject.getValue()方法实现(后面还会详细介绍)。getCollectionValue() 方法和 setCollectionValue() 方法会解析属性表达式的下标信息,然后获取/设置集合中的对应元素,这里解析属性表达式依然是依赖前面介绍的 PropertyTokenizer 工具类。 **BeanWrapper 继承了 BaseWrapper 抽象类** ,底层除了封装了一个 JavaBean 对象之外,还封装了该 JavaBean 类型对应的 MetaClass 对象,以及从 BaseWrapper 继承下来的 MetaObject 对象。 +ObjectWrapper 继承关系图 **BaseWrapper 是 ObjectWrapper 接口的抽象实现**,其中只有一个 MetaObject 类型的字段。BaseWrapper 为子类实现了 resolveCollection()、getCollectionValue() 和 setCollectionValue() 三个针对集合对象的处理方法。其中,resolveCollection() 方法会将指定属性作为集合对象返回,底层依赖 MetaObject.getValue()方法实现(后面还会详细介绍)。getCollectionValue() 方法和 setCollectionValue() 方法会解析属性表达式的下标信息,然后获取/设置集合中的对应元素,这里解析属性表达式依然是依赖前面介绍的 PropertyTokenizer 工具类。**BeanWrapper 继承了 BaseWrapper 抽象类**,底层除了封装了一个 JavaBean 对象之外,还封装了该 JavaBean 类型对应的 MetaClass 对象,以及从 BaseWrapper 继承下来的 MetaObject 对象。 在 get() 方法和 set() 方法实现中,BeanWrapper 会根据传入的属性表达式,获取/设置相应的属性值。以 get() 方法为例,首先会判断表达式中是否含有数组下标,如果含有下标,会通过 resolveCollection() 和 getCollectionValue() 方法从集合中获取相应元素;如果不包含下标,则通过 MetaClass 查找属性名称在 Reflector.getMethods 集合中相应的 GetFieldInvoker,然后调用 Invoker.invoke() 方法读取属性值。 -BeanWrapper 中其他方法的实现也大都与 get() 方法和 set() 方法类似,依赖 MetaClass、MetaObject 完成相关对象中属性信息读写,这里就不再一一介绍,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/mybatis)进行学习。 **CollectionWrapper 是 ObjectWrapper 接口针对 Collection 集合的一个实现** ,其中封装了Collection`` 集合对象,只有 isCollection()、add()、addAll() 方法以及从 BaseWrapper 继承下来的方法是可用的,其他方法都会抛出 UnsupportedOperationException 异常。 **MapWrapper 是针对 Map 类型的一个实现** ,这个实现就比较简单了,所以我就留给你自己去分析了,分析过程中可以参考下面将要介绍的 MetaObject。 +BeanWrapper 中其他方法的实现也大都与 get() 方法和 set() 方法类似,依赖 MetaClass、MetaObject 完成相关对象中属性信息读写,这里就不再一一介绍,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/mybatis)进行学习。**CollectionWrapper 是 ObjectWrapper 接口针对 Collection 集合的一个实现**,其中封装了Collection`` 集合对象,只有 isCollection()、add()、addAll() 方法以及从 BaseWrapper 继承下来的方法是可用的,其他方法都会抛出 UnsupportedOperationException 异常。**MapWrapper 是针对 Map 类型的一个实现**,这个实现就比较简单了,所以我就留给你自己去分析了,分析过程中可以参考下面将要介绍的 MetaObject。 ### MetaObject diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25405\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25405\350\256\262.md" index ee1d5f686..ad308309e 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25405\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25405\350\256\262.md" @@ -8,7 +8,7 @@ 在使用 PreparedStatement 执行 SQL 语句之前,都是需要手动调用 setInt()、setString() 等 set 方法绑定参数,这不仅仅是告诉 JDBC 一个 SQL 模板中哪个占位符需要使用哪个实参,还会将数据从 Java 类型转换成 JDBC 类型。当从 ResultSet 中获取数据的时候,则是一个逆过程,数据会从 JDBC 类型转换为 Java 类型。 -可以使用 MyBatis 中的 **类型转换器** ,完成上述两次类型转换,如下图所示: +可以使用 MyBatis 中的 **类型转换器**,完成上述两次类型转换,如下图所示: ![Drawing 1.png](assets/CioPOWAeMi6AdTRAAAENMX_HsyU054.png) @@ -16,7 +16,7 @@ JDBC 类型数据与 Java 类型数据转换示意图 ### 深入 TypeHandler -说了这么多,类型转换器到底是怎么定义的呢?其实, **MyBatis 中的类型转换器就是 TypeHandler 这个接口** ,其定义如下: +说了这么多,类型转换器到底是怎么定义的呢?其实,**MyBatis 中的类型转换器就是 TypeHandler 这个接口**,其定义如下: ```plaintext public interface TypeHandler { @@ -29,7 +29,7 @@ public interface TypeHandler { } ``` -**MyBatis 中定义了 BaseTypeHandler 抽象类来实现一些 TypeHandler 的公共逻辑** ,BaseTypeHandler 在实现 TypeHandler 的同时,还实现了 TypeReference 抽象类。其继承关系如下图所示: +**MyBatis 中定义了 BaseTypeHandler 抽象类来实现一些 TypeHandler 的公共逻辑**,BaseTypeHandler 在实现 TypeHandler 的同时,还实现了 TypeReference 抽象类。其继承关系如下图所示: ![Drawing 2.png](assets/CioPOWAeMkCANy6LAABJPBfXPJY527.png) @@ -87,7 +87,7 @@ public class LongTypeHandler extends BaseTypeHandler { - MyBatis 如何管理这么多的 TypeHandler 接口实现呢? - 如何在合适的场景中使用合适的 TypeHandler 实现进行类型转换呢? -你若使用过 MyBatis 的话,应该知道我们可以在 mybatis-config.xml 中通过 标签配置自定义的 TypeHandler 实现,也可以在 Mapper.xml 配置文件定义 的时候指定 typeHandler 属性。无论是哪种配置方式,MyBatis 都会在初始化过程中,获取所有已知的 TypeHandler(包括内置实现和自定义实现),然后创建所有 TypeHandler 实例并注册到 TypeHandlerRegistry 中, **由 TypeHandlerRegistry 统一管理所有 TypeHandler 实例** 。 TypeHandlerRegistry 管理 TypeHandler 的时候,用到了以下四个最核心的集合。 +你若使用过 MyBatis 的话,应该知道我们可以在 mybatis-config.xml 中通过 标签配置自定义的 TypeHandler 实现,也可以在 Mapper.xml 配置文件定义 的时候指定 typeHandler 属性。无论是哪种配置方式,MyBatis 都会在初始化过程中,获取所有已知的 TypeHandler(包括内置实现和自定义实现),然后创建所有 TypeHandler 实例并注册到 TypeHandlerRegistry 中,**由 TypeHandlerRegistry 统一管理所有 TypeHandler 实例** 。 TypeHandlerRegistry 管理 TypeHandler 的时候,用到了以下四个最核心的集合。 - jdbcTypeHandlerMap(Map\>类型):该集合记录了 JdbcType 与 TypeHandler 之间的关联关系。JdbcType 是一个枚举类型,每个 JdbcType 枚举值对应一种 JDBC 类型,例如,JdbcType.VARCHAR 对应的就是 JDBC 中的 varchar 类型。在从 ResultSet 中读取数据的时候,就会从 JDBC_TYPE_HANDLER_MAP 集合中根据 JDBC 类型查找对应的 TypeHandler,将数据转换成 Java 类型。 - typeHandlerMap(Map\>>类型):该集合第一层 Key 是需要转换的 Java 类型,第二层 Key 是转换的目标 JdbcType,最终的 Value 是完成此次转换时所需要使用的 TypeHandler 对象。那为什么要有两层 Map 的设计呢?这里我们举个例子:Java 类型中的 String 可能转换成数据库中的 varchar、char、text 等多种类型,存在一对多关系,所以就可能有不同的 TypeHandler 实现。 @@ -115,7 +115,7 @@ private void register(Type javaType, JdbcType jdbcType, TypeHandler handler) } ``` -除了上面的 register() 重载,在有的 register() 重载中会尝试从 TypeHandler 类中的@MappedTypes 注解和 @MappedJdbcTypes 注解中读取信息。其中, **@MappedTypes 注解中可以配置 TypeHandler 实现类能够处理的 Java 类型的集合,@MappedJdbcTypes 注解中可以配置该 TypeHandler 实现类能够处理的 JDBC 类型集合** 。 +除了上面的 register() 重载,在有的 register() 重载中会尝试从 TypeHandler 类中的@MappedTypes 注解和 @MappedJdbcTypes 注解中读取信息。其中,**@MappedTypes 注解中可以配置 TypeHandler 实现类能够处理的 Java 类型的集合,@MappedJdbcTypes 注解中可以配置该 TypeHandler 实现类能够处理的 JDBC 类型集合** 。 如下就是读取 @MappedJdbcTypes 注解的 register() 重载方法: @@ -172,7 +172,7 @@ public void register(TypeHandler typeHandler) { } ``` -我们接下来看最后一个 register() 重载。 **TypeHandlerRegistry 提供了扫描一个包下的全部 TypeHandler 接口实现类的 register() 重载** 。在该重载中,会首先读取指定包下面的全部的 TypeHandler 实现类,然后再交给 register() 重载读取 @MappedTypes 注解和 @MappedJdbcTypes 注解,并最终完成注册。这个 register() 重载的具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/mybatis)进行学习。 +我们接下来看最后一个 register() 重载。**TypeHandlerRegistry 提供了扫描一个包下的全部 TypeHandler 接口实现类的 register() 重载** 。在该重载中,会首先读取指定包下面的全部的 TypeHandler 实现类,然后再交给 register() 重载读取 @MappedTypes 注解和 @MappedJdbcTypes 注解,并最终完成注册。这个 register() 重载的具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/mybatis)进行学习。 最后,我们再来看看 TypeHandlerRegistry 的构造方法,其中会通过 register() 方法注册多个 TypeHandler 对象,下面就展示了为 String 类型注册 TypeHandler 的核心实现: @@ -195,7 +195,7 @@ public TypeHandlerRegistry() { ### TypeHandler 查询 -分析完注册 TypeHandler 实例的具体实现之后,我们接下来就来看看 MyBatis 是如何从 TypeHandlerRegistry 底层的这几个集合中查找正确的 TypeHandler 实例, **该功能的具体实现是在 TypeHandlerRegistry 的 getTypeHandler() 方法中** 。 +分析完注册 TypeHandler 实例的具体实现之后,我们接下来就来看看 MyBatis 是如何从 TypeHandlerRegistry 底层的这几个集合中查找正确的 TypeHandler 实例,**该功能的具体实现是在 TypeHandlerRegistry 的 getTypeHandler() 方法中** 。 这里的 getTypeHandler() 方法也有多个重载,最核心的重载是 getTypeHandler(Type,JdbcType) 这个重载方法,其中会根据传入的 Java 类型和 JDBC 类型,从底层的几个集合中查询相应的 TypeHandler 实例,具体实现如下: @@ -282,9 +282,9 @@ private Map> getJdbcHandlerMapForSuperclass(Class cl ### 别名管理 -在《02 | 订单系统持久层示例分析,20 分钟带你快速上手 MyBatis》分析的 MyBatis 示例中,我们在 mybatis-config.xml 配置文件中使用 `` 标签为 Customer 等 Java 类的完整名称定义了相应的别名,后续编写 SQL 语句、定义 `` 的时候, **直接使用这些别名即可完全替代相应的完整 Java 类名,这样就非常易于代码的编写和维护** 。 +在《02 | 订单系统持久层示例分析,20 分钟带你快速上手 MyBatis》分析的 MyBatis 示例中,我们在 mybatis-config.xml 配置文件中使用 `` 标签为 Customer 等 Java 类的完整名称定义了相应的别名,后续编写 SQL 语句、定义 `` 的时候,**直接使用这些别名即可完全替代相应的完整 Java 类名,这样就非常易于代码的编写和维护** 。 -**TypeAliasRegistry 是维护别名配置的核心实现所在** ,其中提供了别名注册、别名查询的基本功能。在 TypeAliasRegistry 的 typeAliases 字段(Map\>类型)中记录了别名与 Java 类型之间的对应关系,我们可以通过 registerAlias() 方法完成别名的注册,具体实现如下: +**TypeAliasRegistry 是维护别名配置的核心实现所在**,其中提供了别名注册、别名查询的基本功能。在 TypeAliasRegistry 的 typeAliases 字段(Map\>类型)中记录了别名与 Java 类型之间的对应关系,我们可以通过 registerAlias() 方法完成别名的注册,具体实现如下: ```java public void registerAlias(String alias, Class value) { diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25406\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25406\350\256\262.md" index 813fc503d..4b9d64d16 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25406\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25406\350\256\262.md" @@ -20,11 +20,11 @@ Apache Commons Logging、Log4j、Log4j2、java.util.logging 等是 Java 开发 - **需要适配的类/要使用的实现类(Adaptee)** :定义了真正要执行的业务逻辑,但是其接口不能被使用者直接使用。这里依然以处理遗留代码为例,Adaptee 就是遗留业务实现,由于编写 Adaptee 的时候还没有定义 Target 接口,所以 Adaptee 无法实现 Target 接口。 - **适配器(Adapter)** :在实现 Target 接口的同时,维护了一个指向 Adaptee 对象的引用。Adapter 底层会依赖 Adaptee 的逻辑来实现 Target 接口的功能,这样就能够复用 Adaptee 类中的遗留逻辑来完成业务。 -适配器模式带来的最大好处就是 **复用已有的逻辑** ,避免直接去修改 Adaptee 实现的接口,这符合开放-封闭原则(也就是程序要对扩展开放、对修改关闭)。 +适配器模式带来的最大好处就是 **复用已有的逻辑**,避免直接去修改 Adaptee 实现的接口,这符合开放-封闭原则(也就是程序要对扩展开放、对修改关闭)。 -MyBatis 使用的日志接口是自己定义的 Log 接口,但是 Apache Commons Logging、Log4j、Log4j2 等日志框架提供给用户的都是自己的 Logger 接口。为了统一这些第三方日志框架, **MyBatis 使用适配器模式添加了针对不同日志框架的 Adapter 实现** ,使得第三方日志框架的 Logger 接口转换成 MyBatis 中的 Log 接口,从而实现集成第三方日志框架打印日志的功能。 +MyBatis 使用的日志接口是自己定义的 Log 接口,但是 Apache Commons Logging、Log4j、Log4j2 等日志框架提供给用户的都是自己的 Logger 接口。为了统一这些第三方日志框架,**MyBatis 使用适配器模式添加了针对不同日志框架的 Adapter 实现**,使得第三方日志框架的 Logger 接口转换成 MyBatis 中的 Log 接口,从而实现集成第三方日志框架打印日志的功能。 -### 日志模块 **MyBatis 自定义的 Log 接口位于 org.apache.ibatis.logging 包中,相关的适配器也位于该包中** ,下面我们就来看看该模块的具体实现 +### 日志模块 **MyBatis 自定义的 Log 接口位于 org.apache.ibatis.logging 包中,相关的适配器也位于该包中**,下面我们就来看看该模块的具体实现 首先是 LogFactory 工厂类,它负责创建 Log 对象。这些 Log 接口的实现类中,就包含了多种第三方日志框架的适配器,如下图所示: @@ -103,7 +103,7 @@ public class Jdk14LoggingImpl implements Log { 在使用的时候,会将 RealSubject 对象封装到 Proxy 对象中,然后访问 Proxy 的相关方法,而不是直接访问 RealSubject 对象。在 Proxy 的方法实现中,不仅会调用 RealSubject 对象的相应方法完成业务逻辑,还会在 RealSubject 方法执行前后进行预处理和后置处理。 -通过对代理模式的描述可知, **Proxy 能够控制使用方对 RealSubject 对象的访问,或是在执行业务逻辑之前执行统一的预处理逻辑,在执行业务逻辑之后执行统一的后置处理逻辑** 。 **代理模式除了实现访问控制以外,还能用于实现延迟加载** 。例如,查询数据库涉及网络 I/O 和磁盘 I/O,会是一个比较耗时的操作,有些时候从数据库加载到内存的数据,也并非系统真正会使用到的数据,所以就有了延迟加载这种优化操作。 +通过对代理模式的描述可知,**Proxy 能够控制使用方对 RealSubject 对象的访问,或是在执行业务逻辑之前执行统一的预处理逻辑,在执行业务逻辑之后执行统一的后置处理逻辑** 。**代理模式除了实现访问控制以外,还能用于实现延迟加载** 。例如,查询数据库涉及网络 I/O 和磁盘 I/O,会是一个比较耗时的操作,有些时候从数据库加载到内存的数据,也并非系统真正会使用到的数据,所以就有了延迟加载这种优化操作。 延迟加载可以有效地避免数据库资源的浪费,其主要原理是:用户在访问数据库时,会立刻拿到一个代理对象,此时并没有执行任何 SQL 到数据库中查询数据,代理对象中自然也不会包含任何真正的有效数据;当用户真正需要使用数据时,会访问代理对象,此时会由代理对象去执行 SQL,完成数据库的查询。MyBatis 也提供了延迟加载功能,原理大同小异,具体的实现方式也是通过代理实现的。 @@ -150,7 +150,7 @@ public class Main { } ``` -现在假设有多个业务逻辑类,需要相同的预处理逻辑和后置处理逻辑,那么只需要提供一个 InvocationHandler 接口实现类即可。 **在程序运行过程中,JDK 动态代理会为每个业务类动态生成相应的代理类实现** ,并加载到 JVM 中,然后创建对应的代理实例对象。 +现在假设有多个业务逻辑类,需要相同的预处理逻辑和后置处理逻辑,那么只需要提供一个 InvocationHandler 接口实现类即可。**在程序运行过程中,JDK 动态代理会为每个业务类动态生成相应的代理类实现**,并加载到 JVM 中,然后创建对应的代理实例对象。 下面我们就接着来深入分析一下 JDK 动态代理底层动态创建代理类的原理。不同 JDK 版本 Proxy 类的实现会有些许差异,但总体的核心思路基本一致,这里我们就以 JDK 1.8.0 版本为例进行说明。 @@ -276,7 +276,7 @@ public final class $Proxy143 了解了代理模式以及 JDK 动态代理的基础知识之后,下面我们开始分析 org.apache.ibatis.logging.jdbc 包中的内容。 -首先来看其中 **最基础的抽象类—— BaseJdbcLogger,它是 jdbc 包下其他 Logger 类的父类** ,继承关系如下图所示: +首先来看其中 **最基础的抽象类—— BaseJdbcLogger,它是 jdbc 包下其他 Logger 类的父类**,继承关系如下图所示: ![1.png](assets/CioPOWAfYsuAc9WjAAFm7izVaMI477.png) diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25407\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25407\350\256\262.md" index efb316f3c..d5bbd4bb0 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25407\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25407\350\256\262.md" @@ -2,13 +2,13 @@ 数据源是持久层框架中最核心的组件之一,在实际工作中比较常见的数据源有 C3P0、Apache Common DBCP、Proxool 等。作为一款成熟的持久化框架,MyBatis 不仅自己提供了一套数据源实现,而且还能够方便地集成第三方数据源。 -**javax.sql.DataSource 是 Java 语言中用来抽象数据源的接口** ,其中定义了所有数据源实现的公共行为,MyBatis 自身提供的数据源实现也要实现该接口。MyBatis 提供了两种类型的数据源实现,分别是 PooledDataSource 和 UnpooledDataSource,继承关系如下图所示: +**javax.sql.DataSource 是 Java 语言中用来抽象数据源的接口**,其中定义了所有数据源实现的公共行为,MyBatis 自身提供的数据源实现也要实现该接口。MyBatis 提供了两种类型的数据源实现,分别是 PooledDataSource 和 UnpooledDataSource,继承关系如下图所示: ![1.png](assets/Cgp9HWApSnyAeZddAADG9kv9Y-c887.png) 针对不同的 DataSource 实现,MyBatis 提供了不同的工厂实现来进行创建,如下图所示,这是工厂方法模式的一个典型应用场景。 -![2.png](assets/CioPOWApSomAM5hXAADlDsSaiAY054.png) **编写一个设计合理、性能优秀的数据源只是第一步** ,在通过数据源拿到数据库连接之后,还需要开启事务,才能进行数据的修改。MyBatis 对数据库事务进行了一层抽象,也就是我们这一讲后面要介绍的 Transaction 接口,它可以 **管理事务的开启、提交和回滚** 。 +![2.png](assets/CioPOWApSomAM5hXAADlDsSaiAY054.png) **编写一个设计合理、性能优秀的数据源只是第一步**,在通过数据源拿到数据库连接之后,还需要开启事务,才能进行数据的修改。MyBatis 对数据库事务进行了一层抽象,也就是我们这一讲后面要介绍的 Transaction 接口,它可以 **管理事务的开启、提交和回滚** 。 ### 工厂方法模式 @@ -29,7 +29,7 @@ ### 数据源工厂 -了解了工厂方法模式的基础知识之后,下面我们回到 MyBatis 的数据源实现上来。 **MyBatis 的数据源模块也是用到了工厂方法模式,如果需要扩展新的数据源实现时,只需要添加对应的 Factory 实现类,新的数据源就可以被 MyBatis 使用。** DataSourceFactory 接口就扮演了 MyBatis 数据源实现中的 Factory 接口角色。UnpooledDataSourceFactory 和 PooledDataSourceFactory 实现了 DataSourceFactory 接口,也就是 Factory 接口实现类的角色。三者的继承关系如下图所示: +了解了工厂方法模式的基础知识之后,下面我们回到 MyBatis 的数据源实现上来。**MyBatis 的数据源模块也是用到了工厂方法模式,如果需要扩展新的数据源实现时,只需要添加对应的 Factory 实现类,新的数据源就可以被 MyBatis 使用。** DataSourceFactory 接口就扮演了 MyBatis 数据源实现中的 Factory 接口角色。UnpooledDataSourceFactory 和 PooledDataSourceFactory 实现了 DataSourceFactory 接口,也就是 Factory 接口实现类的角色。三者的继承关系如下图所示: ![5.png](assets/Cgp9HWApSreAMsSEAADxE9_08B0637.png) @@ -39,7 +39,7 @@ DataSourceFactory 接口中最核心的方法是 getDataSource() 方法,该方 UnpooledDataSourceFactory 对于 getDataSource() 方法的实现就相对简单了,其中直接返回了上面创建的 UnpooledDataSource 对象。 -从前面介绍的 DataSourceFactory 继承关系图中可以看到, **PooledDataSourceFactory 是通过继承 UnpooledDataSourceFactory 间接实现了 DataSourceFactory 接口** 。在 PooledDataSourceFactory 中并没有覆盖 UnpooledDataSourceFactory 中的任何方法,唯一的变化就是将 dataSource 字段指向的 DataSource 对象类型改为 PooledDataSource 类型。 +从前面介绍的 DataSourceFactory 继承关系图中可以看到,**PooledDataSourceFactory 是通过继承 UnpooledDataSourceFactory 间接实现了 DataSourceFactory 接口** 。在 PooledDataSourceFactory 中并没有覆盖 UnpooledDataSourceFactory 中的任何方法,唯一的变化就是将 dataSource 字段指向的 DataSource 对象类型改为 PooledDataSource 类型。 ### DataSource **JDK 提供的 javax.sql.DataSource 接口在 MyBatis 数据源中扮演了 Product 接口的角色。** MyBatis 提供的数据源实现有两个,一个 UnpooledDataSource 实现,另一个 PooledDataSource 实现,它们都是 Product 具体实现类的角色 @@ -96,7 +96,7 @@ JDBC 连接的创建是非常耗时的,从数据库这一侧看,能够建立 使用池化技术缓存数据库连接会带来很多好处,例如: - 在空闲时段 **缓存** 一定数量的数据库连接备用,防止被突发流量冲垮; -- 实现数据库连接的 **重用** ,从而提高系统的响应速度; +- 实现数据库连接的 **重用**,从而提高系统的响应速度; - **控制** 数据库连接上限,防止连接过多造成数据库假死; - **统一** 管理数据库连接,避免连接泄漏。 @@ -117,7 +117,7 @@ JDBC 连接的创建是非常耗时的,从数据库这一侧看,能够建立 ##### (1)PooledConnection -**PooledConnection 是 MyBatis 中定义的一个 InvocationHandler 接口实现类** ,其中封装了真正的 java.sql.Connection 对象以及相关的代理对象,这里的代理对象就是通过上一讲介绍的 JDK 动态代理产生的。 +**PooledConnection 是 MyBatis 中定义的一个 InvocationHandler 接口实现类**,其中封装了真正的 java.sql.Connection 对象以及相关的代理对象,这里的代理对象就是通过上一讲介绍的 JDK 动态代理产生的。 下面来看 PooledConnection 中的核心字段。 @@ -132,7 +132,7 @@ JDBC 连接的创建是非常耗时的,从数据库这一侧看,能够建立 下面来看 PooledConnection 的构造方法,其中会初始化上述字段,这里尤其关注 proxyConnection 这个 Connection 代理对象的初始化,使用的是 JDK 动态代理的方式实现的,其中传入的 InvocationHandler 实现正是 PooledConnection 自身。 -**PooledConnection.invoke() 方法中只对 close() 方法进行了拦截** ,具体实现如下: +**PooledConnection.invoke() 方法中只对 close() 方法进行了拦截**,具体实现如下: ```plaintext public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { @@ -153,7 +153,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl ##### (2)PoolState -接下来看 **PoolState** 这个类,它 **负责管理连接池中所有 PooledConnection 对象的状态** ,维护了两个 ArrayList `` 集合按照 PooledConnection 对象的状态分类存储,其中 idleConnections 集合用来存储空闲状态的 PooledConnection 对象,activeConnections 集合用来存储活跃状态的 PooledConnection 对象。 +接下来看 **PoolState** 这个类,它 **负责管理连接池中所有 PooledConnection 对象的状态**,维护了两个 ArrayList `` 集合按照 PooledConnection 对象的状态分类存储,其中 idleConnections 集合用来存储空闲状态的 PooledConnection 对象,activeConnections 集合用来存储活跃状态的 PooledConnection 对象。 另外,PoolState 中还定义了多个 long 类型的统计字段。 @@ -172,7 +172,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl 首先是 getConnection() 方法,其中先是依赖 popConnection() 方法获取 PooledConnection 对象,然后从 PooledConnection 中获取数据库连接的代理对象(即前面介绍的 proxyConnection 字段)。 -**这里调用的 popConnection() 方法是从连接池中获取数据库连接的核心** ,具体步骤如下。 +**这里调用的 popConnection() 方法是从连接池中获取数据库连接的核心**,具体步骤如下。 1. 检测当前连接池中是否有空闲的有效连接,如果有,则直接返回连接;如果没有,则继续执行下一步。 1. 检查连接池当前的活跃连接数是否已经达到上限值,如果未达到,则尝试创建一个新的数据库连接,并在创建成功之后,返回新建的连接;如果已达到最大上限,则往下执行。 @@ -310,7 +310,7 @@ protected void pushConnection(PooledConnection conn) throws SQLException { #### (5)检测连接可用性 -通过对上述 pushConnection() 方法和 popConnection() 方法的分析,我们大致了解了 PooledDataSource 的核心实现。正如我们看到的那样, **这两个方法都需要检测一个数据库连接是否可用,这是通过 PooledConnection.isValid() 方法实现的** ,在该方法中会检测三个方面: +通过对上述 pushConnection() 方法和 popConnection() 方法的分析,我们大致了解了 PooledDataSource 的核心实现。正如我们看到的那样,**这两个方法都需要检测一个数据库连接是否可用,这是通过 PooledConnection.isValid() 方法实现的**,在该方法中会检测三个方面: - valid 字段值为 true; - realConnection 字段值不为空; @@ -358,13 +358,13 @@ protected boolean pingConnection(PooledConnection conn) { 当我们从数据源中得到一个可用的数据库连接之后,就可以开启一个数据库事务了,事务成功开启之后,我们才能修改数据库中的数据。在修改完成之后,我们需要提交事务,完成整个事务内的全部修改操作,如果修改过程中出现异常,我们也可以回滚事务,放弃整个事务中的全部修改操作。 -可见, **控制事务在一个以数据库为基础的服务中,是一件非常重要的工作** 。为此,MyBatis 专门抽象出来一个 Transaction 接口,好在相较于我们上面讲述的数据源,这部分内容还是比较简单、比较好理解的。 +可见,**控制事务在一个以数据库为基础的服务中,是一件非常重要的工作** 。为此,MyBatis 专门抽象出来一个 Transaction 接口,好在相较于我们上面讲述的数据源,这部分内容还是比较简单、比较好理解的。 -**Transaction 接口是 MyBatis 中对数据库事务的抽象** ,其中定义了 **提交事务、回滚事务** ,以及 **获取事务底层数据库连接** 的方法。 +**Transaction 接口是 MyBatis 中对数据库事务的抽象**,其中定义了 **提交事务、回滚事务**,以及 **获取事务底层数据库连接** 的方法。 JdbcTransaction、ManagedTransaction 是 MyBatis 自带的两个 Transaction 接口实现,这里也使用到了工厂方法模式,如下图所示: -![6.png](assets/CioPOWApSyGAb7OzAAFWzMbS1T8710.png) **TransactionFactory 是用于创建 Transaction 的工厂接口** ,其中最核心的方法是 newTransaction() 方法,它会根据数据库连接或数据源创建 Transaction 对象。 +![6.png](assets/CioPOWApSyGAb7OzAAFWzMbS1T8710.png) **TransactionFactory 是用于创建 Transaction 的工厂接口**,其中最核心的方法是 newTransaction() 方法,它会根据数据库连接或数据源创建 Transaction 对象。 JdbcTransactionFactory 和 ManagedTransactionFactory 是 TransactionFactory 的两个实现类,分别用来创建 JdbcTransaction 对象和 ManagedTransaction 对象,具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/mybatis)进行学习。 diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25408\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25408\350\256\262.md" index 33621a6ab..02299111e 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25408\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25408\350\256\262.md" @@ -16,9 +16,9 @@ MyBatis 的前身是 iBatis,我们在使用 iBatis 的时候,如果想查询 MyBatis 中的 Mapper 接口就可以很好地解决这个问题。 -示例中的 CustomerMapper 接口中定义了 SQL 语句唯一标识同名的 find() 方法,我们在写代码的时候使用的是 CustomerMapper.find() 方法,如果拼写成 CustomerMapper.finb(),编译会失败。这是因为 MyBatis 初始化的时候会尝试 **将 CustomerMapper 接口中的 find() 方法名与 CustomerMapper.xml 配置文件中的 SQL 唯一标识进行映射** ,如果 SQL 语句唯一标识写错成“finb”,MyBatis 会发现这个错误,并在初始化过程中就抛出异常,这样编译器以及 MyBatis 就可以帮助我们更早发现异常,避免线上流量的损失。 +示例中的 CustomerMapper 接口中定义了 SQL 语句唯一标识同名的 find() 方法,我们在写代码的时候使用的是 CustomerMapper.find() 方法,如果拼写成 CustomerMapper.finb(),编译会失败。这是因为 MyBatis 初始化的时候会尝试 **将 CustomerMapper 接口中的 find() 方法名与 CustomerMapper.xml 配置文件中的 SQL 唯一标识进行映射**,如果 SQL 语句唯一标识写错成“finb”,MyBatis 会发现这个错误,并在初始化过程中就抛出异常,这样编译器以及 MyBatis 就可以帮助我们更早发现异常,避免线上流量的损失。 -**在 MyBatis 中,实现 CustomerMapper 接口与 CustomerMapper.xml 配置文件映射功能的是 binding 模块** ,其中涉及的核心类如下图所示: +**在 MyBatis 中,实现 CustomerMapper 接口与 CustomerMapper.xml 配置文件映射功能的是 binding 模块**,其中涉及的核心类如下图所示: ![图片5.png](assets/CioPOWAs1vuAT8xsAAEXCFQuFx4772.png) @@ -39,7 +39,7 @@ binding 模块核心组件关系图 ### MapperProxyFactory -正如分析 MapperRegistry 时介绍的那样, **MapperProxyFactory 的核心功能就是创建 Mapper 接口的代理对象** ,其底层核心原理就是前面《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》介绍的 JDK 动态代理。 +正如分析 MapperRegistry 时介绍的那样,**MapperProxyFactory 的核心功能就是创建 Mapper 接口的代理对象**,其底层核心原理就是前面《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》介绍的 JDK 动态代理。 在 MapperRegistry 中会依赖 MapperProxyFactory 的 newInstance() 方法创建代理对象,底层则是通过 JDK 动态代理的方式生成代理对象的,如下代码所示,这里使用的 InvocationHandler 实现是 MapperProxy。 @@ -67,7 +67,7 @@ protected T newInstance(MapperProxy mapperProxy) { #### 1. MethodHandle 简介 -从 Java 7 开始,除了反射之外,在 java.lang.invoke 包中新增了 MethodHandle 这个类,它的基本功能与反射中的 Method 类似,但它比反射更加灵活。 **反射是 Java API 层面支持的一种机制,MethodHandle 则是 JVM 层支持的机制,相较而言,反射更加重量级,MethodHandle 则更轻量级,性能也比反射更好些** 。 +从 Java 7 开始,除了反射之外,在 java.lang.invoke 包中新增了 MethodHandle 这个类,它的基本功能与反射中的 Method 类似,但它比反射更加灵活。**反射是 Java API 层面支持的一种机制,MethodHandle 则是 JVM 层支持的机制,相较而言,反射更加重量级,MethodHandle 则更轻量级,性能也比反射更好些** 。 使用 MethodHandle 进行方法调用的时候,往往会涉及下面几个核心步骤: @@ -117,7 +117,7 @@ public class MethodHandleDemo { 介绍完 MethodHandle 的基础之后,我们回到 MethodProxy 继续分析。 -**MapperProxy.invoke() 方法是代理对象执行的入口** ,其中会拦截所有非 Object 方法,针对每个被拦截的方法,都会调用 cachedInvoker() 方法获取对应的 MapperMethod 对象,并调用其 invoke() 方法执行代理逻辑以及目标方法。 +**MapperProxy.invoke() 方法是代理对象执行的入口**,其中会拦截所有非 Object 方法,针对每个被拦截的方法,都会调用 cachedInvoker() 方法获取对应的 MapperMethod 对象,并调用其 invoke() 方法执行代理逻辑以及目标方法。 在 cachedInvoker() 方法中,首先会查询 methodCache 缓存,如果查询的方法为 default 方法,则会根据当前使用的 JDK 版本,获取对应的 MethodHandle 并封装成 DefaultMethodInvoker 对象写入缓存;如果查询的方法是非 default 方法,则创建 PlainMethodInvoker 对象写入缓存。 @@ -177,7 +177,7 @@ public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlS ### MapperMethod -通过对 MapperProxy 的分析我们知道, **MapperMethod 是最终执行 SQL 语句的地方,同时也记录了 Mapper 接口中的对应方法** ,其核心字段也围绕这两方面的内容展开。 +通过对 MapperProxy 的分析我们知道,**MapperMethod 是最终执行 SQL 语句的地方,同时也记录了 Mapper 接口中的对应方法**,其核心字段也围绕这两方面的内容展开。 #### 1. SqlCommand **MapperMethod 的第一个核心字段是 command(SqlCommand 类型),其中维护了关联 SQL 语句的相关信息** 。在 MapperMethodSqlCommand 这个内部类中,通过 name 字段记录了关联 SQL 语句的唯一标识,通过 type 字段(SqlCommandType 类型)维护了 SQL 语句的操作类型,这里 SQL 语句的操作类型分为 INSERT、UPDATE、DELETE、SELECT 和 FLUSH 五种 @@ -331,7 +331,7 @@ public MethodSignature(Configuration configuration, Class mapperInterface, Me 分析完 MapperMethod 中的几个核心内部类,我们回到 MapperMethod 继续介绍。 -execute() 方法是 MapperMethod 中最核心的方法之一。 **execute() 方法会根据要执行的 SQL 语句的具体类型执行 SqlSession 的相应方法完成数据库操作** ,其核心实现如下: +execute() 方法是 MapperMethod 中最核心的方法之一。**execute() 方法会根据要执行的 SQL 语句的具体类型执行 SqlSession 的相应方法完成数据库操作**,其核心实现如下: ```plaintext public Object execute(SqlSession sqlSession, Object[] args) { diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25409\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25409\350\256\262.md" index 4d0c4290a..27ea51866 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25409\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25409\350\256\262.md" @@ -18,7 +18,7 @@ MyBatis 的缓存分为一级缓存、二级缓存两个级别,并且都实现 正是由于这些缺点的存在,所以应该尽量多地使用组合方式进行扩展,尽量少使用继承方式进行扩展,除非迫不得已。 -**装饰器模式就是一种通过组合方式实现扩展的设计模式** ,它可以完美地解决上述功能增强的问题。装饰器的核心思想是为已有实现类创建多个包装类,由这些新增的包装类完成新需求的扩展。 **装饰器模式使用的是组合方式,相较于继承这种静态的扩展方式,装饰器模式可以在运行时根据系统状态,动态决定为一个实现类添加哪些扩展功能。** +**装饰器模式就是一种通过组合方式实现扩展的设计模式**,它可以完美地解决上述功能增强的问题。装饰器的核心思想是为已有实现类创建多个包装类,由这些新增的包装类完成新需求的扩展。**装饰器模式使用的是组合方式,相较于继承这种静态的扩展方式,装饰器模式可以在运行时根据系统状态,动态决定为一个实现类添加哪些扩展功能。** 装饰器模式的核心类图,如下所示: @@ -28,9 +28,9 @@ MyBatis 的缓存分为一级缓存、二级缓存两个级别,并且都实现 从图中可以看到,装饰器模式中的核心类主要有下面四个。 -- Component 接口:已有的业务接口,是 **整个功能的核心抽象** ,定义了 Decorator 和 ComponentImpl 这些实现类的核心行为。JDK 中的 IO 流体系就使用了装饰器模式,其中的 InputStream 接口就扮演了 Component 接口的角色。 -- ComponentImpl 实现类:实现了上面介绍的 Component 接口,其中 **实现了 Component 接口最基础、最核心的功能** ,也就是被装饰的、原始的基础类。在 JDK IO 流体系之中的 FileInputStream 就扮演了 ComponentImpl 的角色,它实现了读取文件的基本能力,例如,读取单个 byte、读取 byte\[\] 数组。 -- Decorator 抽象类:所有装饰器的父类,实现了 Component 接口, **其核心不是提供新的扩展能力,而是封装一个 Component 类型的字段,也就是被装饰的目标对象** 。需要注意的是,这个被装饰的对象可以是 ComponentImpl 对象,也可以是 Decorator 实现类的对象,之所以这么设计,就是为了实现下图的装饰器嵌套。这里的 DecoratorImpl1 装饰了 DecoratorImpl2,DecoratorImpl2 装饰了 ComponentImpl,经过了这一系列装饰之后得到的 Component 对象,除了具有 ComponentImpl 的基础能力之外,还拥有了 DecoratorImpl1 和 DecoratorImpl2 的扩展能力。JDK IO 流体系中的 FilterInputStream 就扮演了 Decorator 的角色。 +- Component 接口:已有的业务接口,是 **整个功能的核心抽象**,定义了 Decorator 和 ComponentImpl 这些实现类的核心行为。JDK 中的 IO 流体系就使用了装饰器模式,其中的 InputStream 接口就扮演了 Component 接口的角色。 +- ComponentImpl 实现类:实现了上面介绍的 Component 接口,其中 **实现了 Component 接口最基础、最核心的功能**,也就是被装饰的、原始的基础类。在 JDK IO 流体系之中的 FileInputStream 就扮演了 ComponentImpl 的角色,它实现了读取文件的基本能力,例如,读取单个 byte、读取 byte\[\] 数组。 +- Decorator 抽象类:所有装饰器的父类,实现了 Component 接口,**其核心不是提供新的扩展能力,而是封装一个 Component 类型的字段,也就是被装饰的目标对象** 。需要注意的是,这个被装饰的对象可以是 ComponentImpl 对象,也可以是 Decorator 实现类的对象,之所以这么设计,就是为了实现下图的装饰器嵌套。这里的 DecoratorImpl1 装饰了 DecoratorImpl2,DecoratorImpl2 装饰了 ComponentImpl,经过了这一系列装饰之后得到的 Component 对象,除了具有 ComponentImpl 的基础能力之外,还拥有了 DecoratorImpl1 和 DecoratorImpl2 的扩展能力。JDK IO 流体系中的 FilterInputStream 就扮演了 Decorator 的角色。 ![图片1.png](assets/Cgp9HWAwziyAK06rAAEYwjbLXEw190.png) @@ -40,9 +40,9 @@ Decorator 与 Component 的引用关系 ### Cache 接口及核心实现 -Cache 接口是 MyBatis 缓存中 **最顶层的抽象接口** ,位于 org.apache.ibatis.cache 包中, **定义了 MyBatis 缓存最核心、最基础的行为** 。 +Cache 接口是 MyBatis 缓存中 **最顶层的抽象接口**,位于 org.apache.ibatis.cache 包中,**定义了 MyBatis 缓存最核心、最基础的行为** 。 -**Cache 接口中的核心方法主要是 putObject()、getObject() 和 removeObject() 三个方法,分别用来写入、查询和删除缓存数据。** Cache 接口有非常多的实现类(如下图), **其中的 PerpetualCache 扮演了装饰器模式中 ComponentImpl 这个角色** ,实现了 Cache 接口缓存数据的基本能力。 +**Cache 接口中的核心方法主要是 putObject()、getObject() 和 removeObject() 三个方法,分别用来写入、查询和删除缓存数据。** Cache 接口有非常多的实现类(如下图),**其中的 PerpetualCache 扮演了装饰器模式中 ComponentImpl 这个角色**,实现了 Cache 接口缓存数据的基本能力。 ![图片2.png](assets/Cgp9HWAwzjuACuz3AAFGuWssY7o524.png) @@ -106,7 +106,7 @@ BlockingCache 核心原理图 MyBatis 中的缓存本质上就是 JVM 堆中的一块内存,我们需要严格控制 Cache 的大小,防止 Cache 占用内存过大而影响程序的性能。操作系统有很多缓存淘汰规则,MyBatis 也提供了类似的规则来清理缓存。 -这就引出了 FifoCache 装饰器,它是 FIFO(先入先出)策略的装饰器。在系统运行过程中,我们会不断向 Cache 中增加缓存条目, **当 Cache 中的缓存条目达到上限的时候,则会将 Cache 中最早写入的缓存条目清理掉,这也就是先入先出的基本原理** 。 +这就引出了 FifoCache 装饰器,它是 FIFO(先入先出)策略的装饰器。在系统运行过程中,我们会不断向 Cache 中增加缓存条目,**当 Cache 中的缓存条目达到上限的时候,则会将 Cache 中最早写入的缓存条目清理掉,这也就是先入先出的基本原理** 。 FifoCache 作为一个 Cache 装饰器,自然也会包含一个指向 Cache 的字段(也就是 delegate 字段),同时它还维护了两个与 FIFO 相关的字段:一个是 keyList 队列(LinkedList),主要利用 LinkedList 集合有序性,记录缓存条目写入 Cache 的先后顺序;另一个是当前 Cache 的大小上限(size 字段),当 Cache 大小超过该值时,就会从 keyList 集合中查找最早的缓存条目并进行清理。 @@ -163,15 +163,15 @@ private void cycleKeyList(Object key) { #### 4. SoftCache -看到 SoftCache 这个名字,有一定 Java 经验的同学可能会立刻联想到 Java 中的软引用(Soft Reference),所以这里我们就先来简单回顾一下什么是强引用和软引用,以及这些引用的相关机制。 **强引用是 JVM 中最普遍的引用,我们常用的赋值操作就是强引用** ,例如,`Person p = new Person();` 这条语句会将新创建的 Person 对象赋值为 p 这个变量,p 这个变量指向这个 Person 对象的引用,就是强引用。这个 Person 对象被引用的时候,即使是 JVM 内存空间不足触发 GC,甚至是内存溢出(OutOfMemoryError),也不会回收这个 Person 对象。 +看到 SoftCache 这个名字,有一定 Java 经验的同学可能会立刻联想到 Java 中的软引用(Soft Reference),所以这里我们就先来简单回顾一下什么是强引用和软引用,以及这些引用的相关机制。**强引用是 JVM 中最普遍的引用,我们常用的赋值操作就是强引用**,例如,`Person p = new Person();` 这条语句会将新创建的 Person 对象赋值为 p 这个变量,p 这个变量指向这个 Person 对象的引用,就是强引用。这个 Person 对象被引用的时候,即使是 JVM 内存空间不足触发 GC,甚至是内存溢出(OutOfMemoryError),也不会回收这个 Person 对象。 软引用比强引用稍微弱一些。当 JVM 内存不足时,GC 才会回收那些只被软引用指向的对象,从而避免 OutOfMemoryError。当 GC 将只被软引用指向的对象全部回收之后,内存依然不足时,JVM 才会抛出 OutOfMemoryError。根据软引用的这一特性,我们会发现 **软引用特别适合做缓存,因为缓存中的数据可以从数据库中恢复,所以即使因为 JVM 内存不足而被回收掉,也可以通过数据库恢复缓存中的对象** 。 在使用软引用的时候,需要注意一点: **当拿到一个软引用的时候,我们需要先判断其 get() 方法返回值是否为 null** 。如果为 null,则表示这个软引用指向的对象在之前的某个时刻,已经被 GC 掉了;如果不为 null,则表示这个软引用指向的对象还存活着。 -在有的场景中,我们可能需要在一个对象的可达性(是否已经被回收)发生变化时,得到相应的通知, **引用队列(Reference Queue)** 就是用来实现这个需求的。在创建 SoftReference 对象的时候,我们可以为其关联一个引用队列,当这个 SoftReference 指向的对象被回收的时候,JVM 就会将这个 SoftReference 作为通知,添加到与其关联的引用队列,之后我们就可以从引用队列中,获取这些通知信息,也就是 SoftReference 对象。 +在有的场景中,我们可能需要在一个对象的可达性(是否已经被回收)发生变化时,得到相应的通知,**引用队列(Reference Queue)** 就是用来实现这个需求的。在创建 SoftReference 对象的时候,我们可以为其关联一个引用队列,当这个 SoftReference 指向的对象被回收的时候,JVM 就会将这个 SoftReference 作为通知,添加到与其关联的引用队列,之后我们就可以从引用队列中,获取这些通知信息,也就是 SoftReference 对象。 -下面我们正式开始介绍 SoftCache。SoftCache 中的 value 是 SoftEntry 类型的对象,这里的 SoftEntry 是 SoftCache 的内部类,继承了 SoftReference, **其中指向 key 的引用是强引用,指向 value 的引用是软引用** ,具体实现如下: +下面我们正式开始介绍 SoftCache。SoftCache 中的 value 是 SoftEntry 类型的对象,这里的 SoftEntry 是 SoftCache 的内部类,继承了 SoftReference,**其中指向 key 的引用是强引用,指向 value 的引用是软引用**,具体实现如下: ```java private static class SoftEntry extends SoftReference { diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25410\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25410\350\256\262.md" index dfedccf05..5798a7ffc 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25410\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25410\350\256\262.md" @@ -2,11 +2,11 @@ 很多开源框架之所以能够流行起来,是因为它们解决了领域内的一些通用问题。但在实际使用这些开源框架的时候,我们都是要解决通用问题中的一个特例问题,所以这时我们就需要使用一种方式来控制开源框架的行为,这就是开源框架提供各种各样配置的核心原因之一。 -现在控制开源框架行为主流的配置方式就是 XML 配置方式和注解方式。在《02 | 订单系统持久层示例分析,20 分钟带你快速上手 MyBatis》这一讲中我们介绍过,MyBatis 有两方面的 XML 配置, **一个是 mybatis-config.xml 配置文件中的整体配置,另一个是 Mapper.xml 配置文件中的 SQL 语句** 。当然,MyBatis 中也有注解,前面的课程中也多少有涉及,其核心实现与 XML 配置基本类似,所以这一讲我们就重点分析 XML 配置的初始化过程,注解相关的内容就留给你自己分析了。 +现在控制开源框架行为主流的配置方式就是 XML 配置方式和注解方式。在《02 | 订单系统持久层示例分析,20 分钟带你快速上手 MyBatis》这一讲中我们介绍过,MyBatis 有两方面的 XML 配置,**一个是 mybatis-config.xml 配置文件中的整体配置,另一个是 Mapper.xml 配置文件中的 SQL 语句** 。当然,MyBatis 中也有注解,前面的课程中也多少有涉及,其核心实现与 XML 配置基本类似,所以这一讲我们就重点分析 XML 配置的初始化过程,注解相关的内容就留给你自己分析了。 -在初始化的过程中,MyBatis 会读取 mybatis-config.xml 这个全局配置文件以及所有的 Mapper 映射配置文件,同时还会加载这两个配置文件中指定的类,解析类中的相关注解,最终将解析得到的信息转换成配置对象。 **完成配置加载之后,MyBatis 就会根据得到的配置对象初始化各个模块** 。 +在初始化的过程中,MyBatis 会读取 mybatis-config.xml 这个全局配置文件以及所有的 Mapper 映射配置文件,同时还会加载这两个配置文件中指定的类,解析类中的相关注解,最终将解析得到的信息转换成配置对象。**完成配置加载之后,MyBatis 就会根据得到的配置对象初始化各个模块** 。 -MyBatis 在加载配置文件、创建配置对象的时候,会使用到经典设计模式中的 **构造者模式** ,所以下面我们就来先介绍一下构造者模式的知识点。 +MyBatis 在加载配置文件、创建配置对象的时候,会使用到经典设计模式中的 **构造者模式**,所以下面我们就来先介绍一下构造者模式的知识点。 ### 构造者模式 @@ -27,13 +27,13 @@ MyBatis 在加载配置文件、创建配置对象的时候,会使用到经典 使用构造者模式一般有两个目的。第一个目的是 **将使用方与复杂对象的内部细节隔离,从而实现解耦的效果** 。使用方提供的所有信息,都是由 Builder 这个“中间商”接收的,然后由 Builder 消化这些信息并构造出一个完整可用的 Product 对象。第二个目的是 **简化复杂对象的构造过程** 。在很多场景中,复杂对象可能有很多默认属性,这时我们就可以将这些默认属性封装到 Builder 中,这样就可以简化创建复杂对象所需的信息。 -通过构建者模式的类图我们还可以看出, **每个 BuilderImpl 实现都是能够独立创建出对应的 ProductImpl 对象** ,那么在程序需要扩展的时候,我们只需要添加新的 BuilderImpl 和 ProductImpl,就能实现功能的扩展,这完全符合“开放-封闭原则”。 +通过构建者模式的类图我们还可以看出,**每个 BuilderImpl 实现都是能够独立创建出对应的 ProductImpl 对象**,那么在程序需要扩展的时候,我们只需要添加新的 BuilderImpl 和 ProductImpl,就能实现功能的扩展,这完全符合“开放-封闭原则”。 ### mybatis-config.xml 解析全流程 介绍完构造者模式相关的知识点之后,下面我们正式开始介绍 MyBatis 的初始化过程。 -**MyBatis 初始化的第一个步骤就是加载和解析 mybatis-config.xml 这个全局配置文件** ,入口是 XMLConfigBuilder 这个 Builder 对象,它由 SqlSessionFactoryBuilder.build() 方法创建。XMLConfigBuilder 会解析 mybatis-config.xml 配置文件得到对应的 Configuration 全局配置对象,然后 SqlSessionFactoryBuilder 会根据得到的 Configuration 全局配置对象创建一个 DefaultSqlSessionFactory 对象返回给上层使用。 +**MyBatis 初始化的第一个步骤就是加载和解析 mybatis-config.xml 这个全局配置文件**,入口是 XMLConfigBuilder 这个 Builder 对象,它由 SqlSessionFactoryBuilder.build() 方法创建。XMLConfigBuilder 会解析 mybatis-config.xml 配置文件得到对应的 Configuration 全局配置对象,然后 SqlSessionFactoryBuilder 会根据得到的 Configuration 全局配置对象创建一个 DefaultSqlSessionFactory 对象返回给上层使用。 这里 **创建的 XMLConfigBuilder 对象的核心功能就是解析 mybatis-config.xml 配置文件** 。XMLConfigBuilder 有一部分能力继承自 BaseBuilder 抽象类,具体继承关系如下图所示: @@ -49,8 +49,8 @@ BaseBuilder 抽象类扮演了构造者模式中 Builder 接口的角色,下 除了关联 Configuration 对象之外,BaseBuilder 还提供了另外两个基本能力: -- **解析别名** ,核心逻辑是在 resolveAlias() 方法中实现的,主要依赖于 TypeAliasRegistry 对象; -- **解析 TypeHandler** ,核心逻辑是在 resolveTypeHandler() 方法中实现的,主要依赖于 TypeHandlerRegistry 对象。 +- **解析别名**,核心逻辑是在 resolveAlias() 方法中实现的,主要依赖于 TypeAliasRegistry 对象; +- **解析 TypeHandler**,核心逻辑是在 resolveTypeHandler() 方法中实现的,主要依赖于 TypeHandlerRegistry 对象。 了解了 BaseBuilder 提供的基础能力之后,我们回到 XMLConfigBuilder 这个 Builder 实现类,看看它是如何解析 mybatis-config.xml 配置文件的。 @@ -61,7 +61,7 @@ BaseBuilder 抽象类扮演了构造者模式中 Builder 接口的角色,下 - environment(String 类型): 标签定义的环境名称。 - localReflectorFactory(ReflectorFactory 类型):ReflectorFactory 接口的核心功能是实现对 Reflector 对象的创建和缓存。 -在 SqlSessionFactoryBuilder.build() 方法中也可以看到,XMLConfigBuilder.parse() 方法触发了 mybatis-config.xml 配置文件的解析, **其中的 parseConfiguration() 方法定义了解析 mybatis-config.xml 配置文件的完整流程** ,核心步骤如下: +在 SqlSessionFactoryBuilder.build() 方法中也可以看到,XMLConfigBuilder.parse() 方法触发了 mybatis-config.xml 配置文件的解析,**其中的 parseConfiguration() 方法定义了解析 mybatis-config.xml 配置文件的完整流程**,核心步骤如下: - 解析 `` 标签; - 解析 `` 标签; @@ -199,7 +199,7 @@ if (context != null) { #### 6. 处理``标签 -在 MyBatis 中,我们可以通过 `` 标签为不同的环境添加不同的配置,例如,线上环境、预上线环境、测试环境等, **每个 标签只会对应一种特定的环境配置** 。 +在 MyBatis 中,我们可以通过 `` 标签为不同的环境添加不同的配置,例如,线上环境、预上线环境、测试环境等,**每个 标签只会对应一种特定的环境配置** 。 environmentsElement() 方法中实现了 XMLConfigBuilder 处理 `` 标签的核心逻辑,它会根据 XMLConfigBuilder.environment 字段值,拿到正确的 `` 标签,然后解析这个环境中使用的 TransactionFactory、DataSource 等核心对象,也就知道了 MyBatis 要请求哪个数据库、如何管理事务等信息。 @@ -238,7 +238,7 @@ private void environmentsElement(XNode context) throws Exception { 在 mybatis-config.xml 配置文件中,我们可以通过 `` 标签定义需要支持的全部数据库的 DatabaseId,在后续编写 Mapper 映射配置文件的时候,就可以为同一个业务场景定义不同的 SQL 语句(带有不同的 DataSourceId),来支持不同的数据库,这里就是靠 DatabaseId 来确定哪个 SQL 语句支持哪个数据库的。 -databaseIdProviderElement() 方法是 XMLConfigBuilder 处理 `` 标签的地方,其中的 **核心就是获取 DatabaseId 值** ,具体实现如下: +databaseIdProviderElement() 方法是 XMLConfigBuilder 处理 `` 标签的地方,其中的 **核心就是获取 DatabaseId 值**,具体实现如下: ```plaintext private void databaseIdProviderElement(XNode context) throws Exception { diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25411\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25411\350\256\262.md" index 5456a388a..a0ed252c8 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25411\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25411\350\256\262.md" @@ -4,7 +4,7 @@ ### Mapper.xml 映射文件解析全流程 -在上一讲分析 mybatis-config.xml 配置文件解析流程的时候我们看到,在 mybatis-config.xml 配置文件中可以定义多个 `` 标签指定 Mapper 配置文件的地址, **MyBatis 会为每个 Mapper.xml 映射文件创建一个 XMLMapperBuilder 实例完成解析** 。 +在上一讲分析 mybatis-config.xml 配置文件解析流程的时候我们看到,在 mybatis-config.xml 配置文件中可以定义多个 `` 标签指定 Mapper 配置文件的地址,**MyBatis 会为每个 Mapper.xml 映射文件创建一个 XMLMapperBuilder 实例完成解析** 。 与 XMLConfigBuilder 类似,XMLMapperBuilder也是具体构造者的角色,继承了 BaseBuilder 这个抽象类,解析 Mapper.xml 映射文件的入口是 XMLMapperBuilder.parse() 方法,其核心步骤如下: @@ -14,7 +14,7 @@ - 处理 configurationElement() 方法中解析失败的 `` 标签; - 处理 configurationElement() 方法中解析失败的SQL 语句标签。 -可以清晰地看到, **configurationElement() 方法才是真正解析 Mapper.xml 映射文件的地方** ,其中定义了处理 Mapper.xml 映射文件的核心流程: +可以清晰地看到,**configurationElement() 方法才是真正解析 Mapper.xml 映射文件的地方**,其中定义了处理 Mapper.xml 映射文件的核心流程: - 获取 `` 标签中的 namespace 属性,同时会进行多种边界检查; - 解析 `` 标签; @@ -27,7 +27,7 @@ #### 1. 处理 `` 标签 -我们知道 Cache 接口及其实现是MyBatis 一级缓存和二级缓存的基础,其中,一级缓存是默认开启的,而二级缓存默认情况下并没有开启,如有需要, **可以通过 标签为指定的namespace 开启二级缓存** 。 +我们知道 Cache 接口及其实现是MyBatis 一级缓存和二级缓存的基础,其中,一级缓存是默认开启的,而二级缓存默认情况下并没有开启,如有需要,**可以通过 标签为指定的namespace 开启二级缓存** 。 XMLMapperBuilder 中解析 `` 标签的 **核心逻辑位于 cacheElement() 方法** 之中,其具体步骤如下: @@ -35,7 +35,7 @@ XMLMapperBuilder 中解析 `` 标签的 **核心逻辑位于 cacheElement - 读取 `` 标签下的子标签信息,这些信息将用于初始化二级缓存; - MapperBuilderAssistant 会根据上述配置信息,创建一个全新的Cache 对象并添加到 Configuration.caches 集合中保存。 -也就是说,解析 `` 标签得到的所有信息将会传给 MapperBuilderAssistant 完成 Cache 对象的创建,创建好的Cache 对象会添加到 Configuration.caches 集合中, **这个 caches 字段是一个StrictMap 类型的集合** ,其中的 Key是Cache 对象的唯一标识,默认值是Mapper.xml 映射文件的namespace,Value 才是真正的二级缓存对应的 Cache 对象。 +也就是说,解析 `` 标签得到的所有信息将会传给 MapperBuilderAssistant 完成 Cache 对象的创建,创建好的Cache 对象会添加到 Configuration.caches 集合中,**这个 caches 字段是一个StrictMap 类型的集合**,其中的 Key是Cache 对象的唯一标识,默认值是Mapper.xml 映射文件的namespace,Value 才是真正的二级缓存对应的 Cache 对象。 这里我们简单介绍一下 StrictMap的特性。 @@ -47,7 +47,7 @@ StrictMap 继承了 HashMap,并且覆盖了 HashMap 的一些行为,例如 了解了 StrictMap 这个集合类的特性之后,我们回到MapperBuilderAssistant 这个类继续分析,在它的 useNewCache() 方法中,会根据前面解析得到的配置信息,通过 CacheBuilder 创建 Cache 对象。 -通过名字你就能猜测到 CacheBuilder 是 Cache 的构造者, **CacheBuilder 中最核心的方法是build() 方法,其中会根据传入的配置信息创建底层存储数据的 Cache 对象以及相关的 Cache 装饰器** ,具体实现如下: +通过名字你就能猜测到 CacheBuilder 是 Cache 的构造者,**CacheBuilder 中最核心的方法是build() 方法,其中会根据传入的配置信息创建底层存储数据的 Cache 对象以及相关的 Cache 装饰器**,具体实现如下: ```java public Cache build() { @@ -84,7 +84,7 @@ public Cache build() { 为了解决这个需求,MyBatis提供了 `` 标签来引用另一个 namespace 的二级缓存。cacheRefElement() 方法是处理 `` 标签的核心逻辑所在,在 Configuration 中维护了一个 cacheRefMap 字段(HashMap\ 类型),其中的 Key 是 `` 标签所属的namespace 标识,Value 值是 `` 标签引用的 namespace 值,这样的话,就可以将两个namespace 关联起来了,即这两个 namespace 共用一个 Cache对象。 -这里会使用到一个叫 CacheRefResolver 的 Cache 引用解析器。 **CacheRefResolver 中记录了被引用的 namespace以及当前 namespace 关联的MapperBuilderAssistant 对象** 。前面在解析 ``标签的时候我们介绍过,MapperBuilderAssistant 会在 useNewCache() 方法中通过 CacheBuilder 创建新的 Cache 对象,并记录到 currentCache 字段。而这里解析 `` 标签的时候,MapperBuilderAssistant 会通过 useCacheRef() 方法从 Configuration.caches 集合中,根据被引用的namespace 查找共享的 Cache 对象来初始化 currentCache,而不再创建新的Cache 对象,从而实现二级缓存的共享。 +这里会使用到一个叫 CacheRefResolver 的 Cache 引用解析器。**CacheRefResolver 中记录了被引用的 namespace以及当前 namespace 关联的MapperBuilderAssistant 对象** 。前面在解析 ``标签的时候我们介绍过,MapperBuilderAssistant 会在 useNewCache() 方法中通过 CacheBuilder 创建新的 Cache 对象,并记录到 currentCache 字段。而这里解析 `` 标签的时候,MapperBuilderAssistant 会通过 useCacheRef() 方法从 Configuration.caches 集合中,根据被引用的namespace 查找共享的 Cache 对象来初始化 currentCache,而不再创建新的Cache 对象,从而实现二级缓存的共享。 #### 3. 处理``标签 @@ -244,7 +244,7 @@ private Discriminator processDiscriminatorElement(XNode context, Class result ### SQL 语句解析全流程 -在 Mapper.xml 映射文件中,除了上面介绍的标签之外,还有一类比较重要的标签,那就是 ``、``、``、`` 等 SQL 语句标签。虽然定义在 Mapper.xml 映射文件中,但是 **这些标签是由 XMLStatementBuilder 进行解析的**,而不再由 XMLMapperBuilder 来完成解析。 在开始介绍 XMLStatementBuilder 解析 SQL 语句标签的具体实现之前,我们先来了解一下 MyBatis 在内存中是如何表示这些 SQL 语句标签的。在内存中,MyBatis 使用 SqlSource 接口来表示解析之后的 SQL 语句,其中的 SQL 语句只是一个中间态,可能包含动态 SQL 标签或占位符等信息,无法直接使用。SqlSource 接口的定义如下: @@ -307,7 +307,7 @@ public void parseStatementNode() { 在有的数据库表设计场景中,我们会添加一个自增 ID 字段作为主键,例如,用户 ID、订单 ID 或者这个自增 ID 本身并没有什么业务含义,只是一个唯一标识而已。在某些业务逻辑里面,我们希望在执行 insert 语句的时候返回这个自增 ID 值,`` 标签就可以实现自增 ID 的获取。`` 标签不仅可以获取自增 ID,还可以指定其他 SQL 语句,从其他表或执行数据库的函数获取字段值。 -**parseSelectKeyNode() 方法是解析 标签的核心所在** ,其中会解析 `` 标签的各个属性,并根据这些属性值将其中的 SQL 语句解析成 MappedStatement 对象,具体实现如下: +**parseSelectKeyNode() 方法是解析 标签的核心所在**,其中会解析 `` 标签的各个属性,并根据这些属性值将其中的 SQL 语句解析成 MappedStatement 对象,具体实现如下: ```java private void parseSelectKeyNode(String id, XNode nodeToHandle, Class parameterTypeClass, LanguageDriver langDriver, String databaseId) { @@ -348,7 +348,7 @@ public void parseStatementNode() { } ``` -这里解析 SQL 语句 **使用的是 LanguageDriver 接口** ,其核心实现是 XMLLanguageDriver,继承关系如下图所示: +这里解析 SQL 语句 **使用的是 LanguageDriver 接口**,其核心实现是 XMLLanguageDriver,继承关系如下图所示: ![图片2.png](assets/Cgp9HWA7ksyAUvrwAADwoAT3J5M370.png) @@ -399,7 +399,7 @@ protected MixedSqlNode parseDynamicTags(XNode node) { ![图片3.png](assets/Cgp9HWA7kvSAHP1yAAEyhRwHGEE543.png) -NodeHandler 继承关系图 **NodeHandler接口负责解析动态 SQL 内的标签** ,生成相应的 SqlNode 对象,通过 NodeHandler 实现类的名称,我们就可以大概猜测到其解析的标签名称。以 IfHandler 为例,它解析的就是 `` 标签,其核心实现如下: +NodeHandler 继承关系图 **NodeHandler接口负责解析动态 SQL 内的标签**,生成相应的 SqlNode 对象,通过 NodeHandler 实现类的名称,我们就可以大概猜测到其解析的标签名称。以 IfHandler 为例,它解析的就是 `` 标签,其核心实现如下: ```java private class IfHandler implements NodeHandler { diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25412\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25412\350\256\262.md" index 4cd271f86..8c0298d09 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25412\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25412\350\256\262.md" @@ -70,7 +70,7 @@ MyBatis 为了提高 OGNL 表达式的工作效率,添加了一层 OgnlCache DynamicContext 中有两个核心属性:一个是 sqlBuilder 字段(StringJoiner 类型),用来记录解析之后的 SQL 语句;另一个是 bindings 字段,用来记录上下文中的一些 KV 信息。 -DynamicContext 定义了一个 ContextMap 内部类,ContextMap 用来记录运行时用户传入的、用来替换“#{}”占位符的实参。在 DynamicContext 构造方法中,会 **根据传入的实参类型决定如何创建对应的 ContextMap 对象** ,核心代码如下: +DynamicContext 定义了一个 ContextMap 内部类,ContextMap 用来记录运行时用户传入的、用来替换“#{}”占位符的实参。在 DynamicContext 构造方法中,会 **根据传入的实参类型决定如何创建对应的 ContextMap 对象**,核心代码如下: ```java public DynamicContext(Configuration configuration, Object parameterObject) { @@ -118,7 +118,7 @@ ContextMap 继承了 HashMap 并覆盖了 get() 方法,在 get() 方法中有 ### SqlNode -在 MyBatis 处理动态 SQL 语句的时候,会将动态 SQL 标签解析为 SqlNode 对象, **多个 SqlNode 对象就是通过组合模式组成树形结构供上层使用的** 。 +在 MyBatis 处理动态 SQL 语句的时候,会将动态 SQL 标签解析为 SqlNode 对象,**多个 SqlNode 对象就是通过组合模式组成树形结构供上层使用的** 。 下面我们就来讲解一下 SqlNode 的相关实现。首先,介绍一下 SqlNode 接口的定义,如下所示: @@ -142,7 +142,7 @@ SqlNode 继承关系图 #### 1. StaticTextSqlNode 和 MixedSqlNode -**StaticTextSqlNode 用于表示非动态的 SQL 片段** ,其中维护了一个 text 字段(String 类型),用于记录非动态 SQL 片段的文本内容,其 apply() 方法会直接将 text 字段值追加到 DynamicContext.sqlBuilder 的最末尾。 **MixedSqlNode 在整个 SqlNode 树中充当了树枝节点,也就是扮演了组合模式中 Composite 的角色** ,其中维护了一个 `List` 集合用于记录 MixedSqlNode 下所有的子 SqlNode 对象。MixedSqlNode 对于 apply() 方法的实现也相对比较简单,核心逻辑就是遍历 `List` 集合中全部的子 SqlNode 对象并调用 apply() 方法,由子 SqlNode 对象完成真正的动态 SQL 处理逻辑。 +**StaticTextSqlNode 用于表示非动态的 SQL 片段**,其中维护了一个 text 字段(String 类型),用于记录非动态 SQL 片段的文本内容,其 apply() 方法会直接将 text 字段值追加到 DynamicContext.sqlBuilder 的最末尾。**MixedSqlNode 在整个 SqlNode 树中充当了树枝节点,也就是扮演了组合模式中 Composite 的角色**,其中维护了一个 `List` 集合用于记录 MixedSqlNode 下所有的子 SqlNode 对象。MixedSqlNode 对于 apply() 方法的实现也相对比较简单,核心逻辑就是遍历 `List` 集合中全部的子 SqlNode 对象并调用 apply() 方法,由子 SqlNode 对象完成真正的动态 SQL 处理逻辑。 #### 2. TextSqlNode **TextSqlNode 实现抽象了包含 “${}”占位符的动态 SQL 片段** 。TextSqlNode 通过一个 text 字段(String 类型)记录了包含“{}”占位符的 SQL 文本内容,在 apply() 方法实现中会结合用户给定的实参解析“{}”占位符,核心代码片段如下 @@ -157,7 +157,7 @@ public boolean apply(DynamicContext context) { } ``` -这里 **使用 GenericTokenParser 识别“${}”占位符** ,在识别到占位符之后,会 **通过 BindingTokenParser 将“${}”占位符替换为用户传入的实参** 。BindingTokenParser 继承了TokenHandler 接口,在其 handleToken() 方法实现中,会根据 DynamicContext.bindings 这个 ContextMap 中的 KV 数据替换 SQL 语句中的“{}”占位符,相关的代码片段如下: +这里 **使用 GenericTokenParser 识别“${}”占位符**,在识别到占位符之后,会 **通过 BindingTokenParser 将“${}”占位符替换为用户传入的实参** 。BindingTokenParser 继承了TokenHandler 接口,在其 handleToken() 方法实现中,会根据 DynamicContext.bindings 这个 ContextMap 中的 KV 数据替换 SQL 语句中的“{}”占位符,相关的代码片段如下: ```plaintext public String handleToken(String content) { @@ -176,7 +176,7 @@ public String handleToken(String content) { } ``` -#### 3. IfSqlNode **IfSqlNode 实现类对应了动态 SQL 语句中的 标签** ,在 MyBatis 的 `` 标签中使用可以通过 test 属性指定一个表达式,当表达式成立时,`` 标签内的 SQL 片段才会出现在完整的 SQL 语句中 +#### 3. IfSqlNode **IfSqlNode 实现类对应了动态 SQL 语句中的 标签**,在 MyBatis 的 `` 标签中使用可以通过 test 属性指定一个表达式,当表达式成立时,`` 标签内的 SQL 片段才会出现在完整的 SQL 语句中 在 IfSqlNode 中,通过 test 字段(String 类型)记录了 `` 标签中的 test 表达式,通过 contents 字段(SqlNode 类型)维护了 `` 标签下的子 SqlNode 对象。在 IfSqlNode 的 apply() 方法实现中,会依赖 ExpressionEvaluator 工具类解析 test 表达式,只有 test 表达式为 true,才会调用子 SqlNode 对象(即 contents 字段)的 apply() 方法。需要说明的是:这里使用到的 ExpressionEvaluator 工具类底层也是依赖 OGNL 表达式实现 test 表达式解析的。 @@ -197,7 +197,7 @@ public boolean apply(DynamicContext context) { } ``` -从 apply() 方法的实现可以看出,TrimSqlNode 处理前后缀的核心逻辑是在 FilteredDynamicContext 中完成的。 **FilteredDynamicContext 可以看作是 DynamicContext 的装饰器** 。除了 DynamicContext 本身临时存储解析结果和参数的功能之外,FilteredDynamicContext 还通过其 applyAll() 方法实现了前后缀的处理,其中会判断 TrimSqlNode 下子 SqlNode 的解析结果的长度,然后执行 applyPrefix() 方法处理前缀,执行 applySuffix() 方法处理后缀。 +从 apply() 方法的实现可以看出,TrimSqlNode 处理前后缀的核心逻辑是在 FilteredDynamicContext 中完成的。**FilteredDynamicContext 可以看作是 DynamicContext 的装饰器** 。除了 DynamicContext 本身临时存储解析结果和参数的功能之外,FilteredDynamicContext 还通过其 applyAll() 方法实现了前后缀的处理,其中会判断 TrimSqlNode 下子 SqlNode 的解析结果的长度,然后执行 applyPrefix() 方法处理前缀,执行 applySuffix() 方法处理后缀。 - applyPrefix() 方法在处理前缀的时候,首先会遍历 prefixesToOverride 集合,从 SQL 片段的 **头部** 逐个尝试进行删除,之后在 SQL 片段的头部插入一个空格以及 prefix 字段指定的前缀字符串。 - applySuffix() 方法在处理后缀的时候,首先会遍历 suffixesToOverride 集合,从 SQL 片段的 **尾部** 逐个尝试进行删除,之后在 SQL 片段的尾部插入一个空格以及 suffix 字段指定的后缀字符串。 diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25413\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25413\350\256\262.md" index f995a314d..807fe53ca 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25413\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25413\350\256\262.md" @@ -89,8 +89,8 @@ VarDeclSqlNode 中的 name 字段维护了 `` 标签中 name 属性的值 SqlSourceBuilder 的核心操作主要有两个: -- **解析“#{}”占位符中携带的各种属性** ,例如,“#{id, javaType=int, jdbcType=NUMERIC, typeHandler=MyTypeHandler}”这个占位符,指定了 javaType、jdbcType、typeHandler 等配置; -- **将 SQL 语句中的“#{}”占位符替换成“?”占位符** ,替换之后的 SQL 语句就可以提交给数据库进行编译了。 +- **解析“#{}”占位符中携带的各种属性**,例如,“#{id, javaType=int, jdbcType=NUMERIC, typeHandler=MyTypeHandler}”这个占位符,指定了 javaType、jdbcType、typeHandler 等配置; +- **将 SQL 语句中的“#{}”占位符替换成“?”占位符**,替换之后的 SQL 语句就可以提交给数据库进行编译了。 SqlSourceBuilder 的入口是 parse() 方法,这里首先会创建一个识别“#{}”占位符的 GenericTokenParser 解析器,当识别到“#{}”占位符的时候,就 **由 ParameterMappingTokenHandler 这个 TokenHandler 实现完成上述两个核心步骤** 。 @@ -114,7 +114,7 @@ SqlSourceBuilder 完成了“#{}”占位符的解析和替换之后,会将最 ### SqlSource -经过上述一系列处理之后,SQL 语句最终会由 SqlSource 进行最后的处理。 **在 SqlSource 接口中只定义了一个 getBoundSql() 方法,它控制着动态 SQL 语句解析的整个流程** ,它会根据从 Mapper.xml 映射文件(或注解)解析到的 SQL 语句以及执行 SQL 时传入的实参,返回一条可执行的 SQL。 +经过上述一系列处理之后,SQL 语句最终会由 SqlSource 进行最后的处理。**在 SqlSource 接口中只定义了一个 getBoundSql() 方法,它控制着动态 SQL 语句解析的整个流程**,它会根据从 Mapper.xml 映射文件(或注解)解析到的 SQL 语句以及执行 SQL 时传入的实参,返回一条可执行的 SQL。 下图展示了 SqlSource 接口的核心实现: @@ -130,7 +130,7 @@ SqlSource 接口继承图 #### 1. DynamicSqlSource -DynamicSqlSource 作为最常用的 SqlSource 实现, **主要负责解析动态 SQL 语句** 。 +DynamicSqlSource 作为最常用的 SqlSource 实现,**主要负责解析动态 SQL 语句** 。 DynamicSqlSource 中维护了一个 SqlNode 类型的字段(rootSqlNode 字段),用于记录整个 SqlNode 树形结构的根节点。在 DynamicSqlSource 的 getBoundSql() 方法实现中,会使用前面介绍的 SqlNode、SqlSourceBuilder 等组件,完成动态 SQL 语句以及“#{}”占位符的解析,具体的实现如下: diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25414\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25414\350\256\262.md" index 241235a18..b86ba95e9 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25414\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25414\350\256\262.md" @@ -1,8 +1,8 @@ # 14 探究 MyBatis 结果集映射机制背后的秘密(上) -在前面介绍 MyBatis 解析 Mapper.xml 映射文件的过程中,我们看到 `` 标签会被解析成 ResultMap 对象,其中定义了 ResultSet 与 Java 对象的映射规则,简单来说,也就是 **一行数据记录如何映射成一个 Java 对象** ,这种映射机制是 MyBatis 作为 ORM 框架的核心功能之一。 +在前面介绍 MyBatis 解析 Mapper.xml 映射文件的过程中,我们看到 `` 标签会被解析成 ResultMap 对象,其中定义了 ResultSet 与 Java 对象的映射规则,简单来说,也就是 **一行数据记录如何映射成一个 Java 对象**,这种映射机制是 MyBatis 作为 ORM 框架的核心功能之一。 -ResultMap 只是定义了一个静态的映射规则,那在运行时,MyBatis 是如何根据映射规则将 ResultSet 映射成 Java 对象的呢?当 MyBatis 执行完一条 select 语句, **拿到 ResultSet 结果集之后,会将其交给关联的 ResultSetHandler 进行后续的映射处理** 。 +ResultMap 只是定义了一个静态的映射规则,那在运行时,MyBatis 是如何根据映射规则将 ResultSet 映射成 Java 对象的呢?当 MyBatis 执行完一条 select 语句,**拿到 ResultSet 结果集之后,会将其交给关联的 ResultSetHandler 进行后续的映射处理** 。 ResultSetHandler 是一个接口,其中定义了三个方法,分别用来处理不同的查询返回值: @@ -21,7 +21,7 @@ public interface ResultSetHandler { ### 结果集处理入口 -你如果有 JDBC 编程经验的话,应该知道在数据库中执行一条 Select 语句通常只能拿到一个 ResultSet,但这只是我们最常用的一种查询数据库的方式,其实数据库还支持同时返回多个 ResultSet 的场景,例如在存储过程中执行多条 Select 语句。MyBatis 作为一个通用的持久化框架,不仅要支持常用的基础功能,还要对其他使用场景进行全面的支持。 **DefaultResultSetHandler 实现的 handleResultSets() 方法支持多个 ResultSet 的处理** (单 ResultSet 的处理只是其中的特例),相关的代码片段如下: +你如果有 JDBC 编程经验的话,应该知道在数据库中执行一条 Select 语句通常只能拿到一个 ResultSet,但这只是我们最常用的一种查询数据库的方式,其实数据库还支持同时返回多个 ResultSet 的场景,例如在存储过程中执行多条 Select 语句。MyBatis 作为一个通用的持久化框架,不仅要支持常用的基础功能,还要对其他使用场景进行全面的支持。**DefaultResultSetHandler 实现的 handleResultSets() 方法支持多个 ResultSet 的处理** (单 ResultSet 的处理只是其中的特例),相关的代码片段如下: ```java public List handleResultSets(Statement stmt) throws SQLException { @@ -56,7 +56,7 @@ public List handleResultSets(Statement stmt) throws SQLException { } ``` -这里我们先来看一下遍历多结果集时使用到的 getFirstResultSet() 方法和 getNextResultSet() 方法,这两个方法底层都是依赖 java.sql.Statement 的 getMoreResults() 方法和 getUpdateCount() 方法检测是否存在后续的 ResultSet 对象,检测成功之后,会通过 getResultSet() 方法获取下一个 ResultSet 对象。 **这里获取到的 ResultSet 对象,会被包装成 ResultSetWrapper 对象返回。** ResultSetWrapper 主要用于封装 ResultSet 的一些元数据,其中记录了 ResultSet 中每列的名称、对应的 Java 类型、JdbcType 类型以及每列对应的 TypeHandler。 +这里我们先来看一下遍历多结果集时使用到的 getFirstResultSet() 方法和 getNextResultSet() 方法,这两个方法底层都是依赖 java.sql.Statement 的 getMoreResults() 方法和 getUpdateCount() 方法检测是否存在后续的 ResultSet 对象,检测成功之后,会通过 getResultSet() 方法获取下一个 ResultSet 对象。**这里获取到的 ResultSet 对象,会被包装成 ResultSetWrapper 对象返回。** ResultSetWrapper 主要用于封装 ResultSet 的一些元数据,其中记录了 ResultSet 中每列的名称、对应的 Java 类型、JdbcType 类型以及每列对应的 TypeHandler。 另外,ResultSetWrapper 可以将底层 ResultSet 的列与一个 ResultMap 映射的列进行交集,得到参与映射的列和未被映射的列,分别记录到 mappedColumnNamesMap 集合和 unMappedColumnNamesMap 集合中。这两个集合都是 Map`>` 类型,其中最外层的 Key 是 ResultMap 的 id,Value 分别是参与映射的列名集合和未被映射的列名集合。 @@ -66,7 +66,7 @@ public List handleResultSets(Statement stmt) throws SQLException { 了解了处理 ResultSet 的入口逻辑之后,下面我们继续来深入了解一下 DefaultResultSetHandler 是如何处理单个结果集的,这部分逻辑的入口是 handleResultSet() 方法,其中会根据第四个参数,也就是 parentMapping,判断当前要处理的 ResultSet 是嵌套映射,还是外层映射。 -无论是处理外层映射还是嵌套映射, **都会依赖 handleRowValues() 方法完成结果集的处理** (通过方法名也可以看出,handleRowValues() 方法是处理多行记录的,也就是一个结果集)。 +无论是处理外层映射还是嵌套映射,**都会依赖 handleRowValues() 方法完成结果集的处理** (通过方法名也可以看出,handleRowValues() 方法是处理多行记录的,也就是一个结果集)。 至于 handleRowValues() 方法,其中会通过 handleRowValuesForNestedResultMap() 方法处理包含嵌套映射的 ResultMap,通过 handleRowValuesForSimpleResultMap() 方法处理不包含嵌套映射的简单 ResultMap,如下所示: @@ -108,7 +108,7 @@ DefaultResultHandler 实现的底层使用 ArrayList`` 存储映射得 #### 1. ResultSet 的预处理 -有 MyBatis 使用经验的同学可能知道,我们可以通过 RowBounds 指定 offset、limit 参数实现分页的效果。 **这里的 skipRows() 方法就会根据 RowBounds 移动 ResultSet 的指针到指定的数据行,这样后续的映射操作就可以从这一行开始** 。 +有 MyBatis 使用经验的同学可能知道,我们可以通过 RowBounds 指定 offset、limit 参数实现分页的效果。**这里的 skipRows() 方法就会根据 RowBounds 移动 ResultSet 的指针到指定的数据行,这样后续的映射操作就可以从这一行开始** 。 skipRows() 方法会检查 ResultSet 的属性,如果是 TYPE_FORWARD_ONLY 类型,则只能通过循环 + ResultSet.next() 方法(指针的逐行前移)定位到指定的数据行;反之,可以通过 ResultSet.absolute() 方法直接移动指针。 @@ -168,7 +168,7 @@ public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap 经过 resolveDiscriminatedResultMap() 方法解析,我们最终确定了当前记录使用哪个 ResultMap 进行映射。 -接下来要做的就是 **按照 ResultMap 规则进行各个列的映射,得到最终的 Java 对象** ,这部分逻辑是在下面要介绍的 getRowValue() 方法完成的,其核心步骤如下: +接下来要做的就是 **按照 ResultMap 规则进行各个列的映射,得到最终的 Java 对象**,这部分逻辑是在下面要介绍的 getRowValue() 方法完成的,其核心步骤如下: - 首先根据 ResultMap 的 type 属性值创建映射的结果对象; - 然后根据 ResultMap 的配置以及全局信息,决定是否自动映射 ResultMap 中未明确映射的列; @@ -217,7 +217,7 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String col 创建完结果对象之后,下面就可以开始映射各个字段了。 -在简单映射流程中,会先通过 shouldApplyAutomaticMappings() 方法 **检测是否开启了自动映射** ,主要检测以下两个地方。 +在简单映射流程中,会先通过 shouldApplyAutomaticMappings() 方法 **检测是否开启了自动映射**,主要检测以下两个地方。 - 检测当前使用的 ResultMap 是否配置了 autoMapping 属性,如果是,则直接根据该 autoMapping 属性的值决定是否开启自动映射功能。 - 检测 mybatis-config.xml 的 `` 标签中配置的 autoMappingBehavior 值,决定是否开启自动映射功能。autoMappingBehavior 指定 MyBatis 框架如何进行自动映射,该属性有三个可选值:①NONE,表示完全关闭自动映射功能;②PARTIAL,表示只会自动映射没有定义嵌套映射的 ResultMap;③FULL,表示完全打开自动映射功能,这里会自动映射所有 ResultMap。autoMappingBehavior 的默认值是 PARTIAL。 @@ -233,7 +233,7 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String col #### 5. 正常映射 -完成自动映射之后,MyBatis 会 **执行 applyPropertyMappings() 方法处理 ResultMap 中明确要映射的列** ,applyPropertyMappings() 方法的核心流程如下所示。 +完成自动映射之后,MyBatis 会 **执行 applyPropertyMappings() 方法处理 ResultMap 中明确要映射的列**,applyPropertyMappings() 方法的核心流程如下所示。 - 首先从 ResultSetWrapper 中明确需要映射的列名集合,以及 ResultMap 中定义的 ResultMapping 对象集合。 - 遍历全部 ResultMapping 集合,针对每个 ResultMapping 对象为 column 属性值添加指定的前缀,得到最终的列名,然后执行 getPropertyMappingValue() 方法完成映射,得到对应的属性值。 diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25415\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25415\350\256\262.md" index 30500cc0a..e1fe45f85 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25415\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25415\350\256\262.md" @@ -23,13 +23,13 @@ handleRowValuesForNestedResultMap() 方法处理嵌套映射的核心流程如 #### 1. 创建 CacheKey -创建 CacheKey 的 **核心逻辑在 createRowKey() 方法中** ,该方法构建 CacheKey 的过程是这样的:尝试使用 `` 标签或 `` 标签中定义的列名以及对应列值组成 CacheKey 对象;没有定义 `` 标签或 `` 标签,则由 ResultMap 中映射的列名和对应列值一起构成 CacheKey 对象;这样如果依然无法创建 CacheKey 的话,就由 ResultSet 中所有列名以及对应列值一起构成 CacheKey 对象。 +创建 CacheKey 的 **核心逻辑在 createRowKey() 方法中**,该方法构建 CacheKey 的过程是这样的:尝试使用 `` 标签或 `` 标签中定义的列名以及对应列值组成 CacheKey 对象;没有定义 `` 标签或 `` 标签,则由 ResultMap 中映射的列名和对应列值一起构成 CacheKey 对象;这样如果依然无法创建 CacheKey 的话,就由 ResultSet 中所有列名以及对应列值一起构成 CacheKey 对象。 -无论是使用 ``、`` 指定的列名和列值来创建 CacheKey 对象,还是使用全部的列名和列值来创建, **最终都是为了使 CacheKey 能够唯一标识结果对象** 。 +无论是使用 ``、`` 指定的列名和列值来创建 CacheKey 对象,还是使用全部的列名和列值来创建,**最终都是为了使 CacheKey 能够唯一标识结果对象** 。 #### 2. 外层映射 -完成 CacheKey 的创建之后,我们开始处理嵌套映射, **整个处理过程的入口是 getRowValue() 方法** 。 +完成 CacheKey 的创建之后,我们开始处理嵌套映射,**整个处理过程的入口是 getRowValue() 方法** 。 因为嵌套映射涉及多层映射,这里我们先来关注外层映射的处理流程。 @@ -37,7 +37,7 @@ handleRowValuesForNestedResultMap() 方法处理嵌套映射的核心流程如 如果发现开启了自动映射,则会指定 applyAutomaticMappings() 方法,处理 ResultMap 中未明确映射的列。然后再通过 applyPropertyMappings() 方法处理 ResultMap 中明确需要进行映射的列。applyAutomaticMappings() 方法和 applyPropertyMappings() 方法我们在上一讲中已经详细分析过了,这里就不再赘述。 -**到此为止,处理外层映射的步骤其实与处理简单映射的步骤基本一致** ,但不同的是:外层映射此时得到的并不是一个完整的对象,而是一个“部分映射”的对象,因为只填充了一部分属性,另一部分属性将由后面得到的嵌套映射的结果对象填充。 **接下来就是与简单映射不一样的步骤了** 。这里会先将“部分映射”的结果对象添加到 ancestorObjects 集合中暂存,ancestorObjects 是一个 HashMap`` 类型,key 是 ResultMap 的唯一标识(即 id 属性值),value 为外层的“部分映射”的结果对象。 +**到此为止,处理外层映射的步骤其实与处理简单映射的步骤基本一致**,但不同的是:外层映射此时得到的并不是一个完整的对象,而是一个“部分映射”的对象,因为只填充了一部分属性,另一部分属性将由后面得到的嵌套映射的结果对象填充。**接下来就是与简单映射不一样的步骤了** 。这里会先将“部分映射”的结果对象添加到 ancestorObjects 集合中暂存,ancestorObjects 是一个 HashMap`` 类型,key 是 ResultMap 的唯一标识(即 id 属性值),value 为外层的“部分映射”的结果对象。 然后通过 applyNestedResultMappings() 方法处理嵌套映射,在处理过程中,会从 ancestorObjects 集合中获取外层对象,并将嵌套映射产生的结果对象设置到外层对象的属性中。 @@ -89,7 +89,7 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey c #### 3. applyNestedResultMappings() 方法 -通过对外层对象的处理我们可以知道, **处理嵌套映射的核心在于 applyNestedResultMappings() 方法** ,其中会遍历 ResultMap 中的每个 ResultMapping 对象。 +通过对外层对象的处理我们可以知道,**处理嵌套映射的核心在于 applyNestedResultMappings() 方法**,其中会遍历 ResultMap 中的每个 ResultMapping 对象。 针对嵌套映射对应的 ResultMapping 对象进行特殊处理,其核心处理步骤如下。 @@ -105,7 +105,7 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey c MyBatis 中的“延迟加载”是指 **在查询数据库的时候,MyBatis 不会立即将完整的对象加载到服务内存中,而是在业务逻辑真正需要使用这个对象或使用到对象中某些属性的时候,才真正执行数据库查询操作,将完整的对象加载到内存中** 。 -MyBatis 实现延迟加载的底层原理是 **动态代理** ,但并不是\[《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》\]中介绍的 JDK 动态代理,而是 **通过字节码生成方式实现的动态代理,底层依赖 cglib 和 javassit 两个库实现动态代码生成** 。 +MyBatis 实现延迟加载的底层原理是 **动态代理**,但并不是\[《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》\]中介绍的 JDK 动态代理,而是 **通过字节码生成方式实现的动态代理,底层依赖 cglib 和 javassit 两个库实现动态代码生成** 。 这里我们简单说明一下,之所以不用 JDK 动态代理是因为 JDK 动态代理在生成代理对象的时候,要求目标类必须实现接口,而通过 MyBatis 映射产生的结果对象基本都是 POJO 对象,没有实现任何接口,所以 JDK 动态代理不适用。 @@ -193,7 +193,7 @@ public class JavassistDemo { } ``` -**javassist 本质上也是通过动态生成目标类的子类的方式实现动态代理的** ,下面我们就使用 javassist 库为 JavassistDemo 生成代理类,具体实现如下: +**javassist 本质上也是通过动态生成目标类的子类的方式实现动态代理的**,下面我们就使用 javassist 库为 JavassistDemo 生成代理类,具体实现如下: ```java public class JavassitMainDemo { @@ -293,7 +293,7 @@ ResultLoaderMap 中还有一个 loadAll() 方法,这个方法会触发 loaderM #### 4\. 代理工厂 -为了同时接入 cglib 和 javassist 两种生成动态代理的方式, **MyBatis 提供了一个抽象的 ProxyFactory 接口来抽象动态生成代理类的基本行为** ,同时提供了下图中的两个实现类来接入上述两种生成方式: +为了同时接入 cglib 和 javassist 两种生成动态代理的方式,**MyBatis 提供了一个抽象的 ProxyFactory 接口来抽象动态生成代理类的基本行为**,同时提供了下图中的两个实现类来接入上述两种生成方式: ![图片11.png](assets/CioPOWBLPvuAEqGrAAD_fS5qAGA047.png) diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25416\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25416\350\256\262.md" index 2f203dab1..e31d3d397 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25416\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25416\350\256\262.md" @@ -1,6 +1,6 @@ # 16 StatementHandler:参数绑定、SQL 执行和结果映射的奠基者 -**StatementHandler 接口是 MyBatis 中非常重要的一个接口** ,其实现类完成 SQL 语句执行中最核心的一系列操作,这也是后面我们要介绍的 Executor 接口实现的基础。 +**StatementHandler 接口是 MyBatis 中非常重要的一个接口**,其实现类完成 SQL 语句执行中最核心的一系列操作,这也是后面我们要介绍的 Executor 接口实现的基础。 StatementHandler 接口的定义如下图所示: @@ -47,7 +47,7 @@ public RoutingStatementHandler(Executor executor, MappedStatement ms, Object par } ``` -在 RoutingStatementHandler 的其他方法中, **都会委托给底层的 delegate 对象来完成具体的逻辑** 。 +在 RoutingStatementHandler 的其他方法中,**都会委托给底层的 delegate 对象来完成具体的逻辑** 。 ### BaseStatementHandler @@ -57,13 +57,13 @@ public RoutingStatementHandler(Executor executor, MappedStatement ms, Object par #### 1. KeyGenerator -这里需要关注的是 generateKeys() 方法,其中会 **通过 KeyGenerator 接口生成主键** ,下面我们就来看看 KeyGenerator 接口的相关内容。 +这里需要关注的是 generateKeys() 方法,其中会 **通过 KeyGenerator 接口生成主键**,下面我们就来看看 KeyGenerator 接口的相关内容。 我们知道不同数据库的自增 id 生成策略并不完全一样。例如,我们常见的 Oracle DB 是通过sequence 实现自增 id 的,如果使用自增 id 作为主键,就需要我们先获取到这个自增的 id 值,然后再使用;MySQL 在使用自增 id 作为主键的时候,insert 语句中可以不指定主键,在插入过程中由 MySQL 自动生成 id。KeyGenerator 接口支持 insert 语句执行前后获取自增的 id,分别对应 processBefore() 方法和 processAfter() 方法,下图展示了 MyBatis 提供的两个 KeyGenerator 接口实现: ![图片4.png](assets/Cgp9HWBR0HmAAYIQAAE2FEB8sfU102.png) -KeyGenerator 接口继承关系图 **Jdbc3KeyGenerator 用于获取数据库生成的自增 id(例如 MySQL 那种生成模式)** ,其 processBefore() 方法是空实现,processAfter() 方法会将 insert 语句执行后生成的主键保存到用户传递的实参中。我们在使用 MyBatis 执行单行 insert 语句时,一般传入的实参是一个 POJO 对象或是 Map 对象,生成的主键会设置到对应的属性中;执行多条 insert 语句时,一般传入实参是 POJO 对象集合或 Map 对象的数组或集合,集合中每一个元素都对应一次插入操作,生成的多个自增 id 也会设置到每个元素的相应属性中。 +KeyGenerator 接口继承关系图 **Jdbc3KeyGenerator 用于获取数据库生成的自增 id(例如 MySQL 那种生成模式)**,其 processBefore() 方法是空实现,processAfter() 方法会将 insert 语句执行后生成的主键保存到用户传递的实参中。我们在使用 MyBatis 执行单行 insert 语句时,一般传入的实参是一个 POJO 对象或是 Map 对象,生成的主键会设置到对应的属性中;执行多条 insert 语句时,一般传入实参是 POJO 对象集合或 Map 对象的数组或集合,集合中每一个元素都对应一次插入操作,生成的多个自增 id 也会设置到每个元素的相应属性中。 Jdbc3KeyGenerator 中获取数据库自增 id 的核心代码片段如下: @@ -142,7 +142,7 @@ if (keyProperties.length == 1) { 经过前面\[《13 | 深入分析动态 SQL 语句解析全流程(下)》\]介绍的一系列 SqlNode 的处理之后,我们得到的 SQL 语句(维护在 BoundSql 对象中)可能包含多个“?”占位符,与此同时,用于替换每个“?”占位符的实参都记录在 BoundSql.parameterMappings 集合中。 -ParameterHandler 接口中定义了两个方法:一个是 getParameterObject() 方法,用来获取传入的实参对象;另一个是 setParameters() 方法,用来替换“?”占位符,这是 ParameterHandler 的 **核心方法** 。 **DefaultParameterHandler 是 ParameterHandler 接口的唯一实现,其 setParameters() 方法会遍历 BoundSql.parameterMappings 集合** ,根据参数名称查找相应实参,最后会通过 PreparedStatement.set\*() 方法与 SQL 语句进行绑定。setParameters() 方法的具体代码如下: +ParameterHandler 接口中定义了两个方法:一个是 getParameterObject() 方法,用来获取传入的实参对象;另一个是 setParameters() 方法,用来替换“?”占位符,这是 ParameterHandler 的 **核心方法** 。**DefaultParameterHandler 是 ParameterHandler 接口的唯一实现,其 setParameters() 方法会遍历 BoundSql.parameterMappings 集合**,根据参数名称查找相应实参,最后会通过 PreparedStatement.set\*() 方法与 SQL 语句进行绑定。setParameters() 方法的具体代码如下: ```plaintext for (int i = 0; i \< parameterMappings.size(); i++) { @@ -192,7 +192,7 @@ typeHandler.setParameter(ps, i + 1, value, jdbcType); ### SimpleStatementHandler -SimpleStatementHandler 是 StatementHandler 的具体实现之一,继承了 BaseStatementHandler 抽象类。SimpleStatementHandler 各个方法接收的是 java.sql.Statement 对象,并通过该对象来完成 CRUD 操作,所以在 SimpleStatementHandler 中 **维护的 SQL 语句不能存在“?”占位符** ,填充占位符的 parameterize() 方法也是空实现。 +SimpleStatementHandler 是 StatementHandler 的具体实现之一,继承了 BaseStatementHandler 抽象类。SimpleStatementHandler 各个方法接收的是 java.sql.Statement 对象,并通过该对象来完成 CRUD 操作,所以在 SimpleStatementHandler 中 **维护的 SQL 语句不能存在“?”占位符**,填充占位符的 parameterize() 方法也是空实现。 在 instantiateStatement() 这个初始化方法中,SimpleStatementHandler 会直接通过 JDBC Connection 创建 Statement 对象,这个对象也是后续 SimpleStatementHandler 其他方法的入参。 @@ -260,7 +260,7 @@ return rows; // 返回影响行数 ### CallableStatementHandler -**CallableStatementHandler 是处理存储过程的 StatementHandler 实现** ,其 instantiateStatement() 方法会通过 JDBC Connection 的 prepareCall() 方法为指定存储过程创建对应的 java.sql.CallableStatement 对象。在 parameterize() 方法中,CallableStatementHandler 除了会通过 ParameterHandler 完成实参的绑定之外,还会指定输出参数的位置和类型。 +**CallableStatementHandler 是处理存储过程的 StatementHandler 实现**,其 instantiateStatement() 方法会通过 JDBC Connection 的 prepareCall() 方法为指定存储过程创建对应的 java.sql.CallableStatement 对象。在 parameterize() 方法中,CallableStatementHandler 除了会通过 ParameterHandler 完成实参的绑定之外,还会指定输出参数的位置和类型。 在 CallableStatementHandler 的 query()、queryCursor()、update() 方法中,除了处理 SQL 语句本身的结果集(ResultSet 结果集或是影响行数),还会通过 ResultSetHandler 的 handleOutputParameters() 方法处理输出参数,这是与 PreparedStatementHandler 最大的不同。下面我们以 query() 方法为例进行简单分析: diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25417\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25417\350\256\262.md" index c4f2a56e9..221117c9f 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25417\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25417\350\256\262.md" @@ -23,13 +23,13 @@ handleRowValuesForNestedResultMap() 方法处理嵌套映射的核心流程如 #### 1. 创建 CacheKey -创建 CacheKey 的 **核心逻辑在 createRowKey() 方法中** ,该方法构建 CacheKey 的过程是这样的:尝试使用 `` 标签或 `` 标签中定义的列名以及对应列值组成 CacheKey 对象;没有定义 `` 标签或 `` 标签,则由 ResultMap 中映射的列名和对应列值一起构成 CacheKey 对象;这样如果依然无法创建 CacheKey 的话,就由 ResultSet 中所有列名以及对应列值一起构成 CacheKey 对象。 +创建 CacheKey 的 **核心逻辑在 createRowKey() 方法中**,该方法构建 CacheKey 的过程是这样的:尝试使用 `` 标签或 `` 标签中定义的列名以及对应列值组成 CacheKey 对象;没有定义 `` 标签或 `` 标签,则由 ResultMap 中映射的列名和对应列值一起构成 CacheKey 对象;这样如果依然无法创建 CacheKey 的话,就由 ResultSet 中所有列名以及对应列值一起构成 CacheKey 对象。 -无论是使用 ``、`` 指定的列名和列值来创建 CacheKey 对象,还是使用全部的列名和列值来创建, **最终都是为了使 CacheKey 能够唯一标识结果对象** 。 +无论是使用 ``、`` 指定的列名和列值来创建 CacheKey 对象,还是使用全部的列名和列值来创建,**最终都是为了使 CacheKey 能够唯一标识结果对象** 。 #### 2. 外层映射 -完成 CacheKey 的创建之后,我们开始处理嵌套映射, **整个处理过程的入口是 getRowValue() 方法** 。 +完成 CacheKey 的创建之后,我们开始处理嵌套映射,**整个处理过程的入口是 getRowValue() 方法** 。 因为嵌套映射涉及多层映射,这里我们先来关注外层映射的处理流程。 @@ -37,7 +37,7 @@ handleRowValuesForNestedResultMap() 方法处理嵌套映射的核心流程如 如果发现开启了自动映射,则会指定 applyAutomaticMappings() 方法,处理 ResultMap 中未明确映射的列。然后再通过 applyPropertyMappings() 方法处理 ResultMap 中明确需要进行映射的列。applyAutomaticMappings() 方法和 applyPropertyMappings() 方法我们在上一讲中已经详细分析过了,这里就不再赘述。 -**到此为止,处理外层映射的步骤其实与处理简单映射的步骤基本一致** ,但不同的是:外层映射此时得到的并不是一个完整的对象,而是一个“部分映射”的对象,因为只填充了一部分属性,另一部分属性将由后面得到的嵌套映射的结果对象填充。 **接下来就是与简单映射不一样的步骤了** 。这里会先将“部分映射”的结果对象添加到 ancestorObjects 集合中暂存,ancestorObjects 是一个 HashMap`` 类型,key 是 ResultMap 的唯一标识(即 id 属性值),value 为外层的“部分映射”的结果对象。 +**到此为止,处理外层映射的步骤其实与处理简单映射的步骤基本一致**,但不同的是:外层映射此时得到的并不是一个完整的对象,而是一个“部分映射”的对象,因为只填充了一部分属性,另一部分属性将由后面得到的嵌套映射的结果对象填充。**接下来就是与简单映射不一样的步骤了** 。这里会先将“部分映射”的结果对象添加到 ancestorObjects 集合中暂存,ancestorObjects 是一个 HashMap`` 类型,key 是 ResultMap 的唯一标识(即 id 属性值),value 为外层的“部分映射”的结果对象。 然后通过 applyNestedResultMappings() 方法处理嵌套映射,在处理过程中,会从 ancestorObjects 集合中获取外层对象,并将嵌套映射产生的结果对象设置到外层对象的属性中。 @@ -89,7 +89,7 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey c #### 3. applyNestedResultMappings() 方法 -通过对外层对象的处理我们可以知道, **处理嵌套映射的核心在于 applyNestedResultMappings() 方法** ,其中会遍历 ResultMap 中的每个 ResultMapping 对象。 +通过对外层对象的处理我们可以知道,**处理嵌套映射的核心在于 applyNestedResultMappings() 方法**,其中会遍历 ResultMap 中的每个 ResultMapping 对象。 针对嵌套映射对应的 ResultMapping 对象进行特殊处理,其核心处理步骤如下。 @@ -105,7 +105,7 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey c MyBatis 中的“延迟加载”是指 **在查询数据库的时候,MyBatis 不会立即将完整的对象加载到服务内存中,而是在业务逻辑真正需要使用这个对象或使用到对象中某些属性的时候,才真正执行数据库查询操作,将完整的对象加载到内存中** 。 -MyBatis 实现延迟加载的底层原理是 **动态代理** ,但并不是《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》中介绍的 JDK 动态代理,而是 **通过字节码生成方式实现的动态代理,底层依赖 cglib 和 javassit 两个库实现动态代码生成** 。 +MyBatis 实现延迟加载的底层原理是 **动态代理**,但并不是《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》中介绍的 JDK 动态代理,而是 **通过字节码生成方式实现的动态代理,底层依赖 cglib 和 javassit 两个库实现动态代码生成** 。 这里我们简单说明一下,之所以不用 JDK 动态代理是因为 JDK 动态代理在生成代理对象的时候,要求目标类必须实现接口,而通过 MyBatis 映射产生的结果对象基本都是 POJO 对象,没有实现任何接口,所以 JDK 动态代理不适用。 @@ -193,7 +193,7 @@ public class JavassistDemo { } ``` -**javassist 本质上也是通过动态生成目标类的子类的方式实现动态代理的** ,下面我们就使用 javassist 库为 JavassistDemo 生成代理类,具体实现如下: +**javassist 本质上也是通过动态生成目标类的子类的方式实现动态代理的**,下面我们就使用 javassist 库为 JavassistDemo 生成代理类,具体实现如下: ```java public class JavassitMainDemo { @@ -293,7 +293,7 @@ ResultLoaderMap 中还有一个 loadAll() 方法,这个方法会触发 loaderM #### 4\. 代理工厂 -为了同时接入 cglib 和 javassist 两种生成动态代理的方式, **MyBatis 提供了一个抽象的 ProxyFactory 接口来抽象动态生成代理类的基本行为** ,同时提供了下图中的两个实现类来接入上述两种生成方式: +为了同时接入 cglib 和 javassist 两种生成动态代理的方式,**MyBatis 提供了一个抽象的 ProxyFactory 接口来抽象动态生成代理类的基本行为**,同时提供了下图中的两个实现类来接入上述两种生成方式: ![图片11.png](assets/CioPOWBLPvuAEqGrAAD_fS5qAGA047.png) diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25418\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25418\350\256\262.md" index 6d613c837..1e2c77999 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25418\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25418\350\256\262.md" @@ -45,7 +45,7 @@ SimpleExecutor 中的 doQueryCursor()、update() 等方法实现与 doQuery() 你如果有过 JDBC 优化经验的话,可能会知道重用 Statement 对象是一种常见的优化手段,主要目的是减少 SQL 预编译开销,同时还会降低 Statement 对象的创建和销毁频率,这在一定程度上可以提升系统性能。 -ReuseExecutor 这个 BaseExecutor 实现就 **实现了重用 Statement 的优化** ,ReuseExecutor 维护了一个 statementMap 字段(HashMap\类型)来缓存已有的 Statement 对象,该缓存的 Key 是 SQL 模板,Value 是 SQL 模板对应的 Statement 对象。这样在执行相同 SQL 模板时,我们就可以复用 Statement 对象了。 +ReuseExecutor 这个 BaseExecutor 实现就 **实现了重用 Statement 的优化**,ReuseExecutor 维护了一个 statementMap 字段(HashMap\类型)来缓存已有的 Statement 对象,该缓存的 Key 是 SQL 模板,Value 是 SQL 模板对应的 Statement 对象。这样在执行相同 SQL 模板时,我们就可以复用 Statement 对象了。 ReuseExecutor 中的 do\*() 方法实现与前面介绍的 SimpleExecutor 实现完全一样,两者唯一的 **区别在于其中依赖的 prepareStatement() 方法** :SimpleExecutor 每次都会创建全新的 Statement 对象,ReuseExecutor 则是先尝试查询 statementMap 缓存,如果缓存命中,则会重用其中的 Statement 对象。 @@ -71,7 +71,7 @@ JDBC 在执行 SQL 语句时,会将 SQL 语句以及实参通过网络请求 不过,有一点需要特别注意:每次向数据库发送的 SQL 语句的条数是有上限的,如果批量执行的时候超过这个上限值,数据库就会抛出异常,拒绝执行这一批 SQL 语句,所以我们 **需要控制批量发送 SQL 语句的条数和频率** 。 -**BatchExecutor 是用于实现批处理的 Executor 实现** ,其中维护了一个 List`` 集合(statementList 字段)用来缓存一批 SQL,每个 Statement 可以写入多条 SQL。 +**BatchExecutor 是用于实现批处理的 Executor 实现**,其中维护了一个 List`` 集合(statementList 字段)用来缓存一批 SQL,每个 Statement 可以写入多条 SQL。 我们知道 JDBC 的批处理操作只支持 insert、update、delete 等修改操作,也就是说 BatchExecutor 对批处理的实现集中在 doUpdate() 方法中。在 doUpdate() 方法中追加一条待执行的 SQL 语句时,BatchExecutor 会先将该条 SQL 语句与最近一次追加的 SQL 语句进行比较,如果相同,则追加到最近一次使用的 Statement 对象中;如果不同,则追加到一个全新的 Statement 对象,同时会将新建的 Statement 对象放入 statementList 缓存中。 @@ -169,7 +169,7 @@ CachingExecutor 是我们最后一个要介绍的 Executor 接口实现类,它 #### 1. 二级缓存 -我们知道一级缓存的生命周期默认与 SqlSession 相同,而这里介绍的 MyBatis 中的二级缓存则与应用程序的生命周期相同。与二级缓存相关的配置主要有下面三项。 **第一项,二级缓存全局开关** 。这个全局开关是 mybatis-config.xml 配置文件中的 cacheEnabled 配置项。当 cacheEnabled 被设置为 true 时,才会开启二级缓存功能,开启二级缓存功能之后,下面两项的配置才会控制二级缓存的行为。 **第二项,命名空间级别开关** 。在 Mapper 配置文件中,可以通过配置 `` 标签或 `` 标签开启二级缓存功能。 +我们知道一级缓存的生命周期默认与 SqlSession 相同,而这里介绍的 MyBatis 中的二级缓存则与应用程序的生命周期相同。与二级缓存相关的配置主要有下面三项。**第一项,二级缓存全局开关** 。这个全局开关是 mybatis-config.xml 配置文件中的 cacheEnabled 配置项。当 cacheEnabled 被设置为 true 时,才会开启二级缓存功能,开启二级缓存功能之后,下面两项的配置才会控制二级缓存的行为。**第二项,命名空间级别开关** 。在 Mapper 配置文件中,可以通过配置 `` 标签或 `` 标签开启二级缓存功能。 - 在解析到 `` 标签时,MyBatis 会为当前 Mapper.xml 文件对应的命名空间创建一个关联的 Cache 对象(默认为 PerpetualCache 类型的对象),作为其二级缓存的实现。此外,`` 标签中还提供了一个 type 属性,我们可以通过该属性使用自定义的 Cache 类型。 - 在解析到 `` 标签时,MyBatis 并不会创建新的 Cache 对象,而是根据 `` 标签的 namespace 属性查找指定命名空间对应的 Cache 对象,然后让当前命名空间与指定命名空间共享同一个 Cache 对象。 @@ -180,7 +180,7 @@ CachingExecutor 是我们最后一个要介绍的 Executor 接口实现类,它 了解了二级缓存的生命周期、基本概念以及相关配置之后,我们开始介绍 CachingExecutor 依赖的底层组件。 -CachingExecutor 底层除了依赖 PerpetualCache 实现来缓存数据之外,还会 **依赖 TransactionalCache 和 TransactionalCacheManager 两个组件** ,下面我们就一一详细介绍下。 +CachingExecutor 底层除了依赖 PerpetualCache 实现来缓存数据之外,还会 **依赖 TransactionalCache 和 TransactionalCacheManager 两个组件**,下面我们就一一详细介绍下。 TransactionalCache 是 Cache 接口众多实现之一,它也是一个装饰器,用来记录一个事务中添加到二级缓存中的缓存。 @@ -231,7 +231,7 @@ public Object getObject(Object key) { 在事务提交的时候,会将 entriesMissedInCache 集合中的 CacheKey 写入底层的二级缓存(写入时的 Value 为 null)。在事务回滚时,会调用底层二级缓存的 removeObject() 方法,删除 entriesMissedInCache 集合中 CacheKey。 -你可能会问,为什么要用 entriesMissedInCache 集合记录未命中缓存的 CacheKey 呢?为什么还要在缓存结束时处理这些 CacheKey 呢?这主要是与\[第 9 讲\]介绍的 BlockingCache 装饰器相关。在前面介绍 Cache 时我们提到过,CacheBuilder 默认会添加 BlockingCache 这个装饰器,而 BlockingCache 的 getObject() 方法会有给 CacheKey 加锁的逻辑,需要在 putObject() 方法或 removeObject() 方法中解锁, **否则这个 CacheKey 会被一直锁住,无法使用** 。 +你可能会问,为什么要用 entriesMissedInCache 集合记录未命中缓存的 CacheKey 呢?为什么还要在缓存结束时处理这些 CacheKey 呢?这主要是与\[第 9 讲\]介绍的 BlockingCache 装饰器相关。在前面介绍 Cache 时我们提到过,CacheBuilder 默认会添加 BlockingCache 这个装饰器,而 BlockingCache 的 getObject() 方法会有给 CacheKey 加锁的逻辑,需要在 putObject() 方法或 removeObject() 方法中解锁,**否则这个 CacheKey 会被一直锁住,无法使用** 。 看完 TransactionalCache 的核心实现之后,我们再来看 TransactionalCache 的管理者—— TransactionalCacheManager,其中定义了一个 transactionalCaches 字段(HashMap\类型)维护当前 CachingExecutor 使用到的二级缓存,该集合的 Key 是二级缓存对象,Value 是装饰二级缓存的 TransactionalCache 对象。 @@ -243,7 +243,7 @@ TransactionalCacheManager 中的方法实现都比较简单,都是基于 trans CachingExecutor 作为一个装饰器,其中自然会维护一个 Executor 类型字段指向被装饰的 Executor 对象,同时它还创建了一个 TransactionalCacheManager 对象来管理使用到的二级缓存。 -**CachingExecutor 的核心在于 query() 方法** ,其核心操作大致可总结为如下。 +**CachingExecutor 的核心在于 query() 方法**,其核心操作大致可总结为如下。 - 获取 BoundSql 对象,创建查询语句对应的 CacheKey 对象。 - 尝试获取当前命名空间使用的二级缓存,如果没有指定二级缓存,则表示未开启二级缓存功能。如果未开启二级缓存功能,则直接使用被装饰的 Executor 对象进行数据库查询操作。如果开启了二级缓存功能,则继续后面的步骤。 diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25419\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25419\350\256\262.md" index d13183477..cc6625ef7 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25419\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25419\350\256\262.md" @@ -20,7 +20,7 @@ ### SqlSession -**SqlSession是MyBatis对外提供的一个 API 接口,整个MyBatis 接口层也是围绕 SqlSession接口展开的** ,SqlSession 接口中定义了下面几类方法。 +**SqlSession是MyBatis对外提供的一个 API 接口,整个MyBatis 接口层也是围绕 SqlSession接口展开的**,SqlSession 接口中定义了下面几类方法。 - select\*() 方法:用来执行查询操作的方法,SqlSession 会将结果集映射成不同类型的结果对象,例如,selectOne() 方法返回单个 Java 对象,selectList()、selectMap() 方法返回集合对象。 - insert()、update()、delete() 方法:用来执行 DML 语句。 @@ -33,7 +33,7 @@ SqlSessionFactory 接口与 SqlSession 接口的实现类 -默认情况下, **我们在使用 MyBatis 的时候用的都是 DefaultSqlSession 这个默认的 SqlSession 实现** 。DefaultSqlSession 中维护了一个 Executor 对象,通过它来完成数据库操作以及事务管理。DefaultSqlSession 在选择使用哪种 Executor 实现的时候,使用到了策略模式:DefaultSqlSession 扮演了策略模式中的 StrategyUser 角色,Executor 接口扮演的是 Strategy 角色,Executor 接口的不同实现则对应 StrategyImpl 的角色。 +默认情况下,**我们在使用 MyBatis 的时候用的都是 DefaultSqlSession 这个默认的 SqlSession 实现** 。DefaultSqlSession 中维护了一个 Executor 对象,通过它来完成数据库操作以及事务管理。DefaultSqlSession 在选择使用哪种 Executor 实现的时候,使用到了策略模式:DefaultSqlSession 扮演了策略模式中的 StrategyUser 角色,Executor 接口扮演的是 Strategy 角色,Executor 接口的不同实现则对应 StrategyImpl 的角色。 另外,DefaultSqlSession 还维护了一个 dirty 字段来标识缓存中是否有脏数据,它在执行 update() 方法修改数据时会被设置为 true,并在后续参与事务控制,决定当前事务是否需要提交或回滚。 @@ -99,7 +99,7 @@ return new DefaultSqlSession(configuration, executor, autoCommit); 通过前面的 SqlSession 继承关系图我们可以看到,SqlSessionManager 同时实现了 SqlSession 和 SqlSessionFactory 两个接口,也就是说,它 **同时具备操作数据库的能力和创建SqlSession的能力** 。 -首先来看 SqlSessionManager **创建SqlSession的实现** 。它与 DefaultSqlSessionFactory 的主要区别是:DefaultSqlSessionFactory 在一个线程多次获取 SqlSession 的时候,都会创建不同的 SqlSession对象;SqlSessionManager 则有 **两种模式** ,一种模式与 DefaultSqlSessionFactory 相同,另一种模式是 SqlSessionManager 在内部维护了一个 ThreadLocal 类型的字段(localSqlSession)来记录与当前线程绑定的 SqlSession 对象,同一线程从 SqlSessionManager 中获取的 SqlSession 对象始终是同一个,这样就减少了创建 SqlSession 对象的开销。 +首先来看 SqlSessionManager **创建SqlSession的实现** 。它与 DefaultSqlSessionFactory 的主要区别是:DefaultSqlSessionFactory 在一个线程多次获取 SqlSession 的时候,都会创建不同的 SqlSession对象;SqlSessionManager 则有 **两种模式**,一种模式与 DefaultSqlSessionFactory 相同,另一种模式是 SqlSessionManager 在内部维护了一个 ThreadLocal 类型的字段(localSqlSession)来记录与当前线程绑定的 SqlSession 对象,同一线程从 SqlSessionManager 中获取的 SqlSession 对象始终是同一个,这样就减少了创建 SqlSession 对象的开销。 无论哪种模式,SqlSessionManager 都可以看作是 SqlSessionFactory 的装饰器,我们可以在 SqlSessionManager 的构造方法中看到,其中会传入一个 SqlSessionFactory 对象。 diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25420\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25420\350\256\262.md" index 1102d812c..ce5c954cb 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25420\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25420\350\256\262.md" @@ -16,7 +16,7 @@ HTTP 协议可简单分为请求头和请求体两部分,Tomcat 在收到一 为了实现像 HTTP 这种多部分构成的协议的处理逻辑,我们可以使用责任链模式来划分协议中各个部分的处理逻辑,将那些臃肿实现类 **拆分成多个 Handler(或 Interceptor)处理器,在每个 Handler(或 Interceptor)处理器中只专注于 HTTP 协议中一部分数据的处理** 。我们可以开发多个 Handler 处理器,然后按照业务需求将多个 Handler 对象组合成一个链条,从而实现整个 HTTP 请求的处理。 -这样做既可以将复杂、臃肿的逻辑拆分,便于维护,又能将不同的 Handler 处理器分配给不同的程序员开发,提高开发效率。 **在责任链模式中,Handler 处理器会持有对下一个 Handler 处理器的引用** ,也就是说当一个 Handler 处理器完成对关注部分的处理之后,会将请求通过这个引用传递给下一个 Handler 处理器,如此往复,直到整个责任链中全部的 Handler 处理器完成处理。责任链模式的核心类图如下所示: +这样做既可以将复杂、臃肿的逻辑拆分,便于维护,又能将不同的 Handler 处理器分配给不同的程序员开发,提高开发效率。**在责任链模式中,Handler 处理器会持有对下一个 Handler 处理器的引用**,也就是说当一个 Handler 处理器完成对关注部分的处理之后,会将请求通过这个引用传递给下一个 Handler 处理器,如此往复,直到整个责任链中全部的 Handler 处理器完成处理。责任链模式的核心类图如下所示: ![图片1.png](assets/Cgp9HWBkGWmAWIlRAAELQ6DrFHI270.png) @@ -30,11 +30,11 @@ HTTP 协议可简单分为请求头和请求体两部分,Tomcat 在收到一 责任链示意图 -由此可见, **责任链模式可以帮助我们复用 Handler 处理器的实现逻辑,提高系统的可维护性和灵活性** ,很好地符合了“开放-封闭”原则。 +由此可见,**责任链模式可以帮助我们复用 Handler 处理器的实现逻辑,提高系统的可维护性和灵活性**,很好地符合了“开放-封闭”原则。 ### Interceptor -介绍完责任链模式的基础知识之后,我们接着就来讲解MyBatis 中插件的相关内容。 **MyBatis 插件模块中最核心的接口就是 Interceptor 接口,它是所有 MyBatis 插件必须要实现的接口** ,其核心定义如下: +介绍完责任链模式的基础知识之后,我们接着就来讲解MyBatis 中插件的相关内容。**MyBatis 插件模块中最核心的接口就是 Interceptor 接口,它是所有 MyBatis 插件必须要实现的接口**,其核心定义如下: ```plaintext public interface Interceptor { @@ -69,7 +69,7 @@ public class DemoPlugin implements Interceptor { } ``` -我们看到 DemoPlugin 这个示例类除了实现 Interceptor 接口外,还被标注了 @Intercepts 和 @Signature 两个注解。@Intercepts 注解中可以配置多个 @Signature 注解, **@Signature 注解用来指定 DemoPlugin 插件实现类要拦截的目标方法信息** ,其中的 type 属性指定了要拦截的类,method 属性指定了要拦截的目标方法名称,args 属性指定了要拦截的目标方法的参数列表。通过 @Signature 注解中的这三个配置,DemoPlugin 就可以确定要拦截的目标方法的方法签名。在上面的示例中,DemoPlugin 会拦截 Executor 接口中的 query(MappedStatement, Object, RowBounds, ResultHandler) 方法和 close(boolean) 方法。 +我们看到 DemoPlugin 这个示例类除了实现 Interceptor 接口外,还被标注了 @Intercepts 和 @Signature 两个注解。@Intercepts 注解中可以配置多个 @Signature 注解,**@Signature 注解用来指定 DemoPlugin 插件实现类要拦截的目标方法信息**,其中的 type 属性指定了要拦截的类,method 属性指定了要拦截的目标方法名称,args 属性指定了要拦截的目标方法的参数列表。通过 @Signature 注解中的这三个配置,DemoPlugin 就可以确定要拦截的目标方法的方法签名。在上面的示例中,DemoPlugin 会拦截 Executor 接口中的 query(MappedStatement, Object, RowBounds, ResultHandler) 方法和 close(boolean) 方法。 完成 DemoPlugin 实现类的编写之后,为了让 MyBatis 知道这个类的存在,我们要在 mybatis-config.xml 全局配置文件中对 DemoPlugin 进行配置,相关配置片段如下: @@ -90,7 +90,7 @@ public class DemoPlugin implements Interceptor { executor = (Executor) interceptorChain.pluginAll(executor); ``` -从名字就可以看出, **InterceptorChain 是 Interceptor 构成的责任链** ,在其 interceptors 字段(ArrayList``类型)中维护了 MyBatis 初始化过程中加载到的全部 Interceptor 对象,在其 pluginAll() 方法中,会调用每个 Interceptor 的 plugin() 方法创建目标类的代理对象,核心实现如下: +从名字就可以看出,**InterceptorChain 是 Interceptor 构成的责任链**,在其 interceptors 字段(ArrayList``类型)中维护了 MyBatis 初始化过程中加载到的全部 Interceptor 对象,在其 pluginAll() 方法中,会调用每个 Interceptor 的 plugin() 方法创建目标类的代理对象,核心实现如下: ```plaintext public Object pluginAll(Object target) { @@ -143,7 +143,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl 这里传入 Interceptor.intercept() 方法的是一个 Invocation 对象,其中封装了目标对象、目标方法以及目标方法的相关参数,在 DemoInterceptor.intercept() 方法实现中,就是通过调用 Invocation.proceed() 方法完成目标方法的执行。当然,我们自定义的 Interceptor 实现并不一定必须调用目标方法。这样,经过DemoInterceptor 的拦截之后,也就改变了 MyBatis 核心组件的行为。 -最后,我们来看一下 Plugin 工具类对外提供的 wrap() 方法是如何创建 JDK 动态代理的。在 wrap() 方法中,Plugin 工具类会解析传入的 Interceptor 实现的 @Signature 注解信息,并与当前传入的目标对象类型进行匹配, **只有在匹配的情况下,才会生成代理对象,否则直接返回目标对象** 。具体的代码实现以及注释说明如下所示: +最后,我们来看一下 Plugin 工具类对外提供的 wrap() 方法是如何创建 JDK 动态代理的。在 wrap() 方法中,Plugin 工具类会解析传入的 Interceptor 实现的 @Signature 注解信息,并与当前传入的目标对象类型进行匹配,**只有在匹配的情况下,才会生成代理对象,否则直接返回目标对象** 。具体的代码实现以及注释说明如下所示: ```java public static Object wrap(Object target, Interceptor interceptor) { diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25421\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25421\350\256\262.md" index b23478974..0ba5dfc91 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25421\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25421\350\256\262.md" @@ -6,7 +6,7 @@ ### Spring -Spring 中最核心的概念就要数 IoC 了。IoC(Inversion of Control, **控制反转** )的核心思想是将业务对象交由 IoC 容器管理,由 IoC 容器控制业务对象的初始化以及不同业务对象之间的依赖关系,这样就可以降低代码的耦合性。 +Spring 中最核心的概念就要数 IoC 了。IoC(Inversion of Control,**控制反转** )的核心思想是将业务对象交由 IoC 容器管理,由 IoC 容器控制业务对象的初始化以及不同业务对象之间的依赖关系,这样就可以降低代码的耦合性。 **依赖注入** (Dependency Injection)是实现 IoC 的常见方式之一。所谓依赖注入,就是我们的系统不再主动维护业务对象之间的依赖关系,而是将依赖关系转移到 IoC 容器中动态维护。Spring 提供了依赖注入机制,我们只需要通过 XML 配置或注解,就可以确定业务对象之间的依赖关系,轻松实现业务逻辑的组合。 @@ -14,7 +14,7 @@ Spring 中另一个比较重要的概念是 AOP(Aspect Oriented Programming) 在面向对象的思想中,我们关注的是代码的封装性、类间的继承关系和多态、对象之间的依赖关系等,通过对象的组合就可以实现核心的业务逻辑,但是总会有一些重要的重复性代码散落在业务逻辑类中,例如,权限检测、日志打印、事务管理相关的逻辑,这些重复逻辑与我们的核心业务逻辑并无直接关系,却又是系统正常运行不能缺少的功能。 -AOP 可以帮我们将这些碎片化的功能抽取出来,封装到一个组件中进行重用,这也被称为切面。 **通过 AOP 的方式,可以有效地减少散落在各处的碎片化代码,提高系统的可维护性** 。为了方便你后面理解 Spring AOP 的代码,这里我简单介绍 AOP中的几个关键概念。 +AOP 可以帮我们将这些碎片化的功能抽取出来,封装到一个组件中进行重用,这也被称为切面。**通过 AOP 的方式,可以有效地减少散落在各处的碎片化代码,提高系统的可维护性** 。为了方便你后面理解 Spring AOP 的代码,这里我简单介绍 AOP中的几个关键概念。 - 横切关注点:如果某些业务逻辑代码横跨业务系统的多个模块,我们可以将这些业务代码称为横切关注点。 - 切面:对横切关注点的抽象。面向对象思想中的类是事物特性的抽象,与之相对的切面则是对横切关注点的抽象。 @@ -36,7 +36,7 @@ MVC 模式示意图 了解了 SpringMVC核心思想之后,我们再进一步分析Spring MVC 工作的核心原理。 -**DispatcherServlet 是 Spring MVC 中的前端控制器** ,也是 Spring MVC 内部非常核心的一个组件,负责 Spring MVC 请求的调度。当 Spring MVC 接收到用户的 HTTP 请求之后,会由 DispatcherServlet 进行截获,然后根据请求的 URL 初始化 WebApplicationContext(上下文信息),最后转发给业务的 Controller 进行处理。待 Controller 处理完请求之后,DispatcherServlet 会根据返回的视图名称选择具体的 View 进行渲染。 +**DispatcherServlet 是 Spring MVC 中的前端控制器**,也是 Spring MVC 内部非常核心的一个组件,负责 Spring MVC 请求的调度。当 Spring MVC 接收到用户的 HTTP 请求之后,会由 DispatcherServlet 进行截获,然后根据请求的 URL 初始化 WebApplicationContext(上下文信息),最后转发给业务的 Controller 进行处理。待 Controller 处理完请求之后,DispatcherServlet 会根据返回的视图名称选择具体的 View 进行渲染。 下图展示了 Spring MVC 处理一次 HTTP 请求的完整流程: @@ -108,7 +108,7 @@ SSM 开发环境中最核心的配置就介绍完了,关于其完整配置,[ #### 1. SqlSessionFactoryBean -在搭建 SSM 环境的时候,我们会在 applicationContext.xml 中配置一个 SqlSessionFactoryBean,其核心作用就是 **读取 MyBatis 配置,初始化 Configuration 全局配置对象,并创建 SqlSessionFactory 对象** ,对应的核心方法是 buildSqlSessionFactory() 方法。 +在搭建 SSM 环境的时候,我们会在 applicationContext.xml 中配置一个 SqlSessionFactoryBean,其核心作用就是 **读取 MyBatis 配置,初始化 Configuration 全局配置对象,并创建 SqlSessionFactory 对象**,对应的核心方法是 buildSqlSessionFactory() 方法。 下面是 buildSqlSessionFactory() 方法的核心代码片段: @@ -188,7 +188,7 @@ SqlSessionDaoSupport 实现了 Spring DaoSupport 接口,核心功能是辅助 #### 4. MapperFactoryBean 与 MapperScannerConfigurer -使用 SqlSessionDaoSupport 或 SqlSessionTemplate 编写 DAO 毕竟是需要我们手写代码的,为了进一步简化 DAO 层的实现,我们可以通过 MapperFactoryBean 直接将 Mapper 接口注入 Service 层的 Bean 中, **由 Mapper 接口完成 DAO 层的功能** 。 +使用 SqlSessionDaoSupport 或 SqlSessionTemplate 编写 DAO 毕竟是需要我们手写代码的,为了进一步简化 DAO 层的实现,我们可以通过 MapperFactoryBean 直接将 Mapper 接口注入 Service 层的 Bean 中,**由 Mapper 接口完成 DAO 层的功能** 。 下面是一段 MapperFactoryBean 的配置示例: diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25422\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25422\350\256\262.md" index 7272dd2a3..2199860f0 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25422\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25422\350\256\262.md" @@ -136,7 +136,7 @@ if (!dialect.skip(ms, parameter, rowBounds)) { return dialect.afterPage(resultList, parameter, rowBounds); ``` -通过对 PageInterceptor 的分析我们看到, **核心的分页逻辑都是在 Dialect 中完成的** ,PageHelper 针对每个数据库都提供了一个 Dialect 接口实现。下图展示了 MySQL 数据库对应的 Dialect 接口实现: +通过对 PageInterceptor 的分析我们看到,**核心的分页逻辑都是在 Dialect 中完成的**,PageHelper 针对每个数据库都提供了一个 Dialect 接口实现。下图展示了 MySQL 数据库对应的 Dialect 接口实现: ![Drawing 1.png](assets/Cgp9HWBtTFKAVlWCAACyAbYHCQg938.png) @@ -152,7 +152,7 @@ AbstractRowBoundsDialect 这条继承线是针对 RowBounds 进行分页的 Dial ### MyBatis-Plus -MyBatis-Plus 是国人开发的一款 MyBatis 增强工具,通过其名字就能看出, **它并没有改变 MyBatis 本身的功能,而是在 MyBatis 的基础上提供了很多增强功能,使我们的开发更加简洁高效** 。也正是由于其“只做增强不做改变”的特性,让我们可以在使用 MyBatis 的项目中无感知地引入 MyBatis-Plus。 +MyBatis-Plus 是国人开发的一款 MyBatis 增强工具,通过其名字就能看出,**它并没有改变 MyBatis 本身的功能,而是在 MyBatis 的基础上提供了很多增强功能,使我们的开发更加简洁高效** 。也正是由于其“只做增强不做改变”的特性,让我们可以在使用 MyBatis 的项目中无感知地引入 MyBatis-Plus。 MyBatis-Plus 对 MyBatis 的很多方面进行了增强,例如: diff --git "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25423\350\256\262.md" "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25423\350\256\262.md" index 0f6f098f4..726bb6cc9 100644 --- "a/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25423\350\256\262.md" +++ "b/docs/Database/\346\267\261\345\205\245\345\211\226\346\236\220 MyBatis \346\240\270\345\277\203\345\216\237\347\220\206/\347\254\25423\350\256\262.md" @@ -6,11 +6,11 @@ 我个人觉得,要想“破圈”,需要有下面几个方面的操作。 -**第一,选择一个上升期的行业或项目,也就是我们常说的“吃行业红利”** 。之所以把行业选择放在首位就是因为“选择大于努力”,在互联网这个大行业里面还有很多细分领域,例如,电商、在线教育、互联网医疗、短视频、各种游戏等,进入一个上升的行业或是上升的企业,拿到期权,等到公司上市是可以实现财富自由的。互联网的“造富”例子虽然减少了,但是依旧在不断发生,现在在风口上的“猪”依旧在飞。 **第二,选对 Leader,也就是所谓的“抱对大腿”** 。Leader 的能力决定了我们当前工作的上限,不仅是互联网行业,其实各个行业都是一样的。在遇到超出我们权限的资源问题、协调问题的时候,我们是需要向 Leader 求助的,如果我们的 Leader 也解决不来,可想而知这项工作的阻力会有多么大,做起来有多么艰辛。而我们的工作大多是以结果为导向的,不出成绩的话,再苦再难也无法被别人认可,所以说,选择一个靠谱的 Leader 是很重要的。 **第三,让自己变得可靠** 。在职场中,上级和下级之间是一个双向选择的关系,每个 Leader 身边围绕的人数是有限的,就那么几个位置。当我们千辛万苦找到一个靠谱的 Leader 之后,如何让 Leader 选择我们呢?那就是让我们自己变得靠谱。 +**第一,选择一个上升期的行业或项目,也就是我们常说的“吃行业红利”** 。之所以把行业选择放在首位就是因为“选择大于努力”,在互联网这个大行业里面还有很多细分领域,例如,电商、在线教育、互联网医疗、短视频、各种游戏等,进入一个上升的行业或是上升的企业,拿到期权,等到公司上市是可以实现财富自由的。互联网的“造富”例子虽然减少了,但是依旧在不断发生,现在在风口上的“猪”依旧在飞。**第二,选对 Leader,也就是所谓的“抱对大腿”** 。Leader 的能力决定了我们当前工作的上限,不仅是互联网行业,其实各个行业都是一样的。在遇到超出我们权限的资源问题、协调问题的时候,我们是需要向 Leader 求助的,如果我们的 Leader 也解决不来,可想而知这项工作的阻力会有多么大,做起来有多么艰辛。而我们的工作大多是以结果为导向的,不出成绩的话,再苦再难也无法被别人认可,所以说,选择一个靠谱的 Leader 是很重要的。**第三,让自己变得可靠** 。在职场中,上级和下级之间是一个双向选择的关系,每个 Leader 身边围绕的人数是有限的,就那么几个位置。当我们千辛万苦找到一个靠谱的 Leader 之后,如何让 Leader 选择我们呢?那就是让我们自己变得靠谱。 举个例子,我懂 MyBatis,我邻桌同事也懂 MyBatis,我带了没几天的应届生也知道如何用 MyBatis 写动态 SQL 代码了,看起来都只是个熟练工。假设碰到一个 MyBatis 的问题,应届生不懂,同事不懂,我也不懂,单就 MyBatis 这项技术来说,我们在 Leader 眼里是完全没有区别的,扩展到其他技术也是一样的。但如果在别人解决不了问题的时候,我能解决,如此往复几次,同事有什么技术难题都会请教我,Leader 在决定技术方案的时候也会咨询我,这时我的影响力就会发生变化。 -上面只是以 MyBatis 这种开源项目为例,其实面对公司内的项目也是一样,很多程序员会觉得自己公司项目代码写得非常垃圾,不愿意花时间读,这是非常错误的想法。其他同事都对“垃圾代码”嗤之以鼻,但是你能对“垃圾代码”了若指掌、如数家珍,这时 Leader 看到你这个人把一件大家不喜欢的事情都能做到八九十分,也会让 Leader 对你形成信任和依赖,更别说你可以通过阅读这些“垃圾代码”解决工作中的疑难问题了。Leader 就只会觉得你靠谱,觉得有你在项目就没有问题,即使有问题你也能解决,你说方案哪里不合理那多半就是不合理了,也就让你成为一个 Leader 和同事眼中靠谱的人,这就是在“垃圾山”里淘到的“宝藏”。 **第四,珍惜自己的时间,尽量将更多时间花到充实自己上,养成学习的惯性** 。我一直认为“拉勾教育 App”与手机里面的各种短视频 App、5v5 推塔 App、第一角色枪战类 App 是竞对,为什么这么说呢?因为这些 App 都是在竞争用户的时间,毕竟世界上最公平的事情就是每个人一天只有 24 小时。就算你守得了高地,推得了水晶,拿得了 5 杀,又能怎样呢?就算你杀得出 G 港,干得翻机场,拿得下 H 港,又能如何呢?都不如打开“拉勾教育 App”去学习、去巩固技能、去完善自己来得安心,所以需要养成学习的惯性。 +上面只是以 MyBatis 这种开源项目为例,其实面对公司内的项目也是一样,很多程序员会觉得自己公司项目代码写得非常垃圾,不愿意花时间读,这是非常错误的想法。其他同事都对“垃圾代码”嗤之以鼻,但是你能对“垃圾代码”了若指掌、如数家珍,这时 Leader 看到你这个人把一件大家不喜欢的事情都能做到八九十分,也会让 Leader 对你形成信任和依赖,更别说你可以通过阅读这些“垃圾代码”解决工作中的疑难问题了。Leader 就只会觉得你靠谱,觉得有你在项目就没有问题,即使有问题你也能解决,你说方案哪里不合理那多半就是不合理了,也就让你成为一个 Leader 和同事眼中靠谱的人,这就是在“垃圾山”里淘到的“宝藏”。**第四,珍惜自己的时间,尽量将更多时间花到充实自己上,养成学习的惯性** 。我一直认为“拉勾教育 App”与手机里面的各种短视频 App、5v5 推塔 App、第一角色枪战类 App 是竞对,为什么这么说呢?因为这些 App 都是在竞争用户的时间,毕竟世界上最公平的事情就是每个人一天只有 24 小时。就算你守得了高地,推得了水晶,拿得了 5 杀,又能怎样呢?就算你杀得出 G 港,干得翻机场,拿得下 H 港,又能如何呢?都不如打开“拉勾教育 App”去学习、去巩固技能、去完善自己来得安心,所以需要养成学习的惯性。 数年之后,当你站到事业巅峰的时候,再回首,会感谢现在坚持学习的自己。 diff --git "a/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25401\350\256\262.md" "b/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25401\350\256\262.md" index 1c13978a4..5cfe7f6ac 100644 --- "a/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25401\350\256\262.md" +++ "b/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25401\350\256\262.md" @@ -48,7 +48,7 @@ DDD 主要关注:从业务领域视角划分领域边界,构建通用语言 除此之外,DDD 战术设计对设计和开发人员的要求相对较高,实现起来相对复杂。不同企业的研发管理能力和个人开发水平可能会存在差异。尤其对于传统企业而言,在战术设计落地的过程中,可能会存在一定挑战和困难,我建议你和你的公司如果有这方面的想法,就一定要谨慎评估自己的能力,选择最合适的方法落地 DDD。 -也不妨根据收获权衡一下, **总体来说,DDD 可以给你带来以下收获:** +也不妨根据收获权衡一下,**总体来说,DDD 可以给你带来以下收获:** 1. DDD 是一套完整而系统的设计方法,它能带给你从战略设计到战术设计的标准设计过程,使得你的设计思路能够更加清晰,设计过程更加规范。 2. DDD 善于处理与领域相关的拥有高复杂度业务的产品开发,通过它可以建立一个核心而稳定的领域模型,有利于领域知识的传递与传承。 diff --git "a/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25407\350\256\262.md" "b/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25407\350\256\262.md" index 78d0798c2..224d976f2 100644 --- "a/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25407\350\256\262.md" +++ "b/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25407\350\256\262.md" @@ -88,7 +88,7 @@ DDD 的分层架构在不断发展。最早是传统的四层架构;后来四 首先,由于层间松耦合,我们可以专注于本层的设计,而不必关心其它层,也不必担心自己的设计会影响其它层。可以说,DDD 成功地降低了层与层之间的依赖。 -其次,分层架构使得程序结构变得清晰,升级和维护更加容易。我们修改某层代码时,只要本层的接口参数不变,其它层可以不必修改。即使本层的接口发生变化,也只影响相邻的上层,修改工作量小且错误可以控制,不会带来意外的风险。 **那我们该怎样转向 DDD 分层架构呢?不妨看看下面这个过程。** +其次,分层架构使得程序结构变得清晰,升级和维护更加容易。我们修改某层代码时,只要本层的接口参数不变,其它层可以不必修改。即使本层的接口发生变化,也只影响相邻的上层,修改工作量小且错误可以控制,不会带来意外的风险。**那我们该怎样转向 DDD 分层架构呢?不妨看看下面这个过程。** 传统企业应用大多是单体架构,而单体架构则大多是三层架构。三层架构解决了程序内代码间调用复杂、代码职责不清的问题,但这种分层是逻辑概念,在物理上它是中心化的集中式架构,并不适合分布式微服务架构。 diff --git "a/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25420\350\256\262.md" "b/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25420\350\256\262.md" index c0ee1b452..ad7fe9a7f 100644 --- "a/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25420\350\256\262.md" +++ "b/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25420\350\256\262.md" @@ -98,7 +98,7 @@ 企业级业务流程往往是多个微服务一起协作完成的,每个单一职责的微服务就像积木块,它们只完成自己特定的功能。那如何组织这些微服务,完成企业级业务编排和协同呢? -你可以在微服务和前端应用之间,增加一层 BFF 微服务(Backend for Frontends)。 **BFF 主要职责是处理微服务之间的服务组合和编排** ,微服务内的应用服务也是处理服务的组合和编排,那这二者有什么差异呢? +你可以在微服务和前端应用之间,增加一层 BFF 微服务(Backend for Frontends)。**BFF 主要职责是处理微服务之间的服务组合和编排**,微服务内的应用服务也是处理服务的组合和编排,那这二者有什么差异呢? BFF 位于中台微服务之上,主要职责是微服务之间的服务协调; **应用服务主要处理微服务内的服务组合和编排。** 在设计时我们应尽可能地将可复用的服务能力往下层沉淀,在实现能力复用的同时,还可以避免跨中心的服务调用。 diff --git "a/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25421\350\256\262.md" "b/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25421\350\256\262.md" index b90b24f0b..34c12bb88 100644 --- "a/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25421\350\256\262.md" +++ "b/docs/Design/DDD \345\256\236\346\210\230\350\257\276/\347\254\25421\350\256\262.md" @@ -38,7 +38,7 @@ DDD 是一个相对复杂的方法体系,它与传统的软件开发模式或 DDD 基于各种考虑,有很多的设计原则,也用到了很多的设计模式。条条框框多了,很多人可能就会被束缚住,总是担心或犹豫这是不是原汁原味的 DDD。其实我们不必追求极致的 DDD,这样做反而会导致过度设计,增加开发复杂度和项目成本。 -DDD 的设计原则或模式,是考虑了很多具体场景或者前提的。有的是为了解耦,如仓储服务、边界以及分层,有的则是为了保证数据一致性,如聚合根管理等。在理解了这些设计原则的根本原因后,有些场景你就可以灵活把握设计方法了,你可以突破一些原则,不必受限于条条框框,大胆选择最合适的方法。 **以上就是我对这三个问题的理解了。** +DDD 的设计原则或模式,是考虑了很多具体场景或者前提的。有的是为了解耦,如仓储服务、边界以及分层,有的则是为了保证数据一致性,如聚合根管理等。在理解了这些设计原则的根本原因后,有些场景你就可以灵活把握设计方法了,你可以突破一些原则,不必受限于条条框框,大胆选择最合适的方法。**以上就是我对这三个问题的理解了。** 用好 DDD 的关键,首先要领悟 DDD 的核心设计思想和理念,了解它为什么适合微服务架构,然后慢慢体会、消化、吸收和实践。DDD 体系虽然复杂,但也是有矩可循的,照着样例多做几个事件风暴,完成领域建模和微服务设计,体会 DDD 的整个设计过程。相信你很快就能领悟到 DDD 的核心设计理念了,这样就可以做到收放自如,趟出一条适合自己的 DDD 实践之路。 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25400\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25400\350\256\262.md" index e07997f16..76ff125c9 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25400\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25400\350\256\262.md" @@ -12,7 +12,7 @@ 记得 2006 年,我怀着无比激动的心情开始研读 Eric Evans 写的《领域驱动设计》一书。这的确是本重量级的巨著,我从中学到了太多软件开发的真谛,随后也开始积极地运用在实践中。 -但是,多年以后,当经历了无数软件项目的磨炼以后,我扪心自问 DDD 真正用起来了吗?没有, **只学到了它的思想,但却没有按照它的方法去实践,这是为什么呢?** DDD 是软件核心复杂性的应对之道,但当时都在忙着开发新项目,如何快速编码开发系统、快速上线才是王道,领域驱动对于客户来说太慢了。并且那个时代,业务也并没有那么复杂,DDD 远远发挥不出应有的优势。但是,最近几年,事情却慢慢发生了变化。 +但是,多年以后,当经历了无数软件项目的磨炼以后,我扪心自问 DDD 真正用起来了吗?没有,**只学到了它的思想,但却没有按照它的方法去实践,这是为什么呢?** DDD 是软件核心复杂性的应对之道,但当时都在忙着开发新项目,如何快速编码开发系统、快速上线才是王道,领域驱动对于客户来说太慢了。并且那个时代,业务也并没有那么复杂,DDD 远远发挥不出应有的优势。但是,最近几年,事情却慢慢发生了变化。 ### 第 2 幕:令人心塞的遗留系统 @@ -28,21 +28,21 @@ 2015 年,互联网技术的飞速发展带给了我们无限发展的空间。越来越多的行业在思考:如何转型互联网?如何开展互联网业务?这时,一个互联网转型的利器——微服务,它恰恰能够帮助很多行业很好地应对互联网业务。于是乎,我们加入了微服务转型的滚滚洪流之中。 -但是, **微服务也不是银弹,它也有很多的“坑”**。 +但是,**微服务也不是银弹,它也有很多的“坑”**。 ![ddd.png](assets/Ciqc1F-yNB-ADKSxAAUaE9dRRdU695.png) -当按照模块拆分微服务以后才发现,每次变更都需要修改多个微服务,不但多个团队都要变更,还要同时打包、同时升级,不仅没有降低维护成本,还使得系统的发布比过去更麻烦,真不如不用微服务。 **是微服务不好吗** ?我又陷入了沉思。 +当按照模块拆分微服务以后才发现,每次变更都需要修改多个微服务,不但多个团队都要变更,还要同时打包、同时升级,不仅没有降低维护成本,还使得系统的发布比过去更麻烦,真不如不用微服务。**是微服务不好吗**?我又陷入了沉思。 这时我才注意到 **Martin Flower 在定义微服务时提到的“小而专”,很多人理解了“小”却忽略了“专”,就会带来微服务系统难于维护的糟糕境地**。这里的“专”,就是要“小团队独立维护”,也就是尽量让每次的需求变更交给某个小团队独立完成,让需求变更落到某个微服务上进行变更,唯有这样才能发挥微服务的优势。 ![1.png](assets/Ciqc1F-yIsyAONSrAAB8S9PQpFM405.png) -通过这样的一番解析,才发现微服务的设计真的不仅仅是一个技术架构更迭的事情,而是对原有的设计提出了更高的要求,即“微服务内高内聚,微服务间低耦合”。 **如何才能更好地做到这一点呢?答案还是 DDD。** 我们转型微服务的重要根源之一就是系统的复杂性,即系统规模越来越大,维护越来越困难,才需要拆分微服务。然而,拆分成微服务以后,并不意味着每个微服务都是各自独立地运行,而是彼此协作地组织在一起。这就好像一个团队,规模越大越需要一些方法来组织,而 DDD 恰恰就是那个帮助我们组织微服务的实践方法。 +通过这样的一番解析,才发现微服务的设计真的不仅仅是一个技术架构更迭的事情,而是对原有的设计提出了更高的要求,即“微服务内高内聚,微服务间低耦合”。**如何才能更好地做到这一点呢?答案还是 DDD。** 我们转型微服务的重要根源之一就是系统的复杂性,即系统规模越来越大,维护越来越困难,才需要拆分微服务。然而,拆分成微服务以后,并不意味着每个微服务都是各自独立地运行,而是彼此协作地组织在一起。这就好像一个团队,规模越大越需要一些方法来组织,而 DDD 恰恰就是那个帮助我们组织微服务的实践方法。 ### 第 4 幕:DDD,想说爱你不容易 -2018 年,经过一番挣扎,我终于说服了开发团队开始使用 DDD,在这个过程中发现, **要让 DDD 在团队中用得好,还需要一个支持 DDD 与微服务的技术中台**。 +2018 年,经过一番挣扎,我终于说服了开发团队开始使用 DDD,在这个过程中发现,**要让 DDD 在团队中用得好,还需要一个支持 DDD 与微服务的技术中台**。 有了这个技术中台的支持,开发团队就可以把更多的精力放到对用户业务的理解,对业务痛点的理解,快速开发用户满意的功能并快速交付上。这样,不仅编写代码减少了,技术门槛降低了,还使得日后的变更更加容易,技术更迭也更加方便。因此,我又开始苦苦求索。 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25401\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25401\350\256\262.md" index af0932b79..92c7b4a8a 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25401\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25401\350\256\262.md" @@ -4,9 +4,9 @@ 不过,这些年随着中国软件业的快速发展,软件规模越来越大,生命周期也越来越长,推倒重新开发的风险越来越大。这时,软件团队急切需要在较低成本的状态下持续维护一个系统很多年。然而,事与愿违。随着时间的推移,程序越来越乱,维护成本越来越高,软件退化成了无数软件团队的噩梦。 -这时,微服务架构成了规模化软件的解决之道。不过, **微服务对设计提出了很高的要求,强调“小而专、高内聚”,否则就不能发挥出微服务的优势,甚至可能令问题更糟糕**。 +这时,微服务架构成了规模化软件的解决之道。不过,**微服务对设计提出了很高的要求,强调“小而专、高内聚”,否则就不能发挥出微服务的优势,甚至可能令问题更糟糕**。 -因此,微服务的设计,微服务的拆分都需要领域驱动设计的指导。那么, **领域驱动为什么能解决软件规模化的问题呢?** 我们先从问题的根源谈起,即软件退化。 +因此,微服务的设计,微服务的拆分都需要领域驱动设计的指导。那么,**领域驱动为什么能解决软件规模化的问题呢?** 我们先从问题的根源谈起,即软件退化。 ## 软件退化的根源 @@ -14,7 +14,7 @@ 那么,在这个过程中,一方面会给我们带来诸多的挑战,另一方面又会给我们带来无尽的机会,它会带来更多的新兴市场、新兴产业与全新业务,给我们带来全新的发展机遇。 -然而,在面对全新业务、全新增长点的时候,我们能不能把握住这样的机遇呢?我们期望能把握住,但每次回到现实,回到正在维护的系统时,却令人沮丧。我们的软件总是经历着这样的轮回, **软件设计质量最高的时候是第一次设计的那个版本,当第一个版本设计上线以后就开始各种需求变更,这常常又会打乱原有的设计。** 因此,需求变更一次,软件就修改一次,软件修改一次,质量就下降一次。不论第一次的设计质量有多高,软件经历不了几次变更,就进入一种低质量、难以维护的状态。进而,团队就不得不在这样的状态下,以高成本的方式不断地维护下去,维护很多年。 +然而,在面对全新业务、全新增长点的时候,我们能不能把握住这样的机遇呢?我们期望能把握住,但每次回到现实,回到正在维护的系统时,却令人沮丧。我们的软件总是经历着这样的轮回,**软件设计质量最高的时候是第一次设计的那个版本,当第一个版本设计上线以后就开始各种需求变更,这常常又会打乱原有的设计。** 因此,需求变更一次,软件就修改一次,软件修改一次,质量就下降一次。不论第一次的设计质量有多高,软件经历不了几次变更,就进入一种低质量、难以维护的状态。进而,团队就不得不在这样的状态下,以高成本的方式不断地维护下去,维护很多年。 这时候,维护好原有的业务都非常不易,又如何再去期望未来更多的全新业务呢?比如,这是一段电商网站支付功能的设计,最初的版本设计质量还是不错的: @@ -32,11 +32,11 @@ 要探寻软件退化的根源,先要从探寻软件的本质及其规律开始,软件的本质就是对真实世界的模拟,每个软件都能在真实世界中找到它的影子。因此,软件中业务逻辑正确与否的唯一标准就是 **是否与真实世界一致**。如果一致,则软件是 OK 的;不一致,则用户会提 Bug、提新需求。 -在这里发现了一个非常重要的线索,那就是, **软件要做成什么样,既不由我们来决定,也不由用户来决定,而是由客观世界决定**。用户为什么总在改需求,是因为他们也不确定客观世界的规则,只有遇到问题了他们才能想得起来。因此,对于我们来说,与其唯唯诺诺地按照用户的要求去做软件,不如主动地理解业务的基础上去分析软件,而后者会更有利于我们减少变更的成本。 +在这里发现了一个非常重要的线索,那就是,**软件要做成什么样,既不由我们来决定,也不由用户来决定,而是由客观世界决定**。用户为什么总在改需求,是因为他们也不确定客观世界的规则,只有遇到问题了他们才能想得起来。因此,对于我们来说,与其唯唯诺诺地按照用户的要求去做软件,不如主动地理解业务的基础上去分析软件,而后者会更有利于我们减少变更的成本。 那么,真实世界是怎样,我们就怎样开发软件,不就简单了吗?其实并非如此,因为真实世界是非常复杂的,要深刻理解真实世界中的这些业务逻辑是需要一个过程的。因此,我们最初只能认识真实世界中那些简单、清晰、易于理解的业务逻辑,把它们做到我们的软件里,即每个软件的第一个版本的需求总是那么清晰明了、易于设计。 -然而,当我们把第一个版本的软件交付用户使用的时候,用户却会发现,还有很多不简单、不明了、不易于理解的业务逻辑没做到软件里。这在使用软件的过程中很不方便,和真实业务不一致,因此用户就会提 Bug、提新需求。 **在我们不断地修复 Bug,实现新需求的过程中,软件的业务逻辑也会越来越接近真实世界,使得我们的软件越来越专业,让用户感觉越来越好用。但是,在软件越来越接近真实世界的过程中,业务逻辑就会变得越来越复杂,软件规模也越来越庞大**。 +然而,当我们把第一个版本的软件交付用户使用的时候,用户却会发现,还有很多不简单、不明了、不易于理解的业务逻辑没做到软件里。这在使用软件的过程中很不方便,和真实业务不一致,因此用户就会提 Bug、提新需求。**在我们不断地修复 Bug,实现新需求的过程中,软件的业务逻辑也会越来越接近真实世界,使得我们的软件越来越专业,让用户感觉越来越好用。但是,在软件越来越接近真实世界的过程中,业务逻辑就会变得越来越复杂,软件规模也越来越庞大**。 你一定有这样一个认识:简单软件有简单软件的设计,复杂软件有复杂软件的设计。 @@ -48,7 +48,7 @@ 这时,如果要保持软件设计质量不退化,就应当逐步调整软件的程序结构,逐渐由简单的程序结构转变为复杂的程序结构。如果我们总是这样做,就能始终保持软件的设计质量,不过非常遗憾的是,我们以往在维护软件的过程中却不是这样做的,而是不断地在原有简单软件的程序结构下,往 payoff() 方法中塞代码,这样做必然会造成软件的退化。 -也就是说, **软件退化的根源不是软件变更,软件变更只是一个诱因**。如果每次软件变更时,适时地进行解耦,进行功能扩展,再实现新的功能,就能保持高质量的软件设计。但如果在每次软件变更时没有调整程序结构,而是在原有的程序结构上不断地塞代码,软件就会退化。这就是软件发展的规律,软件退化的根源。 +也就是说,**软件退化的根源不是软件变更,软件变更只是一个诱因**。如果每次软件变更时,适时地进行解耦,进行功能扩展,再实现新的功能,就能保持高质量的软件设计。但如果在每次软件变更时没有调整程序结构,而是在原有的程序结构上不断地塞代码,软件就会退化。这就是软件发展的规律,软件退化的根源。 ## 杜绝软件退化:两顶帽子 @@ -77,7 +77,7 @@ 前面的设计,在实现新功能的同时,新代码与老代码在同一个类、同一个方法中了,违反了“开放-封闭原则”。怎样才能既满足“开放-封闭原则”,又能够实现新功能呢?在原有的代码上你发现什么都做不了!难道“开放-封闭原则”错了吗? -问题的关键就在于,当我们在实现新需求时,应当采用“两顶帽子”的方式进行设计,这种方式就要求在每次变更时,将变更分为两个步骤。 **两顶帽子** : +问题的关键就在于,当我们在实现新需求时,应当采用“两顶帽子”的方式进行设计,这种方式就要求在每次变更时,将变更分为两个步骤。**两顶帽子** : - 在不添加新功能的前提下,重构代码,调整原有程序结构,以适应新功能; - 实现新的功能。 @@ -90,9 +90,9 @@ **有了“两顶帽子”,我们不再需要焦虑,不再需要过度设计**,正确的思路应当是“活在今天的格子里做今天的事儿”,也就是为当前的需求进行设计,使其刚刚满足当前的需求。所谓的“高质量的软件设计”就是要掌握一个平衡,一方面要满足当前的需求,另一方面要让设计刚刚满足需求,从而使设计最简化、代码最少。这样做,不仅软件设计质量提高了,设计难点也得到了大幅度降低。 -简而言之, **保持软件设计不退化的关键在于每次需求变更的设计,只有保证每次需求变更时做出正确的设计,才能保证软件以一种良性循环的方式不断维护下去。这种正确的设计方式就是“两顶帽子”。** 但是,在实践“两顶帽子”的过程中,比较困难的是第一步。在不添加新功能的前提下,如何重构代码,如何调整原有程序结构,以适应新功能,这是有难度的。很多时候,第一次变更、第二次变更、第三次变更,这些事情还能想清楚;但经历了第十次变更、第二十次变更、第三十次变更,这些事情就想不清楚了,设计开始迷失方向。 +简而言之,**保持软件设计不退化的关键在于每次需求变更的设计,只有保证每次需求变更时做出正确的设计,才能保证软件以一种良性循环的方式不断维护下去。这种正确的设计方式就是“两顶帽子”。** 但是,在实践“两顶帽子”的过程中,比较困难的是第一步。在不添加新功能的前提下,如何重构代码,如何调整原有程序结构,以适应新功能,这是有难度的。很多时候,第一次变更、第二次变更、第三次变更,这些事情还能想清楚;但经历了第十次变更、第二十次变更、第三十次变更,这些事情就想不清楚了,设计开始迷失方向。 -那么, **有没有一种方法,让我们在第十次变更、第二十次变更、第三十次变更时,依然能够找到正确的设计呢?有,那就是“领域驱动设计”**。 +那么,**有没有一种方法,让我们在第十次变更、第二十次变更、第三十次变更时,依然能够找到正确的设计呢?有,那就是“领域驱动设计”**。 ## 保持软件质量:领域驱动 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25402\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25402\350\256\262.md" index 88102e63e..f9c923a5c 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25402\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25402\350\256\262.md" @@ -40,15 +40,15 @@ 这是上一个版本的领域模型,现在我们要在这个模型的基础上增加折扣功能,并且还要分为限时折扣、限量折扣、某类商品的折扣等不同类型。这时,我们应当怎么分析设计呢? -**首先要分析付款与折扣的关系。** 付款与折扣是什么关系呢?你可能会认为折扣是在付款的过程中进行的折扣,因此就应当将折扣写到付款中。这样思考对吗?我们应当基于什么样的思想与原则来设计呢?这时,另外一个重量级的设计原则应该出场了,那就是“单一职责原则”。 **单一职责原则** :软件系统中的每个元素只完成自己职责范围内的事,而将其他的事交给别人去做,我只是去调用。 +**首先要分析付款与折扣的关系。** 付款与折扣是什么关系呢?你可能会认为折扣是在付款的过程中进行的折扣,因此就应当将折扣写到付款中。这样思考对吗?我们应当基于什么样的思想与原则来设计呢?这时,另外一个重量级的设计原则应该出场了,那就是“单一职责原则”。**单一职责原则** :软件系统中的每个元素只完成自己职责范围内的事,而将其他的事交给别人去做,我只是去调用。 单一职责原则是软件设计中一个非常重要的原则,但如何正确地理解它成为一个非常关键的问题。在这句话中,准确理解的关键就在于“ **职责** ”二字,即自己职责的范围到底在哪里。以往,我们错误地理解这个“职责”就是做某一个事,与这个事情相关的所有事情都是它的职责,正因为这个错误的理解,带来了许多错误的设计,而将折扣写到付款功能中。那么,怎样才是对“职责”正确的理解呢? “一个职责就是软件变化的一个原因”是著名的软件大师 Bob 大叔在他的《敏捷软件开发:原则、模式与实践》中的表述。但这个表述过于精简,很难深刻地理解其中的内涵,从而不能有效地提高我们的设计质量。这里我好好解读一下这句话。 -先思考一下什么是高质量的代码。你可能立即会想到“低耦合、高内聚”,以及各种设计原则,但这些评价标准都太“虚”。 **最直接、最落地的评价标准就是,当用户提出一个需求变更时,为了实现这个变更而修改软件的成本越低,那么软件的设计质量就越高。** 当来了一个需求变更时,怎样才能让修改软件的成本降低呢?如果为了实现这个需求,需要修改 3 个模块的代码,完后这 3 个模块都需要测试,其维护成本必然是“高”。那么怎样才能降到最低呢?维护 0 个模块的代码?那显然是不可能的,因此最现实的方案就是只修改 1 个模块,维护成本最低。 +先思考一下什么是高质量的代码。你可能立即会想到“低耦合、高内聚”,以及各种设计原则,但这些评价标准都太“虚”。**最直接、最落地的评价标准就是,当用户提出一个需求变更时,为了实现这个变更而修改软件的成本越低,那么软件的设计质量就越高。** 当来了一个需求变更时,怎样才能让修改软件的成本降低呢?如果为了实现这个需求,需要修改 3 个模块的代码,完后这 3 个模块都需要测试,其维护成本必然是“高”。那么怎样才能降到最低呢?维护 0 个模块的代码?那显然是不可能的,因此最现实的方案就是只修改 1 个模块,维护成本最低。 -那么,怎样才能在每次变更的时候都只修改一个模块就能实现新需求呢?那就需要我们在平时就不断地整理代码,将那些因同一个原因而变更的代码都放在一起,而将因不同原因而变更的代码分开放,放在不同的模块、不同的类中。这样,当因为这个原因而需要修改代码时,需要修改的代码都在这个模块、这个类中,修改范围就缩小了,维护成本降低了,自然设计质量就提高了。 **总之,单一职责原则要求我们在维护软件的过程中需要不断地进行整理,将软件变化同一个原因的代码放在一起,将软件变化不同原因的代码分开放。** 按照这样的设计原则,回到前面那个案例中,那么应当怎样去分析“付款”与“折扣”之间的关系呢?只需要回答两个问题: +那么,怎样才能在每次变更的时候都只修改一个模块就能实现新需求呢?那就需要我们在平时就不断地整理代码,将那些因同一个原因而变更的代码都放在一起,而将因不同原因而变更的代码分开放,放在不同的模块、不同的类中。这样,当因为这个原因而需要修改代码时,需要修改的代码都在这个模块、这个类中,修改范围就缩小了,维护成本降低了,自然设计质量就提高了。**总之,单一职责原则要求我们在维护软件的过程中需要不断地进行整理,将软件变化同一个原因的代码放在一起,将软件变化不同原因的代码分开放。** 按照这样的设计原则,回到前面那个案例中,那么应当怎样去分析“付款”与“折扣”之间的关系呢?只需要回答两个问题: - 当“付款”发生变更时,“折扣”是不是一定要变? - 当“折扣”发生变更时,“付款”是不是一定要变? diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25403\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25403\350\256\262.md" index 610993a9c..c75c6f300 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25403\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25403\350\256\262.md" @@ -38,7 +38,7 @@ ![Drawing 4.png](assets/Ciqc1F-3ad-AASFqAAClQIvmC-A196.png) -总之, **DDD 的数据库设计实际上已经变成了:以领域模型为核心,如何将领域模型转换成数据库设计的过程。** 那么怎样进行转换呢?在领域模型中是一个一个的类,而在数据库设计中是一个一个的表,因此就是将类转换成表的过程。 +总之,**DDD 的数据库设计实际上已经变成了:以领域模型为核心,如何将领域模型转换成数据库设计的过程。** 那么怎样进行转换呢?在领域模型中是一个一个的类,而在数据库设计中是一个一个的表,因此就是将类转换成表的过程。 ![Drawing 6.png](assets/Ciqc1F-3aeaAZwhMAAD1h66weqU767.png) diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25404\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25404\350\256\262.md" index 5b7c5c3ad..2831d6041 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25404\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25404\350\256\262.md" @@ -24,7 +24,7 @@ ### 实体和值对象的区分 -在 DDD 中,对实体与值对象进行了严格的区分。 **可变性** 是 **实体** 的特点,而 **不变性** 则是 **值对象** 的本质。例如,北京是一个城市,架构师是一个职务,人民币是一个币种,这些事物的特性是永远不变的。 +在 DDD 中,对实体与值对象进行了严格的区分。**可变性** 是 **实体** 的特点,而 **不变性** 则是 **值对象** 的本质。例如,北京是一个城市,架构师是一个职务,人民币是一个币种,这些事物的特性是永远不变的。 在实际项目中,我们可以根据业务需求的不同,灵活选用实体还是值对象。比如,在线订餐系统中,根据业务需求的不同,菜单既可以设计成实体,也可以设计成值对象。例如,“宫保鸡丁”是一个菜品,如果将其按照值对象设计,则整个系统中“宫保鸡丁”只有一条记录,所有饭店的菜单如果有这道菜,都是引用的这条记录;如果按照实体进行设计,则是认为每个饭店的“宫保鸡丁”都是不同的,比如每个饭店的“宫保鸡丁”的价格都是不尽相同的。因此,将其设计成有多条记录、有各自不同的 ID,每个饭店都是使用自己的“宫保鸡丁”。 @@ -80,7 +80,7 @@ ![image](assets/Ciqc1F-8wPaAeLA0AACXsgk7a30591.png) -相反,贫血模型就显得更加贫民化。在贫血模型中, **MVC 层** 直接调用 Service,Service 通过 **DAO** 进行数据访问。在这个过程中,每个 DAO 都只查询数据库中的某个表,然后直接交给 Service 去使用,去完成各种处理。 +相反,贫血模型就显得更加贫民化。在贫血模型中,**MVC 层** 直接调用 Service,Service 通过 **DAO** 进行数据访问。在这个过程中,每个 DAO 都只查询数据库中的某个表,然后直接交给 Service 去使用,去完成各种处理。 以订单系统为例,订单有订单 DAO,负责查询订单;订单明细有订单明细 DAO,负责查询订单明细。它们查询出来以后,不需要装配,而是直接交给订单 Service 使用。在保存订单时,订单 DAO 负责保存订单,订单明细 DAO 负责保存订单明细。它们都是通过订单 Service 进行组织,并建立事务。贫血模型不需要仓库,不需要工厂,也不需要缓存,一切都显得那么简单粗暴但一目了然。 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25405\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25405\350\256\262.md" index 41f323780..3bca48a54 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25405\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25405\350\256\262.md" @@ -38,7 +38,7 @@ public class Order { 譬如:订单与用户之间的关系就不是聚合。因为用户不是创建订单时才存在的,而是在创建订单时早就存在了;当删除订单时,用户不会随着订单的删除而删除,因为删除了订单,用户依然还是那个用户。 -那么,饭店和菜单的关系是不是聚合关系呢? **关键要看系统如何设计**。如果系统设计成每个饭店都有各不相同的菜单,每个菜单都是隶属于某个饭店,则饭店和菜单是聚合关系。这种设计让各个饭店都有“宫保鸡丁”,但每个饭店都是各自不同的“宫保鸡丁”,比如在描述、图片或价格上的不同,甚至在数据库中也是有各不相同的记录。这时,要查询菜单就要先查询饭店,离开了饭店的菜单是没有意义的。 +那么,饭店和菜单的关系是不是聚合关系呢?**关键要看系统如何设计**。如果系统设计成每个饭店都有各不相同的菜单,每个菜单都是隶属于某个饭店,则饭店和菜单是聚合关系。这种设计让各个饭店都有“宫保鸡丁”,但每个饭店都是各自不同的“宫保鸡丁”,比如在描述、图片或价格上的不同,甚至在数据库中也是有各不相同的记录。这时,要查询菜单就要先查询饭店,离开了饭店的菜单是没有意义的。 但是,饭店和菜单还可以有另外一种设计思路,那就是所有的菜单都是公用的,去每个饭店只是选择有还是没有这个菜品。这时,系统中有一个菜单对象,“宫保鸡丁”只是这个对象中的一条记录,其他各个饭店,如果他们的菜单上有“宫保鸡丁”,则去引用这个对象,否则不引用。这时,菜单就不再是饭店的一个部分,没有这个饭店,这个菜品依然存在,就不再是聚合关系。 @@ -52,7 +52,7 @@ public class Order { 然而,这样的设计有时是有效的,但并非都有效。譬如,在管理订单时,对订单进行增删改,聚合是有效的。但是,如果要统计销量、分析销售趋势、销售占比时,则需要对大量的订单明细进行汇总、进行统计;如果每次对订单明细的汇总与统计都必须经过订单的查询,必然使得查询统计变得效率极低而无法使用。 -因此,领域驱动设计通常适用于增删改的业务操作,但不适用于分析统计。在一个系统中, **增删改的业务可以采用领域驱动的设计,但在非增删改的分析汇总场景中,则不必采用领域驱动的设计,直接 SQL 查询就好了,也就不必再遵循聚合的约束了。** +因此,领域驱动设计通常适用于增删改的业务操作,但不适用于分析统计。在一个系统中,**增删改的业务可以采用领域驱动的设计,但在非增删改的分析汇总场景中,则不必采用领域驱动的设计,直接 SQL 查询就好了,也就不必再遵循聚合的约束了。** ## 聚合的设计实现 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25406\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25406\350\256\262.md" index c7edfa92f..92c5def65 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25406\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25406\350\256\262.md" @@ -8,7 +8,7 @@ 假如将整个系统中那么多的场景、涉及的那么多领域对象,全部绘制在一张大图上,可以想象这张大图需要绘制出密密麻麻的领域对象,以及它们之间纷繁复杂的对象间关系。绘制这样的图,绘制的人非常费劲,看这张图的人也非常费劲,这样的图也不利于我们理清思路、交流思想及提高设计质量。 -正确的做法就是 **将整个系统划分成许多相对独立的业务场景**,在一个一个的业务场景中进行领域分析与建模,这样的业务场景称为 **“问题子域”**,简称“子域”。 **领域驱动核心的设计思想,就是将对软件的分析与设计还原到真实世界中**,那么就要先分析和理解真实世界的业务与问题。而真实世界的业务与问题叫作 **“问题域”**,这里面的业务规则与知识叫 **“业务领域知识”**,譬如: +正确的做法就是 **将整个系统划分成许多相对独立的业务场景**,在一个一个的业务场景中进行领域分析与建模,这样的业务场景称为 **“问题子域”**,简称“子域”。**领域驱动核心的设计思想,就是将对软件的分析与设计还原到真实世界中**,那么就要先分析和理解真实世界的业务与问题。而真实世界的业务与问题叫作 **“问题域”**,这里面的业务规则与知识叫 **“业务领域知识”**,譬如: - **电商网站** 的“问题域”是人们如何进行在线购物,购物的流程是怎样的; - **在线订餐系统** 的“问题域”是人们如何在线订餐,饭店如何在线接单,系统又是如何派送骑士去配送的。 @@ -28,13 +28,13 @@ DDD 中限界上下文的设计,很好地体现了高质量软件设计中 ** ## 限界上下文与微服务 -**所谓“限界上下文内的高内聚”,也就是每个限界上下文内实现的功能,都是软件变化的同一个原因的代码**。因为这个原因的变化才需要修改这个限界上下文,而不是这个原因的变化就不需要修改这个限界上下文,修改与它无关。正是因为限界上下文有如此好的特性,才使得现在很多微服务团队, **运用限界上下文作为微服务拆分的原则**,即每个限界上下文对应一个微服务 +**所谓“限界上下文内的高内聚”,也就是每个限界上下文内实现的功能,都是软件变化的同一个原因的代码**。因为这个原因的变化才需要修改这个限界上下文,而不是这个原因的变化就不需要修改这个限界上下文,修改与它无关。正是因为限界上下文有如此好的特性,才使得现在很多微服务团队,**运用限界上下文作为微服务拆分的原则**,即每个限界上下文对应一个微服务 ![Drawing 0.png](assets/CgqCHl_GBb6AA9--AACyYcIHmOI823.png) 按照这样的原则拆分出来的微服务系统,在今后变更维护时,可以很好地将每次的需求变更,快速落到某个微服务中变更。这样,变更这个微服务就实现了该需求,升级该服务后就可以交付用户使用了。这样的设计,使得越来越多的规划开发团队,今后可以实现 **低成本维护与快速交付**,进而快速适应市场变化而提升企业竞争力。 -譬如,在电商网站的购物过程中,购物、下单、支付、物流,都是软件变化不同的原因,因此,按照不同的业务场景划分限界上下文,然后以此拆分微服务。那么,当购物变更时就修改购物微服务,下单变更就修改下单微服务,但它们在业务处理过程中都需要读取商品信息,因此调用“商品管理”微服务来获取商品信息。这样,一旦商品信息发生变更,只与“商品管理”微服务有关,与其他微服务无关,那么维护成本将得到降低,交付速度得到提升。 **所谓“限界上下文间的低耦合”,就是限界上下文通过上下文地图相互调用时,通过接口进行调用**。如下图所示,模块 A 需要调用模块 B,那么它就与模块 B 形成了一种耦合,这时: +譬如,在电商网站的购物过程中,购物、下单、支付、物流,都是软件变化不同的原因,因此,按照不同的业务场景划分限界上下文,然后以此拆分微服务。那么,当购物变更时就修改购物微服务,下单变更就修改下单微服务,但它们在业务处理过程中都需要读取商品信息,因此调用“商品管理”微服务来获取商品信息。这样,一旦商品信息发生变更,只与“商品管理”微服务有关,与其他微服务无关,那么维护成本将得到降低,交付速度得到提升。**所谓“限界上下文间的低耦合”,就是限界上下文通过上下文地图相互调用时,通过接口进行调用**。如下图所示,模块 A 需要调用模块 B,那么它就与模块 B 形成了一种耦合,这时: - 如果需要复用模块 A,那么所有有模块 A 的地方都必须有模块 B,否则模块 A 就会报错; - 如果模块 B 还要依赖模块 C,模块 C 还要依赖模块 D,那么所有使用模块 A 的地方都必须有模块 B、C、D,使用模块 A 的成本就会非常高昂。 @@ -70,9 +70,9 @@ DDD 中限界上下文的设计,很好地体现了高质量软件设计中 ** - 接着,按照这样的思路拆分微服务,多个微服务都要读取商品信息表。 - 这样,一旦商品信息表发生变更,多个微服务都需要变更。不仅多个团队都要为了维护这个需求修改代码,而且他们的微服务需要同时修改、同时发布、同时升级。 -如果每次的维护都是这样进行, **不仅微服务的优势不能发挥出来,还会使得维护的成本更高**。如果微服务被设计成这样,还真不如不用微服务。 +如果每次的维护都是这样进行,**不仅微服务的优势不能发挥出来,还会使得维护的成本更高**。如果微服务被设计成这样,还真不如不用微服务。 -这里的关键问题在于,当多个微服务都要读取同一个表时,也就意味着同一个软件变化原因(因商品信息而变更)的代码被分散到多个微服务中。这时,当系统因该原因而变化时,代码的修改自然就会分散到多个微服务上。也就是说,以上设计问题的根源 **违反了“单一职责原则”,使微服务的设计不再高内聚**。微服务该怎样设计、怎样拆分? **关键就在于“小而专”**,这里的“专”就是高内聚。 +这里的关键问题在于,当多个微服务都要读取同一个表时,也就意味着同一个软件变化原因(因商品信息而变更)的代码被分散到多个微服务中。这时,当系统因该原因而变化时,代码的修改自然就会分散到多个微服务上。也就是说,以上设计问题的根源 **违反了“单一职责原则”,使微服务的设计不再高内聚**。微服务该怎样设计、怎样拆分?**关键就在于“小而专”**,这里的“专”就是高内聚。 因此,微服务设计不是简单的拆分,而是对设计提出了更高的要求,即要做到“ **高内聚** ”。只有这样,才能让日后的变更能尽量落到某个微服务上维护,从而降低维护成本。唯有这样才能将微服务的优势发挥出来,才是微服务正确的打开方式。 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25407\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25407\350\256\262.md" index d26a1dbbb..4769c0f32 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25407\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25407\350\256\262.md" @@ -32,7 +32,7 @@ 在这个过程中,对于客户来说: - 客户十分清楚他的业务领域知识,以及他亟待解决的业务痛点; -- 然而, **客户不清楚技术能如何解决他的业务痛点**。 +- 然而,**客户不清楚技术能如何解决他的业务痛点**。 因此,用户在提需求时,是在用他有限的认知,想象技术如何解决他的业务痛点。所以这样提出的业务需求往往不太靠谱,要么技术难于实现,要么并非最优的方案。 @@ -54,9 +54,9 @@ - 我主动学习你的语言,了解你的业务领域知识,并用你的语言与你沟通; - 同时,我也主动地让你了解我的语言,了解我的业务领域知识,并用我的语言与你沟通。 -回到需求分析领域,我们清楚的是技术,但不了解业务,因此,应当主动地去了解业务。那么, **如何了解业务呢** ?找书慢慢地去学习业务吗?也不是,因为我们不是要努力成为业务领域专家,而仅仅是 **要掌握与要开发软件相关的业务领域知识**。在业务领域漫无目的地学习,学习效率低而收效甚微。 +回到需求分析领域,我们清楚的是技术,但不了解业务,因此,应当主动地去了解业务。那么,**如何了解业务呢**?找书慢慢地去学习业务吗?也不是,因为我们不是要努力成为业务领域专家,而仅仅是 **要掌握与要开发软件相关的业务领域知识**。在业务领域漫无目的地学习,学习效率低而收效甚微。 -所以,我们应当从客户那里去学习,比如询问客户,仔细聆听客户对业务的描述,在与客户的探讨中快速地学习业务。然而,在这个过程中,一个非常重要的关键就是, **注意捕获客户在描述业务过程中的那些专用术语,努力学会用这些专用术语与客户探讨业务**。 +所以,我们应当从客户那里去学习,比如询问客户,仔细聆听客户对业务的描述,在与客户的探讨中快速地学习业务。然而,在这个过程中,一个非常重要的关键就是,**注意捕获客户在描述业务过程中的那些专用术语,努力学会用这些专用术语与客户探讨业务**。 久而久之,用客户的语言与客户沟通,你们的沟通就会越来越顺畅,客户也会觉得你越来越专业,愿意与你沟通,并可以与你探讨越来越深的业务领域知识。当你对业务的理解越来越深刻,你就能越来越准确地理解客户的业务及痛点,并运用自己的技术专业知识,用更加合理的技术去解决用户的痛点。这样,你们的软件就会越来越专业,让用户能越来越喜欢购买和使用你们的软件,并形成长期合作关系。 @@ -72,17 +72,17 @@ ### 什么是事件风暴 -在领域驱动设计之初的需求分析阶段, **对需求分析的基本思路就是统一语言建模,它是我们的指导思想**。但落实到具体操作层面,可以采用的实践方法是 **事件风暴(Event Storming)**。它是一种基于工作坊的 DDD 实践方法,可以帮助我们快速发现业务领域中正在发生的事件,指导领域建模及程序开发。它是由意大利人 Alberto Brandolini 发明的一种领域驱动设计实践方法,被广泛应用于业务流程建模和需求工程。 +在领域驱动设计之初的需求分析阶段,**对需求分析的基本思路就是统一语言建模,它是我们的指导思想**。但落实到具体操作层面,可以采用的实践方法是 **事件风暴(Event Storming)**。它是一种基于工作坊的 DDD 实践方法,可以帮助我们快速发现业务领域中正在发生的事件,指导领域建模及程序开发。它是由意大利人 Alberto Brandolini 发明的一种领域驱动设计实践方法,被广泛应用于业务流程建模和需求工程。 这个方法的基本思想,就是将软件开发人员和领域专家聚集在一起,一同讨论、相互学习,即统一语言建模。但它的工作方式类似于头脑风暴,让建模过程变得更加有趣,让学习业务变得更加容易。因此,事件风暴中的“风暴”,就是运用头脑风暴会议进行领域分析建模。 -那么,这里的“事件”是什么意思呢? **事件即事实** (Event as Fact), **即在业务领域中那些已经发生的事件就是事实** (fact)。过去已经发生的事件已经成为了事实就不会再更改,因此信息管理系统就可以将这些事实以信息的形式存储到数据库中,即信息就是一组事实。 +那么,这里的“事件”是什么意思呢?**事件即事实** (Event as Fact),**即在业务领域中那些已经发生的事件就是事实** (fact)。过去已经发生的事件已经成为了事实就不会再更改,因此信息管理系统就可以将这些事实以信息的形式存储到数据库中,即信息就是一组事实。 说到底,一个信息管理系统的作用,就是存储这些事实,对这些事实进行管理与跟踪,进而起到提高工作效率的作用。因此,分析一个信息管理系统的业务需求,就是准确地抓住业务进行过程中那些需要存储的关键事实,并围绕着这些事实进行分析设计、领域建模,这就是“事件风暴”的精髓。 ### 召开事件风暴会议 -因此, **实践“事件风暴”方法,就是让开发人员与领域专家坐在一起,开事件风暴会议**。**会议的目的就是与领域专家一起进行领域建模**,而会议前的准备就是在会场准备一个大大的白板与各色的便笺纸,如下图所示: +因此,**实践“事件风暴”方法,就是让开发人员与领域专家坐在一起,开事件风暴会议**。**会议的目的就是与领域专家一起进行领域建模**,而会议前的准备就是在会场准备一个大大的白板与各色的便笺纸,如下图所示: ![Drawing 2.png](assets/Ciqc1F_GC3OAV1fMAAMURptGIOs120.png) @@ -120,6 +120,6 @@ ## 总结 -按照 DDD 的思想进行微服务设计,首先是从需求分析开始的。但 DDD 彻底改变了我们需求分析的方式,采用统一语言建模,让我们更加主动地理解业务,用客户的语言与客户探讨需求。 **统一语言建模是指导思想,事件风暴会议是实践方法**。运用事件风暴会议与客户探讨需求、建立模型,我们能更加深入地理解需求,而客户也更有参与感。此外,事件风暴会议可以作为敏捷开发中迭代计划会议前的准备会议的一个部分。 +按照 DDD 的思想进行微服务设计,首先是从需求分析开始的。但 DDD 彻底改变了我们需求分析的方式,采用统一语言建模,让我们更加主动地理解业务,用客户的语言与客户探讨需求。**统一语言建模是指导思想,事件风暴会议是实践方法**。运用事件风暴会议与客户探讨需求、建立模型,我们能更加深入地理解需求,而客户也更有参与感。此外,事件风暴会议可以作为敏捷开发中迭代计划会议前的准备会议的一个部分。 然而,通过事件风暴会议形成的领域模型,又该如何落地到微服务的设计呢?还会遇到哪些设计与技术难题呢?下一讲将进一步讲解领域模型的微服务设计实现。 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25408\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25408\350\256\262.md" index cba16846b..c2a78c3ff 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25408\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25408\350\256\262.md" @@ -18,7 +18,7 @@ 正如 [第06讲](../第06讲) 中谈到,领域模型的绘制,不是将整个系统的领域对象都绘制在一张大图上,那样绘制很费劲,阅读也很费劲,不利于相互的交流。因此,领域建模就是将一个系统划分成了多个子域,每个子域都是一个独立的业务场景。围绕着这个业务场景进行分析建模,该业务场景会涉及许多领域对象,而这些领域对象又可能与其他子域的对象进行关联。这样,每个子域的实现就是“限界上下文”,而它们之间的关联关系就是“上下文地图”。 -在本案例中,围绕着领域事件“已下单”进行分析。它属于“用户下单”这个限界上下文,但与之相关的“用户”及其“地址”来源于“用户注册”这个限界上下文,与之相关的“饭店”及其“菜单”来源于“饭店管理”这个限界上下文。因此, **在这个业务场景中**,“ **用户下单** ” **限界上下文属于** “ **主题域** ”, **而** “ **用户注册** ” **与** “ **饭店管理** ” **限界上下文属于** “ **支撑域** ”。同理,围绕着本案例的各个领域事件进行了如下一些设计: +在本案例中,围绕着领域事件“已下单”进行分析。它属于“用户下单”这个限界上下文,但与之相关的“用户”及其“地址”来源于“用户注册”这个限界上下文,与之相关的“饭店”及其“菜单”来源于“饭店管理”这个限界上下文。因此,**在这个业务场景中**,“ **用户下单** ” **限界上下文属于** “ **主题域** ”,**而** “ **用户注册** ” **与** “ **饭店管理** ” **限界上下文属于** “ **支撑域** ”。同理,围绕着本案例的各个领域事件进行了如下一些设计: ![Drawing 0.png](assets/CgqCHl_PFjeATu8NAAC_hYefOkM066.png) @@ -36,7 +36,7 @@ 按照 [第07讲](../第07讲) 所讲到的领域模型设计,以及基于该模型的限界上下文划分,将整个系统划分为了“用户下单”“饭店接单”“骑士派送”等微服务。但是,在设计实现的时候,还有一个设计难题,即 **领域事件该如何通知**。 -譬如,当用户在“用户下单”微服务中下单,那么会在该微服务中形成一个订单;但是,“饭店接单”是另外一个微服务,它必须要及时获得已下单的订单信息,才能执行接单。那么,如何通知“饭店接单”微服务已经有新的订单。诚然,可以让“饭店接单”微服务按照一定的周期不断地去查询“用户下单”微服务中已下单的订单信息。然而,这样的设计,不仅会加大“用户下单”与“饭店接单”微服务的系统负载,形成资源的浪费,还会带来这两个微服务之间的耦合,不利于之后的维护。因此, **最有效的方式就是通过消息队列**,**实现领域事件在微服务间的通知**。 +譬如,当用户在“用户下单”微服务中下单,那么会在该微服务中形成一个订单;但是,“饭店接单”是另外一个微服务,它必须要及时获得已下单的订单信息,才能执行接单。那么,如何通知“饭店接单”微服务已经有新的订单。诚然,可以让“饭店接单”微服务按照一定的周期不断地去查询“用户下单”微服务中已下单的订单信息。然而,这样的设计,不仅会加大“用户下单”与“饭店接单”微服务的系统负载,形成资源的浪费,还会带来这两个微服务之间的耦合,不利于之后的维护。因此,**最有效的方式就是通过消息队列**,**实现领域事件在微服务间的通知**。 ![Drawing 2.png](assets/CgqCHl_PFlaADZxNAACzNn8_lDg752.png) @@ -47,7 +47,7 @@ 如上图所示,具体的设计就是,当“用户下单”微服务在完成下单并保存订单以后,将该订单做成一个消息发送到消息队列中;这时,“饭店接单”微服务就会有一个守护进程不断监听消息队列;一旦有消息就会触发接收消息,并向饭店发送“接收订单”的通知。在这样的设计中: - “ **用户下单** ” **微服务只负责发送消息**,至于谁会接收并处理这些消息,与“用户下单”微服务无关; -- “ **饭店接单** ” **微服务只负责接收消息**,至于谁发送的这个消息,与“饭店接单”微服务无关。 **这样的设计就实现了微服务之间的解耦,使得日后变更的成本降低**。同样,饭店餐食就绪以后,也是通过消息队列通知“骑士接单”。在整个微服务系统中,微服务与微服务之间的领域事件通知会经常存在,所以最好在架构设计中将这个机制下沉到技术中台中。 +- “ **饭店接单** ” **微服务只负责接收消息**,至于谁发送的这个消息,与“饭店接单”微服务无关。**这样的设计就实现了微服务之间的解耦,使得日后变更的成本降低**。同样,饭店餐食就绪以后,也是通过消息队列通知“骑士接单”。在整个微服务系统中,微服务与微服务之间的领域事件通知会经常存在,所以最好在架构设计中将这个机制下沉到技术中台中。 ## DDD 的微服务设计 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25409\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25409\350\256\262.md" index a5afe124d..36fe697d5 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25409\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25409\350\256\262.md" @@ -14,7 +14,7 @@ 图 1 领域驱动设计的真谛 -过去,我们认为软件就是, **用户怎么提需求,软件就怎么开发**。这种开发模式使得我们对需求的认知浅薄,不得不随着用户的需求变动反复地改来改去,导致我们很累而用户还不满意,软件研发风险巨大。 +过去,我们认为软件就是,**用户怎么提需求,软件就怎么开发**。这种开发模式使得我们对需求的认知浅薄,不得不随着用户的需求变动反复地改来改去,导致我们很累而用户还不满意,软件研发风险巨大。 正是 DDD 改变了这一切,它要求我们更加 **主动地去理解业务,掌握业务领域知识。** 这样,我们对业务的理解越深刻,开发出来的产品就越专业,那么客户就越喜欢购买和使用我们的产品。 @@ -22,19 +22,19 @@ 这时就不再是客户提需求了,而是我们 **主动地提需求** 、 **主动地改进功能**,**去解决客户的痛点**,这样做的效果是,客户会感觉“不知道为什么,我就觉得你们的软件好用,用着很顺手”。这时,不但客户不会再改来改去,而且我们的软件做得也越来越专业,越来越有市场竞争力,这才是 DDD 的真谛。 -这里有个问题,如果我们对业务理解不深刻就会影响到产品,那么能不能一开始就对业务理解得非常深刻呢?这几乎是不可能的。我们经常说, **做事不能仅凭一腔热血,一定要符合自然规律**。其实软件的设计开发过程也是这样。 +这里有个问题,如果我们对业务理解不深刻就会影响到产品,那么能不能一开始就对业务理解得非常深刻呢?这几乎是不可能的。我们经常说,**做事不能仅凭一腔热血,一定要符合自然规律**。其实软件的设计开发过程也是这样。 - 在最开始你对业务理解比较粗略的时候,就从主要流程开始领域建模。 - 接着,不断往领域模型中加东西。随着功能一个一个地添加,领域模型也变得越来越丰富、越来越完善。每次添加新功能的时候,运用“两顶帽子”的方式先重构再加新功能,不断地完善每个设计。 - 这样,领域模型就像小树一样一点儿一点儿成长,最后完成所有的功能。 -这样的设计过程叫“ **小步快跑** ”。采用小步快跑的设计方法,一开始不用思考那么多问题,从简单问题开始逐步深入, **设计难度就降低了**。同时,系统始终是处于变更中,使 **设计更加易于变更**。 +这样的设计过程叫“ **小步快跑** ”。采用小步快跑的设计方法,一开始不用思考那么多问题,从简单问题开始逐步深入,**设计难度就降低了**。同时,系统始终是处于变更中,使 **设计更加易于变更**。 ## 基于限界上下文的领域建模 回到 [第08讲](../第08讲) 微服务设计部分,当在线订餐系统完成了事件风暴的分析以后,接着应当怎样设计呢?通过划分限界上下文,已经将系统划分为了"用户注册"、"用户下单"、"饭店接单"、"骑士派送"与"饭店管理"等几个限界上下文,这样的划分也是后端微服务的划分。紧接着,就开始为每一个限界上下文进行领域建模。 -首先, **从 "用户下单" 上下文** 开始 。通过业务领域分析,绘制出了如图 2 所示的领域模型,该模型的核心是“订单”,通过“订单”关联了用户与用户地址。一个订单有多个菜品明细,而每个菜品明细都对应了一个菜单,每个菜单隶属于一个饭店。此外,一个订单还关联了它的支付与发票。起初,它们的属性和方法没有那么全面,随着设计的不断深入,不断地细化与完善模型。 +首先,**从 "用户下单" 上下文** 开始 。通过业务领域分析,绘制出了如图 2 所示的领域模型,该模型的核心是“订单”,通过“订单”关联了用户与用户地址。一个订单有多个菜品明细,而每个菜品明细都对应了一个菜单,每个菜单隶属于一个饭店。此外,一个订单还关联了它的支付与发票。起初,它们的属性和方法没有那么全面,随着设计的不断深入,不断地细化与完善模型。 ![1.png](assets/Ciqc1F_TJRqAG1xCAAF5cwFJos4897.png) @@ -50,7 +50,7 @@ 通过以上设计,就将上一讲的微服务拆分,进一步落实到每一个微服务的设计。紧接着,将每一个微服务的设计,按照 [第03讲](../第03讲) 的思路落实数据库设计,按照 [第04讲](../第04讲) 的思路落实贫血模型与充血模型的设计。 -特别值得注意的是, **订单与菜品明细是一对聚合**。过去按照贫血模型的设计,分别为它们设计订单值对象、Service 与 Dao,菜品明细值对象、Service 与 Dao;现在按照充血模型的设计,只有订单领域对象、Service、仓库、工厂与菜品明细包含在订单对象中,而订单 Dao 被包含在订单仓库中。贫血模型与充血模型在设计上有明显的差别。关于聚合的实现,下一讲再详细探讨。 +特别值得注意的是,**订单与菜品明细是一对聚合**。过去按照贫血模型的设计,分别为它们设计订单值对象、Service 与 Dao,菜品明细值对象、Service 与 Dao;现在按照充血模型的设计,只有订单领域对象、Service、仓库、工厂与菜品明细包含在订单对象中,而订单 Dao 被包含在订单仓库中。贫血模型与充血模型在设计上有明显的差别。关于聚合的实现,下一讲再详细探讨。 ## 深入理解业务与模型重构 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25410\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25410\350\256\262.md" index a4bda5569..bd30d83ca 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25410\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25410\350\256\262.md" @@ -34,9 +34,9 @@ 因此,微服务的设计彼此之间不是孤立的,它们需要相互调用接口实现高内聚。然而,当一个微服务团队向另一个微服务团队提出接口调用需求时,另一个微服务团队该如何设计呢? -首先第一个问题,当多个团队都在向你提出 API 接口时, **你怎么提供接口**。如果每个团队给你提需求,你就必须要做一个新接口,那么你的微服务将变得非常不稳定。因此,当多个团队向你提需求时,必须要对这些接口进行规划,通过复用用尽可能少的接口满足他们的需求;当有新的接口提出时,要尽量通过现有接口解决问题。这样做,你就能用更低的维护成本,更好地维护自己的微服务。 +首先第一个问题,当多个团队都在向你提出 API 接口时,**你怎么提供接口**。如果每个团队给你提需求,你就必须要做一个新接口,那么你的微服务将变得非常不稳定。因此,当多个团队向你提需求时,必须要对这些接口进行规划,通过复用用尽可能少的接口满足他们的需求;当有新的接口提出时,要尽量通过现有接口解决问题。这样做,你就能用更低的维护成本,更好地维护自己的微服务。 -接着,当调用方需要接口变更时怎么办?变更现有接口应当尽可能 **向前兼容**,即接口的名称与参数都不变,只是在内部增加新的功能。这样做是为了不影响其他微服务的调用。如果确实需要更改现有的接口怎么办? **宁愿增加一个新的接口也最好不要去变更原有的接口**。 +接着,当调用方需要接口变更时怎么办?变更现有接口应当尽可能 **向前兼容**,即接口的名称与参数都不变,只是在内部增加新的功能。这样做是为了不影响其他微服务的调用。如果确实需要更改现有的接口怎么办?**宁愿增加一个新的接口也最好不要去变更原有的接口**。 ![DDD 10--金句.png](assets/Ciqc1F_YfkCAJF6yAAEHWZqB_VU738.png) @@ -57,7 +57,7 @@ ## 去中心化的数据管理 -按照前面 DDD 的设计,已经将数据库按照微服务划分为用户库、下单库、接单库、派送库与饭店库。这时候,如何来落地这些数据库的设计呢?微服务系统最大的设计难题就是要 **面对互联网的高并发与大数据**。因此, **可以按照 "去中心化数据管理" 的思想,根据数据量与用户访问特点,选用不同的数据存储方案存储数据** : +按照前面 DDD 的设计,已经将数据库按照微服务划分为用户库、下单库、接单库、派送库与饭店库。这时候,如何来落地这些数据库的设计呢?微服务系统最大的设计难题就是要 **面对互联网的高并发与大数据**。因此,**可以按照 "去中心化数据管理" 的思想,根据数据量与用户访问特点,选用不同的数据存储方案存储数据** : - 微服务“用户注册”与“饭店管理”分别对应的用户库与饭店库,它们的共同特点是 **数据量小但频繁读取**,可以选用小型的 MySQL 数据库并在前面架设 Redis 来提高查询性能; - 微服务“用户下单”“饭店接单”“骑士派送”分别对应的下单库、接单库、派送库,其特点是 **数据量大并且高并发写**,选用一个数据库显然扛不住这样的压力,因此可以选用了 TiDB 这样的 NewSQL 数据库进行分布式存储,将数据压力分散到多个数据节点中,从而解决 I/O 瓶颈; @@ -78,11 +78,11 @@ 查询的过程分为 2 个步骤。 1. 查询订单数据,但不执行 join 操作。这样的查询结果可能有 1 万条,但通过翻页,返回给微服务的只是那一页的 20 条数据。 -2. 再通过调用“用户注册”与“饭店管理”微服务的相关接口,实现对用户与饭店数据的补填。 **这种方式,既解决了跨库关联查询的问题,又提高了海量数据下的查询效率**。注意,传统的数据库设计之所以在数据量越来越大时,查询速度越来越慢,就是因为 **存在 join 操作**。因而,在面对海量数据的查询时,干掉 join 操作,改为分页后的数据补填,就能有效地提高查询性能。 +2. 再通过调用“用户注册”与“饭店管理”微服务的相关接口,实现对用户与饭店数据的补填。**这种方式,既解决了跨库关联查询的问题,又提高了海量数据下的查询效率**。注意,传统的数据库设计之所以在数据量越来越大时,查询速度越来越慢,就是因为 **存在 join 操作**。因而,在面对海量数据的查询时,干掉 join 操作,改为分页后的数据补填,就能有效地提高查询性能。 -然而,在查询订单时,如果要通过用户姓名、联系电话进行过滤,然后再查询时,又该如何设计呢? **这里千万不能先过滤用户数据,再去查询订单,这是一个非常糟糕的设计**。我们过去的数据库设计采用的都是 **3NF(第 3 范式)**,它能够帮助我们 **减少数据冗余**,然而却带来了 **频繁的 join 操作,降低了查询性能**。因此,为了提升海量数据的查询性能,适当增加冗余,即在订单表中增加用户姓名、联系电话等字段。这样,在查询时直接过滤订单表就好了,查询性能就得到了提高。 +然而,在查询订单时,如果要通过用户姓名、联系电话进行过滤,然后再查询时,又该如何设计呢?**这里千万不能先过滤用户数据,再去查询订单,这是一个非常糟糕的设计**。我们过去的数据库设计采用的都是 **3NF(第 3 范式)**,它能够帮助我们 **减少数据冗余**,然而却带来了 **频繁的 join 操作,降低了查询性能**。因此,为了提升海量数据的查询性能,适当增加冗余,即在订单表中增加用户姓名、联系电话等字段。这样,在查询时直接过滤订单表就好了,查询性能就得到了提高。 -最后,当系统要在某些查询模块进行订单查询时,可能对各个字段都需要进行过滤查询。 **这时就不再采用数据补填的方式,而是利用 NoSQL 的特性,采用“宽表”的设计**。按照这种设计思路,当系统通过读写分离从生产库批量导入查询库时,提前进行 join 操作,然后将 join 以后的数据,直接写入查询库的一个表中。由于这个表比一般的表字段更多,因此被称为“宽表”。 +最后,当系统要在某些查询模块进行订单查询时,可能对各个字段都需要进行过滤查询。**这时就不再采用数据补填的方式,而是利用 NoSQL 的特性,采用“宽表”的设计**。按照这种设计思路,当系统通过读写分离从生产库批量导入查询库时,提前进行 join 操作,然后将 join 以后的数据,直接写入查询库的一个表中。由于这个表比一般的表字段更多,因此被称为“宽表”。 由于 NoSQL 独有的特性,为空的字段是不占用空间的,因此字段再多都不影响查询性能。这样,在日后的查询时,就不再需要 join 操作,而是直接在这个单表中进行各种过滤、各种查询,从而在海量历史数据中实现秒级查询。因此,“订单查询”微服务在数据库设计时,就可以通过 NoSQL 数据库建立宽表,从而实现高效的数据查询。 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25411\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25411\350\256\262.md" index b520f8b9e..822827ba4 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25411\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25411\350\256\262.md" @@ -49,7 +49,7 @@ ![图片1.png](assets/CgpVE1_ceLqAPVyTAARGrJWSmWs085.png) -整洁架构(The Clean Architecture)是 Robot C. Martin 在《架构整洁之道》中提出来的架构设计思想。如上图所示,它以圆环的形式把系统分成了几个不同的层次,因此又称为“洋葱头架构(The Onion Architecture)”。在整洁架构的中心是业务实体(黄色部分)与业务应用(红色部分), **业务实体** 就是那些 **核心业务逻辑**,而 **业务应用** 就是 **面向用户的那些服务** (Service)。它们合起来组成了业务领域层,也就是通过领域模型形成的业务代码的实现。 +整洁架构(The Clean Architecture)是 Robot C. Martin 在《架构整洁之道》中提出来的架构设计思想。如上图所示,它以圆环的形式把系统分成了几个不同的层次,因此又称为“洋葱头架构(The Onion Architecture)”。在整洁架构的中心是业务实体(黄色部分)与业务应用(红色部分),**业务实体** 就是那些 **核心业务逻辑**,而 **业务应用** 就是 **面向用户的那些服务** (Service)。它们合起来组成了业务领域层,也就是通过领域模型形成的业务代码的实现。 整洁架构的最外层是各种技术框架,包括: @@ -67,7 +67,7 @@ 如图,进一步细化整洁架构,将其划分为 2 个部分: **主动适配器** 与 **被动适配器**。 - 主动适配器,又称为“北向适配器”,就是由前端用户以不同的形式发起业务请求,然后交由应用层去接收请求,交由领域层去处理业务。用户可以用浏览器、客户端、移动 App、微信端、物联网专用设备等各种不同形式发起请求。然而,通过北向适配器,最后以同样的形式调用应用层。 -- 被动适配器,又称为“南向适配器”,就是在业务领域层完成各种业务处理以后,以某种形式持久化存储最终的结果数据。最终的数据可以存储到关系型数据库、NoSQL 数据库、NewSQL 数据库、Redis 缓存中,或者以消息队列的形式发送给其他应用系统。但不论采用什么形式,业务领域层只有一套,但持久化存储可以有各种不同形式。 **南向适配器将业务逻辑与存储技术解耦**。 +- 被动适配器,又称为“南向适配器”,就是在业务领域层完成各种业务处理以后,以某种形式持久化存储最终的结果数据。最终的数据可以存储到关系型数据库、NoSQL 数据库、NewSQL 数据库、Redis 缓存中,或者以消息队列的形式发送给其他应用系统。但不论采用什么形式,业务领域层只有一套,但持久化存储可以有各种不同形式。**南向适配器将业务逻辑与存储技术解耦**。 ## 整洁架构的落地 @@ -75,9 +75,9 @@ 按照整洁架构的思想如何落地架构设计呢?如上图所示,在这个架构中,将适配器层通过数据接入层、数据访问层与接口层等几个部分的设计,实现与业务的解耦。 -首先,用户可以用浏览器、客户端、移动 App、微信端、物联网专用设备等不同的前端形式, **多渠道地接入到系统中**,不同的渠道的接入形式是不同的。通过数据接入层进行解耦,然后以同样的方式去调用上层业务代码,就能将前端的多渠道接入,与后台的业务逻辑实现了解耦。这样,前端不管怎么变,有多少种渠道形式,后台业务只需要编写一套,维护成本将大幅度降低。 +首先,用户可以用浏览器、客户端、移动 App、微信端、物联网专用设备等不同的前端形式,**多渠道地接入到系统中**,不同的渠道的接入形式是不同的。通过数据接入层进行解耦,然后以同样的方式去调用上层业务代码,就能将前端的多渠道接入,与后台的业务逻辑实现了解耦。这样,前端不管怎么变,有多少种渠道形式,后台业务只需要编写一套,维护成本将大幅度降低。 -接着, **通过数据访问层将业务逻辑与数据库解耦**。前面说了,在未来三五年时间里,我们又将经历一轮大数据转型。转型成大数据以后,数据存储的设计可能不再仅限于关系型数据库与 3NF 的思路设计,而是通过 JSON、增加冗余、设计宽表等设计思路,将其存储到 NoSQL 数据库中,设计思想将发生巨大的转变。但无论怎么转变,都只是存储形式的转变,不变的是 **业务逻辑层中的业务实体**。因此,通过数据访问层的解耦,今后系统向大数据转型的时候,业务逻辑层不需要做任何修改,只需要重新编写数据访问层的实现,就可以转型成大数据技术。转型成本将大大降低,转型将更加容易。 +接着,**通过数据访问层将业务逻辑与数据库解耦**。前面说了,在未来三五年时间里,我们又将经历一轮大数据转型。转型成大数据以后,数据存储的设计可能不再仅限于关系型数据库与 3NF 的思路设计,而是通过 JSON、增加冗余、设计宽表等设计思路,将其存储到 NoSQL 数据库中,设计思想将发生巨大的转变。但无论怎么转变,都只是存储形式的转变,不变的是 **业务逻辑层中的业务实体**。因此,通过数据访问层的解耦,今后系统向大数据转型的时候,业务逻辑层不需要做任何修改,只需要重新编写数据访问层的实现,就可以转型成大数据技术。转型成本将大大降低,转型将更加容易。 最后,就是 **底层的技术架构**。现在我们谈架构,越来越多地是在谈架构演化,也就是底层技术架构要不断地随着市场和技术的更迭而更迭。但是,话虽如此,很多系统的技术架构更迭,是一个非常痛苦的过程。为什么呢?究其原因,是软件在设计时,将太多业务代码与底层框架耦合,底层框架一旦变更,就会导致大量业务代码的变更,各个业务模块的都要更迭,导致架构调整的成本巨大、风险高昂。 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25412\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25412\350\256\262.md" index e66105c97..256b0df51 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25412\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25412\350\256\262.md" @@ -2,7 +2,7 @@ 我们以往建设的系统都分为前台和后台,前台就是与用户交互的 UI 界面,后台就是服务端完成的业务逻辑操作。然而,在我们以往开发的很多业务系统中,有一些内容是共用的部分,在未来开发的业务系统中也要使用。因此,如果能把这些内容提取出来做成公用组件,那么在未来,开发系统就简单了,不用每次都重头开发,复用这些组件就可以了。 -但是,这些公用的组件到底属于前台还是后台呢?都不属于。 **它既包含前台的界面,也包含后台的逻辑**,因此被称为“中台”。所谓的中台,就是将以往业务系统中可以复用的前台与后台代码,剥离个性、提取共性,形成的公用组件。有了这些组件,就可以使日后的系统开发降本增效、提高交付速度。因此,阿里提出了“小前台、大中台”的战略,得到了业界的普遍认可。 +但是,这些公用的组件到底属于前台还是后台呢?都不属于。**它既包含前台的界面,也包含后台的逻辑**,因此被称为“中台”。所谓的中台,就是将以往业务系统中可以复用的前台与后台代码,剥离个性、提取共性,形成的公用组件。有了这些组件,就可以使日后的系统开发降本增效、提高交付速度。因此,阿里提出了“小前台、大中台”的战略,得到了业界的普遍认可。 从分类上看,中台分为 **业务中台** 、 **技术中台** 与 **数据中台**。 @@ -14,7 +14,7 @@ ## 打造快速交付团队 -许多团队都有这样一个经历:项目初期,由于业务简单,参与的人少,往往可以获得一个较快的交付速度;但随着项目的不断推进,业务变得越来越复杂,参与的人越来越多,交付速度就变得越来越慢,使得团队越来越不能适应市场的快速变化,从而处于竞争的劣势。然而,软件规模化发展是所有软件发展的必然趋势。因此, **解决规模化团队与软件快速交付的矛盾** 就成了我们不得不面对的难题。 +许多团队都有这样一个经历:项目初期,由于业务简单,参与的人少,往往可以获得一个较快的交付速度;但随着项目的不断推进,业务变得越来越复杂,参与的人越来越多,交付速度就变得越来越慢,使得团队越来越不能适应市场的快速变化,从而处于竞争的劣势。然而,软件规模化发展是所有软件发展的必然趋势。因此,**解决规模化团队与软件快速交付的矛盾** 就成了我们不得不面对的难题。 ![Drawing 0.png](assets/CgpVE1_bMJqAQUcNAARYP35oK-g668.png) diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25415\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25415\350\256\262.md" index 945439d58..e030de801 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25415\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25415\350\256\262.md" @@ -8,7 +8,7 @@ ## 解决技术不确定性的问题 -如今的微服务架构,基本已经形成了 Spring Cloud 一统天下的局势。然而,在 Spring Cloud 框架下的各种技术组件依然存在 **诸多不确定性**,如:注册中心是否采用 Eureka、服务网关是采用 Zuul 还是 Gateway,等等。同时,服务网格 Service Mesh 方兴未艾,不排除今后所有的微服务都要切换到 Service Mesh 的可能。在这种情况下如何 **决策微服务的技术架构** ?代码尽量不要与 Spring Cloud 耦合,才能在将来更容易地切换到 Service Mesh。那么,具体又该如何做到呢? +如今的微服务架构,基本已经形成了 Spring Cloud 一统天下的局势。然而,在 Spring Cloud 框架下的各种技术组件依然存在 **诸多不确定性**,如:注册中心是否采用 Eureka、服务网关是采用 Zuul 还是 Gateway,等等。同时,服务网格 Service Mesh 方兴未艾,不排除今后所有的微服务都要切换到 Service Mesh 的可能。在这种情况下如何 **决策微服务的技术架构**?代码尽量不要与 Spring Cloud 耦合,才能在将来更容易地切换到 Service Mesh。那么,具体又该如何做到呢? ![Drawing 0.png](assets/Cip5yF_q1GWABFvEAAK9qvAoHxc276.png) @@ -32,7 +32,7 @@ 然而,转型为微服务后,有一个技术难题亟待解决,那就是 **跨库的数据操作**。当一个单体应用拆分成多个微服务后,不仅应用程序需要拆分,数据库也需要拆分。譬如,经过微服务拆分,订单有订单数据库,用户有用户数据库。这时,当查询订单,需要补填其对应的用户信息时,就不能从自己本地的数据库中查询了,而是调用“用户”微服务的远程接口,在用户数据库中查询,然后返回给“订单”微服务。这时,原有的技术中台就必须做出调整。 -如何调整呢? **通用 DDD 仓库** 在执行查询或者装载操作时,查询完订单补填用户信息,不是通过 DAO 去查询本地数据库,而是改成调用远程接口,去调用用户微服务。这时,可以先在订单微服务的本地编写一个用户 Service 的 Feign 接口,订单仓库与工厂调用这个接口就可以了。然后通过 Feign 接口实现对用户微服务的远程调用。 +如何调整呢?**通用 DDD 仓库** 在执行查询或者装载操作时,查询完订单补填用户信息,不是通过 DAO 去查询本地数据库,而是改成调用远程接口,去调用用户微服务。这时,可以先在订单微服务的本地编写一个用户 Service 的 Feign 接口,订单仓库与工厂调用这个接口就可以了。然后通过 Feign 接口实现对用户微服务的远程调用。 ## 采用 Feign 接口实现远程调用 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25417\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25417\350\256\262.md" index 3b8850cfe..e8f0960cd 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25417\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25417\350\256\262.md" @@ -10,7 +10,7 @@ 几年前,我组织了一个大数据团队,开始 **大数据相关产品的设计开发**。众所周知,大数据相关产品,就是运用大数据技术对海量的数据进行分析处理,并且最终的结果是通过各种报表来查询并且展现。因此,这样的项目,除了后台的各种分析处理以外,还要在前端展现各种报表,而且这些报表非常多而繁杂,动辄就是数百张之多。同时,使用这个系统的都是决策层领导,他们一会儿这样分析,一会儿那样分析,每个需求还非常急,恨不得马上就能用上。因此,我们必须具备快速开发报表的能力,而传统的从头一个一个制作报表根本来不及。 -通过对现有报表进行反复分析,提取共性、保留个性,我发现每个报表都有许多相同或者相似的地方。 **每个报表在 Service 中的代码基本相同**,无非就是从前端获取查询参数,然后调用 Dao 执行查询,最多再做一些翻页的操作。既然如此,那么何必要为每个功能设计 Service 呢?把它们合并到一个 Service,然后注入不同的 Dao,不就可以进行不同的查询了吗? +通过对现有报表进行反复分析,提取共性、保留个性,我发现每个报表都有许多相同或者相似的地方。**每个报表在 Service 中的代码基本相同**,无非就是从前端获取查询参数,然后调用 Dao 执行查询,最多再做一些翻页的操作。既然如此,那么何必要为每个功能设计 Service 呢?把它们合并到一个 Service,然后注入不同的 Dao,不就可以进行不同的查询了吗? 那么,这些 Dao 怎么设计呢?以往采用 MyBatis 的方式,每个 Dao 都要写一个自己的接口,然后配置一个 Mapper。然而,这些 Dao 接口都长得一模一样,只是接口名与 Mapper 不 同。此外,过去的设计,每个 Service 都对应一个 Dao,现在一个 Service 要对应很多个Dao,那用注解的方式就搞不定了。针对以上的设计难题,经过反复的调试,将架构设计成这样。 @@ -155,7 +155,7 @@ - Supplier loadSupplier(id),即通过某个 ID 进行查找; - List loadSupplier(Listids),通过多个 ID 进行批量查找。 -在这里, **method 配置的是对单个 ID 进行查找的方法**,**listMethod 配置的是对多个 ID 批量查找的方法**。通过这 2 个配置,就可以用 Feign 接口实现微服务的远程调用,完成跨微服务的数据补填。通过这样的设计,在 Product 微服务的 vObj.xml 中就不用配置 Supplier 了。 +在这里,**method 配置的是对单个 ID 进行查找的方法**,**listMethod 配置的是对多个 ID 批量查找的方法**。通过这 2 个配置,就可以用 Feign 接口实现微服务的远程调用,完成跨微服务的数据补填。通过这样的设计,在 Product 微服务的 vObj.xml 中就不用配置 Supplier 了。 ## 通用仓库与工厂的设计 @@ -163,7 +163,7 @@ 传统的 DDD 设计,每个模块都有自己的 **仓库与工厂**,工厂是领域对象创建与装配的地方,是生命周期的开始。创建出来后放到仓库的缓存中,供上层应用访问。当领域对象在经过一系列操作以后,最后通过仓库完成数据的持久化。这个领域对象数据持久化的过程,对于普通领域对象来说就是存入某个单表,然而对于有聚合关系的领域对象来说,需要存入多个表中,并将其放到同一事务中。 -在这个过程中, **聚合关系会出现跨库的事务操作吗** ?即具有聚合关系的多个领域对象会被拆分为多个微服务吗?我认为是不可能的,因为聚合就是一种 **强相关的封装**,是不可能因微服务而拆分的。如果出现了,要么不是聚合关系,要么就是微服务设计出现了问题。因此,仓库是不可能完成跨库的事务处理的。 +在这个过程中,**聚合关系会出现跨库的事务操作吗**?即具有聚合关系的多个领域对象会被拆分为多个微服务吗?我认为是不可能的,因为聚合就是一种 **强相关的封装**,是不可能因微服务而拆分的。如果出现了,要么不是聚合关系,要么就是微服务设计出现了问题。因此,仓库是不可能完成跨库的事务处理的。 弄清楚了传统的 DDD 设计,与以往 Dao 的设计进行比较,就会发现仓库和工厂就是对 Dao 的替换。然而,这种替换不是简单的替换,它们对 Dao 替换的同时,还 **扩展了许多的功能**,如数据的补填、领域对象的映射与装配、聚合的处理,等等。当我们把这些关系思考清楚了,通用仓库与工厂的设计就出来了。 diff --git "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25418\350\256\262.md" "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25418\350\256\262.md" index a2041425e..a9cbdd870 100644 --- "a/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25418\350\256\262.md" +++ "b/docs/Design/DDD \345\276\256\346\234\215\345\212\241\350\220\275\345\234\260\345\256\236\346\210\230/\347\254\25418\350\256\262.md" @@ -42,7 +42,7 @@ } ``` -在这里,不同的领域事件后面的参数是不一样的,有的可能是一个 **领域对象**,有的可能是一个 **数组参数**,抑或是一个 **Map**,甚至没有参数。譬如,一些领域事件就是一个状态的改变,所以不包含参数。 **什么领域事件跟着什么参数**,**是事件的发布者设计的**,**然后将协议告知所有订阅者**。这样,所有的订阅者就根据这个协议,自己去定义后续的操作。 +在这里,不同的领域事件后面的参数是不一样的,有的可能是一个 **领域对象**,有的可能是一个 **数组参数**,抑或是一个 **Map**,甚至没有参数。譬如,一些领域事件就是一个状态的改变,所以不包含参数。**什么领域事件跟着什么参数**,**是事件的发布者设计的**,**然后将协议告知所有订阅者**。这样,所有的订阅者就根据这个协议,自己去定义后续的操作。 依据这样的思路落地到项目中,事件发布者要在方法的最后完成 **一个事件的发布**。至于到底要做什么事件,交由 **底层技术中台去定义**,比如发送消息队列,或者写入领域事件表中。例如,在“用户接单”中完成事件发布: diff --git "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25401\350\256\262.md" "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25401\350\256\262.md" index 29da518ab..f35e01554 100644 --- "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25401\350\256\262.md" +++ "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25401\350\256\262.md" @@ -56,7 +56,7 @@ - 防恶意操作 - 交易过程防篡改 -**互联网金融高并发场景下的条件竞争安全设计** 在互联网金融的高并发场景下,涉及到资金方面安全的问题尤为重要。服务端在做并发编程时,往往需要考虑到竞争条件的情况。在多个并发线程同时访问同一资源时,由于对请求的处理不是原子性的,无法预测调度的顺序,就可能由于时间序列上的冲突而造成对共享资源的操作混乱。 **条件竞争安全漏洞危害** 通过高并发操作触及程序处理临界区,绕过程序线性执行顺序,使原有的逻辑限制失效。经典场景有: +**互联网金融高并发场景下的条件竞争安全设计** 在互联网金融的高并发场景下,涉及到资金方面安全的问题尤为重要。服务端在做并发编程时,往往需要考虑到竞争条件的情况。在多个并发线程同时访问同一资源时,由于对请求的处理不是原子性的,无法预测调度的顺序,就可能由于时间序列上的冲突而造成对共享资源的操作混乱。**条件竞争安全漏洞危害** 通过高并发操作触及程序处理临界区,绕过程序线性执行顺序,使原有的逻辑限制失效。经典场景有: 1. 超额取款,提现 1. 重复兑换积分 @@ -86,7 +86,7 @@ 对于互联网金融架构系统来说,涉及到以资金交易为核心的业务领域,最重要的指标是高可用。 -高可用HA(High Availability)是分布式架构设计中必须考虑的因素之一,它通常的是指,通过设计减少系统不能提供服务的时间。 **高可用的指标** 我们通常会形容高可用如: +高可用HA(High Availability)是分布式架构设计中必须考虑的因素之一,它通常的是指,通过设计减少系统不能提供服务的时间。**高可用的指标** 我们通常会形容高可用如: - 不能“挂” - 可用性99.99%四个九 @@ -127,14 +127,14 @@ ## 互联网金融高并发设计 -互联网金融的场景下,在高可用的基础上,对于高并发的要求是必不可少的。为满足日益剧增的用户增长和交易量,往往需要在架构设计时,考虑高并发的特性。 **高并发的指标** 我们通常会通过很多方式来衡量说明一个高并发系统的架构设计,如: +互联网金融的场景下,在高可用的基础上,对于高并发的要求是必不可少的。为满足日益剧增的用户增长和交易量,往往需要在架构设计时,考虑高并发的特性。**高并发的指标** 我们通常会通过很多方式来衡量说明一个高并发系统的架构设计,如: 1. 通过设计来保证系统能够同时处理很多的事情,比如亿级并发支付交易,百万级并发保单下单等 1. 低响应时间:系统对请求作出的响应时间维持在一个较低的水平,通常不超过3秒。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统响应时间。 1. 高吞吐量:单位时间内处理的请求量。 1. QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没那么明显。 1. TPS:每秒处理的事务数。 -1. 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。 **如何提升系统的并发能力** **1) 提升系统的单机处理能力** 垂直扩展的方式有两种: +1. 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。**如何提升系统的并发能力** **1) 提升系统的单机处理能力** 垂直扩展的方式有两种: 增强单机硬件性能,例如:增加CPU的核数,由8核扩展到16核;升级更好的网卡,由千兆网卡升级到万兆网卡;升级更好的硬盘,如SSD;扩展硬盘的容量,如由500G升级到10T;扩展系统内存,如由16G升级到64G等。 @@ -146,15 +146,15 @@ ## 互联网金融高性能设计 -在互联网金融分布式架构中,高性能是一项涉及众多方面因素的系统工程,并不是单一高新技术和设备的简单应用或堆叠,应该进行合理的规划与优化设计,以适合用户在性能、成本等方面对系统建设的综合需求。 **高性能的指标** 高性能的指标通常有: +在互联网金融分布式架构中,高性能是一项涉及众多方面因素的系统工程,并不是单一高新技术和设备的简单应用或堆叠,应该进行合理的规划与优化设计,以适合用户在性能、成本等方面对系统建设的综合需求。**高性能的指标** 高性能的指标通常有: -1)通过合理的架构设计,实现互联网金融系统高吞吐、低延时(相对时间)。 2)可用性指标计算:平均相应时间、95线的响应时间、99线的响应时间。 **如何提升系统的性能** 互联网金融系统,涉及到各方面的性能问题,如:系统软件平台服务的性能,网络和硬件的性能,数据库及存储的性能等。 **1)微服务化设计** 将对庞大金融服务进行领域规划,将臃肿的系统进行拆分解耦,将每一个模块进行解耦,把每个服务都尽可能做成无状态化,每个独立模块均可以作为一个微服务,这样每个微服务的关联性都比较小,每一个微服务都可能做到最大化的性能。 +1)通过合理的架构设计,实现互联网金融系统高吞吐、低延时(相对时间)。 2)可用性指标计算:平均相应时间、95线的响应时间、99线的响应时间。**如何提升系统的性能** 互联网金融系统,涉及到各方面的性能问题,如:系统软件平台服务的性能,网络和硬件的性能,数据库及存储的性能等。**1)微服务化设计** 将对庞大金融服务进行领域规划,将臃肿的系统进行拆分解耦,将每一个模块进行解耦,把每个服务都尽可能做成无状态化,每个独立模块均可以作为一个微服务,这样每个微服务的关联性都比较小,每一个微服务都可能做到最大化的性能。 > 备注:微服务技术和消费金融领域的规划,我们会在后面的章节再独立介绍。 ![img](assets/986825d4dd7c21ea9e92f5ab29b3b8bf.png) **2)CDN加速技术** 互联网消费金融的产品,涉及到众多前端,使用CDN缓存技术,能大大提升用户的产品体验。 -CND加速将网站的内容缓存在网络边缘(离用户接入网络最近的地方),然后在用户访问网站内容的时候,通过调度系统将用户的请求路由或者引导到离用户接入网络最近或者访问效果最佳的缓存服务器上,由该缓存服务器为用户提供内容服务;相对于直接访问源站,这种方式缩短了用户和内容之间的网络距离,从而达到加速的效果。 **3)网络与硬件性能** +CND加速将网站的内容缓存在网络边缘(离用户接入网络最近的地方),然后在用户访问网站内容的时候,通过调度系统将用户的请求路由或者引导到离用户接入网络最近或者访问效果最佳的缓存服务器上,由该缓存服务器为用户提供内容服务;相对于直接访问源站,这种方式缩短了用户和内容之间的网络距离,从而达到加速的效果。**3)网络与硬件性能** 带宽性能:足够的带宽应该满足在网站峰值的情况还能足够快速的使用,所以网络带宽应该大于峰值流量=峰值QPS * 平均请求大小。只有在保证带宽的情况才能实现高性能服务。 @@ -166,13 +166,13 @@ CND加速将网站的内容缓存在网络边缘(离用户接入网络最近 > 一般来说,在系统横向扩展能力足够强的情况下,高并发的压力会打到数据库,所以分布式缓存的建设对于互联网消费金融产品架构设计来说非常重要。 -缓存的本质是通过Key-Value形式的Hash表提升读写速度,一般情况是O(1)的读写速度。读量比较高,变化量不大的数据比较适合使用缓存。目前比较常用的分布式缓存技术有Redis,Memcache等。缓存这块的中间件建设,后面的章节会在细化讲解。 **5)操作异步化设计** 目前在大型的互联网消费金融系统架构设计中,普遍会考虑用消息队列来讲调用异步化,不仅可以提升系统的性能,还可以提升系统的扩展性。 +缓存的本质是通过Key-Value形式的Hash表提升读写速度,一般情况是O(1)的读写速度。读量比较高,变化量不大的数据比较适合使用缓存。目前比较常用的分布式缓存技术有Redis,Memcache等。缓存这块的中间件建设,后面的章节会在细化讲解。**5)操作异步化设计** 目前在大型的互联网消费金融系统架构设计中,普遍会考虑用消息队列来讲调用异步化,不仅可以提升系统的性能,还可以提升系统的扩展性。 -对于大量的数据库写请求,数据库的压力很大,同时也会造成数据库的响应不及时。可以引入使用消息队列机制,数据库的写请求可以直接写入到消息队列,然后通过多线程或者多进程从消息队列读取数据慢慢写入到数据库。消息队列服务器的处理速度会远远快于数据库,所以用户在写入操作时会感觉到很快写入速度。 **6)代码的优化** 对于IO操作的请求可以采用基于状态机的异步化编程。如: +对于大量的数据库写请求,数据库的压力很大,同时也会造成数据库的响应不及时。可以引入使用消息队列机制,数据库的写请求可以直接写入到消息队列,然后通过多线程或者多进程从消息队列读取数据慢慢写入到数据库。消息队列服务器的处理速度会远远快于数据库,所以用户在写入操作时会感觉到很快写入速度。**6)代码的优化** 对于IO操作的请求可以采用基于状态机的异步化编程。如: > 多线程模型 多进程模型 多协作模型 事件驱动模型 -处理算法的模型优化(时间复杂度和空间复杂度),对于数据结构的设计可以采用高效的数据结构,比如典型的key-value缓存系统就是基于hash的基本原理来实现的,hash表的查询效率是O(1),效率极快。 **7)高性能的本地存储设计** 提供更高的存储硬件,更高的吞吐量和IPOS,读写性能。合理的数据连接池和缓存。 **8)数据分片设计** 在互联网消费金融领域,涉及到很多账务数据的处理,引入分片技术能大大提升数据处理的性能。 +处理算法的模型优化(时间复杂度和空间复杂度),对于数据结构的设计可以采用高效的数据结构,比如典型的key-value缓存系统就是基于hash的基本原理来实现的,hash表的查询效率是O(1),效率极快。**7)高性能的本地存储设计** 提供更高的存储硬件,更高的吞吐量和IPOS,读写性能。合理的数据连接池和缓存。**8)数据分片设计** 在互联网消费金融领域,涉及到很多账务数据的处理,引入分片技术能大大提升数据处理的性能。 > 比如:借贷业务涉及到的借据数据、财务数据的夜间批量处理时,利用分片技术进行处理,提供了更高的扩展性,提升了整体的性能。 @@ -196,7 +196,7 @@ CND加速将网站的内容缓存在网络边缘(离用户接入网络最近 1)运用自动化环境,实现一次性部署测试环境,一键测试; 2)方便对程序的回归测试; 3)可以运行更多更繁琐的测试; 4)可以执行一些手工测试困难的测试; 5)测试具有一致性和可重复性; 6)增加软件信任度; 7)释放测试资源,提升测试人员能力等。 -> 常用的自动化建设,一般分为前端页面的自动化测试,和接口的自动化测试。比较流行的工具有:appium,selenium,httprunner,loadrunner等。有能力的企业会自主研发自动化框架,加入更多定制化的功能,以满足实际的业务需要。 **2)性能测试问题和解决方案** 互联网消费金融业务复杂度高,面对性能测试往往会遇到诸多问题。 +> 常用的自动化建设,一般分为前端页面的自动化测试,和接口的自动化测试。比较流行的工具有:appium,selenium,httprunner,loadrunner等。有能力的企业会自主研发自动化框架,加入更多定制化的功能,以满足实际的业务需要。**2)性能测试问题和解决方案** 互联网消费金融业务复杂度高,面对性能测试往往会遇到诸多问题。 性能测试的场景多,业务复杂,比如支付功能可能涉及到从发起支服务的业务服务,到支付网关,在到银行内部系统等五六个服务。 diff --git "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25402\350\256\262.md" "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25402\350\256\262.md" index 9dfeca2f0..b77629446 100644 --- "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25402\350\256\262.md" +++ "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25402\350\256\262.md" @@ -16,7 +16,7 @@ ## 打造互联网金融服务框架 -服务框架的选型,目前主流的有两类,一是由阿里背书的Dubbo体系,二是由Spring背书的Spring Cloud,下面就两类框架分别进行介绍。 **服务框架Dubbo介绍** 对于互联网消费金融架构来说,Dubbo适用于系统技术相对简单,业务调用链短,系统对并发量和吞吐量要求很高,对生态的要求不高,服务治理等外围系统不需要非常强大的业务场景。对迭代迅速、小短快,控制流程不需要很严格的互联网金融公司。 **服务框架Dubbo架构图** ![img](assets/cd9d8105535e7c6a1e9e8a534f5c6a83.png) **服务框架Dubbo简单介绍** > 背景:中国Alibaba公司出品,由Alibaba官方背书,捐献给了Apache开源组; 定位:本土化、高性能、轻量级、封闭式的开源RPC调用和SOA框架 技术:基于Spring(低版本)/Spring Boot(高版本),服务注册发现(依赖zookeeper),负载均衡RPC、REST(3.x版本支持)调用; 协议:Apache License 2.0 **服务框架Dubbo发展历程** > 诞生:2009年初开源,推出1.0版,10年多发展历史; 成熟:2012年10月23日推出2.5.3版成熟稳定版后,停止维护和升级,当当网接手维护,推出DubboX版; 恢复:2017年9月7日恢复维护,2018年1月8日合并DubboX,发布2.6.0版; 近况:阿里巴巴中间件部门计划推出3.0版 **服务框架Dubbo的未来规划** > Streaming:支持Streaming为内核,取代同时兼容RPC; Reactor:支持“反应式编程”模式(Reactive Programming),去掉一切阻塞; Spring Cloud:支持Dubbo接入Spring Cloud,使两者达到互通,Spring Cloud Alibaba产品组已经着手支持; Service Mesh:支持Service Mesh,由Dubbo-Mesh进行IPC,路由、负载均衡和熔断机制将由 **服务框架Dubbo组件体系** > 服务注册发现中心:Apache Zookeeper,Alibaba Nacos 服务调用方式: RPC:Netty、Mina、Grizzly、Hessian、RMI、Thrift等 REST:DubboX 服务调用序列化: FastJson、FST、Hessian2、Kryo、ProtoStuff、JDK 服务负载均衡: ConsistentHash、LeastActiveLoadBalance、RoundRobin、Random **服务框架Spring Cloud介绍** 对于中大型互联网消费金融公司来说,Spring Cloud是个不错的选择,但同时开发的预支也较大,适用于系统较为复杂,业务调用链较长,对生态的要求很高,微服务配套包括服务治理、监控、路由、网关、灰度发布等需要面面俱到的互联网金融公司。要求公司基础设施强大,架构团队、DevOps、运维等力量雄厚,自动化部署能力较强。同时具备,迭代周期较长,流程控制较为严格,较为正规化。 **服务框架Spring Cloud架构图** ![img](assets/ae0c4e9e3998c29f2f38ba31bec40b76.png) **服务框架Spring Cloud简单介绍** > 背景:由美国Pivotal公司出品,由Spring官方背书,由众多知名互联网企业技术输出,如:Netflix,Alibaba等; 定位:国际化、全生态、全开放、全插件式的开源微服务架构解决方案和体系,拥抱全球知名云厂商; 技术:基于Spring/Spring Boot,服务注册发现,负载均衡,熔断降级RPC、REST调用,API网关等 协议:Apache License 2.0 **服务框架Spirng Cloud发展历程** +服务框架的选型,目前主流的有两类,一是由阿里背书的Dubbo体系,二是由Spring背书的Spring Cloud,下面就两类框架分别进行介绍。**服务框架Dubbo介绍** 对于互联网消费金融架构来说,Dubbo适用于系统技术相对简单,业务调用链短,系统对并发量和吞吐量要求很高,对生态的要求不高,服务治理等外围系统不需要非常强大的业务场景。对迭代迅速、小短快,控制流程不需要很严格的互联网金融公司。**服务框架Dubbo架构图**![img](assets/cd9d8105535e7c6a1e9e8a534f5c6a83.png) **服务框架Dubbo简单介绍** > 背景:中国Alibaba公司出品,由Alibaba官方背书,捐献给了Apache开源组; 定位:本土化、高性能、轻量级、封闭式的开源RPC调用和SOA框架 技术:基于Spring(低版本)/Spring Boot(高版本),服务注册发现(依赖zookeeper),负载均衡RPC、REST(3.x版本支持)调用; 协议:Apache License 2.0 **服务框架Dubbo发展历程** > 诞生:2009年初开源,推出1.0版,10年多发展历史; 成熟:2012年10月23日推出2.5.3版成熟稳定版后,停止维护和升级,当当网接手维护,推出DubboX版; 恢复:2017年9月7日恢复维护,2018年1月8日合并DubboX,发布2.6.0版; 近况:阿里巴巴中间件部门计划推出3.0版 **服务框架Dubbo的未来规划** > Streaming:支持Streaming为内核,取代同时兼容RPC; Reactor:支持“反应式编程”模式(Reactive Programming),去掉一切阻塞; Spring Cloud:支持Dubbo接入Spring Cloud,使两者达到互通,Spring Cloud Alibaba产品组已经着手支持; Service Mesh:支持Service Mesh,由Dubbo-Mesh进行IPC,路由、负载均衡和熔断机制将由 **服务框架Dubbo组件体系** > 服务注册发现中心:Apache Zookeeper,Alibaba Nacos 服务调用方式: RPC:Netty、Mina、Grizzly、Hessian、RMI、Thrift等 REST:DubboX 服务调用序列化: FastJson、FST、Hessian2、Kryo、ProtoStuff、JDK 服务负载均衡: ConsistentHash、LeastActiveLoadBalance、RoundRobin、Random **服务框架Spring Cloud介绍** 对于中大型互联网消费金融公司来说,Spring Cloud是个不错的选择,但同时开发的预支也较大,适用于系统较为复杂,业务调用链较长,对生态的要求很高,微服务配套包括服务治理、监控、路由、网关、灰度发布等需要面面俱到的互联网金融公司。要求公司基础设施强大,架构团队、DevOps、运维等力量雄厚,自动化部署能力较强。同时具备,迭代周期较长,流程控制较为严格,较为正规化。**服务框架Spring Cloud架构图**![img](assets/ae0c4e9e3998c29f2f38ba31bec40b76.png) **服务框架Spring Cloud简单介绍** > 背景:由美国Pivotal公司出品,由Spring官方背书,由众多知名互联网企业技术输出,如:Netflix,Alibaba等; 定位:国际化、全生态、全开放、全插件式的开源微服务架构解决方案和体系,拥抱全球知名云厂商; 技术:基于Spring/Spring Boot,服务注册发现,负载均衡,熔断降级RPC、REST调用,API网关等 协议:Apache License 2.0 **服务框架Spirng Cloud发展历程** - 诞生:2015年7月开源,推出Angle版,将近4年多发展历史,每年定期推出打迭代版本; - 成熟:相继陆续推出了Brixton版、Camden版等 @@ -69,7 +69,7 @@ OpenResty 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分 1)消费金融涉及众多业务功能,大量的开关功能是免不了的,我们可以业务开关放在Apollo进行统一管理。如:自动审批开关、新功能验证开关、风控规则启用开关等; 2)还有消费金融业务配置项管理:如利率范围根据国家政策经常变动,可以用Apollo配置管理起来;又如审批的节点管理,根据贷款类型,有抵押、无抵押,类型不一样,审批的节点也不一样,可以用Apollo管理; 3)同城双活、蓝绿发布的流量管理、Ip路由管理等等。 打造互联网消费金融数据访问层DAL ------------------ **数据访问层DAL的主要特性** > 支持多数据源:Oracle、MySQL等 统一的API封装 简单、安全 统一数据源 支持分库分表策略 Read/Write Mod N Range Hash 代码生成技术,比如统一加时间戳等等 统一的监控和统计 **数据访问层DAL架构设计** ![img](assets/b0a199573781aa83b99cefb8a6899835.png) **互联网消费金融数据访问层DAL实践** 在互联网消费金融领域,业务复杂,建设好DAL数据访问层,能为我们带来很多便利: +----------------- **数据访问层DAL的主要特性** > 支持多数据源:Oracle、MySQL等 统一的API封装 简单、安全 统一数据源 支持分库分表策略 Read/Write Mod N Range Hash 代码生成技术,比如统一加时间戳等等 统一的监控和统计 **数据访问层DAL架构设计**![img](assets/b0a199573781aa83b99cefb8a6899835.png) **互联网消费金融数据访问层DAL实践** 在互联网消费金融领域,业务复杂,建设好DAL数据访问层,能为我们带来很多便利: 1. 金融业务表众多,开发团队大,在DAL层为每张表统一封装好时间戳,这样做能为以后的大数据平台增量同步数据提供便利; 1. 金融行业涉及到的账务数据,数据量大,对每日并行报批,查询服务都有不小挑战,建设统一的分库分表组件,应对未来数据量10倍100倍的增长; @@ -86,9 +86,9 @@ OpenResty 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分 3)跨平台支持 -> JAVA .NET **消息队列MQ架构设计** ![img](assets/f07a590292dd1c7de87aa564879985bc.png) **互联网消费金融消息队列MQ架构实践** 1)服务之间的解耦:消费金融的业务链路特别长的场景,可以用MQ来解耦,比如一笔进件,经历贷前校验,到风控平台风险规则,风险探测,准入,核额,再到贷中审批流程,调用链比较长,业务环节也比较多,可以通过消息队列MQ进行系统&模块间的解耦; +> JAVA .NET **消息队列MQ架构设计**![img](assets/f07a590292dd1c7de87aa564879985bc.png) **互联网消费金融消息队列MQ架构实践** 1)服务之间的解耦:消费金融的业务链路特别长的场景,可以用MQ来解耦,比如一笔进件,经历贷前校验,到风控平台风险规则,风险探测,准入,核额,再到贷中审批流程,调用链比较长,业务环节也比较多,可以通过消息队列MQ进行系统&模块间的解耦; -2)异步的处理提升系统性能:在一些耗时环节,设计成异步的交互方式,通过MQ进行异步的结果通知,可以大大减少系统的同步响应处理,提升系统的吞吐量。例如:用户进行还款时,在进行跨行转账支付时可能会耗时比较长,而且要等待他行的返回结果,与支付服务的交互时,可以通过异步MQ的方式进交互,异步的返回交易的结果,成功或者失败。 **互联网消费金融消息队列MQ技术选型** > 目前MQ中间件开源技术众多,比较流行的有Kafka,RocketMQ,RabbitMQ,ActiveMQ。 **Kafka介绍** > 消息存储:内存、磁盘、数据库。支持大量堆积。 单节点吞吐量:十万级。 分布式集群架构:支持较好。天然的‘Leader-Slave’无状态集群,每台服务器既是Master也是Slave。 社区活跃度:高 适用场景:大数据日志采集 **RocketMQ介绍** > 消息存储:磁盘。支持大量堆积。 单节点吞吐量:十万级。 分布式集群架构:支持较好。常用 多对'Master-Slave' 模式,开源版本需手动切换Slave变成Master。 社区活跃度:高 适用场景:较大型公司使用,需要有专业人员研究源码,主要是有阿里背书,大公司用的比较广泛。 **RabbitMQ介绍** > 消息存储:内存、磁盘。支持少量堆积。 单节点吞吐量:万级。 分布式集群架构:支持不太好。支持简单集群,'复制'模式,对高级集群模式支持不好。 社区活跃度:高 适用场景:中小型公司,比较稳定成熟。 **ActiveMQ介绍** > 消息存储:内存、磁盘、数据库。支持少量堆积。 单节点吞吐量:万级。 分布式集群架构:支持不好。支持简单集群模式,比如'主-备',对高级集群模式支持不好。 社区活跃度:低 适用场景:中小型公司,比较稳定成熟。 +2)异步的处理提升系统性能:在一些耗时环节,设计成异步的交互方式,通过MQ进行异步的结果通知,可以大大减少系统的同步响应处理,提升系统的吞吐量。例如:用户进行还款时,在进行跨行转账支付时可能会耗时比较长,而且要等待他行的返回结果,与支付服务的交互时,可以通过异步MQ的方式进交互,异步的返回交易的结果,成功或者失败。**互联网消费金融消息队列MQ技术选型** > 目前MQ中间件开源技术众多,比较流行的有Kafka,RocketMQ,RabbitMQ,ActiveMQ。**Kafka介绍** > 消息存储:内存、磁盘、数据库。支持大量堆积。 单节点吞吐量:十万级。 分布式集群架构:支持较好。天然的‘Leader-Slave’无状态集群,每台服务器既是Master也是Slave。 社区活跃度:高 适用场景:大数据日志采集 **RocketMQ介绍** > 消息存储:磁盘。支持大量堆积。 单节点吞吐量:十万级。 分布式集群架构:支持较好。常用 多对'Master-Slave' 模式,开源版本需手动切换Slave变成Master。 社区活跃度:高 适用场景:较大型公司使用,需要有专业人员研究源码,主要是有阿里背书,大公司用的比较广泛。**RabbitMQ介绍** > 消息存储:内存、磁盘。支持少量堆积。 单节点吞吐量:万级。 分布式集群架构:支持不太好。支持简单集群,'复制'模式,对高级集群模式支持不好。 社区活跃度:高 适用场景:中小型公司,比较稳定成熟。**ActiveMQ介绍** > 消息存储:内存、磁盘、数据库。支持少量堆积。 单节点吞吐量:万级。 分布式集群架构:支持不好。支持简单集群模式,比如'主-备',对高级集群模式支持不好。 社区活跃度:低 适用场景:中小型公司,比较稳定成熟。 打造互联网消费金融缓存服务Redis ------------------ **缓存服务Redis的主要特性** > 高性能,高吞吐,读的速度是110000次/s,写的速度是81000次/s ; 丰富的数据类型: Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作; 原子性:Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行; **缓存服务Redis的架构设计** 我们在上一章举了一个贷款进度查询的例子,首先进行查询缓存,如缓存没有,再去查数据库,大大降低了数据库的压力。下面我将这个图扩展一下,重点示例Redis的集群结构: ![img](assets/a2465df95a52a14e8ee54b1e958d7577.png) @@ -116,6 +116,6 @@ Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在 数据量大:互联网消费金融业务的线上交易量的增长,无疑会大大增加作业Job的数据量。而且批量作业的数据跟交易量是10倍级别的增长,比如一笔贷款分12期还(一年12个月),这样就是1:12的关系。 -监控的难度增加。 **互联网消费金融作业调度Job的架构设计** ![img](assets/220696a22ee7cbda13fd97136972422b.png) **作业调度Job分布式设计** 支持集群部署,提升调度系统可用性,同时提升任务处理能力。构建作业注册中心,实现的全局作业注册控制中心。用于注册,控制和协调分布式作业执行。 **作业调度Job分片设计** 前面的章节也介绍过分片设计的好处,能够并行处理海量数据,支持动态横向扩展,提升系统的处理能力。将任务拆分为n个任务项后,各个服务器分别执行各自分配到的任务项。一旦有新的服务器加入集群,或现有服务器下线,将在保留本次任务执行不变的情况下,下次任务开始前触发任务重分片。 +监控的难度增加。**互联网消费金融作业调度Job的架构设计**![img](assets/220696a22ee7cbda13fd97136972422b.png) **作业调度Job分布式设计** 支持集群部署,提升调度系统可用性,同时提升任务处理能力。构建作业注册中心,实现的全局作业注册控制中心。用于注册,控制和协调分布式作业执行。**作业调度Job分片设计** 前面的章节也介绍过分片设计的好处,能够并行处理海量数据,支持动态横向扩展,提升系统的处理能力。将任务拆分为n个任务项后,各个服务器分别执行各自分配到的任务项。一旦有新的服务器加入集群,或现有服务器下线,将在保留本次任务执行不变的情况下,下次任务开始前触发任务重分片。 -![img](assets/31642ba23822e502932695ac0bfdf9e2.png) **作业调度Job监控设计** 互联网消费金融作业Job的监控,涉及到的方面: 1)作业的进度监控; 2)作业状态监控,是否正常或异常; 3)异常分类与报警; 4)消息通知。 **互联网消费金融作业调度Job的架构选型** **Quartz** :Java事实上的定时任务标准。但Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定制化的流程。虽然Quartz可以基于数据库实现作业的高可用,但缺少分布式并行调度的功能 **TBSchedule** :阿里早期开源的分布式任务调度系统。代码略陈旧,使用timer而非线程池执行任务调度。众所周知,timer在处理异常状况时是有缺陷的。而且TBSchedule作业类型较为单一,只能是获取/处理数据一种模式。还有就是文档缺失比较严重 **elastic-job** :当当开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片,目前是版本2.15,并且可以支持云开发 **Saturn** :是唯品会自主研发的分布式的定时任务的调度平台,基于当当的elastic-job 版本1开发,并且可以很好的部署到docker容器上。 **xxl-job** : 是大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。 +![img](assets/31642ba23822e502932695ac0bfdf9e2.png) **作业调度Job监控设计** 互联网消费金融作业Job的监控,涉及到的方面: 1)作业的进度监控; 2)作业状态监控,是否正常或异常; 3)异常分类与报警; 4)消息通知。**互联网消费金融作业调度Job的架构选型** **Quartz** :Java事实上的定时任务标准。但Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定制化的流程。虽然Quartz可以基于数据库实现作业的高可用,但缺少分布式并行调度的功能 **TBSchedule** :阿里早期开源的分布式任务调度系统。代码略陈旧,使用timer而非线程池执行任务调度。众所周知,timer在处理异常状况时是有缺陷的。而且TBSchedule作业类型较为单一,只能是获取/处理数据一种模式。还有就是文档缺失比较严重 **elastic-job** :当当开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片,目前是版本2.15,并且可以支持云开发 **Saturn** :是唯品会自主研发的分布式的定时任务的调度平台,基于当当的elastic-job 版本1开发,并且可以很好的部署到docker容器上。**xxl-job** : 是大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。 diff --git "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25403\350\256\262.md" "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25403\350\256\262.md" index 6a3599487..f56392bb1 100644 --- "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25403\350\256\262.md" +++ "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25403\350\256\262.md" @@ -91,7 +91,7 @@ ![img](assets/8c25fae0680ca42c43dd2c91e79017eb.png) -**注册中心服务注册发现的具体过程** 1)服务提供者启动,向注册中心注册自己提供的服务; 2)消费者启动,向注册中心订阅自己需要的服务; 3)注册中心返回服务提供者的列表给消费者; 4)消费者从服务提供者列表中,按照软负载均衡算法,选择一台发起请求; **注册中心的服务治理的特点** > 注册中心职责简单,只负责注册查找,不负责请求转发,压力小; 消费者本地缓存服务地址列表,注册中心宕机影响不影响服务调用; 注册中心可搭建集群,宕掉一台自动切换到另外一台; 服务提供者无状态,可动态部署,注册中心负责推送; 消费者调用服务者,自动软负载均衡; **注册中心搭建选型** **服务注册发现中心Zookeeper介绍** > 社区活跃度:中 CAP模型:CP 控制台管理:不支持 适用规模(建议):十万级 健康检查:Keep Alive 易用性:易用性比较差,Zookeeper的客户端使用比较复杂,没有针对服务发现的模型设计以及相应的API封装,需要依赖方自己处理。对多语言的支持也不太好,同时没有比较好用的控制台进行运维管理。 综合建议:更新较慢,功能匮乏,使用部署较复杂,不易上手,维护成本较高。 **服务注册发现中心Eureka介绍** > 社区活跃度:低,已停止开源维护 CAP模型:AP 控制台管理:支持 适用规模(建议):十万级 健康检查:Client Beat 易用性:较好,基于SpringCloud体系的starter,帮助用户以非常低的成本无感知的做到服务注册与发现。提供官方的控制台来查询服务注册情况。 综合建议:eureka当前停止开源不建议企业级使用 **服务注册发现中心Consul介绍** > 社区活跃度:高 CAP模型:CP 控制台管理:支持 适用规模(建议):百万级 健康检查:TCP/HTTP/gRPC/Cmd 易用性:较好,能够帮助用户以非常低的成本无感知的做到服务注册与发现。提供官方的控制台来查询服务注册情况。 综合建议:集成简单,不依赖其他工具,推荐大中型企业使用。 **服务注册发现中心Nacos介绍** > 社区活跃度:高 CAP模型:CP+AP 控制台管理:支持 适用规模(建议):百万级 健康检查:TCP/HTTP/MYSQL/Client Beat 易用性:较好,能够帮助用户以非常低的成本无感知的做到服务注册与发现。提供官方的控制台来查询服务注册情况。 综合建议:阿里巴巴背书,更新速度快,文档完善,社区活跃度高,推荐大中型企业使用。 +**注册中心服务注册发现的具体过程** 1)服务提供者启动,向注册中心注册自己提供的服务; 2)消费者启动,向注册中心订阅自己需要的服务; 3)注册中心返回服务提供者的列表给消费者; 4)消费者从服务提供者列表中,按照软负载均衡算法,选择一台发起请求; **注册中心的服务治理的特点** > 注册中心职责简单,只负责注册查找,不负责请求转发,压力小; 消费者本地缓存服务地址列表,注册中心宕机影响不影响服务调用; 注册中心可搭建集群,宕掉一台自动切换到另外一台; 服务提供者无状态,可动态部署,注册中心负责推送; 消费者调用服务者,自动软负载均衡; **注册中心搭建选型** **服务注册发现中心Zookeeper介绍** > 社区活跃度:中 CAP模型:CP 控制台管理:不支持 适用规模(建议):十万级 健康检查:Keep Alive 易用性:易用性比较差,Zookeeper的客户端使用比较复杂,没有针对服务发现的模型设计以及相应的API封装,需要依赖方自己处理。对多语言的支持也不太好,同时没有比较好用的控制台进行运维管理。 综合建议:更新较慢,功能匮乏,使用部署较复杂,不易上手,维护成本较高。**服务注册发现中心Eureka介绍** > 社区活跃度:低,已停止开源维护 CAP模型:AP 控制台管理:支持 适用规模(建议):十万级 健康检查:Client Beat 易用性:较好,基于SpringCloud体系的starter,帮助用户以非常低的成本无感知的做到服务注册与发现。提供官方的控制台来查询服务注册情况。 综合建议:eureka当前停止开源不建议企业级使用 **服务注册发现中心Consul介绍** > 社区活跃度:高 CAP模型:CP 控制台管理:支持 适用规模(建议):百万级 健康检查:TCP/HTTP/gRPC/Cmd 易用性:较好,能够帮助用户以非常低的成本无感知的做到服务注册与发现。提供官方的控制台来查询服务注册情况。 综合建议:集成简单,不依赖其他工具,推荐大中型企业使用。**服务注册发现中心Nacos介绍** > 社区活跃度:高 CAP模型:CP+AP 控制台管理:支持 适用规模(建议):百万级 健康检查:TCP/HTTP/MYSQL/Client Beat 易用性:较好,能够帮助用户以非常低的成本无感知的做到服务注册与发现。提供官方的控制台来查询服务注册情况。 综合建议:阿里巴巴背书,更新速度快,文档完善,社区活跃度高,推荐大中型企业使用。 互联网消费金融微服务流量治理实践 ---------------- **互联网消费金融系统微服务流量控制设计** 金融系统平时平稳,但遇到大促的时候,机器的load会爆发式增长,这时候对系统的负载保护就显得非常重要,以防止雪崩。流量控制提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。 @@ -106,7 +106,7 @@ 限流原则: 1)限流前置 2)集群限流 **微服务流量治理框架选型** 目前比较流行的开源流量治理框架有: -> Spring Cloud官方默认的熔断组件Hystrix(已停止维护); 较轻量的熔断降级库resilience4j(轻量级); Google开源工具包Guava提供了限流工具类RateLimiter(功能较单一); 阿里巴巴的开源框架Sentinel(推荐); **Sentinel 的优势和特性:** ![img](assets/d3ed089bef37f682af6e2c4f81dd066d.png) +> Spring Cloud官方默认的熔断组件Hystrix(已停止维护); 较轻量的熔断降级库resilience4j(轻量级); Google开源工具包Guava提供了限流工具类RateLimiter(功能较单一); 阿里巴巴的开源框架Sentinel(推荐); **Sentinel 的优势和特性:**![img](assets/d3ed089bef37f682af6e2c4f81dd066d.png) 1)轻量级,核心库无多余依赖,性能损耗小。 @@ -131,9 +131,9 @@ **Failfast快速失败策略:** 在业务高峰期,对于一些非核心的服务,希望只调用一次,失败也不再重试,为重要的核心服务节约宝贵的运行资源。此时,快速失败是个不错的选择。 -**互联网消费金融的微服务灰度发布设计** ![img](assets/11b5fbb45c39dbe8adc4b33708f228bf.png) +**互联网消费金融的微服务灰度发布设计**![img](assets/11b5fbb45c39dbe8adc4b33708f228bf.png) -灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。 基于微服务的多版本管理机制 灰度路由策略,即可实现基于业务规则的灰度发布。 **通常灰度策略** 1)首先选取种子用户,哪些群体用户能够进行灰度版本体验; 2)流量路由控制:可以根据服务名(serviceName)、方法名(methodName)、版本号(versionName) 进行、 ip 规则等进行流量路由。 3)版本管理:包括版本信息、升级地址、升级方案、是否全量发布。其中升级方案包括热更新及官网更新,全量发布以最新的全量版本为准; 4)服务部署:每个服务集群管理一个版本,正式服务集群和灰度服务集群尽量配置和数量相等,也可以根据流量的多少进行动态分配。 5)灰度验证:将灰度流量逐步增加,需要同时验证业务功能的效果和系统架构的性能;验证完毕后可以考虑将所有集群统一升级至希望的版本。 **架构资源治理** > 服务器资源:服务器是否闲置,访问量,吞吐量等; 数据库DB资源:慢查询治理,高频调用性能问题治理; 缓存cache资源:缓存命中率过低,读写比例是否合理; 消息队列MQ资源:消息是否堆积等。 +灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。 基于微服务的多版本管理机制 灰度路由策略,即可实现基于业务规则的灰度发布。**通常灰度策略** 1)首先选取种子用户,哪些群体用户能够进行灰度版本体验; 2)流量路由控制:可以根据服务名(serviceName)、方法名(methodName)、版本号(versionName) 进行、 ip 规则等进行流量路由。 3)版本管理:包括版本信息、升级地址、升级方案、是否全量发布。其中升级方案包括热更新及官网更新,全量发布以最新的全量版本为准; 4)服务部署:每个服务集群管理一个版本,正式服务集群和灰度服务集群尽量配置和数量相等,也可以根据流量的多少进行动态分配。 5)灰度验证:将灰度流量逐步增加,需要同时验证业务功能的效果和系统架构的性能;验证完毕后可以考虑将所有集群统一升级至希望的版本。**架构资源治理** > 服务器资源:服务器是否闲置,访问量,吞吐量等; 数据库DB资源:慢查询治理,高频调用性能问题治理; 缓存cache资源:缓存命中率过低,读写比例是否合理; 消息队列MQ资源:消息是否堆积等。 互联网消费金融的微服务容量治理 --------------- **动态扩容、减容设计** 基于PaaS弹性云化平台或者Docker容器服务,可以实现基于负载的微服务弹性伸缩。 ![img](assets/4828bee976263875bce5acf6702a69f1.png) diff --git "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25404\350\256\262.md" "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25404\350\256\262.md" index 08a927bef..9817eee56 100644 --- "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25404\350\256\262.md" +++ "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25404\350\256\262.md" @@ -68,7 +68,7 @@ ## 贷款风险 -贷款风险是指贷款发放后因各种原因出现本金及收益损失的不确定性,风险不可能被消灭但可以被控制。 **1.信用风险:** 即借款人信用风险,贷款损失的最主要风险,产生原因为借款人还款能力或还款意愿下降的可能性; **2.流动性风险:** 银行借入短期资金,贷出长期资金,短借长贷存在期限不匹配,导致流动性风险的增加; **3.市场风险:** +贷款风险是指贷款发放后因各种原因出现本金及收益损失的不确定性,风险不可能被消灭但可以被控制。**1.信用风险:** 即借款人信用风险,贷款损失的最主要风险,产生原因为借款人还款能力或还款意愿下降的可能性; **2.流动性风险:** 银行借入短期资金,贷出长期资金,短借长贷存在期限不匹配,导致流动性风险的增加; **3.市场风险:** - 利率风险:银行以低利率进行发放贷款,当市场利率升高后,导致利息收入损失甚至存贷款利率倒挂; - 汇率风险:汇率变化导致国际信贷业务损失; @@ -79,9 +79,9 @@ ## 互联网消费金融的产品逐步丰富 -随着互联网消费金融的业务发展,产品的种类也越来越多,目前的互联网消费金融产品分为以下几大类型: **1. 电商消费金融** 典型的产品代表,如蚂蚁花呗、京东白条等。电商平台本来就是一个巨大的消费平台,通过基于这个巨大的电商体系打造信用消费,无疑是对平台自身生态建设的一种补充。如今,BAT、京东、苏宁、国美、小米等互联网公司,都纷纷加入消费金融业务争夺战,围绕供应链和消费者打造金融产品,希望借此构建“生态”。消费者在电商平台上进行购物的时候,有的时候会出现支付不方便或者资金暂时紧张的情况,这个时候他们就会很自然地选择电商平台的信用消费。 **2. 汽车消费金融** 汽车金融是由消费者在购买汽车需要贷款时,可以直接向汽车金融公司申请优惠的支付方式,可以按照自身的个性化需求来选择不同的车型和不同的支付方法。对比银行,汽车金融是一种购车新选择。汽车金融是汽车产业与金融的结合,是金融产业的重要领域。与购买房子一样,购买汽车同样也是一笔不小的开支,贷款无形之中就成为众多消费者的一种选择。汽车消费金融中,尤以二手车消费金融为蓝海。由于目前银行的汽车金融业务主要集中在新车领域,尤其是和汽车厂商的合作,二手车金融**\*率非常低,这是一个巨大的发展机会。 +随着互联网消费金融的业务发展,产品的种类也越来越多,目前的互联网消费金融产品分为以下几大类型: **1. 电商消费金融** 典型的产品代表,如蚂蚁花呗、京东白条等。电商平台本来就是一个巨大的消费平台,通过基于这个巨大的电商体系打造信用消费,无疑是对平台自身生态建设的一种补充。如今,BAT、京东、苏宁、国美、小米等互联网公司,都纷纷加入消费金融业务争夺战,围绕供应链和消费者打造金融产品,希望借此构建“生态”。消费者在电商平台上进行购物的时候,有的时候会出现支付不方便或者资金暂时紧张的情况,这个时候他们就会很自然地选择电商平台的信用消费。**2. 汽车消费金融** 汽车金融是由消费者在购买汽车需要贷款时,可以直接向汽车金融公司申请优惠的支付方式,可以按照自身的个性化需求来选择不同的车型和不同的支付方法。对比银行,汽车金融是一种购车新选择。汽车金融是汽车产业与金融的结合,是金融产业的重要领域。与购买房子一样,购买汽车同样也是一笔不小的开支,贷款无形之中就成为众多消费者的一种选择。汽车消费金融中,尤以二手车消费金融为蓝海。由于目前银行的汽车金融业务主要集中在新车领域,尤其是和汽车厂商的合作,二手车金融**\*率非常低,这是一个巨大的发展机会。 -**3. 旅游消费金融** 旅游消费金融是基于旅游为消费场景的,对具有旅游消费需求方提供的贷款产品。旅游消费金融正在成为旅游平台竞争的新焦点。从消费者的需求角度来看,旅游对于很多人来说都是一件非常向往的事情,尤其是对于一些收入并不高的年轻人来说,他们心中或多或少都会有几个特别想去的地方,但是由于经费不足等问题让他们的旅行只能成为泡影。对于一些费用昂贵的出国旅行来说,就更承担不起了,那么这个旅游金融分期消费就自然而然就会成为他们考虑的一种需求。 **4. 医疗消费金融** 很多家庭由于经济原因负担不起昂贵的医疗费用,这个时候分期医疗付费也就由此诞生了。眼下国内有少数医院通过与银行合作,推出了一种分期付费的方式。不过国内还没有单独的医疗金融平台通过与各大医院达成合作。整体看来,医疗消费金融是一件利国利民的事情,尤其是对于很多没什么资金实力老百姓来说,但是当前国内的医疗消费金融普及程度还过低,要让医疗消费金融顺利进行,需要医院与金融平台以及机构的共同配合。 **5. 教育消费金融** 教育消费金融不同于校园电商消费金融,虽然他们同样都是针对学生,但是一个是针对学生们的购买消费,另一个是针对学生们学习上的消费,是两种完全不同的消费。目前学好贷、龙门社交金融等平台以及众多的培训机构都推出了针对大学生的学费分期贷款。对于推出教育消费金融产品的互联网平台来说,要给学生放贷的话,必须要确保学生将来有一定的偿还能力,否则教育学费贷款尤其是留学贷款也不是个小数目,一旦平台的坏账率过高,就会导致平台的资金链出现问题。 **6. 农村消费金融** 农村金融是当前BAT进军的领域,但一些小的互联网公司也已经开始在农村消费金融领域进行布局。随着电商平台不断发展到农村,未来农村消费金融将会成为下一个新的风口。 **7. 房产消费金融** 传统的银行也一直都在深耕耘房产金融领域,包括新房金融、二手房金融、装修金融、租房金融等多个方面。房产消费金融市场规模庞大,竞争同样十分激烈。互联网房产消费金融最大的威胁就是对传统银行的威胁,但是房产金融是传统银行非常大的一块利润来源,传统银行对于互联网房产消费金融平台的反击是他们最大的威胁。 +**3. 旅游消费金融** 旅游消费金融是基于旅游为消费场景的,对具有旅游消费需求方提供的贷款产品。旅游消费金融正在成为旅游平台竞争的新焦点。从消费者的需求角度来看,旅游对于很多人来说都是一件非常向往的事情,尤其是对于一些收入并不高的年轻人来说,他们心中或多或少都会有几个特别想去的地方,但是由于经费不足等问题让他们的旅行只能成为泡影。对于一些费用昂贵的出国旅行来说,就更承担不起了,那么这个旅游金融分期消费就自然而然就会成为他们考虑的一种需求。**4. 医疗消费金融** 很多家庭由于经济原因负担不起昂贵的医疗费用,这个时候分期医疗付费也就由此诞生了。眼下国内有少数医院通过与银行合作,推出了一种分期付费的方式。不过国内还没有单独的医疗金融平台通过与各大医院达成合作。整体看来,医疗消费金融是一件利国利民的事情,尤其是对于很多没什么资金实力老百姓来说,但是当前国内的医疗消费金融普及程度还过低,要让医疗消费金融顺利进行,需要医院与金融平台以及机构的共同配合。**5. 教育消费金融** 教育消费金融不同于校园电商消费金融,虽然他们同样都是针对学生,但是一个是针对学生们的购买消费,另一个是针对学生们学习上的消费,是两种完全不同的消费。目前学好贷、龙门社交金融等平台以及众多的培训机构都推出了针对大学生的学费分期贷款。对于推出教育消费金融产品的互联网平台来说,要给学生放贷的话,必须要确保学生将来有一定的偿还能力,否则教育学费贷款尤其是留学贷款也不是个小数目,一旦平台的坏账率过高,就会导致平台的资金链出现问题。**6. 农村消费金融** 农村金融是当前BAT进军的领域,但一些小的互联网公司也已经开始在农村消费金融领域进行布局。随着电商平台不断发展到农村,未来农村消费金融将会成为下一个新的风口。**7. 房产消费金融** 传统的银行也一直都在深耕耘房产金融领域,包括新房金融、二手房金融、装修金融、租房金融等多个方面。房产消费金融市场规模庞大,竞争同样十分激烈。互联网房产消费金融最大的威胁就是对传统银行的威胁,但是房产金融是传统银行非常大的一块利润来源,传统银行对于互联网房产消费金融平台的反击是他们最大的威胁。 ## 互联网消费金融贷款的生命周期 @@ -111,7 +111,7 @@ 4). **核额** : 根据用户的信誉,个人信息,和征信等计算用户贷款的额度; -5). **提交进件** 把用户进件的信息存储、落库,并通过接口、MQ触发后续的进件审批审核等流程。 **评分核额流程设计** ![img](assets/483d4ff9aaa75666aa50f2abc6f95045.png) **内评准入流程设计** ![img](assets/6d765e0f69f736c35cabde91d6e4744f.png) **决策引擎流程设计** ![img](assets/f9ea14e87c728b1b5b64eee05541a456.png) **进件提交流程设计** ![img](assets/3cb06e05c43da2adf66bb945ba24bd05.png) **互联网消费风控领域模型整体设计** ![img](assets/06aee0f5d478502685b88c92dd1d8228.png) **风控规则模型介绍** 风控模型应该是从两个角度去考虑,第一个角度是资产端风控策略,第二个角度是资金端风控策略。考虑主要出发点应该是从贷前、袋中、贷后三个方向去考虑,结合传统业务的风控模型和互联用户的行为数据。针对资金,资产进行风险等级划分,防欺诈系统、袋中的舆情监控、贷后的权重叠加。 +5). **提交进件** 把用户进件的信息存储、落库,并通过接口、MQ触发后续的进件审批审核等流程。**评分核额流程设计**![img](assets/483d4ff9aaa75666aa50f2abc6f95045.png) **内评准入流程设计**![img](assets/6d765e0f69f736c35cabde91d6e4744f.png) **决策引擎流程设计**![img](assets/f9ea14e87c728b1b5b64eee05541a456.png) **进件提交流程设计**![img](assets/3cb06e05c43da2adf66bb945ba24bd05.png) **互联网消费风控领域模型整体设计**![img](assets/06aee0f5d478502685b88c92dd1d8228.png) **风控规则模型介绍** 风控模型应该是从两个角度去考虑,第一个角度是资产端风控策略,第二个角度是资金端风控策略。考虑主要出发点应该是从贷前、袋中、贷后三个方向去考虑,结合传统业务的风控模型和互联用户的行为数据。针对资金,资产进行风险等级划分,防欺诈系统、袋中的舆情监控、贷后的权重叠加。 1). 准入规则:对不同客户制定不同的贷款门槛,比如根据注册年限和消费次数等设置一个基本的准入门槛,对于后期可以分层次分批次的制定不同的风控策略。 diff --git "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25405\350\256\262.md" "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25405\350\256\262.md" index 5c426c64e..407600a3c 100644 --- "a/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25405\350\256\262.md" +++ "b/docs/Design/\344\272\222\350\201\224\347\275\221\346\266\210\350\264\271\351\207\221\350\236\215\351\253\230\345\271\266\345\217\221\351\242\206\345\237\237\350\256\276\350\256\241/\347\254\25405\350\256\262.md" @@ -14,7 +14,7 @@ ![img](assets/4c2d9d825712929c09eaa73340ce6015.png) -**调用链监控核心概念** ![img](assets/41d6a1fa7e5af3addf7238cdaa50fdbc.png) +**调用链监控核心概念**![img](assets/41d6a1fa7e5af3addf7238cdaa50fdbc.png) 1)Trace:一次分布式调用的链路踪迹。 Trace是指一次请求调用的链路过程,trace id 是指这次请求调用的ID。在一次请求中,会在网络的最开始生成一个全局唯一的用于标识此次请求的trace id,这个trace id在这次请求调用过程中无论经过多少个节点都会保持不变,并且在随着每一层的调用不停的传递。最终,可以通过trace id将这一次用户请求在系统中的路径全部串起来。 2)Span:一个方法(局部或远程)调用踪迹。 Span是指一个模块的调用过程,一般用span id来标识。在一次请求的过程中会调用不同的节点/模块/服务,每一次调用都会生成一个新的span id来记录。这样,就可以通过span id来定位当前请求在整个系统调用链中所处的位置,以及它的上下游节点分别是什么。 3)Annotation:附着在Span上的日志信息。 可以是业务自定义的埋点信息,可以是sql、用户ID等关键信息。 4)Sampling:采样率 **调用链监控的常用工具** **Zipkin** Twitter开源的zipkin,提供了完整的跟踪记录收集、存储功能,以及查询API与界面。其存储支持多种数据库:MySql、ElasticSearch、Cassandra、Redis等等,收集API支持HTTP和Thrift。 @@ -209,4 +209,4 @@ - 通过服务发现或者静态配置来发现目标服务对象。 - 支持多种多样的图表和界面展示,比如Grafana等。 -**Prometheus的基本原理** Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。 **Prometheus的功能** 1).业务应用层监控 我们可以通过在业务层添加埋点来监控系统,如贷款进度、支付放款埋点等。Prometheus支持多种语言(Go,java,python,ruby官方提供客户端,其他语言有第三方开源客户端)。 2). 中间件应用监控 一些主流应用可以通过官方或第三方的导出器,来对这些应用做核心指标的收集。如redis,mysql。 3). 在系统层用作系统监控 除了常用软件, prometheus也有相关系统层和网络层exporter,用于监控服务器或网络。 4). 集成其他的监控 prometheus还可以通过各种export,集成其他的监控系统,收集监控数据,如AWS CloudWatch,JMX,Pingdom等等 +**Prometheus的基本原理** Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。**Prometheus的功能** 1).业务应用层监控 我们可以通过在业务层添加埋点来监控系统,如贷款进度、支付放款埋点等。Prometheus支持多种语言(Go,java,python,ruby官方提供客户端,其他语言有第三方开源客户端)。 2). 中间件应用监控 一些主流应用可以通过官方或第三方的导出器,来对这些应用做核心指标的收集。如redis,mysql。 3). 在系统层用作系统监控 除了常用软件, prometheus也有相关系统层和网络层exporter,用于监控服务器或网络。 4). 集成其他的监控 prometheus还可以通过各种export,集成其他的监控系统,收集监控数据,如AWS CloudWatch,JMX,Pingdom等等 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25402\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25402\350\256\262.md" index 15c5bcb27..5c846a3de 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25402\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25402\350\256\262.md" @@ -141,7 +141,7 @@ CAP 理论最早是2000年由 Eric Brewer 教授在 PODC 的研讨会上提出 根据 CAP 理论,在分布式系统中,CAP 三者不可能同时被满足,在设计分布式系统时,工程师必须做出取舍,一般认为,CAP 只能选择其二。 -##### **CA without P** 放弃 P(分区容忍),以保证 C(强一致性)和 A(可用性)。其实分区容忍并不是能否放弃的问题,只能是阻止,即不允许分区出现,一种直接的策略就是所有服务部署在一台服务器上,退化为单机系统。 **这里存在一个争议问题,即怎样才算“舍弃 P”?其含义并不明确。** 在分布式系统中,分区是无法完全避免的,设计师即便舍弃分区容忍,就一定可以保证一致性和可用性吗?当分区出现的时候,还是需要在 C 和 A 之间做出选择:选择一致性则需等待分区恢复,在此期间牺牲可用性;选择可用性,则无法保证各个分区数据的一致性 +##### **CA without P** 放弃 P(分区容忍),以保证 C(强一致性)和 A(可用性)。其实分区容忍并不是能否放弃的问题,只能是阻止,即不允许分区出现,一种直接的策略就是所有服务部署在一台服务器上,退化为单机系统。**这里存在一个争议问题,即怎样才算“舍弃 P”?其含义并不明确。** 在分布式系统中,分区是无法完全避免的,设计师即便舍弃分区容忍,就一定可以保证一致性和可用性吗?当分区出现的时候,还是需要在 C 和 A 之间做出选择:选择一致性则需等待分区恢复,在此期间牺牲可用性;选择可用性,则无法保证各个分区数据的一致性 某种意义上,舍弃分区容忍是基于一种假设,即分区出现的概率很低,远低于其它系统性错误。基于不存在分区问题的假设,CA 之间仍然存在矛盾:为了保证服务的可用性,那就必须避免单节点故障问题,即服务需部署在多个节点上,即便其中一个节点故障而不能提供服务,其它节点也能替代它继续提供服务,从而保证可用性;但是,这些服务是分布在不同节点上的,为了保证一致性,节点之间必须进行同步,任何一个节点的更新都需要向其它节点同步,只有同步完成之后,才能继续提供服务,而同步期间,服务是不可用的,因此,即便没有分区,可用性和一致性也不可能在任何时刻都同时成立。 @@ -292,7 +292,7 @@ synchronized 本质上是通过锁来实现的。对于同一个代码块,为 ### 8. 总结 -本文首先介绍了单机系统到分布式系统的演进,并对分布式系统的特性和常见问题进行了阐述。而后进入正题,从 CAP 理论切入,介绍了三大分布式中间件:分布式缓存、分布式锁以及分布式消息队列。该文涉及到了很多理论知识,是学习本课程重要的基础知识,请大家好好理解。 **参考文献与致谢** +本文首先介绍了单机系统到分布式系统的演进,并对分布式系统的特性和常见问题进行了阐述。而后进入正题,从 CAP 理论切入,介绍了三大分布式中间件:分布式缓存、分布式锁以及分布式消息队列。该文涉及到了很多理论知识,是学习本课程重要的基础知识,请大家好好理解。**参考文献与致谢** 本文的一些图片和文字引用了一些博客和论文,尊重原创是每一个写作者应坚守的底线,在此,将本文引用过的文章一一列出,以表敬意: diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25404\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25404\350\256\262.md" index c626a80f1..cd9cf470e 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25404\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25404\350\256\262.md" @@ -275,11 +275,11 @@ mstime() + 500ms + random()%500ms + rank*1000ms ### 6. Redis Cluster 扩容 -随着应用场景的升级,缓存可能需要扩容,扩容的方式有两种:垂直扩容(Scale Up)和水平扩容(Scale Out)。垂直扩容无需详述。实际应用场景中,采用水平扩容更多一些,根据是否增加主节点数量,水平扩容方式有两种。 **方式1:主节点数量不变。** 比如,当前有一台物理机 A,构建了一个包含3个 Redis 实例的集群;扩容时,我们新增一台物理机 B,拉起一个 Redis 实例并加入物理机 A 的集群;B 上 Redis 实例对 A 上的一个主节点进行复制,然后进行主备倒换;如此,Redis 集群还是3个主节点,只不过变成了 A2-B1 的结构,将一部分请求压力分担到了新增的节点上,同时物理容量上限也会增加,主要步骤如下: +随着应用场景的升级,缓存可能需要扩容,扩容的方式有两种:垂直扩容(Scale Up)和水平扩容(Scale Out)。垂直扩容无需详述。实际应用场景中,采用水平扩容更多一些,根据是否增加主节点数量,水平扩容方式有两种。**方式1:主节点数量不变。** 比如,当前有一台物理机 A,构建了一个包含3个 Redis 实例的集群;扩容时,我们新增一台物理机 B,拉起一个 Redis 实例并加入物理机 A 的集群;B 上 Redis 实例对 A 上的一个主节点进行复制,然后进行主备倒换;如此,Redis 集群还是3个主节点,只不过变成了 A2-B1 的结构,将一部分请求压力分担到了新增的节点上,同时物理容量上限也会增加,主要步骤如下: 1. 将新增节点加入集群; 1. 将新增节点设置为某个主节点的从节点,进而对其进行复制; -1. 进行主备倒换,将新增的节点调整为主。 **方式2:增加主节点数量。** +1. 进行主备倒换,将新增的节点调整为主。**方式2:增加主节点数量。** 不增加主节点数量的方式扩容比较简单,但是,从负载均衡的角度来看,并不是很好的选择。例如,如果主节点数量较少,那么单个节点所负责的 Slot 的数量必然较多,很容易出现大量 Key 的读写集中于少数节点的现象,而增加主节点的数量,可以更有效的分摊访问压力,充分利用资源。主要步骤如下: diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25405\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25405\350\256\262.md" index 6a3332650..4b205ce78 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25405\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25405\350\256\262.md" @@ -561,7 +561,7 @@ slaveConn.sync().clusterFailover(true) ![enter image description here](assets/cab6ab40-9671-11e8-bee3-b1dbef72ca56) -上面代码只从一个节点的视角进行了检查,完整的代码将遍历所有节点,从所有节点的视角分别检查。 **Redis 集群创建** 大家可参考第二节“2. 基于 Lettuce 创建 Redis 集群”中的内容。 **替换故障节点** (1)加入新节点 +上面代码只从一个节点的视角进行了检查,完整的代码将遍历所有节点,从所有节点的视角分别检查。**Redis 集群创建** 大家可参考第二节“2. 基于 Lettuce 创建 Redis 集群”中的内容。**替换故障节点** (1)加入新节点 替换上来的新节点本质上是“孤立”的,需要先加入现有集群:通过集群命令 RedisAdvancedClusterCommands 对象调用 `clusterMeet()` 方法,便可实现: diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25407\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25407\350\256\262.md" index bb7c5f64c..75a68919b 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25407\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25407\350\256\262.md" @@ -151,7 +151,7 @@ Redis-Cluster 发生故障后,集群的拓扑结构一定会发生改变,如 #### 4.1 客户端如何感知 Redis-Cluster 发生故障? -结合上面介绍的故障场景, **思考这样一个问题** :当 Redis-Cluster 发生故障,集群拓扑结构变化时,如果客户端没有及时感知到,继续试图对已经故障的节点进行“读写操作”,势必会出现异常,那么,如何应对这种场景呢? +结合上面介绍的故障场景,**思考这样一个问题** :当 Redis-Cluster 发生故障,集群拓扑结构变化时,如果客户端没有及时感知到,继续试图对已经故障的节点进行“读写操作”,势必会出现异常,那么,如何应对这种场景呢? Redis 的高级客户端很多,覆盖数十种编程语言,如常见的 Java 客户端 Jedis、Lettuce、Redisson。不同客户端针对上述问题的处理方式也不尽相同,一般有以下几种处理策略: diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25408\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25408\350\256\262.md" index d5073ca69..4aca655b8 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25408\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25408\350\256\262.md" @@ -79,7 +79,7 @@ OK #### 2.2 设置锁自动过期时间以预防死锁存在的隐患 -为了避免死锁,可利用 Redis 为锁数据(Key-Value)设置自动过期时间,虽然可以解决死锁的问题,但却存在隐患。 **我们看下面这个典型场景。** 1. 客户端 A 获取锁成功; +为了避免死锁,可利用 Redis 为锁数据(Key-Value)设置自动过期时间,虽然可以解决死锁的问题,但却存在隐患。**我们看下面这个典型场景。** 1. 客户端 A 获取锁成功; 2\. 客户端 A 在某个操作上阻塞了很长时间(对于 Java 而言,如发生 Full-GC); 3\. 过期时间到,锁自动释放; 4\. 客户端 B 获取到了对应同一个资源的锁; @@ -101,7 +101,7 @@ OK 5\. 客户端 A 由于某个原因阻塞了很长时间; 6\. 过期时间到了,锁自动释放了; 7\. 客户端 B 获取到了对应同一个资源的锁; -8\. 客户端 A 从阻塞中恢复过来,执行 DEL 操纵,释放掉了客户端 B 持有的锁。 **下面给出解决方案。** 如何保障解锁操作的原子性呢?在实践中,我总结出两种方案。 **1.** 使用 Redis 事务功能,使用 Watch 命令监控锁对应的 Key,释放锁则采用事务功能(Multi 命令),如果持有的锁已经因过期而释放(或者过期释放后又被其它客户端持有),则 Key 对应的 Value 将改变,释放锁的事务将不会被执行,从而避免错误的释放锁,示例代码如下: +8\. 客户端 A 从阻塞中恢复过来,执行 DEL 操纵,释放掉了客户端 B 持有的锁。**下面给出解决方案。** 如何保障解锁操作的原子性呢?在实践中,我总结出两种方案。**1.** 使用 Redis 事务功能,使用 Watch 命令监控锁对应的 Key,释放锁则采用事务功能(Multi 命令),如果持有的锁已经因过期而释放(或者过期释放后又被其它客户端持有),则 Key 对应的 Value 将改变,释放锁的事务将不会被执行,从而避免错误的释放锁,示例代码如下: ```java Jedis jedis = new Jedis("127.0.0.1", 6379); @@ -205,13 +205,13 @@ jedis.close(); #### 2.4 Redis 节点故障后,主备切换的数据一致性 -考虑 Redis 节点宕机,如果长时间无法恢复,则导致锁服务长时间不可用。为了保证锁服务的可用性,通常的方案是给这个 Redis 节点挂一个 Slave(多个也可以),当 Master 节点不可用的时候,系统自动切到 Slave 上。但是由于 Redis 的主从复制(Replication)是异步的,这可能导致在宕机切换过程中丧失锁的安全性。 **我们看下典型场景。** 1. 客户端 A 从 Master 获取了锁; +考虑 Redis 节点宕机,如果长时间无法恢复,则导致锁服务长时间不可用。为了保证锁服务的可用性,通常的方案是给这个 Redis 节点挂一个 Slave(多个也可以),当 Master 节点不可用的时候,系统自动切到 Slave 上。但是由于 Redis 的主从复制(Replication)是异步的,这可能导致在宕机切换过程中丧失锁的安全性。**我们看下典型场景。** 1. 客户端 A 从 Master 获取了锁; 2. Master 宕机了,存储锁的 Key 还没有来得及同步到 Slave 上; 3. Slave 升级为 Master; 4. 客户端 B 从新的 Master 获取到了对应同一个资源的锁; -5. 客户端 A 和客户端 B 同时持有了同一个资源的锁,锁的安全性被打破。 **解决方案有两个。** **方案1** ,设想下,如果要避免上述情况,可以采用一个比较“土”的方法,即自认为持有锁的客户端在对敏感公共资源进行写操作前,先进行校验,确认自己是否确实持有锁,校验的方式前面已经介绍过——通过比较自己的 `my_random_value` 和 Redis 服务端中实际存储的 `my_random_value`。 +5. 客户端 A 和客户端 B 同时持有了同一个资源的锁,锁的安全性被打破。**解决方案有两个。** **方案1**,设想下,如果要避免上述情况,可以采用一个比较“土”的方法,即自认为持有锁的客户端在对敏感公共资源进行写操作前,先进行校验,确认自己是否确实持有锁,校验的方式前面已经介绍过——通过比较自己的 `my_random_value` 和 Redis 服务端中实际存储的 `my_random_value`。 -显然,这里仍存在一个问题。如果校验完毕后,Master 数据尚未同步到 Slave 的情况下 Master 宕机,该如何是好?诚然,我们可以为 Redis 服务端设置较短的主从复置周期,以尽量避免上述情况出现,但是,隐患还是客观存在的。 **方案2** ,针对该问题场景,Redis 的作者 Antirez 提出了 RedLock,其原理基于分布式一致性算法的核心理念:多数派思想。下面对 RedLock 做简要介绍。 +显然,这里仍存在一个问题。如果校验完毕后,Master 数据尚未同步到 Slave 的情况下 Master 宕机,该如何是好?诚然,我们可以为 Redis 服务端设置较短的主从复置周期,以尽量避免上述情况出现,但是,隐患还是客观存在的。**方案2**,针对该问题场景,Redis 的作者 Antirez 提出了 RedLock,其原理基于分布式一致性算法的核心理念:多数派思想。下面对 RedLock 做简要介绍。 #### 2.5 RedLock 简要介绍 @@ -227,7 +227,7 @@ jedis.close(); **我们再来了解下解锁步骤。** 上面描述的只是获取锁的过程,而释放锁的过程比较简单,即客户端向所有 Redis 节点发起释放锁的操作,不管这些节点在获取锁的时候成功与否。 -**该方法在理论上的可靠性如何呢?** N 个 Redis 节点中的大多数能正常工作,就能保证 Redlock 正常工作,因此理论上它的可用性更高。2.4 节中所描述的问题在 Redlock 中就不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。 **它有哪些潜在问题呢,我们来看下面这个例子。** +**该方法在理论上的可靠性如何呢?** N 个 Redis 节点中的大多数能正常工作,就能保证 Redlock 正常工作,因此理论上它的可用性更高。2.4 节中所描述的问题在 Redlock 中就不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。**它有哪些潜在问题呢,我们来看下面这个例子。** 从加锁的过程,读者应该可以看出:RedLock 对系统时间是强依赖的,那么,一旦节点系统时间出现异常(Redis 节点不在同一台服务器上),问题便又来了,如下场景,假设一共有 5 个 Redis 节点:A、B、C、D、E。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25409\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25409\350\256\262.md" index f114c9e41..ba59fdace 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25409\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25409\350\256\262.md" @@ -34,7 +34,7 @@ Raft 算法在斯坦福 Diego Ongaro 和 John Ousterhout 于 2013 年发表的 ### 2. Raft 算法之 Leader Election 原理 -根据 Raft 协议,一个应用 Raft 协议的集群在刚启动时,所有节点的状态都是 Follower。由于没有 Leader,Followers 无法与 Leader 保持心跳(Heart Beat),因此,Followers 会认为 Leader 已经下线,进而转为 Candidate 状态。然后,Candidate 将向集群中其它节点请求投票,同意自己升级为 Leader。如果 Candidate 收到超过半数节点的投票(N/2 + 1),它将获胜成为 Leader。 **第一阶段:所有节点都是 Follower。** 上面提到,一个应用 Raft 协议的集群在刚启动(或 Leader 宕机)时,所有节点的状态都是 Follower,初始 Term(任期)为 0。同时启动选举定时器,每个节点的选举定时器超时时间都在 100~500 毫秒之间且并不一致(避免同时发起选举)。 +根据 Raft 协议,一个应用 Raft 协议的集群在刚启动时,所有节点的状态都是 Follower。由于没有 Leader,Followers 无法与 Leader 保持心跳(Heart Beat),因此,Followers 会认为 Leader 已经下线,进而转为 Candidate 状态。然后,Candidate 将向集群中其它节点请求投票,同意自己升级为 Leader。如果 Candidate 收到超过半数节点的投票(N/2 + 1),它将获胜成为 Leader。**第一阶段:所有节点都是 Follower。** 上面提到,一个应用 Raft 协议的集群在刚启动(或 Leader 宕机)时,所有节点的状态都是 Follower,初始 Term(任期)为 0。同时启动选举定时器,每个节点的选举定时器超时时间都在 100~500 毫秒之间且并不一致(避免同时发起选举)。 ![enter image description here](assets/0e6c7fa0-b831-11e8-9da0-3bed0a166513) **第二阶段:Follower 转为 Candidate 并发起投票。** 没有 Leader,Followers 无法与 Leader 保持心跳(Heart Beat),节点启动后在一个选举定时器周期内未收到心跳和投票请求,则状态转为候选者 Candidate 状态,且 Term 自增,并向集群中所有节点发送投票请求并且重置选举定时器。 @@ -55,15 +55,15 @@ Raft 算法在斯坦福 Diego Ongaro 和 John Ousterhout 于 2013 年发表的 在一个 Raft 集群中,只有 Leader 节点能够处理客户端的请求(如果客户端的请求发到了 Follower,Follower 将会把请求重定向到 Leader),客户端的每一个请求都包含一条被复制状态机执行的指令。Leader 把这条指令作为一条新的日志条目(Entry)附加到日志中去,然后并行得将附加条目发送给 Followers,让它们复制这条日志条目。 -当这条日志条目被 Followers 安全复制,Leader 会将这条日志条目应用到它的状态机中,然后把执行的结果返回给客户端。如果 Follower 崩溃或者运行缓慢,再或者网络丢包,Leader 会不断得重复尝试附加日志条目(尽管已经回复了客户端)直到所有的 Follower 都最终存储了所有的日志条目,确保强一致性。 **第一阶段:客户端请求提交到 Leader。** 如下图所示,Leader 收到客户端的请求,比如存储数据 5。Leader 在收到请求后,会将它作为日志条目(Entry)写入本地日志中。需要注意的是,此时该 Entry 的状态是未提交(Uncommitted),Leader 并不会更新本地数据,因此它是不可读的。 +当这条日志条目被 Followers 安全复制,Leader 会将这条日志条目应用到它的状态机中,然后把执行的结果返回给客户端。如果 Follower 崩溃或者运行缓慢,再或者网络丢包,Leader 会不断得重复尝试附加日志条目(尽管已经回复了客户端)直到所有的 Follower 都最终存储了所有的日志条目,确保强一致性。**第一阶段:客户端请求提交到 Leader。** 如下图所示,Leader 收到客户端的请求,比如存储数据 5。Leader 在收到请求后,会将它作为日志条目(Entry)写入本地日志中。需要注意的是,此时该 Entry 的状态是未提交(Uncommitted),Leader 并不会更新本地数据,因此它是不可读的。 ![enter image description here](assets/46017c00-b835-11e8-9469-d363e3097731) **第二阶段:Leader 将 Entry 发送到其它 Follower** Leader 与 Floolwers 之间保持着心跳联系,随心跳 Leader 将追加的 Entry(AppendEntries)并行地发送给其它的 Follower,并让它们复制这条日志条目,这一过程称为复制(Replicate)。 有几点需要注意: **1.** 为什么 Leader 向 Follower 发送的 Entry 是 AppendEntries 呢? -因为 Leader 与 Follower 的心跳是周期性的,而一个周期间 Leader 可能接收到多条客户端的请求,因此,随心跳向 Followers 发送的大概率是多个 Entry,即 AppendEntries。当然,在本例中,我们假设只有一条请求,自然也就是一个Entry了。 **2.** Leader 向 Followers 发送的不仅仅是追加的 Entry(AppendEntries)。 +因为 Leader 与 Follower 的心跳是周期性的,而一个周期间 Leader 可能接收到多条客户端的请求,因此,随心跳向 Followers 发送的大概率是多个 Entry,即 AppendEntries。当然,在本例中,我们假设只有一条请求,自然也就是一个Entry了。**2.** Leader 向 Followers 发送的不仅仅是追加的 Entry(AppendEntries)。 -在发送追加日志条目的时候,Leader 会把新的日志条目紧接着之前条目的索引位置(prevLogIndex), Leader 任期号(Term)也包含在其中。如果 Follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么它就会拒绝接收新的日志条目,因为出现这种情况说明 Follower 和 Leader 不一致。 **3.** 如何解决 Leader 与 Follower 不一致的问题? +在发送追加日志条目的时候,Leader 会把新的日志条目紧接着之前条目的索引位置(prevLogIndex), Leader 任期号(Term)也包含在其中。如果 Follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么它就会拒绝接收新的日志条目,因为出现这种情况说明 Follower 和 Leader 不一致。**3.** 如何解决 Leader 与 Follower 不一致的问题? 在正常情况下,Leader 和 Follower 的日志保持一致,所以追加日志的一致性检查从来不会失败。然而,Leader 和 Follower 一系列崩溃的情况会使它们的日志处于不一致状态。Follower可能会丢失一些在新的 Leader 中有的日志条目,它也可能拥有一些 Leader 没有的日志条目,或者两者都发生。丢失或者多出日志条目可能会持续多个任期。 @@ -110,7 +110,7 @@ Raft 通过比较两份日志中最后一条日志条目的索引值和任期号 ### 5. Etcd 介绍 -Etcd 是一个高可用、强一致的分布式键值(Key-Value)数据库,主要用途是共享配置和服务发现。其内部采用 Raft 算法作为分布式一致性协议,因此,Etcd 集群作为一个分布式系统“天然” 具有强一致性;而副本机制(一个 Leader,多个 Follower)又保证了其高可用性(点击进入 [Etcd 官网](https://coreos.com/etcd/))。 **Etcd 命名的由来** +Etcd 是一个高可用、强一致的分布式键值(Key-Value)数据库,主要用途是共享配置和服务发现。其内部采用 Raft 算法作为分布式一致性协议,因此,Etcd 集群作为一个分布式系统“天然” 具有强一致性;而副本机制(一个 Leader,多个 Follower)又保证了其高可用性(点击进入 [Etcd 官网](https://coreos.com/etcd/))。**Etcd 命名的由来** 在 Unix 系统中,`/etc` 目录用于存放系统管理和配置文件。分布式系统(Distributed System)第一个字母是“d”。两者看上去并没有直接联系,但它们加在一起就有点意思了:分布式的关键数据(系统管理和配置文件)存储系统,这便是 Etcd 命名的灵感之源。 @@ -158,7 +158,7 @@ Etcd 的架构图如下,从架构图中可以看出,Etcd 主要分为四个 #### 5.4 Etcd 主要应用场景 -从 5.3 节的介绍可以看出,Etcd 的功能非常强大,其功能点或功能组合可以实现众多的需求,以下列举一些典型应用场景。 **应用场景 1:服务发现** +从 5.3 节的介绍可以看出,Etcd 的功能非常强大,其功能点或功能组合可以实现众多的需求,以下列举一些典型应用场景。**应用场景 1:服务发现** 服务发现(Service Discovery)要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。服务发现的实现原理如下。 @@ -168,7 +168,7 @@ Etcd 的架构图如下,从架构图中可以看出,Etcd 主要分为四个 **应用场景 2: 消息发布和订阅** 在分布式系统中,组件间通信常用的方式是消息发布-订阅机制。具体而言,即配置一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅它们关心的主题,一旦有关主题有消息发布,就会实时通知订阅者。通过这种方式可以实现分布式系统配置的集中式管理和实时动态更新。显然,通过 Watch 机制可以实现。 -应用在启动时,主动从 Etcd 获取一次配置信息,同时,在 Etcd 节点上注册一个 Watcher 并等待,以后每次配置有更新,Etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。 **应用场景 3: 分布式锁** 前面已经提及,Etcd 支持 Revision 机制,那么对于同一个 Lock,即便有多个客户端争夺(本质上就是 `put(lockName, value)` 操作),Revision 机制可以保证它们的 Revision 编号有序且唯一,那么,客户端只要根据 Revision 的大小顺序就可以确定获得锁的先后顺序,从而很容易实现“公平锁”。 **应用场景4: 集群监控与 Leader 竞选** +应用在启动时,主动从 Etcd 获取一次配置信息,同时,在 Etcd 节点上注册一个 Watcher 并等待,以后每次配置有更新,Etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。**应用场景 3: 分布式锁** 前面已经提及,Etcd 支持 Revision 机制,那么对于同一个 Lock,即便有多个客户端争夺(本质上就是 `put(lockName, value)` 操作),Revision 机制可以保证它们的 Revision 编号有序且唯一,那么,客户端只要根据 Revision 的大小顺序就可以确定获得锁的先后顺序,从而很容易实现“公平锁”。**应用场景4: 集群监控与 Leader 竞选** - **集群监控** :通过 Etcd 的 Watch 机制,当某个 Key 消失或变动时,Watcher 会第一时间发现并告知用户。节点可以为 Key 设置租约(TTL),比如每隔 30 s 向 Etcd 发送一次心跳续约,使代表该节点的 Key 保持存活,一旦节点故障,续约停止,对应的 Key 将失效删除。如此,通过 Watch 机制就可以第一时间检测到各节点的健康状态,以完成集群的监控要求。 - **Leader 竞选** :使用分布式锁,可以很好地实现 Leader 竞选(抢锁成功的成为 Leader)。Leader 应用的经典场景是在搜索系统中建立全量索引。如果每个机器分别进行索引建立,不仅耗时,而且不能保证索引的一致性。通过在 Etcd 实现的锁机制竞选 Leader,由 Leader 进行索引计算,再将计算结果分发到其它节点。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25410\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25410\350\256\262.md" index 66109c4f2..76db4f096 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25410\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25410\350\256\262.md" @@ -185,7 +185,7 @@ maintClient.alarmDisarm(alarmList.get(0)); #### 3.1 基于 Etcd 的分布式锁业务流程 -下面描述了使用 Etcd 实现分布式锁的业务流程,假设对某个共享资源设置的锁名为:`/lock/mylock`。 **步骤1:准备** 客户端连接 Etcd,以 `/lock/mylock` 为前缀创建全局唯一的 Key,假设第一个客户端对应的 `Key="/lock/mylock/UUID1"`,第二个为 `Key="/lock/mylock/UUID2"`;客户端分别为自己的 Key 创建租约 Lease,租约的长度根据业务耗时确定,假设为 15s。 **步骤2:创建定时任务作为租约的“心跳”** 在一个客户端持有锁期间,其它客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务作为“心跳”进行续约。此外,如果持有锁期间客户端崩溃,心跳停止,Key 将因租约到期而被删除,从而锁释放,避免死锁。 **步骤3:客户端将自己全局唯一的 Key 写入 Etcd** 进行 Put 操作,将步骤 1 中创建的 Key 绑定租约写入 Etcd,根据 Etcd 的 Revision 机制,假设两个客户端 Put 操作返回的 Revision 分别为1、2,客户端需记录 Revision 用以接下来判断自己是否获得锁。 **步骤4:客户端判断是否获得锁** 客户端以前缀 `/lock/mylock` 读取 Key-Value 列表(Key-Value 中带有 Key 对应的 Revision),判断自己 Key 的 Revision 是否为当前列表中最小的,如果是则认为获得锁;否则监听列表中前一个 Revision 比自己小的 Key 的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,则自己获得锁。 **步骤5:执行业务** 获得锁后,操作共享资源,执行业务代码。 **步骤6:释放锁** +下面描述了使用 Etcd 实现分布式锁的业务流程,假设对某个共享资源设置的锁名为:`/lock/mylock`。**步骤1:准备** 客户端连接 Etcd,以 `/lock/mylock` 为前缀创建全局唯一的 Key,假设第一个客户端对应的 `Key="/lock/mylock/UUID1"`,第二个为 `Key="/lock/mylock/UUID2"`;客户端分别为自己的 Key 创建租约 Lease,租约的长度根据业务耗时确定,假设为 15s。**步骤2:创建定时任务作为租约的“心跳”** 在一个客户端持有锁期间,其它客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务作为“心跳”进行续约。此外,如果持有锁期间客户端崩溃,心跳停止,Key 将因租约到期而被删除,从而锁释放,避免死锁。**步骤3:客户端将自己全局唯一的 Key 写入 Etcd** 进行 Put 操作,将步骤 1 中创建的 Key 绑定租约写入 Etcd,根据 Etcd 的 Revision 机制,假设两个客户端 Put 操作返回的 Revision 分别为1、2,客户端需记录 Revision 用以接下来判断自己是否获得锁。**步骤4:客户端判断是否获得锁** 客户端以前缀 `/lock/mylock` 读取 Key-Value 列表(Key-Value 中带有 Key 对应的 Revision),判断自己 Key 的 Revision 是否为当前列表中最小的,如果是则认为获得锁;否则监听列表中前一个 Revision 比自己小的 Key 的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,则自己获得锁。**步骤5:执行业务** 获得锁后,操作共享资源,执行业务代码。**步骤6:释放锁** 完成业务流程后,删除对应的 Key 释放锁。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25411\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25411\350\256\262.md" index 161d689db..3a1236cf1 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25411\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25411\350\256\262.md" @@ -66,7 +66,7 @@ kafka 包含四种核心 API。 如上图所示,一个典型的 Kafka 体系架构包括若干 Producer(消息生产者),若干 Broker(Kafka 支持水平扩展,一般 Broker 数量越多,集群吞吐率越高),若干 Consumer(Group),以及一个 Zookeeper 集群。Kafka 通过 Zookeeper 管理集群配置,选举 Leader,以及在 Consumer Group 发生变化时进行 Rebalance。Producer 使用 Push(推)模式将消息发布到 Broker,Consumer 使用 Pull(拉)模式从 Broker 订阅并消费消息。 -**各个名词的解释请见下表:** ![enter image description here](assets/67e36670-c5ec-11e8-94bc-4f9499501dff) +**各个名词的解释请见下表:**![enter image description here](assets/67e36670-c5ec-11e8-94bc-4f9499501dff) #### 1.5 Kafka 高可用方案 @@ -83,7 +83,7 @@ Kafka 高可用性的保障来源于其健壮的副本(Replication)策略。 1. 支持批量操作; 1. 消费者采用 Pull 方式获取消息,消息有序,通过控制能够保证所有消息被消费且仅被消费一次; 1. 有优秀的第三方 Kafka Web 管理界面 Kafka-Manager; -1. 在日志领域比较成熟,被多家公司和多个开源项目使用。 **缺点主要有:** 1. Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,Load 越高,发送消息响应时间越长; +1. 在日志领域比较成熟,被多家公司和多个开源项目使用。**缺点主要有:** 1. Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,Load 越高,发送消息响应时间越长; 1. 使用短轮询方式,实时性取决于轮询间隔时间; 1. 消费失败不支持重试; 1. 支持消息顺序,但是一台代理宕机后,就会产生消息乱序; @@ -123,7 +123,7 @@ ActiveMQ 的特点,官网在 Features 一栏中做了非常详细的说明, #### 2.3 ActiveMQ 部署环境 -相较于 Kafka,ActiveMQ 的部署简单很多,支持多个版本的 Windows 和 Unix 系统,此外,ActiveMQ 由 Java 语言开发而成,因此需要 JRE 支持。 **硬件要求** +相较于 Kafka,ActiveMQ 的部署简单很多,支持多个版本的 Windows 和 Unix 系统,此外,ActiveMQ 由 Java 语言开发而成,因此需要 JRE 支持。**硬件要求** - 如果以二进制文件安装,ActiveMQ 5.x 需要 60M 空间。当然,需要额外的磁盘空间来持久化消息; - 如果下载 ActiveMQ 5.x 源文件,自行编译构建, 则需要 300M 空间。 @@ -145,7 +145,7 @@ ActiveMQ 的主体架构如下图所示。 ![enter image description here](assets/18fba290-c96e-11e8-97d9-49b68de7724a) -**传输协议:** 消息之间的传递,无疑需要协议进行沟通,启动一个 ActiveMQ 便打开一个监听端口。ActiveMQ 提供了广泛的连接模式,主要包括 SSL、STOMP、XMPP。ActiveMQ 默认的使用协议为 OpenWire,端口号为 61616。 **通信方式:** ActiveMQ 有两种通信方式,Point-to-Point Model(点对点模式),Publish/Subscribe Model (发布/订阅模式),其中在 Publich/Subscribe 模式下又有持久化订阅和非持久化订阅两种消息处理方式。 **消息存储:** 在实际应用中,重要的消息通常需要持久化到数据库或文件系统中,确保服务器崩溃时,信息不会丢失。 **Cluster(集群):** 最常见到集群方式包括 Network of Brokers 和 Master Slave。 **Monitor(监控):** ActiveMQ 一般由 JMX 进行监控。 +**传输协议:** 消息之间的传递,无疑需要协议进行沟通,启动一个 ActiveMQ 便打开一个监听端口。ActiveMQ 提供了广泛的连接模式,主要包括 SSL、STOMP、XMPP。ActiveMQ 默认的使用协议为 OpenWire,端口号为 61616。**通信方式:** ActiveMQ 有两种通信方式,Point-to-Point Model(点对点模式),Publish/Subscribe Model (发布/订阅模式),其中在 Publich/Subscribe 模式下又有持久化订阅和非持久化订阅两种消息处理方式。**消息存储:** 在实际应用中,重要的消息通常需要持久化到数据库或文件系统中,确保服务器崩溃时,信息不会丢失。**Cluster(集群):** 最常见到集群方式包括 Network of Brokers 和 Master Slave。**Monitor(监控):** ActiveMQ 一般由 JMX 进行监控。 默认配置下的 ActiveMQ 只适合学习而不适用于实际生产环境,ActiveMQ 的性能需要通过配置挖掘,其性能提高包括代码级性能、规则性能、存储性能、网络性能以及多节点协同方法(集群方案),所以我们优化 ActiveMQ 的中心思路也是这样的: @@ -165,7 +165,7 @@ ActiveMQ 的主体架构如下图所示。 关于几种 HA 方案的详细介绍,读者可查看[官网说明](http://activemq.apache.org/masterslave.html),在此,我仅做简单介绍。 -**方案一:Shared Nothing Master/Slave** 这是一种最简单最典型的 Master-Slave 模式,Master 与 Slave 有各自的存储系统,不共享任何数据。“Shared Nothing” 模式有很多局限性,存在丢失消息、“双主”等问题。目前,在要求严格的生产环境中几乎没有应用,是一种趋于淘汰的方案,因此,本文就不作介绍了。 **方案二:Shared Storage Master/Slave** 这是很常用的一种架构。“共享存储”意味着 Master 与 Slave 之间的数据是共享的。为了实现数据共享,有两种方式: +**方案一:Shared Nothing Master/Slave** 这是一种最简单最典型的 Master-Slave 模式,Master 与 Slave 有各自的存储系统,不共享任何数据。“Shared Nothing” 模式有很多局限性,存在丢失消息、“双主”等问题。目前,在要求严格的生产环境中几乎没有应用,是一种趋于淘汰的方案,因此,本文就不作介绍了。**方案二:Shared Storage Master/Slave** 这是很常用的一种架构。“共享存储”意味着 Master 与 Slave 之间的数据是共享的。为了实现数据共享,有两种方式: 1. Shared Database Master/Slave 1. Shared File system Master/Slave @@ -180,7 +180,7 @@ ActiveMQ 的主体架构如下图所示。 显而易见,数据存储引擎为 Database,ActiveMQ 通过 JDBC 方式与 Database 交互,排他锁使用 Database 的表级排他锁。JDBC Store 相对于日志文件而言,通常被认为是低效的,尽管数据的可见性较好,但是 Database 的扩容能力非常弱,无法良好地适应高并发、大数据情况(严格来说,单组 M-S 架构是无法支持大数据的),况且 ActiveMQ 的消息通常存储时间较短,频繁地写入,频繁地删除,都是性能的影响点。我们通常在研究 ActiveMQ 存储原理时使用 JDBC Store,或者在对数据一致性(可靠性、可见性)要求较高的中小型应用环境中使用,比如订单系统中交易流程支撑系统等。但由于 JDBC 架构实施简便,易于管理,我们仍然倾向于首选这种方式。 -在使用 JDBC Store 之前,必须有一个稳定的 Database,且为 AcitveMQ 中的链接用户授权“创建表”和普通 CRUD 的权限。Master 与 Slave 中的配置文件基本一样,开发者需要注意 brokerName 和 brokerId 全局不可重复。此外还需要把相应的 jdbc-connector 的 Jar 包复制到 `{acitvemq}/lib/optional` 目录下。 **方案三: Replicated LevelDB Store** 基于复制的 LevelDB Store,是 ActiveMQ 最新的 HA 方案,在 5.9+ 版本中获得支持。相较于方案二中的两种“Shared Storage”模式,本方案在存储和通讯机制上,更符合“Master-Slave”模型。 +在使用 JDBC Store 之前,必须有一个稳定的 Database,且为 AcitveMQ 中的链接用户授权“创建表”和普通 CRUD 的权限。Master 与 Slave 中的配置文件基本一样,开发者需要注意 brokerName 和 brokerId 全局不可重复。此外还需要把相应的 jdbc-connector 的 Jar 包复制到 `{acitvemq}/lib/optional` 目录下。**方案三: Replicated LevelDB Store** 基于复制的 LevelDB Store,是 ActiveMQ 最新的 HA 方案,在 5.9+ 版本中获得支持。相较于方案二中的两种“Shared Storage”模式,本方案在存储和通讯机制上,更符合“Master-Slave”模型。 “Replicated LevelDB”同样允许有多个 Slaves,而且 Slaves 的个数有了约束性的限制,这归结于其使用 ZooKeeper 选举 Master。要进行选举,则需要多数派的“参与者”。因为 Replicated LevelDB Store 中有多个 Broker,从多个 Broker 中选举出一个成为 Master,其他的则成为 Slave。只有 Master 接收 Client 的连接,Slave 负责连接到 Master,并接收(同步方式、异步方式)Master 上的数据。每个 Broker 实例将消息数据保存本地(类似于“Shared Nothing”),它们之间并不共享任何数据,因此,某种意义上把“Replicated LevelDB”归类为“Shared Storage”并不妥当。 @@ -214,7 +214,7 @@ Producers 和 Consumers 可以与任何 Group 中的 Master 建立连接并进 1. 拥有完善的监控体系,包括 Web Console、JMX、Shell 命令行,以及 Jolokia 的 REST API; -1. 界面友善:提供的 Web Console 可以满足大部分需求,此外,还有很多第三方组件可以使用,如 Hawtio。 **其缺点主要有以下几点:** +1. 界面友善:提供的 Web Console 可以满足大部分需求,此外,还有很多第三方组件可以使用,如 Hawtio。**其缺点主要有以下几点:** 1. 社区活跃度较低,更新慢,增加维护成本; @@ -291,11 +291,11 @@ RabbitMQ 支持多个版本的 Windows 和 Unix 系统,此外,ActiveMQ 由 E 有三种类型的 Exchange,即 Direct、Fanout、Topic,每个实现了不同的路由算法(Routing Algorithm)。 -**Direct Exchange:** 完全根据 Key 投递。如果 Routing Key 匹配,Message 就会被传递到相应的 Queue 中。其实在 Queue 创建时,它会自动地以 Queue 的名字作为 Routing Key 来绑定 Exchange。例如,绑定时设置了 Routing Key 为“abc”,那么客户端提交的消息,只有设置了 Key为“abc”的才会投递到队列中。 **Fanout Exchange:** 该类型 Exchange 不需要 Key。它采取广播模式,一个消息进来时,便投递到与该交换机绑定的所有队列中。 **Topic Exchange:** 对 Key 进行模式匹配后再投递。比如符号“#”匹配一个或多个词,符号“.”正好匹配一个词。例如“abc.#”匹配“abc.def.ghi”,“abc.”只匹配“abc.def”。 +**Direct Exchange:** 完全根据 Key 投递。如果 Routing Key 匹配,Message 就会被传递到相应的 Queue 中。其实在 Queue 创建时,它会自动地以 Queue 的名字作为 Routing Key 来绑定 Exchange。例如,绑定时设置了 Routing Key 为“abc”,那么客户端提交的消息,只有设置了 Key为“abc”的才会投递到队列中。**Fanout Exchange:** 该类型 Exchange 不需要 Key。它采取广播模式,一个消息进来时,便投递到与该交换机绑定的所有队列中。**Topic Exchange:** 对 Key 进行模式匹配后再投递。比如符号“#”匹配一个或多个词,符号“.”正好匹配一个词。例如“abc.#”匹配“abc.def.ghi”,“abc.”只匹配“abc.def”。 #### 3.5 RabbitMQ 高可用方案 -就分布式系统而言,实现高可用(High Availability,HA)的策略基本一致,即副本思想,当主节点宕机之后,作为副本的备节点迅速“顶上去”继续提供服务。此外,单机的吞吐量是极为有限的,为了提升性能,通常都采用“人海战术”,也就是所谓的集群模式。 **RabbitMQ 集群配置方式主要包括以下几种。** +就分布式系统而言,实现高可用(High Availability,HA)的策略基本一致,即副本思想,当主节点宕机之后,作为副本的备节点迅速“顶上去”继续提供服务。此外,单机的吞吐量是极为有限的,为了提升性能,通常都采用“人海战术”,也就是所谓的集群模式。**RabbitMQ 集群配置方式主要包括以下几种。** - Cluster:不支持跨网段,用于同一个网段内的局域网;可以随意得动态增加或者减少;节点之间需要运行相同版本的 RabbitMQ 和 Erlang。 - Federation:应用于广域网,允许单台服务器上的交换机或队列接收发布到另一台服务器上的交换机或队列的消息,可以是单独机器或集群。Federation 队列类似于单向点对点连接,消息会在联盟队列之间转发任意次,直到被消费者接受。通常使用 Federation 来连接 Internet 上的中间服务器,用作订阅分发消息或工作队列。 @@ -312,7 +312,7 @@ RabbitMQ 支持多个版本的 Windows 和 Unix 系统,此外,ActiveMQ 由 E **Erlang Cookie** Erlang Cookie 是保证不同节点可以相互通信的密钥,要保证集群中的不同节点相互通信必须共享相同的 Erlang Cookie。具体的目录存放在 `/var/lib/rabbitmq/.erlang.cookie`。 -它的起源要从 rabbitmqctl 命令的工作原理说起。RabbitMQ 底层基于 Erlang 架构实现,所以 rabbitmqctl 会启动 Erlang 节点,并基于 Erlang 节点使用 Erlang 系统连接 RabbitMQ 节点,在连接过程中需要正确的 Erlang Cookie 和节点名称,Erlang 节点通过交换 Erlang Cookie 以获得认证。 **镜像队列** +它的起源要从 rabbitmqctl 命令的工作原理说起。RabbitMQ 底层基于 Erlang 架构实现,所以 rabbitmqctl 会启动 Erlang 节点,并基于 Erlang 节点使用 Erlang 系统连接 RabbitMQ 节点,在连接过程中需要正确的 Erlang Cookie 和节点名称,Erlang 节点通过交换 Erlang Cookie 以获得认证。**镜像队列** RabbitMQ 的 Cluster 集群模式一般分为两种,普通模式和镜像模式。 @@ -326,7 +326,7 @@ RabbitMQ 的 Cluster 集群模式一般分为两种,普通模式和镜像模 3\. 有消息确认机制和持久化机制,可靠性高; 4\. 高度可定制的路由; 5\. 管理界面较丰富,在互联网公司也有较大规模的应用; -6\. 社区活跃度高,更新快。 **缺点主要有:** 1. 尽管结合 Erlang 语言本身的并发优势,性能较好,但是不利于做二次开发和维护; +6\. 社区活跃度高,更新快。**缺点主要有:** 1. 尽管结合 Erlang 语言本身的并发优势,性能较好,但是不利于做二次开发和维护; 2\. 实现了代理架构,意味着消息在发送到客户端之前可以在中央节点上排队。此特性使得 RabbitMQ 易于使用和部署,但使得其运行速度较慢,因为中央节点增加了延迟,消息封装后也比较大; 3\. 需要学习比较复杂的接口和协议,学习和维护成本较高。 @@ -387,21 +387,21 @@ Broker 主要负责消息的存储、投递、查询以及服务高可用保证 - HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能; - Index Service:根据特定的 Message Key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询。 -**Producer 集群** 充当消息生产者的角色,支持分布式集群方式部署。Producers 通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递。投递的过程支持快速失败并且低延迟。 **Consumer 集群** 充当消息消费者的角色,支持分布式集群方式部署。支持以 Push、pull 两种模式对消息进行消费。同时也支持集群方式和广播形式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。 +**Producer 集群** 充当消息生产者的角色,支持分布式集群方式部署。Producers 通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递。投递的过程支持快速失败并且低延迟。**Consumer 集群** 充当消息消费者的角色,支持分布式集群方式部署。支持以 Push、pull 两种模式对消息进行消费。同时也支持集群方式和广播形式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。 #### 4.5 RocketMQ 高可用实现原理 毫无悬念,RocketMQ 实现高可用(HA)的方案仍然是基于最淳朴的“副本思想”,但与 Kafka、Redis、Etcd 采用的副本机制有所不同:RocketMQ 的 Master 和 Slave 没有 Election 机制,也没有 Failover 机制。 -RocketMQ 不具备选举功能,在集群模式下,Master、Slave 角色需预先设置,是固定的;Master 与 Slave 配对是通过指定相同的 brokerName 参数来实现,Master 的 BrokerId 必须是 0,Slave 的 BrokerId 必须是大于 0 的数。一个 Master 下面可以挂载多个 Slave,同一个 Master 下的多个 Slave 通过指定不同的 BrokerId 来区分。当 Master 节点宕机后,消费者仍然可以从 Slave 消费,从而保证生产者已经 Push 的消息不丢失;由于该 Master 宕机,生产者将消息 Push 到其它 Master,不影响可用性。RocketMQ 的 Broker 有 4 种部署方式。 **1. 单个 Master 模式** 除了配置简单,没什么优点。 +RocketMQ 不具备选举功能,在集群模式下,Master、Slave 角色需预先设置,是固定的;Master 与 Slave 配对是通过指定相同的 brokerName 参数来实现,Master 的 BrokerId 必须是 0,Slave 的 BrokerId 必须是大于 0 的数。一个 Master 下面可以挂载多个 Slave,同一个 Master 下的多个 Slave 通过指定不同的 BrokerId 来区分。当 Master 节点宕机后,消费者仍然可以从 Slave 消费,从而保证生产者已经 Push 的消息不丢失;由于该 Master 宕机,生产者将消息 Push 到其它 Master,不影响可用性。RocketMQ 的 Broker 有 4 种部署方式。**1. 单个 Master 模式** 除了配置简单,没什么优点。 -它的缺点是不可靠。该机器重启或宕机,将导致整个服务不可用,因此,生产环境几乎不采用这种方案。 **2. 多个 Master 模式** 配置简单,性能最高,是它的优点。 +它的缺点是不可靠。该机器重启或宕机,将导致整个服务不可用,因此,生产环境几乎不采用这种方案。**2. 多个 Master 模式** 配置简单,性能最高,是它的优点。 它的缺点是:可能会有少量消息丢失(异步刷盘丢失少量消息,同步刷盘不丢失),单台机器重启或宕机期间,该机器下未被消费的消息在机器恢复前不可订阅,影响消息实时性。 -特别说明:当使用多 Master 无 Slave 的集群搭建方式时,Master 的 brokerRole 配置必须为 `ASYNC_MASTER`。如果配置为 `SYNC_MASTER`,则 producer 发送消息时,返回值的 SendStatus 会一直是 `SLAVE_NOT_AVAILABLE`。 **3. 多 Master 多 Slave 模式:异步复制** 其优点为:即使磁盘损坏,消息丢失得非常少,消息实时性不会受影响,因为 Master 宕机后,消费者仍然可以从 Slave 消费,此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样。 +特别说明:当使用多 Master 无 Slave 的集群搭建方式时,Master 的 brokerRole 配置必须为 `ASYNC_MASTER`。如果配置为 `SYNC_MASTER`,则 producer 发送消息时,返回值的 SendStatus 会一直是 `SLAVE_NOT_AVAILABLE`。**3. 多 Master 多 Slave 模式:异步复制** 其优点为:即使磁盘损坏,消息丢失得非常少,消息实时性不会受影响,因为 Master 宕机后,消费者仍然可以从 Slave 消费,此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样。 -它的缺点为:Master 宕机或磁盘损坏时会有少量消息丢失。 **4. 多 Master 多 Slave 模式:同步双写** 其优点为:数据与服务都无单点,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高。 +它的缺点为:Master 宕机或磁盘损坏时会有少量消息丢失。**4. 多 Master 多 Slave 模式:同步双写** 其优点为:数据与服务都无单点,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高。 其缺点为:性能比异步复制模式稍低,大约低 10% 左右,发送单个消息的 RT 会稍高,目前主宕机后,备机不能自动切换为主机,后续会支持自动切换功能。 @@ -417,7 +417,7 @@ RocketMQ 不具备选举功能,在集群模式下,Master、Slave 角色需 1. 各个环节分布式扩展设计,主从 HA; -1. 社区较活跃,版本更新较快。 **缺点主要有:** +1. 社区较活跃,版本更新较快。**缺点主要有:** 1. 支持的客户端语言不多,目前是 Java、C++ 和 Go,后两种尚不成熟; diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25412\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25412\350\256\262.md" index 29898b394..fa08f930d 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25412\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25412\350\256\262.md" @@ -69,7 +69,7 @@ tar -zxvf zookeeper-3.4.13.tar.gz -rw-rw-r--. 1 1000 1000 922 Feb 20 2014 zoo_sample.cfg ``` -需要注意: `zoo_sample.cfg` 文件是官方给我们提供的 ZooKeeper 样板文件,重新复制一份命名为 zoo.cfg。ZooKeeper 启动时将默认加载该路径下名为 zoo.cfg 的配置文件,这是官方指定的文件命名规则。 **(4)修改三台服务器的配置文件** +需要注意: `zoo_sample.cfg` 文件是官方给我们提供的 ZooKeeper 样板文件,重新复制一份命名为 zoo.cfg。ZooKeeper 启动时将默认加载该路径下名为 zoo.cfg 的配置文件,这是官方指定的文件命名规则。**(4)修改三台服务器的配置文件** 使用 `vi` 命令打开配置文件 zoo.cfg 并进行如下修改: @@ -129,7 +129,7 @@ echo "3" > /opt/zookeeper/zkdata/myid 下面是清除旧快照和日志文件的一些方法。 -**第一种:** 使用 ZooKeeper 工具类 PurgeTxnLog。它实现了一种简单的历史文件清理策略,可以在[这里](https://zookeeper.apache.org/doc/r3.4.6/zookeeperAdmin.html)了解它的使用方法。 **第二种:** 针对上面这个操作,ZooKeeper 已经写好相应的脚本,存放在 `bin/zkCleanup.sh` 中,所以直接使用该脚本也可以执行清理工作。 **第三种:** 从 3.4.0 开始,ZooKeeper 提供了自动清理 Snapshot 和事务日志的功能。配置 `autopurge.snapRetainCount` 和 `autopurge.purgeInterval` 这两个参数可实现定时清理。这两个参数均在 zoo.cfg 中进行配置: +**第一种:** 使用 ZooKeeper 工具类 PurgeTxnLog。它实现了一种简单的历史文件清理策略,可以在[这里](https://zookeeper.apache.org/doc/r3.4.6/zookeeperAdmin.html)了解它的使用方法。**第二种:** 针对上面这个操作,ZooKeeper 已经写好相应的脚本,存放在 `bin/zkCleanup.sh` 中,所以直接使用该脚本也可以执行清理工作。**第三种:** 从 3.4.0 开始,ZooKeeper 提供了自动清理 Snapshot 和事务日志的功能。配置 `autopurge.snapRetainCount` 和 `autopurge.purgeInterval` 这两个参数可实现定时清理。这两个参数均在 zoo.cfg 中进行配置: * autopurge.purgeInterval:指定了清理频率,单位是小时,需要填写一个 1 或更大的整数,默认是 0,表示不开启自动清理功能; * autopurge.snapRetainCount:该参数和上面参数搭配使用,它指定了需要保留的文件数目。默认保留 3 个。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25413\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25413\350\256\262.md" index 9613c2d42..1310c1c16 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25413\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25413\350\256\262.md" @@ -94,7 +94,7 @@ drwxr-xr-x 2 root root 4096 Oct 15 13:21 mytopic_test-3 通过以上命令,我们创建了一个 名为“mytopic_test”的 Topic,同时为它指定了 4 个 Partition,也就是上面的 4 个文件夹(由于 Partition 不是最终的存储粒度,所以是文件夹而不是文件)。Partition 的名称规则为:Topic 名称+索引号,索引的范围为:`[0,num.partitions - 1]`。 -在初步了解了 Kafka 的文件存储机制后,不知读者是否思考过如下几个问题。 **问题1:为什么不能以 Partition 作为存储单位?** 任何一个 Topic 中的 Partition 数量受限于 Kafka Broker 的数量,不可能太多。如果以 Partition 为存储粒度,随着消息源源不断写入,数量有限的 Partition 将急剧扩张,会对消息文件的维护以及对已消费消息的清理工作带来严重影响。 +在初步了解了 Kafka 的文件存储机制后,不知读者是否思考过如下几个问题。**问题1:为什么不能以 Partition 作为存储单位?** 任何一个 Topic 中的 Partition 数量受限于 Kafka Broker 的数量,不可能太多。如果以 Partition 为存储粒度,随着消息源源不断写入,数量有限的 Partition 将急剧扩张,会对消息文件的维护以及对已消费消息的清理工作带来严重影响。 Partition 的数量为什么受限于 Kafka Broker 的数量?为了保证可靠性,每个 Partition 都应有若干个副本(Replica),其中一个 Replica 为 Leader,其它都为 Follower。Leader 负责处理 Partition 的所有读写请求,Follower 则负责被动地复制 Leader 上的数据。不难理解,对于任意一个 Topic,有多少个 Partition 就有多少个 Leader,并且,这些 Leader 应尽量分散到不同的 Broker 上,否则,一旦某台 Broker(如果它部署有大量 Leader)故障下线,势必引起连锁反应:大量的 Partition 需要重选 Leader,而这期间是不可用的。 @@ -126,7 +126,7 @@ log.cleaner.enable=false #是否启用log压缩,一般不用启用,启用的 `.index` 文件作为索引文件,存储的是元数据;`.log` 文件作为数据文件,存储的是消息。如何通过索引访问具体的消息呢?事实上,索引文件中的元数据指向的是对应数据文件中消息的物理偏移地址,有了消息的物理地址,自然也就可以访问对应的消息了。 -其中以 `.index` 索引文件中的元数据 `[2, 365]` 为例,在 `.log` 数据文件表示第 2 个消息,即在全局 Partition 中表示 170410+2=170412 个消息,该消息的物理偏移地址为 365。 **问题3:如何从 Partition 中通过 Offset 查找 Message?** 基于问题 2 中的数据和图形,如何读取 offset=170425 的 Message(消息)呢?关键在于通过 Offset 定位出消息的物理偏移地址。首先,列出各个 Segment 的索引文件及其偏移量范围,如下: +其中以 `.index` 索引文件中的元数据 `[2, 365]` 为例,在 `.log` 数据文件表示第 2 个消息,即在全局 Partition 中表示 170410+2=170412 个消息,该消息的物理偏移地址为 365。**问题3:如何从 Partition 中通过 Offset 查找 Message?** 基于问题 2 中的数据和图形,如何读取 offset=170425 的 Message(消息)呢?关键在于通过 Offset 定位出消息的物理偏移地址。首先,列出各个 Segment 的索引文件及其偏移量范围,如下: ```plaintext 索引文件1:00000000000000000000.index,偏移量范围:\[0,170410\]; @@ -134,7 +134,7 @@ log.cleaner.enable=false #是否启用log压缩,一般不用启用,启用的 索引文件3:00000000000000239430.index,偏移量范围:\[239430+1,239430+1+X\],X大于1,具体值与消息量和大小有关; ``` -根据索引文件的偏移量范围,`170410< offset=170425 <239430`,因此,Offset=170425 对应的索引文件为 `00000000000000170410.index`。索引文件是“有序的”,通过二分查找(又称折半查找)便可快速定位具体文件位置。此后,根据 `00000000000000170410.index` 文件中的 `[15,2369]` 定位到数据文件 `00000000000000170410.log` 中的 2369 位置,这便是目标消息的位置,读取即可。 **问题4:读取一条消息时,如何确定何时读完本条消息呢?** 通过对问题 3 的解答,相信读者已经理解了如何根据 Offset 定位一条消息的物理偏移地址。但这个物理偏移地址实际上是一个起始地址,如何确定本条消息的结尾(终止地址)呢? +根据索引文件的偏移量范围,`170410< offset=170425 <239430`,因此,Offset=170425 对应的索引文件为 `00000000000000170410.index`。索引文件是“有序的”,通过二分查找(又称折半查找)便可快速定位具体文件位置。此后,根据 `00000000000000170410.index` 文件中的 `[15,2369]` 定位到数据文件 `00000000000000170410.log` 中的 2369 位置,这便是目标消息的位置,读取即可。**问题4:读取一条消息时,如何确定何时读完本条消息呢?** 通过对问题 3 的解答,相信读者已经理解了如何根据 Offset 定位一条消息的物理偏移地址。但这个物理偏移地址实际上是一个起始地址,如何确定本条消息的结尾(终止地址)呢? 这个问题可通过消息在磁盘上的物理存储结构来解决(其中包含偏移量、消息体长度等可度量消息终止地址的数据),消息的存储结构如下(取自[官网](https://kafka.apache.org/documentation/#messages)): @@ -203,7 +203,7 @@ Kafka 机制中,Leader 将负责维护和跟踪一个 ISR(In-Sync Replicas #### 2.3 同步副本 ISR -上节中讲到了同步副本队列 ISR(In-Sync Replicas),即 Leader 副本 + 所有与 Leader 副本保持同步的 Follower 副本。虽然副本可以提高可用性,但副本数量对 Kafka 的吞吐率有一定影响。默认情况下 Kafka 的 Replica 数量为 2(部分 Kafka 版本默认值为 1),即每个 Partition 都有一个 Leader,一个 Follower。所有的副本(Replica)统称为 Assigned Replicas(AR)。显然,ISR 是 AR 中的一个子集,由 Leader 维护 ISR 列表,Follower 从 Leader 那里同步数据会有一些延迟(由参数 `replica.lag.time.max.ms` 设置超时阈值),超过阈值的 Follower 将被剔除出 ISR, 存入 OSR(Outof-Sync Replicas)列表,新加入的 Follower 也会先存放在 OSR 中,即有关系式 AR=ISR+OSR。 **LEO & HW** +上节中讲到了同步副本队列 ISR(In-Sync Replicas),即 Leader 副本 + 所有与 Leader 副本保持同步的 Follower 副本。虽然副本可以提高可用性,但副本数量对 Kafka 的吞吐率有一定影响。默认情况下 Kafka 的 Replica 数量为 2(部分 Kafka 版本默认值为 1),即每个 Partition 都有一个 Leader,一个 Follower。所有的副本(Replica)统称为 Assigned Replicas(AR)。显然,ISR 是 AR 中的一个子集,由 Leader 维护 ISR 列表,Follower 从 Leader 那里同步数据会有一些延迟(由参数 `replica.lag.time.max.ms` 设置超时阈值),超过阈值的 Follower 将被剔除出 ISR, 存入 OSR(Outof-Sync Replicas)列表,新加入的 Follower 也会先存放在 OSR 中,即有关系式 AR=ISR+OSR。**LEO & HW** 前面提到 Kafka 中,Topic 的每个 Partition 可能有多个副本(Replica)用于实现冗余,从而实现高可用。每个副本又有两个重要的属性 LEO 和 HW。 @@ -230,7 +230,7 @@ LEO、HW 以及 Offset 的关系图如下: #### 2.4 Kafka 消息生产的可靠性 -就 Kafka 而言,可靠性贯穿消息的生产、发送、存储及消费等过程,本节将介绍消息生产过程中的可靠性,即当 Producer 向 Leader 发送消息时的可靠性,在客户端,可以通过 `request.required.acks` 参数来设置数据可靠性的级别。 **(1)request.required.acks = 1** 这是默认情况,即消息的强制备份数量为 1,Producer 发送数据到 Leader,只要 Leader 成功写入本地日志,即成功返回客户端,不要求 ISR 中的其它副本与 Leader 保持同步。对于 Producer 发来的消息,如果 Leader 刚写入并成功返回后便宕机,此次发送的消息就会丢失。 **(2)request.required.acks = 0** 即消息的强制备份数量为 0,Producer 不停向 Leader 发送数据,而不需要 Leader 反馈成功消息,这种情况下数据传输效率最高,与此同时,可靠性也是最低的。可能在发送过程中丢失数据,可能在 Leader 宕机时丢失数据。 **(3)request.required.acks = -1(all)** 即消息的强制备份数量为 ISR 列表中副本的数量,Producer 发送数据给 Leader,Leader 收到数据后要等到 ISR 列表中的所有副本都完成数据同步后(强一致性),才向生产者返回成功消息。如果一直收不到成功消息,则认为发送数据失败会自动重发数据。这是可靠性最高的方案,当然,性能也会受到一定影响。 **单纯设置 request.required.acks 保障不了消息生产的可靠性** 回顾之前讲得,由于 Follower 从 Leader 同步数据有一些延迟(由参数 `replica.lag.time.max.ms` 设置超时阈值),超过阈值的 Follower 将被剔除出 ISR, 存入 OSR(Outof-Sync Replicas)列表,因此,ISR 列表实际上是动态变化的。 +就 Kafka 而言,可靠性贯穿消息的生产、发送、存储及消费等过程,本节将介绍消息生产过程中的可靠性,即当 Producer 向 Leader 发送消息时的可靠性,在客户端,可以通过 `request.required.acks` 参数来设置数据可靠性的级别。**(1)request.required.acks = 1** 这是默认情况,即消息的强制备份数量为 1,Producer 发送数据到 Leader,只要 Leader 成功写入本地日志,即成功返回客户端,不要求 ISR 中的其它副本与 Leader 保持同步。对于 Producer 发来的消息,如果 Leader 刚写入并成功返回后便宕机,此次发送的消息就会丢失。**(2)request.required.acks = 0** 即消息的强制备份数量为 0,Producer 不停向 Leader 发送数据,而不需要 Leader 反馈成功消息,这种情况下数据传输效率最高,与此同时,可靠性也是最低的。可能在发送过程中丢失数据,可能在 Leader 宕机时丢失数据。**(3)request.required.acks = -1(all)** 即消息的强制备份数量为 ISR 列表中副本的数量,Producer 发送数据给 Leader,Leader 收到数据后要等到 ISR 列表中的所有副本都完成数据同步后(强一致性),才向生产者返回成功消息。如果一直收不到成功消息,则认为发送数据失败会自动重发数据。这是可靠性最高的方案,当然,性能也会受到一定影响。**单纯设置 request.required.acks 保障不了消息生产的可靠性** 回顾之前讲得,由于 Follower 从 Leader 同步数据有一些延迟(由参数 `replica.lag.time.max.ms` 设置超时阈值),超过阈值的 Follower 将被剔除出 ISR, 存入 OSR(Outof-Sync Replicas)列表,因此,ISR 列表实际上是动态变化的。 我们思考一个问题。由于 ISR 列表是动态变化的,如果 ISR 中的副本因网络延迟等原因被踢出,只剩下 Leader,即便设置参数 `request.required.acks=-1` 也无法保证可靠性。鉴于此,需要对 ISR 列表的最小副本数加以约束,即 ISR 列表中的副本数不得小于一个阈值。Kafka 提供了这样一个参数:`min.insync.replicas`,该参数用于设定 ISR 中的最小副本数,默认值为 1,当且仅当 `request.required.acks` 参数设置为 -1 时,此参数才生效。当 ISR 中的副本数少于 `min.insync.replicas` 配置的数量时,客户端会返回如下异常: @@ -242,11 +242,11 @@ org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected #### 2.5 Leader 选举机制解读 -关于 Kafka 的可靠性,前面介绍了消息生产过程中的可靠性保障机制:设置参数 `request.required.acks=-1` 时,对于任意一条消息,只有在它被对应 Partition 的 ISR 中的所有副本都复制完毕后,才会被认为已提交,并返回信息给 Producer。进一步,通过参数 `min.insync.replicas` 协同,可以提高消息生产过程中的可靠性。 **问题1:如何在保证可靠性的前提下避免吞吐量下降?** 根据[官网](https://kafka.apache.org/documentation/#replication)的解释,如果 Leader 永远不会宕机,我们根本不需要 Follower,当然,这是不可能的。考虑这样一个问题:当 Leader 宕机了,怎样从 Follower 中选举出新的 Leader?由于各种原因,Follower 可能滞后很多或者直接崩溃掉,因此,我们必须确保选择 “最新(Up-to-Date)” 的 Follower 作为新的 Leader。 +关于 Kafka 的可靠性,前面介绍了消息生产过程中的可靠性保障机制:设置参数 `request.required.acks=-1` 时,对于任意一条消息,只有在它被对应 Partition 的 ISR 中的所有副本都复制完毕后,才会被认为已提交,并返回信息给 Producer。进一步,通过参数 `min.insync.replicas` 协同,可以提高消息生产过程中的可靠性。**问题1:如何在保证可靠性的前提下避免吞吐量下降?** 根据[官网](https://kafka.apache.org/documentation/#replication)的解释,如果 Leader 永远不会宕机,我们根本不需要 Follower,当然,这是不可能的。考虑这样一个问题:当 Leader 宕机了,怎样从 Follower 中选举出新的 Leader?由于各种原因,Follower 可能滞后很多或者直接崩溃掉,因此,我们必须确保选择 “最新(Up-to-Date)” 的 Follower 作为新的 Leader。 对于日志复制算法而言,必须提供这样一个基本的保证:如果 Leader 挂掉,新晋 Leader 必须拥有原来的 Leader 已经 Commit(即响应客户端 Push 消息成功)的所有消息。显然,ISR 中的副本都具有该特征。 -但存在一个问题,ISR 列表维持多大的规模合适呢?换言之,Leader 在一条消息被 Commit 前需要等待多少个 Follower 确认呢?等待 Follower 的数量越多,与 Leader 保持同步的 Follower 就越多,可靠性就越高,但这也会造成吞吐率的下降。 **少数服从多数的选举原则** +但存在一个问题,ISR 列表维持多大的规模合适呢?换言之,Leader 在一条消息被 Commit 前需要等待多少个 Follower 确认呢?等待 Follower 的数量越多,与 Leader 保持同步的 Follower 就越多,可靠性就越高,但这也会造成吞吐率的下降。**少数服从多数的选举原则** “少数服从多数”是最常用的选举原则,它也称为“多数派原则”,第 8 课中介绍的 Raft 算法就采用了这种选举原则。不过,Kafka 并不是采用这种方式。 @@ -260,7 +260,7 @@ Leader 选举有很多实用的算法,比如 ZooKeeper 的 Zab、Raft 以及 V Kafka 通过 ZooKeeper 为每一个 Partition 动态维护了一个 ISR 列表,通过前面的学习,我们知道 ISR 里的所有 Replica 都与 Leader 保持同步,严格得讲,为了保证可靠性,只有 ISR 里的成员才能有被选为 Leader 的可能(通过参数配置:`unclean.leader.election.enable=false`)。基于该策略,如果有 f+1 个副本,一个 Kafka Topic 能在保证不丢失已经 Commit 消息的前提下容忍 f 个副本的失败,在大多数使用场景下,这种折衷策略是合理的。 -实际上,对于任意一条消息,只有它被 ISR 中的所有 Follower 都从 Leader 复制过去才会被认为 Committed,并返回信息给 Producer,从而保证可靠性。但与“少数服从多数”策略不同的是,Kafka ISR 列表中副本的数量不需要超过副本总数的一半,即不需要满足“多数派”原则,通常,ISR 列表副本数大于等于 2 即可,如此,便在可靠性和吞吐量方面取得平衡。 **极端情况下的 Leader 选举策略** 前已述及,当 ISR 中至少有一个 Follower 时(ISR 包括 Leader),Kafka 可以确保已经 Commit 的消息不丢失,但如果某一个 Partition 的所有 Replica 都挂了,所谓的保证自然也就不存在了。这种情况下如何进行 Leader 选举呢?通常有两种方案。 +实际上,对于任意一条消息,只有它被 ISR 中的所有 Follower 都从 Leader 复制过去才会被认为 Committed,并返回信息给 Producer,从而保证可靠性。但与“少数服从多数”策略不同的是,Kafka ISR 列表中副本的数量不需要超过副本总数的一半,即不需要满足“多数派”原则,通常,ISR 列表副本数大于等于 2 即可,如此,便在可靠性和吞吐量方面取得平衡。**极端情况下的 Leader 选举策略** 前已述及,当 ISR 中至少有一个 Follower 时(ISR 包括 Leader),Kafka 可以确保已经 Commit 的消息不丢失,但如果某一个 Partition 的所有 Replica 都挂了,所谓的保证自然也就不存在了。这种情况下如何进行 Leader 选举呢?通常有两种方案。 1. 等待 ISR 中任意一个 Replica 恢复过来,并且选它作为 Leader(希望它仍然有它所有的数据); 2. 选择第一个恢复过来的 Replica(不一定非在 ISR 中)作为 Leader。 @@ -313,7 +313,7 @@ ZooKeeper 是一个分布式、开放源码的分布式应用程序协调服务 * owners:记录该 Consumer Group 可消费的 Topic 信息; * offsets:记录 owners 中每个 Topic 的所有 Partition 的 Offset。 -**Consumer 注册** 原理同 Consumer Group 注册,不过需要注意的是,其节点路径比较特殊,需在路径 `/ consumers/{group_id}/ids` 下创建专属子节点,它是临时的。比如,某 Consumer 的临时节点路径为 `/ consumers/{group_id}/ids/my_consumer_for_test-1223234-fdfv1233df23`。 **负载均衡** +**Consumer 注册** 原理同 Consumer Group 注册,不过需要注意的是,其节点路径比较特殊,需在路径 `/ consumers/{group_id}/ids` 下创建专属子节点,它是临时的。比如,某 Consumer 的临时节点路径为 `/ consumers/{group_id}/ids/my_consumer_for_test-1223234-fdfv1233df23`。**负载均衡** 通过前面的学习,我们知道,对于一条消息,订阅了它的 Consumer Group 中只能有一个 Consumer 消费它。那么就存在一个问题:一个 Consumer Group 中有多个 Consumer,如何让它们尽可能均匀地消费订阅的消息呢(也就是负载均衡)?这里不讨论实现细节,但要实现负载均衡,实时获取 Consumer 的数量显然是必要的,通过 Watch 机制监听 `/ consumers/{group_id}/ids` 下子节点的事件便可实现。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25414\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25414\350\256\262.md" index 5ad2a0b1e..e77c29e92 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25414\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\344\270\255\351\227\264\344\273\266\345\256\236\350\267\265\344\271\213\350\267\257/\347\254\25414\350\256\262.md" @@ -66,9 +66,9 @@ Kafka 新版 Java 客户端(Kafka Clients 2.0.0)使用异步方式发送消 `linger.ms` 设置批处理延迟上限时,请注意,一旦我们得到一个分区的 `batch.size` 值的记录,它将立即发送,而不管该设置如何;但如果这个分区的累积字节数少于 `batch.size`,我们将“逗留”指定的等待时间,到达 `linger.ms` 指定的延迟后,即使累积的消息字节数少于 `batch.size`,也会发送。 linger.ms 默认为 0(即没有延迟)。例如,设置 `linger.ms=5` 将具有减少发送请求数量的效果,但在没有负载的情况下发送记录,将增加高达 5ms 的延迟。 #### 4.3 参数 `buffer.memory` 和 `max.block.ms` -4.2 节提到了发送方消息堆积的问题,当程序的发送速率大于后台线程发送到 Broker 的速率时,生产的消息会在发送方堆积,为此 Kafka 提供了相应的堆积控制策略,主要涉及到的参数有 `buffer.memory` 和 `max.block.ms`。 **参数 buffer.memory** > **关于 buffer.memory** > +4.2 节提到了发送方消息堆积的问题,当程序的发送速率大于后台线程发送到 Broker 的速率时,生产的消息会在发送方堆积,为此 Kafka 提供了相应的堆积控制策略,主要涉及到的参数有 `buffer.memory` 和 `max.block.ms`。**参数 buffer.memory** > **关于 buffer.memory** > > `buffer.memory` 为 Long 型,默认值为 33554432,单位为字节,即默认大小为 32MB。 -根据官网解释,我们来了解下该参数。Producer 可以用来缓冲等待发送给服务器的消息的总字节数。如果消息的发送速度快于传送到服务器的速度,那么缓冲区将被占满,之后 Producer 将阻塞 `max.block.ms`,随后将抛出异常。`buffer.memory` 的大小大致与 Producer 可使用的总内存相对应,但不是硬绑定,因为并非 Producer 使用的所有内存都用于缓冲。一些额外的内存将用于压缩(如果启用压缩)以及维护等请求。 **参数 max.block.ms** > 关于 `max.block.ms` +根据官网解释,我们来了解下该参数。Producer 可以用来缓冲等待发送给服务器的消息的总字节数。如果消息的发送速度快于传送到服务器的速度,那么缓冲区将被占满,之后 Producer 将阻塞 `max.block.ms`,随后将抛出异常。`buffer.memory` 的大小大致与 Producer 可使用的总内存相对应,但不是硬绑定,因为并非 Producer 使用的所有内存都用于缓冲。一些额外的内存将用于压缩(如果启用压缩)以及维护等请求。**参数 max.block.ms** > 关于 `max.block.ms` > > `max.block.ms` 为 Long 型,默认值为 60000,单位为毫秒,即大小为 60s。 在 `buffer.memory` 指定的缓存被占满后,Producer 相关的方法可阻塞的最大时间由 `max.block.ms` 控制,之后将抛出异常。比如,`KafkaProducer.send()` 和 `KafkaProducer.partitionsFor()`。注意,用户提供的序列化器或分区器中的阻塞将不计入该超时。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25400\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25400\350\256\262.md" index bacffcee0..f0f18063a 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25400\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25400\350\256\262.md" @@ -14,7 +14,7 @@ 总结来说,这往往是从业者没有在实际的分布式业务场景中实践过,或者对分步式技术缺乏体系化的认知,或者对一些原理和底层的内容未曾深入研究,导致可以解决常见问题,而没有系统化的解决思路。 -因此, **我梳理了一套分布式技术的方法论,希望可以帮助你快速而体系化地补齐分布式知识** 。此外,一路走来,我在分布式系统设计中踩过的坑,在开发实践中看到和经历过的一些典型问题,也将在这里一并分享给你,希望能够帮到更多开发者,并减轻你学习分布式的畏难心理。 +因此,**我梳理了一套分布式技术的方法论,希望可以帮助你快速而体系化地补齐分布式知识** 。此外,一路走来,我在分布式系统设计中踩过的坑,在开发实践中看到和经历过的一些典型问题,也将在这里一并分享给你,希望能够帮到更多开发者,并减轻你学习分布式的畏难心理。 ### 分布式是工程师进阶的必经之路 @@ -28,11 +28,11 @@ 要想进入大公司并拿到高薪 Offer,分布式技术也是一个很好的敲门砖。大型互联网公司每天都要面对海量的业务请求,处理各种复杂的系统问题是工作常态,所以需要应聘人员掌握常用的分布式技术,并在面试过程中重点考察你对分布式系统的理解和经验水平。 -针对 **高级岗位** ,除了掌握在分布式环境下进行开发的能力,你还需要了解其中的原理、机制,以便能够快速定位线上问题;而对于 **架构师** 来说,你还需要具备独立设计分布式系统的能力,这就需要了解高并发、高可用的相关知识了。 +针对 **高级岗位**,除了掌握在分布式环境下进行开发的能力,你还需要了解其中的原理、机制,以便能够快速定位线上问题;而对于 **架构师** 来说,你还需要具备独立设计分布式系统的能力,这就需要了解高并发、高可用的相关知识了。 -**在** **拉勾网上搜索后端工程师的招聘岗位,可以看到很多岗位都要求掌握缓存、分布式服务、消息队列等分布式组件应用,部分岗位还要求在高并发等分布式设计方向有一定的积累。** ![img](assets/Ciqah16FzVKAUHomABFJwSsmtFg192.png) +**在** **拉勾网上搜索后端工程师的招聘岗位,可以看到很多岗位都要求掌握缓存、分布式服务、消息队列等分布式组件应用,部分岗位还要求在高并发等分布式设计方向有一定的积累。**![img](assets/Ciqah16FzVKAUHomABFJwSsmtFg192.png) -结合拉勾对海量招聘启事的大数据分析,我们也总结出了后端开发者在面试中要求 **掌握的分布式技能点** ,同时也把它们融入到了课程设计中: +结合拉勾对海量招聘启事的大数据分析,我们也总结出了后端开发者在面试中要求 **掌握的分布式技能点**,同时也把它们融入到了课程设计中: - 分布式系统理论和设计; - 分布式事务和一致性; @@ -48,15 +48,15 @@ 分布式系统在工作和面试中如此重要,但是掌握起来并不容易。 -- **理论众多、难以入手。** 分布式系统不仅涉及一致性、事务等众多的理论知识,还包括非常多的复杂算法,比如 Paxos 和 Zab 算法,如果 **没有一个明确的抓手** , **学习起来会很吃力** 。 -- **领域庞杂、关联技术栈多。** 分布式系统涉及很多领域,比如 RPC 服务调用、分库分表,这些不同的领域需要了解和掌握不同的技术栈。因此我的建议是,要想快速提升分布式技术能力,那么 **需要明确哪些才是你日常工作中最迫切需要的** ,从实践中开始体验和学习,积累经验。要知道,分布式不是一堆理论的堆砌,而是和日常开发息息相关。 -- **工作特点,接触不到分布式** **。** 鉴于现在一些软件开发公司,或者传统公司的 IT 部门,还在使用集中式系统架构,所以部分开发者平时在工作中很少接触分布式系统,因此,我在这个课程中,将会侧重 **讲解很多实际场景的实践内容** ,以帮助你更有效地掌握分布式。 +- **理论众多、难以入手。** 分布式系统不仅涉及一致性、事务等众多的理论知识,还包括非常多的复杂算法,比如 Paxos 和 Zab 算法,如果 **没有一个明确的抓手**,**学习起来会很吃力** 。 +- **领域庞杂、关联技术栈多。** 分布式系统涉及很多领域,比如 RPC 服务调用、分库分表,这些不同的领域需要了解和掌握不同的技术栈。因此我的建议是,要想快速提升分布式技术能力,那么 **需要明确哪些才是你日常工作中最迫切需要的**,从实践中开始体验和学习,积累经验。要知道,分布式不是一堆理论的堆砌,而是和日常开发息息相关。 +- **工作特点,接触不到分布式** **。** 鉴于现在一些软件开发公司,或者传统公司的 IT 部门,还在使用集中式系统架构,所以部分开发者平时在工作中很少接触分布式系统,因此,我在这个课程中,将会侧重 **讲解很多实际场景的实践内容**,以帮助你更有效地掌握分布式。 工作多年,我从一个初入行的新人,一步步晋升一线互联网公司的核心业务负责人,我深知分布式知识的重要性和学习痛点,为了让你在短时间内能够快速掌握分布式知识,我对这门课程进行了精心设计。 **(1)知识体系化,快速学习** 碎片化知识很难有效学习,体系化的学习才是重点。分布式系统知识足够庞杂,本课程从理论开始,一步一步落地到实践中,帮助你快速构建知识框架,让你对分布式技术有个总体的认知。 -![img](assets/Ciqah16ERqmAGJjpAACXHV15Oyg347.png) **(2)选取最常用的知识点** 分布式系统博大精深,但并不是每个人都在做基础架构研发,也不是每一项技术都能直接落地,因而本课程选取了在工程开发中最常用的技术栈,比如在分布式服务模块中选取了网关、注册中心、容器化等内容来讲解,在数据库模块中选择了读写分离、分库分表等内容,这些都是在开发中打交道最多的知识点。 **(3)拒绝空谈理论,结合实际业务场景** 技术是为业务服务的,再高深的技术都要落地,我们的课程内容不是干巴巴的讲理论,而是结合了实际业务场景,带着问题去讲解,让你能够在实际的场景中理解并应用,达到事半功倍的效果。 **(4)面试真题解析,帮你赢取高薪 Offer** 为了帮助你更好地准备面试,每个模块后面都附上了一个“ **加餐** ”内容,并梳理出了面试中经常出现的考点,以及高频面试真题。虽然是加餐,但是内容绝对有料。 +![img](assets/Ciqah16ERqmAGJjpAACXHV15Oyg347.png) **(2)选取最常用的知识点** 分布式系统博大精深,但并不是每个人都在做基础架构研发,也不是每一项技术都能直接落地,因而本课程选取了在工程开发中最常用的技术栈,比如在分布式服务模块中选取了网关、注册中心、容器化等内容来讲解,在数据库模块中选择了读写分离、分库分表等内容,这些都是在开发中打交道最多的知识点。**(3)拒绝空谈理论,结合实际业务场景** 技术是为业务服务的,再高深的技术都要落地,我们的课程内容不是干巴巴的讲理论,而是结合了实际业务场景,带着问题去讲解,让你能够在实际的场景中理解并应用,达到事半功倍的效果。**(4)面试真题解析,帮你赢取高薪 Offer** 为了帮助你更好地准备面试,每个模块后面都附上了一个“ **加餐** ”内容,并梳理出了面试中经常出现的考点,以及高频面试真题。虽然是加餐,但是内容绝对有料。 当然,快速通关面试只是我们的目标之一,我更希望你在这个课程中,真正学有所得,将知识和经验融入到个人能力中,做一些长期主义的事情。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25401\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25401\350\256\262.md" index 94a231ffd..82630f071 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25401\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25401\350\256\262.md" @@ -4,7 +4,7 @@ ## 分布式系统的特点 -随着移动互联网的快速发展,互联网的用户数量越来越多,产生的数据规模也越来越大,对应用系统提出了更高的要求,我们的系统必须支持高并发访问和海量数据处理。 分布式系统技术就是用来解决集中式架构的性能瓶颈问题,来适应快速发展的业务规模,一般来说,分布式系统是建立在网络之上的硬件或者软件系统,彼此之间通过消息等方式进行通信和协调。 分布式系统的核心是 **可扩展性** ,通过对服务、存储的扩展,来提高系统的处理能力,通过对多台服务器协同工作,来完成单台服务器无法处理的任务,尤其是高并发或者大数据量的任务。 除了对可扩展性的需求,分布式系统 **还有不出现单点故障、服务或者存储无状态等特点** 。 +随着移动互联网的快速发展,互联网的用户数量越来越多,产生的数据规模也越来越大,对应用系统提出了更高的要求,我们的系统必须支持高并发访问和海量数据处理。 分布式系统技术就是用来解决集中式架构的性能瓶颈问题,来适应快速发展的业务规模,一般来说,分布式系统是建立在网络之上的硬件或者软件系统,彼此之间通过消息等方式进行通信和协调。 分布式系统的核心是 **可扩展性**,通过对服务、存储的扩展,来提高系统的处理能力,通过对多台服务器协同工作,来完成单台服务器无法处理的任务,尤其是高并发或者大数据量的任务。 除了对可扩展性的需求,分布式系统 **还有不出现单点故障、服务或者存储无状态等特点** 。 - 单点故障(Single Point Failure)是指在系统中某个组件一旦失效,这会让整个系统无法工作,而不出现单点故障,单点不影响整体,就是分布式系统的设计目标之一; - 无状态,是因为无状态的服务才能满足部分机器宕机不影响全部,可以随时进行扩展的需求。 由于分布式系统的特点,在分布式环境中更容易出现问题,比如节点之间通信失败、网络分区故障、多个副本的数据不一致等,为了更好地在分布式系统下进行开发,学者们提出了一系列的理论,其中具有代表性的就是 CAP 理论。 @@ -13,7 +13,7 @@ CAP 理论可以表述为,一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)这三项中的两项。 ![img](assets/Ciqah16ER_SAGmCqAADG3jNX34o901.png) -**一致性** 是指“所有节点同时看到相同的数据”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,等同于所有节点拥有数据的最新版本。 **可用性** 是指“任何时候,读写都是成功的”,即服务一直可用,而且是正常响应时间。我们平时会看到一些 IT 公司的对外宣传,比如系统稳定性已经做到 3 个 9、4 个 9,即 99.9%、99.99%,这里的 N 个 9 就是对可用性的一个描述,叫做 SLA,即服务水平协议。比如我们说月度 99.95% 的 SLA,则意味着每个月服务出现故障的时间只能占总时间的 0.05%,如果这个月是 30 天,那么就是 21.6 分钟。 **分区容忍性** 具体是指“当部分节点出现消息丢失或者分区故障的时候,分布式系统仍然能够继续运行”,即系统容忍网络出现分区,并且在遇到某节点或网络分区之间网络不可达的情况下,仍然能够对外提供满足一致性和可用性的服务。 在分布式系统中,由于系统的各层拆分,P 是确定的,CAP 的应用模型就是 CP 架构和 AP 架构。分布式系统所关注的,就是在 Partition Tolerance 的前提下,如何实现更好的 A 和更稳定的 C。 +**一致性** 是指“所有节点同时看到相同的数据”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,等同于所有节点拥有数据的最新版本。**可用性** 是指“任何时候,读写都是成功的”,即服务一直可用,而且是正常响应时间。我们平时会看到一些 IT 公司的对外宣传,比如系统稳定性已经做到 3 个 9、4 个 9,即 99.9%、99.99%,这里的 N 个 9 就是对可用性的一个描述,叫做 SLA,即服务水平协议。比如我们说月度 99.95% 的 SLA,则意味着每个月服务出现故障的时间只能占总时间的 0.05%,如果这个月是 30 天,那么就是 21.6 分钟。**分区容忍性** 具体是指“当部分节点出现消息丢失或者分区故障的时候,分布式系统仍然能够继续运行”,即系统容忍网络出现分区,并且在遇到某节点或网络分区之间网络不可达的情况下,仍然能够对外提供满足一致性和可用性的服务。 在分布式系统中,由于系统的各层拆分,P 是确定的,CAP 的应用模型就是 CP 架构和 AP 架构。分布式系统所关注的,就是在 Partition Tolerance 的前提下,如何实现更好的 A 和更稳定的 C。 ### CAP 理论的证明 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25402\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25402\350\256\262.md" index 8f8345704..844346ea7 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25402\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25402\350\256\262.md" @@ -4,7 +4,7 @@ ## Base 理论 -Base 是三个短语的简写,即基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)。 ![img](assets/Ciqah16FrueAWLATAABOyQi2X3M251.png) Base 理论的核心思想是 **最终一致性** ,即使无法做到强一致性(Strong Consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual Consistency)。 接下来我们着重对 Base 理论中的三要素进行讲解。 +Base 是三个短语的简写,即基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)。 ![img](assets/Ciqah16FrueAWLATAABOyQi2X3M251.png) Base 理论的核心思想是 **最终一致性**,即使无法做到强一致性(Strong Consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual Consistency)。 接下来我们着重对 Base 理论中的三要素进行讲解。 ## 三个要素详解 @@ -22,7 +22,7 @@ Base 是三个短语的简写,即基本可用(Basically Available)、软 ## 全局时钟和逻辑时钟 -接下来我会分析不同数据一致性模型的分类,在这之前,我们先来看一个分布式系统中的全局时钟概念。 分布式系统解决了 **传统单体架构的单点问题和性能容量问题** ,另一方面也带来了很多新的问题,其中一个问题就是 **多节点的时间同步问题** :不同机器上的物理时钟难以同步,导致无法区分在分布式系统中多个节点的事件时序。 没有 **全局时钟** ,绝对的内部一致性是没有意义的,一般来说,我们讨论的一致性都是外部一致性,而外部一致性主要指的是多并发访问时更新过的数据如何获取的问题。 和全局时钟相对的,是 **逻辑时钟** ,逻辑时钟描绘了分布式系统中事件发生的时序,是为了区分现实中的物理时钟提出来的概念。 ![img](assets/Cgq2xl6FrueAdqXGAAARaVLIyqg649.png) 一般情况下我们提到的时间都是指物理时间,但实际上很多应用中,只要所有机器有相同的时间就够了,这个时间不一定要跟实际时间相同。更进一步解释:如果两个节点之间不进行交互,那么它们的时间甚至都不需要同步。 因此问题的关键点在于 **节点间的交互要在事件的发生顺序上达成一致,而不是对于时间达成一致** 。 逻辑时钟的概念也被用来解决分布式一致性问题,这里我们不展开,感兴趣的可以找一些相关的资料来学习。 +接下来我会分析不同数据一致性模型的分类,在这之前,我们先来看一个分布式系统中的全局时钟概念。 分布式系统解决了 **传统单体架构的单点问题和性能容量问题**,另一方面也带来了很多新的问题,其中一个问题就是 **多节点的时间同步问题** :不同机器上的物理时钟难以同步,导致无法区分在分布式系统中多个节点的事件时序。 没有 **全局时钟**,绝对的内部一致性是没有意义的,一般来说,我们讨论的一致性都是外部一致性,而外部一致性主要指的是多并发访问时更新过的数据如何获取的问题。 和全局时钟相对的,是 **逻辑时钟**,逻辑时钟描绘了分布式系统中事件发生的时序,是为了区分现实中的物理时钟提出来的概念。 ![img](assets/Cgq2xl6FrueAdqXGAAARaVLIyqg649.png) 一般情况下我们提到的时间都是指物理时间,但实际上很多应用中,只要所有机器有相同的时间就够了,这个时间不一定要跟实际时间相同。更进一步解释:如果两个节点之间不进行交互,那么它们的时间甚至都不需要同步。 因此问题的关键点在于 **节点间的交互要在事件的发生顺序上达成一致,而不是对于时间达成一致** 。 逻辑时钟的概念也被用来解决分布式一致性问题,这里我们不展开,感兴趣的可以找一些相关的资料来学习。 ## 不同数据一致性模型 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25403\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25403\350\256\262.md" index 2bef7832c..6f2c1537e 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25403\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25403\350\256\262.md" @@ -95,11 +95,11 @@ Accpetor 收到 Accpet 请求 后,判断: # Paxos 常见的问题 -关于Paxos协议,有几个常见的问题,简单介绍下。 **1.如果半数以内的 Acceptor 失效,如何正常运行?** 在Paxos流程中,如果出现半数以内的 Acceptor 失效,可以分为两种情况: +关于Paxos协议,有几个常见的问题,简单介绍下。**1.如果半数以内的 Acceptor 失效,如何正常运行?** 在Paxos流程中,如果出现半数以内的 Acceptor 失效,可以分为两种情况: 第一种,如果半数以内的 Acceptor 失效时还没确定最终的 value,此时所有的 Proposer 会重新竞争提案,最终有一个提案会成功提交。 -第二种,如果半数以内的 Acceptor 失效时已确定最终的 value,此时所有的 Proposer 提交前必须以最终的 value 提交,也就是Value实际已经生效,此值可以被获取,并不再修改。 **2. Acceptor需要接受更大的N,也就是ProposalID有什么意义?** 这种机制可以防止其中一个Proposer崩溃宕机产生阻塞问题,允许其他Proposer用更大ProposalID来抢占临时的访问权。 **3. 如何产生唯一的编号,也就是 ProposalID?** +第二种,如果半数以内的 Acceptor 失效时已确定最终的 value,此时所有的 Proposer 提交前必须以最终的 value 提交,也就是Value实际已经生效,此值可以被获取,并不再修改。**2. Acceptor需要接受更大的N,也就是ProposalID有什么意义?** 这种机制可以防止其中一个Proposer崩溃宕机产生阻塞问题,允许其他Proposer用更大ProposalID来抢占临时的访问权。**3. 如何产生唯一的编号,也就是 ProposalID?** 在《Paxos made simple》论文中提到,唯一编号是让所有的 Proposer 都从不相交的数据集合中进行选择,需要保证在不同Proposer之间不重复,比如系统有 5 个 Proposer,则可为每一个 Proposer 分配一个标识 j(0~4),那么每一个 Proposer 每次提出决议的编号可以为 5\*i + j,i 可以用来表示提出议案的次数。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25404\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25404\350\256\262.md" index b090ece2c..a352683bf 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25404\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25404\350\256\262.md" @@ -87,7 +87,7 @@ Zab 中的节点有三种状态,伴随着的 Zab 不同阶段的转换,节 选举过程如下: -**1** **.各个节点** **变更状态** **,变更为** **Looking** ZooKeeper 中除了 Leader 和 Follower,还有 Observer 节点,Observer 不参与选举,Leader 挂后,余下的 Follower 节点都会将自己的状态变更为 Looking,然后开始进入 Leader 选举过程。 **2** **.各个** **Server** **节点都** **会发出一个投票** **,参与选举** 在第一次投票中,所有的 Server 都会投自己,然后各自将投票发送给集群中所有机器,在运行期间,每个服务器上的 Zxid 大概率不同。 **3** **.** **集群接收来自各个服务器的投票,开始处理投票和选举** 处理投票的过程就是对比 Zxid 的过程,假定 Server3 的 Zxid 最大,Server1 判断 Server3 可以成为 Leader,那么 Server1 就投票给 Server3,判断的依据如下: +**1** **.各个节点** **变更状态** **,变更为** **Looking** ZooKeeper 中除了 Leader 和 Follower,还有 Observer 节点,Observer 不参与选举,Leader 挂后,余下的 Follower 节点都会将自己的状态变更为 Looking,然后开始进入 Leader 选举过程。**2** **.各个** **Server** **节点都** **会发出一个投票** **,参与选举** 在第一次投票中,所有的 Server 都会投自己,然后各自将投票发送给集群中所有机器,在运行期间,每个服务器上的 Zxid 大概率不同。**3** **.** **集群接收来自各个服务器的投票,开始处理投票和选举** 处理投票的过程就是对比 Zxid 的过程,假定 Server3 的 Zxid 最大,Server1 判断 Server3 可以成为 Leader,那么 Server1 就投票给 Server3,判断的依据如下: > 首先选举 epoch 最大的 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25405\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25405\350\256\262.md" index e857be60f..78561702a 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25405\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25405\350\256\262.md" @@ -2,7 +2,7 @@ 本课时我们主要讲解“共识问题:区块链如何确认记账权?” -区块链可以说是最近几年最热的技术领域之一,区块链起源于中本聪的比特币,作为比特币的底层技术,本质上是一个去中心化的数据库,其特点是 **去中心化** 、 **公开透明** ,作为分布式账本技术,每个节点都可以参与数据库的记录。 +区块链可以说是最近几年最热的技术领域之一,区块链起源于中本聪的比特币,作为比特币的底层技术,本质上是一个去中心化的数据库,其特点是 **去中心化** 、 **公开透明**,作为分布式账本技术,每个节点都可以参与数据库的记录。 区块链是一个注重安全和可信度胜过效率的一项技术,如果说互联网技术解决的是通讯问题,区块链技术解决的则是信任问题。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25406\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25406\350\256\262.md" index f706e1568..9afee51a9 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25406\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25406\350\256\262.md" @@ -10,7 +10,7 @@ ![image](assets/CgoCgV6hUdCAHsw6AAC5j_lXmsI267.png) -**1. 看重数据结构和算法等计算机基础知识** 一线互联网公司在面试中更加关注计算机基础知识的考察,比如数据结构和算法,操作系统、网络原理,目前,很多国内公司在招聘上也看齐 Google、Facebook 等海外企业,面试重点考察算法,如果没有 ACM 经验,不刷题很难通过。 **2. 深入技术栈,考察对原理和源码的掌握程度** 深入底层实现,考察对相关组件的原理掌握程度,以及是否读过源码等。因为互联网用户基数比较大,一个细微的优化可能会带来很大的收益,同样,一个很小的问题可能会对线上业务造成毁灭性的影响,所以要知其然还要知其所以然,对技术栈的掌握要求比较深入。 **3. 偏向实际问题,考察业务中的应用** 面试中通常会结合实际业务场景来提问,其考察的是在真实业务中如何设计。我们知道,条条大路通罗马,一个功能点,技术方案可能有很多,但是从落地到代码实现,就要限制于整体方案、上下游约束等,典型的比如秒杀系统、微博会员关注关系设计等。 **4. 重视分布式系统、高可用等设计方向** +**1. 看重数据结构和算法等计算机基础知识** 一线互联网公司在面试中更加关注计算机基础知识的考察,比如数据结构和算法,操作系统、网络原理,目前,很多国内公司在招聘上也看齐 Google、Facebook 等海外企业,面试重点考察算法,如果没有 ACM 经验,不刷题很难通过。**2. 深入技术栈,考察对原理和源码的掌握程度** 深入底层实现,考察对相关组件的原理掌握程度,以及是否读过源码等。因为互联网用户基数比较大,一个细微的优化可能会带来很大的收益,同样,一个很小的问题可能会对线上业务造成毁灭性的影响,所以要知其然还要知其所以然,对技术栈的掌握要求比较深入。**3. 偏向实际问题,考察业务中的应用** 面试中通常会结合实际业务场景来提问,其考察的是在真实业务中如何设计。我们知道,条条大路通罗马,一个功能点,技术方案可能有很多,但是从落地到代码实现,就要限制于整体方案、上下游约束等,典型的比如秒杀系统、微博会员关注关系设计等。**4. 重视分布式系统、高可用等设计方向** 大型互联网公司,特别是 C 端的业务,面对的是海量的用户和请求,牵一发而动全身,对系统可用性、分布式高可用等有极高的要求,所以在面试中会重点考察分布式系统设计,如何构建高并发高可用的系统。 @@ -33,15 +33,15 @@ ![image](assets/CgoCgV6hUdmAVkBQAADSNFZJjQU555.png) -**1. 对学历和专业的要求,硬性标准** 对学历和专业的要求,这一条一般都会注明,不过计算机行业比较包容,不拘一格,非科班以及转专业的技术大牛也有很多,这里不展开。 **2. 加强计算机基础,提高算法和数据结构、操作系统等底层能力** 计算机基础能力是面试的重点,在校招中更是着重考察。 +**1. 对学历和专业的要求,硬性标准** 对学历和专业的要求,这一条一般都会注明,不过计算机行业比较包容,不拘一格,非科班以及转专业的技术大牛也有很多,这里不展开。**2. 加强计算机基础,提高算法和数据结构、操作系统等底层能力** 计算机基础能力是面试的重点,在校招中更是着重考察。 数据结构方面,基本的数组、栈和队列、字符串、二叉树等结构,比如二叉树是面试中的重点,手写红黑树有点夸张,不过基本的遍历、二叉树重建、二叉树深度等必须掌握,需要在白纸上写写代码,考的是白板编程能力。 算法方面,基本的排序和查找、递归、分治、动态规划之类都会考察,这方面可以多看看《剑指 offer》《编程珠玑》,国内推荐牛客网,国外就是 LeetCode 的高频题。 -操作系统和网络原理,比如基本的调度算法、文件系统,还有各种网络协议,比如 TCP/IP 协议、拥塞控制等。操作系统推荐机械工业出版社的华章系列教材,网络原理也有一些经典书籍,如果觉得《TCP/IP 详解》太厚,可以看《图解 HTTP 协议》和《图解 TCP/IP 协议》。 **3. 深入一门编程语言,了解底层实现,各种语法糖和特性** 后端工程师不管学习多少语言,都要有一门自己的主编程语言,什么是主编程语言,就是对这个编程语言你可以达到精通的程度,不是只会用,要从代码编译开始就知道程序是怎么运行的。典型的主语言有 Java、C++、PHP 及 Python 等。 +操作系统和网络原理,比如基本的调度算法、文件系统,还有各种网络协议,比如 TCP/IP 协议、拥塞控制等。操作系统推荐机械工业出版社的华章系列教材,网络原理也有一些经典书籍,如果觉得《TCP/IP 详解》太厚,可以看《图解 HTTP 协议》和《图解 TCP/IP 协议》。**3. 深入一门编程语言,了解底层实现,各种语法糖和特性** 后端工程师不管学习多少语言,都要有一门自己的主编程语言,什么是主编程语言,就是对这个编程语言你可以达到精通的程度,不是只会用,要从代码编译开始就知道程序是怎么运行的。典型的主语言有 Java、C++、PHP 及 Python 等。 -针对 Java 语言,要了解 Java 语言的底层机制,字节码怎么用,为什么 Java 是平台无关型语言,这些都要搞明白,应用层面,对集合框架、网络 IO、并发编程、泛型、异常、反射等技术都要有比较深入的了解,一些常见的组件,还要学习源码,优化层面,Java 虚拟机调优、常见 JVM 问题的处理,这些都是面试经常考察的,也是一定要掌握的。 **4. 加强数据库和缓存应用,掌握 NoSQL 技术** 数据存储是业务的基石,从关系型数据库 MySQL 到 NoSQL,从 Memcached 到 Redis 的各种缓存,这些都是面试的必考题,从应用到底层逻辑都必须了解,数据库本身这块的知识点更是重要,Redis 也是面试的重点,作为应用最多的缓存,Redis 在开发中已经和 MySQL 一样重要。 **5. 学习高并发和高可用的分布式系统设计** 高并发是技术人一直追求的,为什么我们说双十一是对系统架构的挑战,就是天量的 QPS 请求,在这种情况下,如何保障系统的高可用,保证业务正常,是每个工程师都要思考的。分布式系统架构,以及高并发和高可用知识,则需要在工作中注意积累,如果工作中没有类似的上手锻炼机会,也可以通过各种书籍和专栏等渠道来学习。 **6. 增强软性指标,包括快速学习,良好的沟通能力** +针对 Java 语言,要了解 Java 语言的底层机制,字节码怎么用,为什么 Java 是平台无关型语言,这些都要搞明白,应用层面,对集合框架、网络 IO、并发编程、泛型、异常、反射等技术都要有比较深入的了解,一些常见的组件,还要学习源码,优化层面,Java 虚拟机调优、常见 JVM 问题的处理,这些都是面试经常考察的,也是一定要掌握的。**4. 加强数据库和缓存应用,掌握 NoSQL 技术** 数据存储是业务的基石,从关系型数据库 MySQL 到 NoSQL,从 Memcached 到 Redis 的各种缓存,这些都是面试的必考题,从应用到底层逻辑都必须了解,数据库本身这块的知识点更是重要,Redis 也是面试的重点,作为应用最多的缓存,Redis 在开发中已经和 MySQL 一样重要。**5. 学习高并发和高可用的分布式系统设计** 高并发是技术人一直追求的,为什么我们说双十一是对系统架构的挑战,就是天量的 QPS 请求,在这种情况下,如何保障系统的高可用,保证业务正常,是每个工程师都要思考的。分布式系统架构,以及高并发和高可用知识,则需要在工作中注意积累,如果工作中没有类似的上手锻炼机会,也可以通过各种书籍和专栏等渠道来学习。**6. 增强软性指标,包括快速学习,良好的沟通能力** 除了技术实力,软性指标也很重要,在平时的工作中,要注意梳理文档,养成良好的文档能力,和同事的沟通中,多学习下《金字塔原理》等沟通技巧,在面试中就可以更好的表现自己。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25407\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25407\350\256\262.md" index 8ed1c9d2c..0b8b4437d 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25407\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25407\350\256\262.md" @@ -18,7 +18,7 @@ 在数据库执行中,多个并发执行的事务如果涉及到同一份数据的读写就容易出现数据不一致的情况,不一致的异常现象有以下几种。 -**脏读** ,是指一个事务中访问到了另外一个事务未提交的数据。例如事务 T1 中修改的数据项在尚未提交的情况下被其他事务(T2)读取到,如果 T1 进行回滚操作,则 T2 刚刚读取到的数据实际并不存在。 **不可重复读** ,是指一个事务读取同一条记录 2 次,得到的结果不一致。例如事务 T1 第一次读取数据,接下来 T2 对其中的数据进行了更新或者删除,并且 Commit 成功。这时候 T1 再次读取这些数据,那么会得到 T2 修改后的数据,发现数据已经变更,这样 T1 在一个事务中的两次读取,返回的结果集会不一致。 **幻读** ,是指一个事务读取 2 次,得到的记录条数不一致。例如事务 T1 查询获得一个结果集,T2 插入新的数据,T2 Commit 成功后,T1 再次执行同样的查询,此时得到的结果集记录数不同。 +**脏读**,是指一个事务中访问到了另外一个事务未提交的数据。例如事务 T1 中修改的数据项在尚未提交的情况下被其他事务(T2)读取到,如果 T1 进行回滚操作,则 T2 刚刚读取到的数据实际并不存在。**不可重复读**,是指一个事务读取同一条记录 2 次,得到的结果不一致。例如事务 T1 第一次读取数据,接下来 T2 对其中的数据进行了更新或者删除,并且 Commit 成功。这时候 T1 再次读取这些数据,那么会得到 T2 修改后的数据,发现数据已经变更,这样 T1 在一个事务中的两次读取,返回的结果集会不一致。**幻读**,是指一个事务读取 2 次,得到的记录条数不一致。例如事务 T1 查询获得一个结果集,T2 插入新的数据,T2 Commit 成功后,T1 再次执行同样的查询,此时得到的结果集记录数不同。 脏读、不可重复读和幻读有以下的包含关系,如果发生了脏读,那么幻读和不可重复读都有可能出现。 @@ -112,7 +112,7 @@ TCC 是一个分布式事务的处理模型,将事务过程拆分为 Try、Con ### 不要求最终一致性的柔性事务 -除了上述几种,还有一种不保证最终一致性的柔性事务,也称为 **尽最大努力通知** ,这种方式适合可以接受部分不一致的业务场景。 +除了上述几种,还有一种不保证最终一致性的柔性事务,也称为 **尽最大努力通知**,这种方式适合可以接受部分不一致的业务场景。 ## 分布式事务有哪些开源组件 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25408\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25408\350\256\262.md" index fd6101603..34678f340 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25408\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25408\350\256\262.md" @@ -20,7 +20,7 @@ - 所有节点都采用预写式日志,日志被写入后被保存在可靠的存储设备上,即使节点损坏也不会导致日志数据的丢失; - 所有节点不会永久性损坏,即使损坏后仍然可以恢复。 -两阶段提交中的两个阶段,指的是 **Commit-request 阶段** 和 **Commit 阶段** ,两阶段提交的流程如下: +两阶段提交中的两个阶段,指的是 **Commit-request 阶段** 和 **Commit 阶段**,两阶段提交的流程如下: ![分1.png](assets/CgoCgV6elbmATRwxAAFU68JiQU0596.png) @@ -70,14 +70,14 @@ A. 假如协调者从所有的参与者获得的反馈都是 Yes 响应,那么就会进行事务的预执行: -- **发送预提交请求** ,协调者向参与者发送 PreCommit 请求,并进入 Prepared 阶段; -- **事务预提交** ,参与者接收到 PreCommit 请求后,会执行事务操作; -- **响应反馈** ,如果参与者成功执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。 +- **发送预提交请求**,协调者向参与者发送 PreCommit 请求,并进入 Prepared 阶段; +- **事务预提交**,参与者接收到 PreCommit 请求后,会执行事务操作; +- **响应反馈**,如果参与者成功执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。 B. 假如有任何一个参与者向协调者发送了 No 响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就中断事务: -- **发送中断请求** ,协调者向所有参与者发送 abort 请求; -- **中断事务** ,参与者收到来自协调者的 abort 请求之后,执行事务的中断。 +- **发送中断请求**,协调者向所有参与者发送 abort 请求; +- **中断事务**,参与者收到来自协调者的 abort 请求之后,执行事务的中断。 #### DoCommit 阶段 @@ -98,7 +98,7 @@ C.超时提交 参与者如果没有收到协调者的通知,超时之后会 #### 引入超时机制 -在 2PC 中, **只有协调者拥有超时机制** ,如果在一定时间内没有收到参与者的消息则默认失败,3PC 同时在协调者和参与者中都引入超时机制。 +在 2PC 中,**只有协调者拥有超时机制**,如果在一定时间内没有收到参与者的消息则默认失败,3PC 同时在协调者和参与者中都引入超时机制。 #### 添加预提交阶段 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25409\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25409\350\256\262.md" index d21c1093a..0183ecdc9 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25409\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25409\350\256\262.md" @@ -10,15 +10,15 @@ MySQL 为我们提供了分布式事务解决方案,在前面的内容中提 问你一个问题,如果 MySQL 数据库断电了,未提交的事务怎么办? -答案是 **依靠日志** ,因为在执行一个操作之前,数据库会首先把这个操作的内容写入到文件系统日志里记录起来,然后再进行操作。当宕机或者断电的时候,即使操作并没有执行完,但是日志在操作前就已经写好了,我们仍然可以根据日志的内容来进行恢复。 +答案是 **依靠日志**,因为在执行一个操作之前,数据库会首先把这个操作的内容写入到文件系统日志里记录起来,然后再进行操作。当宕机或者断电的时候,即使操作并没有执行完,但是日志在操作前就已经写好了,我们仍然可以根据日志的内容来进行恢复。 MySQL InnoDB 引擎中和一致性相关的有 **重做日志** (redo log)、 **回滚日志** (undo log)和 **二进制日志** (binlog)。 -**redo 日志** ,每当有操作执行前,在数据真正更改前,会先把相关操作写入 redo 日志。这样当断电,或者发生一些意外,导致后续任务无法完成时,待系统恢复后,可以继续完成这些更改。 +**redo 日志**,每当有操作执行前,在数据真正更改前,会先把相关操作写入 redo 日志。这样当断电,或者发生一些意外,导致后续任务无法完成时,待系统恢复后,可以继续完成这些更改。 -和 redo 日志对应的 undo 日志,也叫 **撤消日志** ,记录事务开始前数据的状态,当一些更改在执行一半时,发生意外而无法完成,就可以根据撤消日志恢复到更改之前的状态。举个例子,事务 T1 更新数据 X,对 X 执行 Update 操作,从 10 更新到 20,对应的 Redo 日志为 \,Undo 日志为 \。 **binlog 日志** 是 MySQL sever 层维护的一种二进制日志,是 MySQL 最重要的日志之一,它记录了所有的 DDL 和 DML 语句,除了数据查询语句 select、show 等,还包含语句所执行的消耗时间。 +和 redo 日志对应的 undo 日志,也叫 **撤消日志**,记录事务开始前数据的状态,当一些更改在执行一半时,发生意外而无法完成,就可以根据撤消日志恢复到更改之前的状态。举个例子,事务 T1 更新数据 X,对 X 执行 Update 操作,从 10 更新到 20,对应的 Redo 日志为 \,Undo 日志为 \。**binlog 日志** 是 MySQL sever 层维护的一种二进制日志,是 MySQL 最重要的日志之一,它记录了所有的 DDL 和 DML 语句,除了数据查询语句 select、show 等,还包含语句所执行的消耗时间。 -binlog 与 InnoDB 引擎中的 redo/undo log 不同,binlog 的主要目的是 **复制和恢复** ,用来记录对 MySQL 数据更新或潜在发生更新的 SQL 语句,并以事务日志的形式保存在磁盘中。binlog 主要应用在 MySQL 的主从复制过程中,MySQL 集群在 Master 端开启 binlog,Master 把它的二进制日志传递给 slaves 节点,再从节点回放来达到 master-slave 数据一致的目的。 +binlog 与 InnoDB 引擎中的 redo/undo log 不同,binlog 的主要目的是 **复制和恢复**,用来记录对 MySQL 数据更新或潜在发生更新的 SQL 语句,并以事务日志的形式保存在磁盘中。binlog 主要应用在 MySQL 的主从复制过程中,MySQL 集群在 Master 端开启 binlog,Master 把它的二进制日志传递给 slaves 节点,再从节点回放来达到 master-slave 数据一致的目的。 你可以连接到 MySQL 服务器,使用下面的命令查看真实的 binlog 数据: @@ -39,13 +39,13 @@ XA 是由 X/Open 组织提出的分布式事务规范,XA 规范主要定义了 ![分布式8.png](assets/Ciqah16n5eWABApiAACuPsJ6_T0711.png) **事务协调者(Transaction Manager** ),因为 XA 事务是基于两阶段提交协议的,所以需要有一个协调者,来保证所有的事务参与者都完成了准备工作,也就是 2PC 的第一阶段。如果事务协调者收到所有参与者都准备好的消息,就会通知所有的事务都可以提交,也就是 2PC 的第二阶段。 -在前面的内容中我们提到过,之所以需要引入事务协调者,是因为在分布式系统中,两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。协调者,也就是事务管理器控制着全局事务,管理事务生命周期,并协调资源。 **资源管理器(Resource Manager** ),负责控制和管理实际资源,比如数据库或 JMS 队列。 +在前面的内容中我们提到过,之所以需要引入事务协调者,是因为在分布式系统中,两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。协调者,也就是事务管理器控制着全局事务,管理事务生命周期,并协调资源。**资源管理器(Resource Manager** ),负责控制和管理实际资源,比如数据库或 JMS 队列。 目前,主流数据库都提供了对 XA 的支持,在 JMS 规范中,即 Java 消息服务(Java Message Service)中,也基于 XA 定义了对事务的支持。 ### XA 事务的执行流程 -XA 事务是两阶段提交的一种实现方式,根据 2PC 的规范,XA 将一次事务分割成了两个阶段,即 Prepare 和 Commit 阶段。 **Prepare 阶段** ,TM 向所有 RM 发送 prepare 指令,RM 接受到指令后,执行数据修改和日志记录等操作,然后返回可以提交或者不提交的消息给 TM。如果事务协调者 TM 收到所有参与者都准备好的消息,会通知所有的事务提交,然后进入第二阶段。 **Commit 阶段** ,TM 接受到所有 RM 的 prepare 结果,如果有 RM 返回是不可提交或者超时,那么向所有 RM 发送 Rollback 命令;如果所有 RM 都返回可以提交,那么向所有 RM 发送 Commit 命令,完成一次事务操作。 +XA 事务是两阶段提交的一种实现方式,根据 2PC 的规范,XA 将一次事务分割成了两个阶段,即 Prepare 和 Commit 阶段。**Prepare 阶段**,TM 向所有 RM 发送 prepare 指令,RM 接受到指令后,执行数据修改和日志记录等操作,然后返回可以提交或者不提交的消息给 TM。如果事务协调者 TM 收到所有参与者都准备好的消息,会通知所有的事务提交,然后进入第二阶段。**Commit 阶段**,TM 接受到所有 RM 的 prepare 结果,如果有 RM 返回是不可提交或者超时,那么向所有 RM 发送 Rollback 命令;如果所有 RM 都返回可以提交,那么向所有 RM 发送 Commit 命令,完成一次事务操作。 ### MySQL 如何实现 XA 规范 @@ -69,9 +69,9 @@ MySQL 外部 XA 主要应用在数据库代理层,实现对 MySQL 数据库的 当事务提交时,在 binlog 依赖的内部 XA 中,额外添加了 Xid 结构,binlog 有多种数据类型,包括以下三种: -- **statement 格式** ,记录为基本语句,包含 Commit -- **row 格式** ,记录为基于行 -- **mixed 格式** ,日志记录使用混合格式 +- **statement 格式**,记录为基本语句,包含 Commit +- **row 格式**,记录为基于行 +- **mixed 格式**,日志记录使用混合格式 不论是 statement 还是 row 格式,binlog 都会添加一个 XID_EVENT 作为事务的结束,该事件记录了事务的 ID 也就是 Xid,在 MySQL 进行崩溃恢复时根据 binlog 中提交的情况来决定如何恢复。 @@ -89,7 +89,7 @@ MySQL 外部 XA 主要应用在数据库代理层,实现对 MySQL 数据库的 如果是在第一步和第二步失败,则整个事务回滚;如果是在第三步失败,则 MySQL 在重启后会检查 XID 是否已经提交,若没有提交,也就是事务需要重新执行,就会在存储引擎中再执行一次提交操作,保障 redo log 和 binlog 数据的一致性,防止数据丢失。 -在实际执行中,还牵扯到操作系统缓存 Buffer 何时同步到文件系统中,所以 MySQL 支持用户自定义在 Commit 时如何将 log buffer 中的日志刷到 log file 中,通过变量 innodb_flush_log_at_trx_Commit 的值来决定。在 log buffer 中的内容称为 **脏日志** ,感兴趣的话可以查询资料了解下。 +在实际执行中,还牵扯到操作系统缓存 Buffer 何时同步到文件系统中,所以 MySQL 支持用户自定义在 Commit 时如何将 log buffer 中的日志刷到 log file 中,通过变量 innodb_flush_log_at_trx_Commit 的值来决定。在 log buffer 中的内容称为 **脏日志**,感兴趣的话可以查询资料了解下。 ### 总结 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25410\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25410\350\256\262.md" index 563833427..70f7c4b98 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25410\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25410\350\256\262.md" @@ -14,7 +14,7 @@ TCC 的具体流程如下图所示: ![1.png](assets/Ciqc1F6qgbmAC6GbAAJF3yzrcWs383.png) -**Try 阶段** :调用 Try 接口,尝试执行业务,完成所有业务检查,预留业务资源。 **Confirm 或 Cancel 阶段** :两者是互斥的,只能进入其中一个,并且都满足幂等性,允许失败重试。 +**Try 阶段** :调用 Try 接口,尝试执行业务,完成所有业务检查,预留业务资源。**Confirm 或 Cancel 阶段** :两者是互斥的,只能进入其中一个,并且都满足幂等性,允许失败重试。 - **Confirm 操作** :对业务系统做确认提交,确认执行业务操作,不做其他业务检查,只使用 Try 阶段预留的业务资源。 - **Cancel 操作** :在业务执行错误,需要回滚的状态下执行业务取消,释放预留资源。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25411\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25411\350\256\262.md" index c9130202a..1b8761691 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25411\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25411\350\256\262.md" @@ -47,7 +47,7 @@ insert into methodLock(method_name) values ('method_name'); 基于数据库实现分布式锁操作简单,但是并不是一个可以落地的方案,有很多地方需要优化。 -**存在单点故障风险** 数据库实现方式强依赖数据库的可用性,一旦数据库挂掉,则会导致业务系统不可用,为了解决这个问题,需要配置数据库主从机器,防止单点故障。 **超时无法失效** 如果一旦解锁操作失败,则会导致锁记录一直在数据库中,其他线程无法再获得锁,解决这个问题,可以添加独立的定时任务,通过时间戳对比等方式,删除超时数据。 **不可重入** 可重入性是锁的一个重要特性,以 Java 语言为例,常见的 Synchronize、Lock 等都支持可重入。在数据库实现方式中,同一个线程在没有释放锁之前无法再次获得该锁,因为数据已经存在,再次插入会失败。实现可重入,需要改造加锁方法,额外存储和判断线程信息,不阻塞获得锁的线程再次请求加锁。 **无法实现阻塞** +**存在单点故障风险** 数据库实现方式强依赖数据库的可用性,一旦数据库挂掉,则会导致业务系统不可用,为了解决这个问题,需要配置数据库主从机器,防止单点故障。**超时无法失效** 如果一旦解锁操作失败,则会导致锁记录一直在数据库中,其他线程无法再获得锁,解决这个问题,可以添加独立的定时任务,通过时间戳对比等方式,删除超时数据。**不可重入** 可重入性是锁的一个重要特性,以 Java 语言为例,常见的 Synchronize、Lock 等都支持可重入。在数据库实现方式中,同一个线程在没有释放锁之前无法再次获得该锁,因为数据已经存在,再次插入会失败。实现可重入,需要改造加锁方法,额外存储和判断线程信息,不阻塞获得锁的线程再次请求加锁。**无法实现阻塞** 其他线程在请求对应方法时,插入数据失败会直接返回,不会阻塞线程,如果需要阻塞其他线程,需要不断的重试 insert 操作,直到数据插入成功,这个操作是服务器和数据库资源的极大浪费。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25412\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25412\350\256\262.md" index 1405ca781..5eab46f0d 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25412\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25412\350\256\262.md" @@ -10,11 +10,11 @@ 一般来说,生产环境可用的分布式锁需要满足以下几点: -- **互斥性** ,互斥是锁的基本特征,同一时刻只能有一个线程持有锁,执行临界操作; -- **超时释放** ,超时释放是锁的另一个必备特性,可以对比 MySQL InnoDB 引擎中的 innodb_lock_wait_timeout 配置,通过超时释放,防止不必要的线程等待和资源浪费; -- **可重入性** ,在分布式环境下,同一个节点上的同一个线程如果获取了锁之后,再次请求还是可以成功; -- **高性能和高可用** ,加锁和解锁的开销要尽可能的小,同时也需要保证高可用,防止分布式锁失效; -- **支持阻塞和非阻塞性** ,对比 Java 语言中的 wait() 和 notify() 等操作,这个一般是在业务代码中实现,比如在获取锁时通过 while(true) 或者轮询来实现阻塞操作。 +- **互斥性**,互斥是锁的基本特征,同一时刻只能有一个线程持有锁,执行临界操作; +- **超时释放**,超时释放是锁的另一个必备特性,可以对比 MySQL InnoDB 引擎中的 innodb_lock_wait_timeout 配置,通过超时释放,防止不必要的线程等待和资源浪费; +- **可重入性**,在分布式环境下,同一个节点上的同一个线程如果获取了锁之后,再次请求还是可以成功; +- **高性能和高可用**,加锁和解锁的开销要尽可能的小,同时也需要保证高可用,防止分布式锁失效; +- **支持阻塞和非阻塞性**,对比 Java 语言中的 wait() 和 notify() 等操作,这个一般是在业务代码中实现,比如在获取锁时通过 while(true) 或者轮询来实现阻塞操作。 可以看到,实现一个相对完备的分布式锁,并不是锁住资源就可以了,还需要满足一些额外的特性,否则会在业务开发中出现各种各样的问题。 @@ -108,7 +108,7 @@ nx 表示仅在键不存在时设置,这样可以在同一时间内完成设 #### Redlock 算法的流程 -**Redlock 算法** 是在单 Redis 节点基础上引入的 **高可用模式** ,Redlock 基于 N 个完全独立的 Redis 节点,一般是 **大于 3 的奇数个** (通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。 +**Redlock 算法** 是在单 Redis 节点基础上引入的 **高可用模式**,Redlock 基于 N 个完全独立的 Redis 节点,一般是 **大于 3 的奇数个** (通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。 假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作: diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25416\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25416\350\256\262.md" index e99b2a998..98bc70b34 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25416\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25416\350\256\262.md" @@ -2,7 +2,7 @@ 你好,欢迎来到第 14 课时,本课时主要讲解如何实现服务注册与发现。 -在分布式服务中, **服务注册和发现** 是一个特别重要的概念,为什么需要服务注册和发现?常用的服务发现组件有哪些?服务注册和发现对一致性有哪些要求呢?下面我们就来学习服务发现相关的知识。 +在分布式服务中,**服务注册和发现** 是一个特别重要的概念,为什么需要服务注册和发现?常用的服务发现组件有哪些?服务注册和发现对一致性有哪些要求呢?下面我们就来学习服务发现相关的知识。 ### 为什么需要服务注册和发现 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25417\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25417\350\256\262.md" index 2b701a54c..d6d34e446 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25417\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25417\350\256\262.md" @@ -18,7 +18,7 @@ **分布式调用跟踪技术** 就是解决上面的业务问题,即通过调用链的方式,把一次请求调用过程完整的串联起来,这样就实现了对请求调用路径的监控。 -分布式调用链其实就是将一次分布式请求还原成 **调用链路** ,显式的在后端查看一次分布式请求的调用情况,比如各个节点上的耗时、请求具体打到了哪台机器上、每个服务节点的请求状态等。 +分布式调用链其实就是将一次分布式请求还原成 **调用链路**,显式的在后端查看一次分布式请求的调用情况,比如各个节点上的耗时、请求具体打到了哪台机器上、每个服务节点的请求状态等。 一般来说,分布式调用跟踪可以应用在以下的场景中。 @@ -33,7 +33,7 @@ Dapper 用 Span 来表示一个服务调用开始和结束的时间,也就是时间区间,并记录了 Span 的名称以及每个 Span 的 ID 和父 ID,如果一个 Span 没有父 ID 则被称之为 Root Span。 -一个请求到达应用后所调用的所有服务,以及所有服务组成的调用链就像是一个树结构,追踪这个调用链路得到的树结构称之为 **Trace** ,所有的 Span 都挂在一个特定的 Trace 上,共用一个 TraceId。 +一个请求到达应用后所调用的所有服务,以及所有服务组成的调用链就像是一个树结构,追踪这个调用链路得到的树结构称之为 **Trace**,所有的 Span 都挂在一个特定的 Trace 上,共用一个 TraceId。 ![image](assets/CgqCHl7M6aGALudMAAG903WelvM769.png) diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25418\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25418\350\256\262.md" index 92b087a8c..008439432 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25418\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25418\350\256\262.md" @@ -30,11 +30,11 @@ 一个合格的分布式配置管理系统,除了配置发布和推送,还需要满足以下的特性: -- **高可用性** ,服务器集群应该无单点故障,只要集群中还有存活的节点,就能提供服务; -- **容错性** ,保证在配置平台不可用时,也不影响客户端的正常运行; -- **高性能** ,对于配置平台,应该是尽可能低的性能开销,不能因为获取配置给应用带来不可接受的性能损耗; -- **可靠存储** ,包括数据的备份容灾,一致性等,尽可能保证不丢失配置数据; -- **实时生效** ,对于配置的变更,客户端应用能够及时感知。 +- **高可用性**,服务器集群应该无单点故障,只要集群中还有存活的节点,就能提供服务; +- **容错性**,保证在配置平台不可用时,也不影响客户端的正常运行; +- **高性能**,对于配置平台,应该是尽可能低的性能开销,不能因为获取配置给应用带来不可接受的性能损耗; +- **可靠存储**,包括数据的备份容灾,一致性等,尽可能保证不丢失配置数据; +- **实时生效**,对于配置的变更,客户端应用能够及时感知。 可以看到,一个好的配置管理系统,不只是提供配置发布和推送就可以,还有许多高级特性的要求。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25421\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25421\350\256\262.md" index 6e148646f..c0011a618 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25421\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25421\350\256\262.md" @@ -14,11 +14,11 @@ Dubbo 是阿里开源的一个分布式服务框架,目的是支持高性能 包括了下面几个角色: -- **Provider** ,也就是服务提供者,通过 Container 容器来承载; -- **Consumer** ,调用远程服务的服务消费方; -- **Registry** ,服务注册中心和发现中心; -- **Monitor** ,Dubbo 服务调用的控制台,用来统计和管理服务的调用信息; -- **Container** ,服务运行的容器,比如 Tomcat 等。 +- **Provider**,也就是服务提供者,通过 Container 容器来承载; +- **Consumer**,调用远程服务的服务消费方; +- **Registry**,服务注册中心和发现中心; +- **Monitor**,Dubbo 服务调用的控制台,用来统计和管理服务的调用信息; +- **Container**,服务运行的容器,比如 Tomcat 等。 #### 应用特性 @@ -66,11 +66,11 @@ Spring Cloud 目前主要的解决方案包括 Spring Cloud Netflix 系列,以 Spring Cloud 典型的应用如下: -- **配置中心** ,一般使用 Spring Cloud Config 实现,服务发现也可以管理部分配置; -- **服务发现** ,使用 Eureka 实现,也可以扩展 Consul 等; -- **API 网关** ,使用 Zuul 实现,另外还有 Kong 等应用; -- **负载均衡** ,使用 Ribbon 实现,也可以选择 Feign; -- **限流降级** ,使用 Hystrix 实现熔断机制,也可以选择 Sentinel。 +- **配置中心**,一般使用 Spring Cloud Config 实现,服务发现也可以管理部分配置; +- **服务发现**,使用 Eureka 实现,也可以扩展 Consul 等; +- **API 网关**,使用 Zuul 实现,另外还有 Kong 等应用; +- **负载均衡**,使用 Ribbon 实现,也可以选择 Feign; +- **限流降级**,使用 Hystrix 实现熔断机制,也可以选择 Sentinel。 ### Dubbo 和 Spring Cloud 对比 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25424\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25424\350\256\262.md" index 6d073734d..7520023ea 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25424\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25424\350\256\262.md" @@ -4,7 +4,7 @@ 在上一课时中讲到了读写分离,读写分离优化了互联网 **读多写少** 场景下的性能问题,考虑一个业务场景,如果读库的数据规模非常大,除了增加多个从库之外,还有其他的手段吗? -方法总比问题多,实现 **数据库高可用** ,还有另外一个撒手锏,就是 **分库分表** ,分库分表也是面试的常客,今天一起来看一下相关的知识。 +方法总比问题多,实现 **数据库高可用**,还有另外一个撒手锏,就是 **分库分表**,分库分表也是面试的常客,今天一起来看一下相关的知识。 ### 分库分表的背景 @@ -18,7 +18,7 @@ > 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。 -**基于阿里巴巴的海量业务数据和多年实践** ,这一条数据库规约,可以认为是数据库应用中的一个最佳实践。也就是在新业务建表规划时,或者当前数据库单表已经超过对应的限制,可以进行分库分表,同时也要避免过度设计。因为分库分表虽然可以提高性能,但是盲目地进行分库分表只会增加系统的复杂度。 +**基于阿里巴巴的海量业务数据和多年实践**,这一条数据库规约,可以认为是数据库应用中的一个最佳实践。也就是在新业务建表规划时,或者当前数据库单表已经超过对应的限制,可以进行分库分表,同时也要避免过度设计。因为分库分表虽然可以提高性能,但是盲目地进行分库分表只会增加系统的复杂度。 #### 数据库连接限制 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25426\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25426\350\256\262.md" index 9d6d2fd6c..564536cc1 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25426\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25426\350\256\262.md" @@ -6,11 +6,11 @@ 假设这样一个业务场景,现在要设计电商网站的订单数据库模块,经过对业务增长的估算,预估三年后,数据规模可能达到 6000 万,每日订单数会超过 10 万。 -首先选择 **存储实现** ,订单作为电商业务的核心数据,应该尽量避免数据丢失,并且对数据一致性有强要求,肯定是选择支持事务的关系型数据库,比如使用 MySQL 及 InnoDB 存储引擎。 +首先选择 **存储实现**,订单作为电商业务的核心数据,应该尽量避免数据丢失,并且对数据一致性有强要求,肯定是选择支持事务的关系型数据库,比如使用 MySQL 及 InnoDB 存储引擎。 -然后是 **数据库的高可用** ,订单数据是典型读多写少的数据,不仅要面向消费者端的读请求,内部也有很多上下游关联的业务模块在调用,针对订单进行数据查询的调用量会非常大。基于这一点,我们在业务中配置基于主从复制的读写分离,并且设置多个从库,提高数据安全。 +然后是 **数据库的高可用**,订单数据是典型读多写少的数据,不仅要面向消费者端的读请求,内部也有很多上下游关联的业务模块在调用,针对订单进行数据查询的调用量会非常大。基于这一点,我们在业务中配置基于主从复制的读写分离,并且设置多个从库,提高数据安全。 -最后是 **数据规模** ,6000 万的数据量,显然超出了单表的承受范围,参考《阿里巴巴 Java 开发手册》中「单表行数超过 500 万行」进行分表的建议,此时需要考虑进行分库分表,那么如何设计路由规则和拆分方案呢?接下来会对此展开讨论。 +最后是 **数据规模**,6000 万的数据量,显然超出了单表的承受范围,参考《阿里巴巴 Java 开发手册》中「单表行数超过 500 万行」进行分表的建议,此时需要考虑进行分库分表,那么如何设计路由规则和拆分方案呢?接下来会对此展开讨论。 ### 路由规则与扩容方案 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25427\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25427\350\256\262.md" index bd75ece2e..90a2cbacb 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25427\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25427\350\256\262.md" @@ -8,7 +8,7 @@ 关系型数据库通过关系模型来组织数据,在关系型数据库当中一个表就是一个模型,一个关系数据库可以包含多个表,不同数据表之间的联系反映了关系约束。 -不知道你是否应用过 ER 图?在早期的软件工程中,数据表的创建都会通过 ER 图来定义,ER 图(Entity Relationship Diagram)称为 **实体-联系图** ,包括实体、属性和关系三个核心部分。 +不知道你是否应用过 ER 图?在早期的软件工程中,数据表的创建都会通过 ER 图来定义,ER 图(Entity Relationship Diagram)称为 **实体-联系图**,包括实体、属性和关系三个核心部分。 下面是在电商领域中,一个简化的会员、商品和订单的 ER 图: diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25428\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25428\350\256\262.md" index adf8299fb..352f7d5c7 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25428\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25428\350\256\262.md" @@ -32,7 +32,7 @@ ElasticSearch 是由 Elastic 公司创建的,除了 ElasticSearch,Elastic ### 索引是如何建立的 -ElasticSearch 存储的单元是 **索引** ,这一点区别于很多关系型数据库和 NoSQL 数据库,比如关系型数据库是按照关系表的形式组织数据,大部分 NoSQL 数据库是 K-Value 的键值对方式。 +ElasticSearch 存储的单元是 **索引**,这一点区别于很多关系型数据库和 NoSQL 数据库,比如关系型数据库是按照关系表的形式组织数据,大部分 NoSQL 数据库是 K-Value 的键值对方式。 ElasticSearch 存储的基本单元是索引,那么索引是如何创建的呢? diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25430\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25430\350\256\262.md" index 99b013e1c..0dd7b76b3 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25430\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25430\350\256\262.md" @@ -28,7 +28,7 @@ 异步化是一个非常重要的机制,在处理高并发、高可用等系统设计时,如果不需要或者限制于系统承载能力,不能立即处理消息,此时就可以应用消息队列,将请求异步化。 -异步处理的一个典型场景是 **流量削峰** ,我们用电商的秒杀场景来举例。秒杀抢购的流量峰值是很高的,很多时候服务并不能承载这么高的瞬间流量,于是可以引入消息队列,结合限流工具,对超过系统阈值的请求,在消息队列中暂存,等待流量高峰过去以后再进行处理。 +异步处理的一个典型场景是 **流量削峰**,我们用电商的秒杀场景来举例。秒杀抢购的流量峰值是很高的,很多时候服务并不能承载这么高的瞬间流量,于是可以引入消息队列,结合限流工具,对超过系统阈值的请求,在消息队列中暂存,等待流量高峰过去以后再进行处理。 #### 请求缓冲 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25433\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25433\350\256\262.md" index 7bbc28136..43611d22a 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25433\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25433\350\256\262.md" @@ -4,7 +4,7 @@ ### 对业务幂等的理解 -首先明确一下, **幂等并不是问题** ,而是业务的一个特性。幂等问题体现在对于不满足幂等性的业务,在消息重复消费,或者远程服务调用失败重试时,出现的数据不一致,业务数据错乱等现象。 +首先明确一下,**幂等并不是问题**,而是业务的一个特性。幂等问题体现在对于不满足幂等性的业务,在消息重复消费,或者远程服务调用失败重试时,出现的数据不一致,业务数据错乱等现象。 幂等最早是一个数学上的概念,幂等函数指的是对一个函数或者方法,使用相同的参数执行多次,数据结果是一致的。 @@ -108,7 +108,7 @@ RocketMQ 支持 At least once 的投递语义,也就是保证每个消息至 西方有一句谚语:当你有了一个锤子,你看什么都像钉子。在我刚开始学习分布式系统时,学习了各种中间件,每个中间件都希望能用上,这其实脱离了系统设计的初衷。 -课程内容到这里,已经展开了许多分布式系统的常用组件,提到这个谚语,主要是希望你在做技术方案,特别是做分布式系统设计方案时,不是为了设计而设计。方案设计的目的是 **实现业务目标** ,并不是在系统中加入各种高大上的中间件,这个方案就是正确的。 +课程内容到这里,已经展开了许多分布式系统的常用组件,提到这个谚语,主要是希望你在做技术方案,特别是做分布式系统设计方案时,不是为了设计而设计。方案设计的目的是 **实现业务目标**,并不是在系统中加入各种高大上的中间件,这个方案就是正确的。 我之前读过一本《系统之美》的图书,从复杂系统的角度来看,系统中的元素越多,为了维持系统的平衡,需要付出的势能必然也越大。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25435\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25435\350\256\262.md" index eaa8f2519..9850ae7ec 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25435\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25435\350\256\262.md" @@ -20,7 +20,7 @@ Kafka 实现高性能的手段,是面试中经常被问到的问题。下面 Kafka 消息是存储在磁盘上的,大家都知道,普通的机械磁盘读取是比较慢的,那 Kafka 文件在磁盘上,如何实现高性能的读写呢? -Kafka 对磁盘的应用,得益于消息队列的存储特性。与普通的关系型数据库、各类 NoSQL 数据库等不同,消息队列对外提供的主要方法是 **生产和消费** ,不涉及数据的 CRUD。所以在写入磁盘时,可以使用顺序追加的方式来避免低效的磁盘寻址。 +Kafka 对磁盘的应用,得益于消息队列的存储特性。与普通的关系型数据库、各类 NoSQL 数据库等不同,消息队列对外提供的主要方法是 **生产和消费**,不涉及数据的 CRUD。所以在写入磁盘时,可以使用顺序追加的方式来避免低效的磁盘寻址。 我们知道,数据存储在硬盘上,而硬盘有机械硬盘和固态硬盘之分。机械硬盘成本低、容量大,但每次读写都会寻址,再写入数据(在机械硬盘上,寻址是一个物理动作,耗时最大);SSD 固态硬盘性能很高,有着非常低的寻道时间和存取时间,但成本也特别高。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25438\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25438\350\256\262.md" index 42e8e81ae..0867a80df 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25438\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25438\350\256\262.md" @@ -30,11 +30,11 @@ Application Cache 是基于 manifest 文件实现的缓存机制,浏览器会 大多数业务请求都是通过 HTTP/HTTPS 协议实现的,它们工作在 TCP 协议之上,多次握手以后,浏览器和服务器建立 TCP 连接,然后进行数据传输,在传输过程中,会涉及多层缓存,比如 CDN 缓存等。 -网络中缓存包括 CDN 缓存,CDN(Content Delivery Network,内容分发网络)实现的关键包括 **内容存储** 和 **内容分发** ,内容存储就是对数据的缓存功能,内容分发则是 CDN 节点支持的负载均衡。 +网络中缓存包括 CDN 缓存,CDN(Content Delivery Network,内容分发网络)实现的关键包括 **内容存储** 和 **内容分发**,内容存储就是对数据的缓存功能,内容分发则是 CDN 节点支持的负载均衡。 前端请求在经过 DNS 之后,首先会被指向网络中最近的 CDN 节点,该节点从真正的应用服务器获取资源返回给前端,同时将静态信息缓存。在新的请求过来以后,就可以只请求 CDN 节点的数据,同时 CDN 节点也可以和服务器之间同步更新数据。 -网络缓存还包括 **负载均衡中的缓存** ,负载均衡服务器主要实现的是请求路由,也就是负载均衡功能;也可以实现部分数据的缓存,比如一些配置信息等很少修改的数据。 +网络缓存还包括 **负载均衡中的缓存**,负载均衡服务器主要实现的是请求路由,也就是负载均衡功能;也可以实现部分数据的缓存,比如一些配置信息等很少修改的数据。 目前业务开发中大部分负载均衡都是通过 Nginx 实现的,用户请求在达到应用服务器之前,会先访问 Nginx 负载均衡器。如果发现有缓存信息,则直接返回给用户,如果没有发现缓存信息,那么 Nginx 会 回源 到应用服务器获取信息。 @@ -42,7 +42,7 @@ Application Cache 是基于 manifest 文件实现的缓存机制,浏览器会 前端请求经过负载均衡落到 Web 服务器之后,就进入服务端缓存,服务端缓存是缓存的重点,也是业务开发平时打交道最多的缓存。它还可以进一步分为 **本地缓存** 和 **外部缓存** 。 -本地缓存也可以叫作 **应用内缓存** ,比如 Guava 实现的各级缓存,或者 Java 语言中使用各类 Map 结构实现的数据存储,都属于本地缓存的范畴。应用内缓存的特点是随着服务重启后失效,作用时间很短,好处是应用比较灵活。 +本地缓存也可以叫作 **应用内缓存**,比如 Guava 实现的各级缓存,或者 Java 语言中使用各类 Map 结构实现的数据存储,都属于本地缓存的范畴。应用内缓存的特点是随着服务重启后失效,作用时间很短,好处是应用比较灵活。 外部缓存就是我们平常应用的 Redis、Memchaed 等 NoSQL 存储的分布式缓存,它也是在系统设计中对整体性能提升最大的缓存。但如果外部缓存使用不当,则会导致缓存穿透、缓存雪崩等业务问题,关于如何处理这类问题,我们将在下一课时进行分析。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25440\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25440\350\256\262.md" index 4ac9bd0cb..7f5ff2424 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25440\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25440\350\256\262.md" @@ -66,7 +66,7 @@ 再看一个实际应用中的问题,多级缓存如何更新? -多级缓存是系统中一个常用的设计,我们在第 32 课时“缓存分类”中提过,服务端缓存分为 **应用内缓存** 和 **外部缓存** ,比如在电商的商品信息展示中,可能会有多级缓存协同。 +多级缓存是系统中一个常用的设计,我们在第 32 课时“缓存分类”中提过,服务端缓存分为 **应用内缓存** 和 **外部缓存**,比如在电商的商品信息展示中,可能会有多级缓存协同。 那么多级缓存之间如何同步数据呢? diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25441\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25441\350\256\262.md" index f640e9505..b2e6e954f 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25441\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25441\350\256\262.md" @@ -10,9 +10,9 @@ 在操作系统的页面空间中,对应淘汰旧页面的机制不同,所以会有不同页面调度方法,常见的有 FIFO、LRU、LFU 过期策略: -- **FIFO(First In First Out,先进先出)** ,根据缓存被存储的时间,离当前最远的数据优先被淘汰; -- **LRU(Least Recently Used,最近最少使用)** ,根据最近被使用的时间,离当前最远的数据优先被淘汰; -- **LFU(Least Frequently Used,最不经常使用)** ,在一段时间内,缓存数据被使用次数最少的会被淘汰。 +- **FIFO(First In First Out,先进先出)**,根据缓存被存储的时间,离当前最远的数据优先被淘汰; +- **LRU(Least Recently Used,最近最少使用)**,根据最近被使用的时间,离当前最远的数据优先被淘汰; +- **LFU(Least Frequently Used,最不经常使用)**,在一段时间内,缓存数据被使用次数最少的会被淘汰。 这三种策略也是经典的缓存淘汰策略,大部分缓存应用模型,都是基于这几种策略实现的。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25445\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25445\350\256\262.md" index e080e4029..d5b2716e2 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25445\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25445\350\256\262.md" @@ -26,15 +26,15 @@ 回到电商大促,我们结合电商的业务场景,讨论一下在电商大促时,如果要保证高可用性,可以从哪些方面入手呢? -第一个特点是 **海量用户请求** , **万倍日常流量** ,大促期间的流量是平时的千百倍甚至万倍,从这一点来讲,要做好容量规划,在平时的演练中需做好调度。目前大部分公司的部署都是应用 Docker 容器化编排,分布式需要快速扩展集群,而容器化编排操作简单,可以快速扩展实例,可以说,容器化和分布式是天生一对,提供了一个很好的解决方案。 +第一个特点是 **海量用户请求**,**万倍日常流量**,大促期间的流量是平时的千百倍甚至万倍,从这一点来讲,要做好容量规划,在平时的演练中需做好调度。目前大部分公司的部署都是应用 Docker 容器化编排,分布式需要快速扩展集群,而容器化编排操作简单,可以快速扩展实例,可以说,容器化和分布式是天生一对,提供了一个很好的解决方案。 -第二点是 **流量突增** ,是典型的秒杀系统请求曲线,我们都知道秒杀系统的流量是在瞬间达到一个峰值,流量曲线非常陡峭。为了吸引用户下单,电商大促一般都会安排若干场的秒杀抢购,为了应对这个特性,可以通过独立热点集群部署、消息队列削峰、相关活动商品预热缓存等方案来解决。 +第二点是 **流量突增**,是典型的秒杀系统请求曲线,我们都知道秒杀系统的流量是在瞬间达到一个峰值,流量曲线非常陡峭。为了吸引用户下单,电商大促一般都会安排若干场的秒杀抢购,为了应对这个特性,可以通过独立热点集群部署、消息队列削峰、相关活动商品预热缓存等方案来解决。 关于活动商品预热,这里简单展开说一下,秒杀活动都会提前给用户预告商品,为了避免抢购时服务不可用,我们可以提前把相关商品数据都加载到缓存中,通过缓存来支撑海量请求。 在模块六“分布式缓存”中,我花了很多篇幅对缓存的高可用、缓存命中率等知识点做了分享,你可以回顾并思考一下,秒杀活动中如何预热商品数据,可以更好地支撑前端请求? -最后一点是 **高并发** , **支撑海量用户请求** ,对于业务系统来说就是高并发,QPS 会是平时的几百倍甚至更高。开发经验比较多的同学都知道,如果在系统设计时没有考虑过高并发的情况,即使业务系统平时运行得好好的,如果并发量一旦增加,就会经常出现各种诡异的业务问题。比如,在电商业务中,可能会出现用户订单丢失、库存扣减异常、超卖等问题。 +最后一点是 **高并发**,**支撑海量用户请求**,对于业务系统来说就是高并发,QPS 会是平时的几百倍甚至更高。开发经验比较多的同学都知道,如果在系统设计时没有考虑过高并发的情况,即使业务系统平时运行得好好的,如果并发量一旦增加,就会经常出现各种诡异的业务问题。比如,在电商业务中,可能会出现用户订单丢失、库存扣减异常、超卖等问题。 应对高并发,需要我们在前期系统设计时,考虑到并发系统容易出现的问题,比如在 Java 语言中,高并发时的 ThreadLocal 数据异常,数据库高并发的锁冲突、死锁等问题点,进行针对性的设计,避免出现业务异常。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25447\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25447\350\256\262.md" index 16b1eb091..1d67b648e 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25447\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25447\350\256\262.md" @@ -6,7 +6,7 @@ 我们在第 39 课时提过服务降级是电商大促等高并发场景的常见稳定性手段,那你有没有想过,为什么在大促时要开启降级,平时不去应用呢? -在大促场景下,请求量剧增,可我们的系统资源是有限的,服务器资源是企业的固定成本,这个成本不可能无限扩张,所以说, **降级是解决系统资源不足和海量业务请求之间的矛盾** 。 +在大促场景下,请求量剧增,可我们的系统资源是有限的,服务器资源是企业的固定成本,这个成本不可能无限扩张,所以说,**降级是解决系统资源不足和海量业务请求之间的矛盾** 。 降级的具体实现手段是,在暴增的流量请求下,对一些非核心流程业务、非关键业务,进行有策略的放弃,以此来释放系统资源,保证核心业务的正常运行。我们在第 34 课时中提过二八策略,换一个角度,服务降级就是尽量避免这种系统资源分配的不平衡,打破二八策略,让更多的机器资源,承载主要的业务请求。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25449\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25449\350\256\262.md" index da54d9185..f85accce9 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25449\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230 45 \350\256\262/\347\254\25449\350\256\262.md" @@ -70,7 +70,7 @@ load15 #### 系统运行指标 -系统指标主要监控服务运行时状态、JVM 指标等,这些监控项都可以在 Open-Falcon 等组件中找到,比如 JVM 的 block 线程数,具体在 Falcon 中指标是 jvm.thread.blocked.count。下面我只是列举了部分监控指标,具体的你可以根据自己工作中应用的监控组件来进行取舍。 **监控项** **指标描述** **说明** JVM 线程数 +系统指标主要监控服务运行时状态、JVM 指标等,这些监控项都可以在 Open-Falcon 等组件中找到,比如 JVM 的 block 线程数,具体在 Falcon 中指标是 jvm.thread.blocked.count。下面我只是列举了部分监控指标,具体的你可以根据自己工作中应用的监控组件来进行取舍。**监控项** **指标描述** **说明** JVM 线程数 线程总数量 @@ -118,7 +118,7 @@ GC 的时间 #### 基础组件指标 -在基础组件这里,主要包括对数据库、缓存、消息队列的监控,下面我以数据库为例进行描述,虽然各个中间件对数据库监控的侧重点不同,但是基本都会包括以下的监控项。如果你对这部分指标感兴趣,我建议你咨询一下公司里的 DBA 了解更多的细节。 **监控项** **指标描述** +在基础组件这里,主要包括对数据库、缓存、消息队列的监控,下面我以数据库为例进行描述,虽然各个中间件对数据库监控的侧重点不同,但是基本都会包括以下的监控项。如果你对这部分指标感兴趣,我建议你咨询一下公司里的 DBA 了解更多的细节。**监控项** **指标描述** 写入 QPS diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25400\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25400\350\256\262.md" index 980ca8bb9..e5d01a7ad 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25400\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25400\350\256\262.md" @@ -12,7 +12,7 @@ **我接手拉勾的基础组件研发工作后,开始站在全局去思考解决这个问题的方法,最终通过不断地探索、实践,在拉勾内部打造了一套切实可行的可观测系统,对拉勾日均上亿级别的请求进行有效观测。** 这套系统可以很好地应对问题,并早于用户反馈解决。它不仅提高了用户体验,也提高了拉勾的口碑,对拉勾的发展起到了不可忽视的作用,我在拉勾内部的技术宣讲中,也经常提到可观测系统对拉勾的帮助,以及它的重要性。 -链路追踪通常与可观测性一起出现,它为可观测性提供了强有力的数据支持,也是可观测性中必不可少的一环。通过对这部分数据源的可视化,开发人员可以看到链路中每一环的执行流程。链路追踪通常还可以和链路分析结合在一起,除了链路追踪,还可以进行性能诊断并给出优化建议,为可观测性提供了多维度的数据和展现方式的支持。 **随着微服务架构的持续演进,应用和服务器的数量不断增加,调用关系也越来越复杂,能否有效地对系统进行观测就变得至关重要。** 此时,国内的大厂都逐渐有了自己的一套可观测系统,比如阿里的“鹰眼”。大厂对可观测性越发重视,与之相关的岗位的薪资也水涨船高。 +链路追踪通常与可观测性一起出现,它为可观测性提供了强有力的数据支持,也是可观测性中必不可少的一环。通过对这部分数据源的可视化,开发人员可以看到链路中每一环的执行流程。链路追踪通常还可以和链路分析结合在一起,除了链路追踪,还可以进行性能诊断并给出优化建议,为可观测性提供了多维度的数据和展现方式的支持。**随着微服务架构的持续演进,应用和服务器的数量不断增加,调用关系也越来越复杂,能否有效地对系统进行观测就变得至关重要。** 此时,国内的大厂都逐渐有了自己的一套可观测系统,比如阿里的“鹰眼”。大厂对可观测性越发重视,与之相关的岗位的薪资也水涨船高。 ![屏幕截图](assets/CgqCHl8nht6AOb5_AAA9A9g3YuE969.png) @@ -24,7 +24,7 @@ 从招聘的需求中,我们可以明确看到“熟悉分布式系统的开发原则”“优化故障处理流程”“提升排障效能”等职位要求。阿里全链路监控系统“鹰眼”的成功,已经证明了可观测系统对这些问题的解决能力,可观测性也必然会在系统愈发复杂的未来变得更加实用。 -为了让你能够系统地了解可观测性,并将它集成到自己公司的系统中,我决定将我的实践经验系统性地分享给你,希望能够帮助你建立对“可观测性”的全面理解,在工作中少走弯路,并能够更好地规划自己的技术成长路径。 **那么,如果没有很好的可观测系统,会存在哪些问题呢?** #### 1. 无法有效地处理问题 +为了让你能够系统地了解可观测性,并将它集成到自己公司的系统中,我决定将我的实践经验系统性地分享给你,希望能够帮助你建立对“可观测性”的全面理解,在工作中少走弯路,并能够更好地规划自己的技术成长路径。**那么,如果没有很好的可观测系统,会存在哪些问题呢?** #### 1. 无法有效地处理问题 开发人员,职责是编写好业务代码,并保证其持续且稳定地运行,但如何实现这个职责却是一大难题。如果运维人员告诉你线上出现了问题,但你翻遍日志也找不出问题的原因;如果用户反馈说出现了问题,但你测试没有任何异常,这个问题就像定时炸弹一样被埋了下来,不知道什么时候就会爆炸。可观测性可以通过一套完整的数据观测系统帮助你更快且更有效地发现问题、解决问题,可以说是保障线上稳定的关键。 @@ -34,7 +34,7 @@ #### 3. 无法有效地利用系统资源 -由于系统的数量越来越多,相应机器的资源管控也越来越复杂,同时每个服务之间还存在着一定的依赖关系。因此,我们很难了解每台机器上的资源是否都被充分利用了。而可观测性就可以帮助你分析出哪些服务利用率不够,哪些服务可以进行资源缩减。 **综上不难发现,“可观测性”所解决的核心是效率问题。无论是处理问题、了解系统还是分配系统资源,“可观测性”都可以提高从公司到个人的整体效率。** 这也是为什么,越来越多的公司开始重视可观测性。 +由于系统的数量越来越多,相应机器的资源管控也越来越复杂,同时每个服务之间还存在着一定的依赖关系。因此,我们很难了解每台机器上的资源是否都被充分利用了。而可观测性就可以帮助你分析出哪些服务利用率不够,哪些服务可以进行资源缩减。**综上不难发现,“可观测性”所解决的核心是效率问题。无论是处理问题、了解系统还是分配系统资源,“可观测性”都可以提高从公司到个人的整体效率。** 这也是为什么,越来越多的公司开始重视可观测性。 ### 可观测性≠监控 @@ -42,7 +42,7 @@ 1. **核心不同。** 监控是以运维为核心的系统,它通过各项指标数据来定义整体的运行状态、失败情况等;观测则是以开发为核心的系统,除了监控,它还会对整个系统进行分析。很多时候,运维给出的错误数据,只能算是提出了问题,但可观测性除了提出问题,还可以清晰地给出导致错误的原因。 2. **维度不同。** 监控是从外围的角度,通过各种指标(机器CPU、负载、网络的维度等)来判断整个系统的执行情况;而可观测性则在这种外部指标的基础上,以应用内的各个维度来展开推测,最后,通过二者结合的数据更加真实地反映出我们应用的运行情况。 -3. **展现的信息不同。** 有些系统在正常运行时十分稳定,但是一到高并发的时候就会出现问题。此时,监控只能汇报问题出现的状况,但可观测性就可以很好地通过图形化的方式告知我们问题的原因,而不是由我们用经验来猜测。它可以将未知或者不确定的信息展现出来,使我们可以更好地了解系统的整体情况。 **可观测性打破了开发和运维原有的问题解决方式,不再是运维发现问题开发解决,而是以开发为中心。** 开发人员以什么样的形式去暴露关键的指标等,是与业务开发中的可扩展性和高可用性同等重要的内容。 +3. **展现的信息不同。** 有些系统在正常运行时十分稳定,但是一到高并发的时候就会出现问题。此时,监控只能汇报问题出现的状况,但可观测性就可以很好地通过图形化的方式告知我们问题的原因,而不是由我们用经验来猜测。它可以将未知或者不确定的信息展现出来,使我们可以更好地了解系统的整体情况。**可观测性打破了开发和运维原有的问题解决方式,不再是运维发现问题开发解决,而是以开发为中心。** 开发人员以什么样的形式去暴露关键的指标等,是与业务开发中的可扩展性和高可用性同等重要的内容。 ### 课程设置 @@ -56,7 +56,7 @@ ### 本课程适合你吗? -如果你是中高级的 **开发人员** ,如果你对系统调优有兴趣、希望从事监控相关的工作、想要了解分布式系统;如果你是 **运维人员** ,想要提高系统资源利用率,想要推动公司监控体系建立并制定一套规范的告警流程,想要帮助开发提高解决问题的效率,这个课程正好可以帮到你。 +如果你是中高级的 **开发人员**,如果你对系统调优有兴趣、希望从事监控相关的工作、想要了解分布式系统;如果你是 **运维人员**,想要提高系统资源利用率,想要推动公司监控体系建立并制定一套规范的告警流程,想要帮助开发提高解决问题的效率,这个课程正好可以帮到你。 ### 讲师寄语 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25401\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25401\350\256\262.md" index c59e917c6..6911bf7ed 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25401\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25401\350\256\262.md" @@ -108,9 +108,9 @@ 1. **链路追踪+统计指标** (Request-scoped metrics) **,请求级别的统计** :在链路追踪的基础上,与相关的统计数据结合,从而得知数据与数据、应用与应用之间的关系。 2. **链路追踪+日志** (Request-scoped events) **,请求级别的事件** :这是链路中一个比较常见的组合模式。日志本身是每一条单独存在的,将链路追踪收集到的信息集成在日志中,可以让日志之间具备关联性,使其具有除了事件维度以外的另一个新的维度,上下文信息。 3. **日志+统计指标** (Aggregatable events) **,聚合级别的事件** :这是在日志中的比较常见的组合。通过解析这部分具有统计指标的信息,我们可以获取相关的指标数据。 -4. **三者结合** (Request-scoped,aggregatable events):三者结合可以理解为 **请求级别+聚合级别的事件** ,由此就形成了一个丰富的、全局的观测体系。 +4. **三者结合** (Request-scoped,aggregatable events):三者结合可以理解为 **请求级别+聚合级别的事件**,由此就形成了一个丰富的、全局的观测体系。 -根据以上这 3 个概念,我们再来想想它们最终会输出的 **数据量(Volume)** 。 **统计指标** 是数值的形式,同时又可以压缩,所以它所需的存储量是最小的; **日志** 的输出量最大,但相对的,它也有比较全的内容记录; **链路追踪** 则正好处于二者之间,它不会像日志一样大量地输出,也不像统计指标一样节能。 +根据以上这 3 个概念,我们再来想想它们最终会输出的 **数据量(Volume)** 。**统计指标** 是数值的形式,同时又可以压缩,所以它所需的存储量是最小的; **日志** 的输出量最大,但相对的,它也有比较全的内容记录; **链路追踪** 则正好处于二者之间,它不会像日志一样大量地输出,也不像统计指标一样节能。 于是,这三者的关系就形成了我们图中最左侧的竖线。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25402\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25402\350\256\262.md" index b160e7bfd..90afd22fd 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25402\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25402\350\256\262.md" @@ -25,7 +25,7 @@ 1. **debug** :调试级别。在这个级别,通常会记录一些调试的内容,比如程序进入方法或函数时,其中的参数信息。debug 级别的日志会极大地影响 CPU 和磁盘 I/O 写入的性能,所以我们一般只会在测试或本机环境中使用。除了自己编写的日志,一些常见的第三方框架也会记录一些日志以方便对程序的调试。这种日志量,在生产环境中你很难抓到重点,会浪费大量的时间在日志检索,所以我并不建议在生产环境中使用。 2. **info** :信息级别。这个应该是开发人员最常用的日志等级了。我们一般也是通过这个日志等级完成上面提到的功能,比如信息埋点、追踪数据变化、数据分析等。虽然大家都在使用,但我发现有些时候,开发同学经常会把 info 级别当作 debug 级别,输出了很多没必要的日志内容,导致线上环境产生了大量的垃圾和重复信息,很不便于日志检索。 -3. **warning 与 error** :许多人会使用 error 级别来记录 warning 级别的内容,这使得不太关键的信息也会在查看故障日志时被筛选出来,导致对故障原因产生误判,浪费大量的时间。这里我会带你区分这两个级别的日志。 **a.** warning:警告级别。这一级别经常用来记录一些虽然出现了错误,但是并不会真正对程序执行构成影响的内容。当你想要使用 error 级别,如果感觉这个错误并不会影响程序往后执行或业务逻辑不会产生错误,就可以使用 warning 级别。 **b.** error:错误级别。只有当整个接口、方法调用都产生了不可避免的问题,对业务的主流程造成影响时才会采用的日志级别。 +3. **warning 与 error** :许多人会使用 error 级别来记录 warning 级别的内容,这使得不太关键的信息也会在查看故障日志时被筛选出来,导致对故障原因产生误判,浪费大量的时间。这里我会带你区分这两个级别的日志。**a.** warning:警告级别。这一级别经常用来记录一些虽然出现了错误,但是并不会真正对程序执行构成影响的内容。当你想要使用 error 级别,如果感觉这个错误并不会影响程序往后执行或业务逻辑不会产生错误,就可以使用 warning 级别。**b.** error:错误级别。只有当整个接口、方法调用都产生了不可避免的问题,对业务的主流程造成影响时才会采用的日志级别。 ### 日志常见来源 @@ -54,7 +54,7 @@ 应用层一般指的是我们业务程序代码的执行位置。我们一般将应用程序分为 **基于容器托管的应用程序** 和 **普通的应用程序** 。 -1. 基于容器托管的应用程序,比如 Java 开发人员使用最多,最熟悉的 Tomcat。这一类型的应用程序会有以下 2 个相对关键的日志文件: **a.** 容器启动日志:以 Tomcat 为例,容器的 logs 目录中经常会有“catalina.日期.log”,这部分日志就是 Tomcat 在启动时的日志,它通常会随着控制台日志一同被打印出来。有时候某些程序异常没有被记录,在这里会有显示。下次如果你发现程序启动时莫名挂掉、无法启动,但是在自己的应用程序日志中又找不到输出信息,不妨到这个日志里看看。 **b.** 请求访问日志:这个和我们刚才在网关层讲的类似,请求访问日志会记录与上游相关的访问时间、访问地址等信息,这里的日志信息和网关层的日志是一一对应的。 +1. 基于容器托管的应用程序,比如 Java 开发人员使用最多,最熟悉的 Tomcat。这一类型的应用程序会有以下 2 个相对关键的日志文件: **a.** 容器启动日志:以 Tomcat 为例,容器的 logs 目录中经常会有“catalina.日期.log”,这部分日志就是 Tomcat 在启动时的日志,它通常会随着控制台日志一同被打印出来。有时候某些程序异常没有被记录,在这里会有显示。下次如果你发现程序启动时莫名挂掉、无法启动,但是在自己的应用程序日志中又找不到输出信息,不妨到这个日志里看看。**b.** 请求访问日志:这个和我们刚才在网关层讲的类似,请求访问日志会记录与上游相关的访问时间、访问地址等信息,这里的日志信息和网关层的日志是一一对应的。 1. 普通应用程序:其日志文件我们通常会通过框架编写,这里面的写法就相对多样和自由。 “如何更好地书写日志”我会在下个章节中讲解,这里我们先继续介绍日志的来源。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25403\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25403\350\256\262.md" index 4781fbcaf..f77b58952 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25403\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25403\350\256\262.md" @@ -73,8 +73,8 @@ logger.debug("用户{}开始下单:{},请求信息:", userId, orderNo, () -> Gso 日志编写的位置可以说是重中之重,好的日志位置可以帮你解决问题,也可以让你更加了解代码的运行情况。我总结了几点比较重要的编写日志的位置,以供参考。 1. **系统/应用启动和参数变更** :当系统启动时,可以将相关的参数信息进行打印,以便出现问题时,更准确地查询原因;参数信息可能并不存储在本地,需要通过配置中心获取,而参数信息有变更时,也需要将变更后的内容输出在日志中。 - 2. **关键操作节点** :最典型的就是 **在关键位置添加日志,记录用户进行的某个操作** 。当出现问题时,你可以通过这个位置的日志了解到用户的操作。同样你也可以 **在系统进行某些操作时添加日志** ,比如你准备启动某个线程池来进行数据处理时,可以加上日志便于以后分析问题。 - 3. **大型任务进度上报** :当系统在处理某个比较大型的任务时,可以在这个过程中增加相关的日志来表明任务处理的进度, **防止因为长时间没有处理而无法得知程序执行的状态** ,比如在文件下载时,可以按照百分比来定时/定次地上报数据。 + 2. **关键操作节点** :最典型的就是 **在关键位置添加日志,记录用户进行的某个操作** 。当出现问题时,你可以通过这个位置的日志了解到用户的操作。同样你也可以 **在系统进行某些操作时添加日志**,比如你准备启动某个线程池来进行数据处理时,可以加上日志便于以后分析问题。 + 3. **大型任务进度上报** :当系统在处理某个比较大型的任务时,可以在这个过程中增加相关的日志来表明任务处理的进度,**防止因为长时间没有处理而无法得知程序执行的状态**,比如在文件下载时,可以按照百分比来定时/定次地上报数据。 4. **异常** :当程序出现异常时,我们通常是通过 try-catch 来记录当时的情况,然后以日志的形式表现出来。如果是通过 try-catch 处理,你需要注意日志编写的位置。如果你需要将日志在本层抛出,则不需要进行日志记录,否则会出现日志重复的问题。如果你除了异常以外还需要记录其他的内容,则可以通过定制异常信息来实现。 #### 写入性能 @@ -85,7 +85,7 @@ logger.debug("用户{}开始下单:{},请求信息:", userId, orderNo, () -> Gso 2. **日志数量** :如果你大量地编写日志,那么日志的质量一定会降低。同时,大量的日志会让你很难去查看问题,反而成了一种负担。在高访问量时,过多的日志也会影响程序的执行效率。 3. **日志编写等级** :我在上一节中讲过,日志等级很容易被滥用,不正确的日志等级会导致我们查询问题的时间增加。 4. **日志输出级别** :这里指的是对于配置日志输出级别的选择。在线上环境,不建议使用 debug 级别,因为线上一直有请求,debug 级别会输出大量的基础和请求信息,极其浪费资源,因此建议开启 info 或者以上。 -5. **无用输出参数** :不对大字段、无用字段输出,因为这很影响程序执行效率和日志的内容。我曾遇到一个案例,A 同学在线上打印了一个完整的 HTML 内容,导致当日的部分日志内容错乱,部分日志无法检索,原因就在于 HTML 存在多行内容导致无法解析的问题。当开发人员到线上服务器上查看时,日志文件的大小已经扩大了 3 倍。 **好的日志一定是便于你去排查问题的** 。在编写日志时你一定要思考这个日志可以帮你做什么。 +5. **无用输出参数** :不对大字段、无用字段输出,因为这很影响程序执行效率和日志的内容。我曾遇到一个案例,A 同学在线上打印了一个完整的 HTML 内容,导致当日的部分日志内容错乱,部分日志无法检索,原因就在于 HTML 存在多行内容导致无法解析的问题。当开发人员到线上服务器上查看时,日志文件的大小已经扩大了 3 倍。**好的日志一定是便于你去排查问题的** 。在编写日志时你一定要思考这个日志可以帮你做什么。 #### 占位符 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25404\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25404\350\256\262.md" index ece9664cd..d76c6b156 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25404\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25404\350\256\262.md" @@ -37,7 +37,7 @@ 直方图相对复杂一些,它是将多个数值聚合在一起的数据结构,可以表示数据的分布情况。 -如下图,它可以将数据分成多个 **桶(Bucket)** ,每个桶代表一个范围区间(图下横向数),比如第 1 个桶代表 0~10,第二个桶就代表 10~15,以此类推,最后一个桶代表 100 到正无穷。每个桶之间的数字大小可以是不同的,并没有规定要有规律。每个桶和一个数字挂钩(图左纵向数),代表了这个桶的数值。 +如下图,它可以将数据分成多个 **桶(Bucket)**,每个桶代表一个范围区间(图下横向数),比如第 1 个桶代表 0~10,第二个桶就代表 10~15,以此类推,最后一个桶代表 100 到正无穷。每个桶之间的数字大小可以是不同的,并没有规定要有规律。每个桶和一个数字挂钩(图左纵向数),代表了这个桶的数值。 ![2.png](assets/Ciqc1F8yXAyAAr1_AANyRfEvYDI870.png) diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25405\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25405\350\256\262.md" index 5f4486437..84421237c 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25405\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25405\350\256\262.md" @@ -2,7 +2,7 @@ 我在上一节课带你了解了统计指标的基本概念。本课时我会向你介绍每个平台都有哪些监控指标,让你更轻松地通过统计数据来了解系统隐患。 -介于篇幅较长,我将它们分为上、下两篇(05、06 课时)。对应我们在 01 课时介绍数据来源时的顺序,从用户侧到服务器, **监控指标** 依次有端上访问、应用程序、组件和机器信息。在上篇,我会从端上访问和应用程序 2 个方面来说明。 +介于篇幅较长,我将它们分为上、下两篇(05、06 课时)。对应我们在 01 课时介绍数据来源时的顺序,从用户侧到服务器,**监控指标** 依次有端上访问、应用程序、组件和机器信息。在上篇,我会从端上访问和应用程序 2 个方面来说明。 ### 端上访问 @@ -46,7 +46,7 @@ App 和网页,在发送请求和获取数据内容的过程中,除了以上 ### 应用程序 -介绍完端上访问后,我们再来看看应用程序。对于应用程序,需要监控的指标就更多了,我会分请求、数据处理、组件协作资源、业务指标、VM 监控这 5 个部分来介绍。 **业务指标** 更多是倾向于开发人员在编写代码时需要自定义控制的指标内容,这一课时我会略过这一内容的介绍,在“07 | 指标编写:如何编写出更加了解系统的指标?”这一课时我会有详细的讲解。 +介绍完端上访问后,我们再来看看应用程序。对于应用程序,需要监控的指标就更多了,我会分请求、数据处理、组件协作资源、业务指标、VM 监控这 5 个部分来介绍。**业务指标** 更多是倾向于开发人员在编写代码时需要自定义控制的指标内容,这一课时我会略过这一内容的介绍,在“07 | 指标编写:如何编写出更加了解系统的指标?”这一课时我会有详细的讲解。 #### 请求 **请求** 指的是端上通过 HTTP 等方式发起的请求,与请求相关的指标有以下几个需要关注 @@ -78,7 +78,7 @@ App 和网页,在发送请求和获取数据内容的过程中,除了以上 我在工作中就遇到过这样的情况:因为一个 Dubbo 接口耗时较长,线程池也没有做到很好的隔离,导致当前服务的资源完全处于等待状态,没有线程可以去处理其他的业务请求,造成了线上的故障。 1. **数据库** :业务系统与数据库之间的交互一般会有专门的 TCP 链接资源保持并处理,每个数据库都有各自的数据协议。因此,我们通常将链接池化,以提高资源使用效率,不浪费资源。并监控 **活跃数** 、 **闲置数** 和 **总共的资源数** 。当资源缺少时,我们需要注意,是否是配置不足导致的资源减少。 -2. **队列** :对于 **异步** 和 **大量需要处理的任务** ,我们通常会使用队列,以起到 **削峰** 的作用。所以我们也会监控任务的 **发送量** 、 **处理量** 、 **Lag 值** 、 **处理耗时** ,确保不会出现大面积的延迟进而影响业务处理的情况。 +2. **队列** :对于 **异步** 和 **大量需要处理的任务**,我们通常会使用队列,以起到 **削峰** 的作用。所以我们也会监控任务的 **发送量** 、 **处理量** 、 **Lag 值** 、 **处理耗时**,确保不会出现大面积的延迟进而影响业务处理的情况。 3. **缓存** :缓存框架也经常会在系统中使用,正确使用它可以减少部分数据库查询的压力,从而提升我们接口的响应性能,比如拉勾教育中就会经常用 Redis 作为部分数据的查询缓存。在缓存中,我们通常会更加关注命中率、内存使用率、数据量等指标。尤其是命中率,命中率越高表明接口性能越高,而接口性能可以缩短响应耗时。 4. **请求** :系统经常会依赖于其他需要进行 HTTP 请求等方式的第三方服务,像微信的创建订单,就需要通过 HTTP 的请求,创建订单后再返回数据。在这里同样要监控其请求数、耗时情况等指标。虽然这是个常见的现象,但在与第三方服务通信的时候,我们一定要做好熔断降级策略,最好不要因为第三方服务的不稳定导致自己业务的主流程受到阻碍。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25406\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25406\350\256\262.md" index 71de1a48b..9f44cb27b 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25406\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25406\350\256\262.md" @@ -1,6 +1,6 @@ # 06 监控指标:如何通过分析数据快速定位系统隐患?(下) -上一节课我介绍了监控指标中的 **端上访问** 和 **应用程序** ,端上访问是指从网页或者 App 中访问,应用程序是指接收到端上请求后的业务处理程序。这一课时,我将带你了解监控指标中的另外两个指标, **组件** 和 **机器信息** 。 +上一节课我介绍了监控指标中的 **端上访问** 和 **应用程序**,端上访问是指从网页或者 App 中访问,应用程序是指接收到端上请求后的业务处理程序。这一课时,我将带你了解监控指标中的另外两个指标,**组件** 和 **机器信息** 。 ### 组件 @@ -34,7 +34,7 @@ 缓存如我之前所讲的,也是一个十分重要的部分,如果正确使用它则可以减少部分数据库查询的压力,从而提升我们接口的响应性能,缓存中也有十分多的关键指标: 1. **响应时间** :说到缓存中的关键指标,首先就要说到响应时间。一般这个指标的值都很低,因为缓存大多数时候是存储在内存中的。如果这个值偏高,说明使用方或者缓存出现了问题,这时就需要从更细的维度跟踪问题的原因了。 -2. **缓存命中率** :命中率其实就是请求中 **查询到数据的请求** 除 **请求总数** ,最终获得的百分比。百分比越高说明命中率越高,程序也会有更好的性能;如果命中率相对较低,则要考虑是否是写法出现了问题,或者是这个内容适不适合使用缓存。如果不适合的话可以考虑不用缓存,因为引入了一个新的组件,会增加运维和开发的成本。 +2. **缓存命中率** :命中率其实就是请求中 **查询到数据的请求** 除 **请求总数**,最终获得的百分比。百分比越高说明命中率越高,程序也会有更好的性能;如果命中率相对较低,则要考虑是否是写法出现了问题,或者是这个内容适不适合使用缓存。如果不适合的话可以考虑不用缓存,因为引入了一个新的组件,会增加运维和开发的成本。 3. **网络延迟时间** :对缓存来说,如果交互中出现了较高的延迟会影响到业务系统,因为缓存一般的调用频率都不低,如果延迟较高的话,会影响接口的性能,所以保证网络延迟低也是一个很关键因素。 4. **已使用内存** :缓存一般是存储在内存中的,所以对于内存的使用量有严格的要求,如果没有满足要求,缓存系统会执行淘汰策略,比如 LRU。执行淘汰策略之后可能会导致缓存命中率下降,而如果内存使用过高,缓存系统则被系统 kill。 5. **资源链接** :除了与数据库,业务系统还会与缓存系统有链接的情况,所以我们也需要监控它们的链接情况。我们常被用作缓存的 Redis,它其实也是一种 KV 类型的 NoSQL 数据库。 @@ -52,11 +52,11 @@ 最后我们来说说机器的统计信息。机器的处理性能如果不够好,会直接影响服务的运行情况,毕竟服务是依托机器运行。机器信息的指标可以按照组成部分,分为以下几个: -1. **CPU** :CPU 的运行情况肯定是应用程序中最重要的。我们一般会比较关注 **CPU 的整体使用率** ,然后再细分为 **系统侧** 、 **用户侧** 的使用率。同样,我们也会关注系统的 **Load 情况** ,如果 Load 值越高说明系统承受的处理任务越多,系统执行也会更缓慢。 -2. **内存** :内存的大小会影响程序的可使用内存空间,除了 **重内存使用程序** 。内存中我们也会关注 **内存的整体使用率** ,以及 **swap 区的使用率** 。一般我不太建议使用 swap 区,因为它会利用磁盘的空间来代替内存,而这可能会影响到程序的使用性能。如果 swap 区的使用率较高,可以考虑将其关闭,通过升级内存来提高程序性能。 +1. **CPU** :CPU 的运行情况肯定是应用程序中最重要的。我们一般会比较关注 **CPU 的整体使用率**,然后再细分为 **系统侧** 、 **用户侧** 的使用率。同样,我们也会关注系统的 **Load 情况**,如果 Load 值越高说明系统承受的处理任务越多,系统执行也会更缓慢。 +2. **内存** :内存的大小会影响程序的可使用内存空间,除了 **重内存使用程序** 。内存中我们也会关注 **内存的整体使用率**,以及 **swap 区的使用率** 。一般我不太建议使用 swap 区,因为它会利用磁盘的空间来代替内存,而这可能会影响到程序的使用性能。如果 swap 区的使用率较高,可以考虑将其关闭,通过升级内存来提高程序性能。 3. **磁盘** :在一般的应用程序中,磁盘更多的是用于 **日志记录** 和 **临时缓存文件记录** 。同 CPU 和内存一样,关注磁盘的使用率即可。 -4. **网络** :网络情况可以说是现在应用中的重中之重,无论是链接组件还是微服务中的 RPC,到处都有服务器之间的通信。一般我们会更关注 **出/入流量** ,如果当到达网卡限制的大小后,则一般只能考虑扩容服务来解决,因为网卡的提升是有限的。在此之外,我们还会监控 **网络丢包率** 、 **连接错误数** 等信息,这些信息可以帮助我们的程序在网络出现问题时,判断是否是网卡的原因。 -5. **I/O** :在 Linux 平台中,任何的网络请求、消息或是其他内容都是基于文件来构成的,所以 I/O 在 Linux 中无处不在。我们会更关注 I/O 的 **文件读取/写入中的速度** 、 **耗时** 、 **次数** 等信息,这些都是最能直观体现出写入和读取速度的内容。同时我们还会关注 **使用率(util)** ,如果磁盘的使用率过高,则说明应用对磁盘的使用量很大,很有可能会因为磁盘的问题而导致应用程序上的问题。 +4. **网络** :网络情况可以说是现在应用中的重中之重,无论是链接组件还是微服务中的 RPC,到处都有服务器之间的通信。一般我们会更关注 **出/入流量**,如果当到达网卡限制的大小后,则一般只能考虑扩容服务来解决,因为网卡的提升是有限的。在此之外,我们还会监控 **网络丢包率** 、 **连接错误数** 等信息,这些信息可以帮助我们的程序在网络出现问题时,判断是否是网卡的原因。 +5. **I/O** :在 Linux 平台中,任何的网络请求、消息或是其他内容都是基于文件来构成的,所以 I/O 在 Linux 中无处不在。我们会更关注 I/O 的 **文件读取/写入中的速度** 、 **耗时** 、 **次数** 等信息,这些都是最能直观体现出写入和读取速度的内容。同时我们还会关注 **使用率(util)**,如果磁盘的使用率过高,则说明应用对磁盘的使用量很大,很有可能会因为磁盘的问题而导致应用程序上的问题。 6. **句柄** :随着 I/O 的使用,我们也需要关注句柄的 **使用量** 。如果程序中出现了资源流未关闭的情况,则有可能会导致句柄数激增,最终导致句柄耗尽,影响程序执行。在“04 | 统计指标:"五个九"对系统稳定的意义?”这一课时中,我就说到了之前我们就曾出现过因 HTTP 中流未关闭,使句柄耗尽,导致程序无法再次发起 HTTP 请求。 ### 结语 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25407\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25407\350\256\262.md" index e3a8ffa1a..42de19f7d 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25407\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25407\350\256\262.md" @@ -4,7 +4,7 @@ ### 编写方向 -咱们先来讨论一下哪些地方需要添加指标信息,它们一般分为 **产品层** 和 **性能层** ,对应 **业务数据** 和 **性能数据** 。 +咱们先来讨论一下哪些地方需要添加指标信息,它们一般分为 **产品层** 和 **性能层**,对应 **业务数据** 和 **性能数据** 。 #### 业务数据 @@ -34,11 +34,11 @@ 1. **关键路径** :业务实现时,会经过一些比较关键的路径,这其中有些指标并不是业务人员所关心的,但它们对开发人员十分重要。以拉勾教育为例,拉勾教育中有一个接口是用来记录用户学习时长的。当初产品提出,需要一个可以记录用户学习时长的功能的时候,他们只需要这个功能可以上线,但并不知道这个功能是如何实现的。对于开发人员,就需要保证这个功能上线之后可以稳定运行,所以我们会在打点上报时,通过监控打点的次数和耗时,保证服务稳定。 2. **处理流程** :在业务实现时,可能有很多关键节点是需要你关注的,通过统计处理流程中的关键点,我们可以在出现问题时,确定是哪一个环节导致的 。我还是以拉勾教育为例,在拉勾教育中购买课程时,从购买开始到购买完成是一个处理流程,在这个过程中会有获取用户信息、创建订单、购买等关键节点,通过这些关键节点,我们可以更好地找出问题的根源。假设有一万个用户在拉勾教育购买了课程,拉勾教育会创建一万笔订单,但支付时需要调用微信接口,如果最后只有九千个订单创建成功了,我们从拉勾教育的程序中看到订单减少,可以判断是微信接口出现了问题。 支付业务中,可能会有很多不同的支付渠道,比如支付宝,微信等。针对支付业务,你就可以关注这些不同渠道中的 **创建订单数** 、 **成单数量** 、 **平均成单时间** 等。通过这些信息,你可以了解哪个渠道支付人数更多,然后优化相关渠道的购买流程,提升用户的购买体验。 -3. **触发行为** :业务在执行流程时会触发一些业务行为,这就是 **触发行为** 。假设我们要通知用户,根据用户联系方式的不同,比如手机号或者邮箱,我们会通过不同的渠道通知。这时候就可以统计每个渠道的 **发送次数** 和 **耗时情况** ,来了解这个业务哪个渠道的用户更多。 +3. **触发行为** :业务在执行流程时会触发一些业务行为,这就是 **触发行为** 。假设我们要通知用户,根据用户联系方式的不同,比如手机号或者邮箱,我们会通过不同的渠道通知。这时候就可以统计每个渠道的 **发送次数** 和 **耗时情况**,来了解这个业务哪个渠道的用户更多。 ##### 自定义数据处理 -相信你在业务开发过程中,肯定有因为某些业务流程 **处理复杂** 或者相对 **耗时较长** ,而选择使用 **自定义线程池** 或是 **内部队列的形式** 去实现某个业务逻辑的情况。 **生产者消费者模式** 就是一个很典型的例子。这样的处理方式使你可以充分利用系统资源,从而提升效率。在数据处理时的有 2 个常用技术方案点,分别是队列和线程池。 +相信你在业务开发过程中,肯定有因为某些业务流程 **处理复杂** 或者相对 **耗时较长**,而选择使用 **自定义线程池** 或是 **内部队列的形式** 去实现某个业务逻辑的情况。**生产者消费者模式** 就是一个很典型的例子。这样的处理方式使你可以充分利用系统资源,从而提升效率。在数据处理时的有 2 个常用技术方案点,分别是队列和线程池。 1. **队列** :队列是在任务处理时的数据容器。当我们将任务放入队列准备让其异步执行时,我们需要关注两个比较关键的内容:放入的数据个数、队列剩余任务数。 2. **线程池** :线程池是进行数据处理时的线程集合。任务处理时,线程池也是必不可少。这时候你可能会区分不同的线程池模型来定义不同的统计指标: @@ -69,7 +69,7 @@ 讲到这里,我想你应该对指标的内容有了一个比较清晰的认识。在编写指标后,我再来介绍一下,怎样才能看到指标的结果。 -通常我们会通过一些指标函数进行计算,这些指标函数一般是与时间相关的,计算方式一般有 2 种, **当前时间段的计算** , **与之前的某个指标值的计算** 。 +通常我们会通过一些指标函数进行计算,这些指标函数一般是与时间相关的,计算方式一般有 2 种,**当前时间段的计算**,**与之前的某个指标值的计算** 。 #### 当前时间段的计算 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25408\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25408\350\256\262.md" index 90529d5a6..0b063363a 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25408\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25408\350\256\262.md" @@ -13,15 +13,15 @@ 我在“ **01 课时 | 数据观测:数据追踪的基石从哪里来?** ”中介绍了数据的来源,数据来源分为很多层,我们可以把这些层代入到这张图中。 1. **A 代表端上访问** 。比如网站发起了一个 Ajax 的请求,以拉勾教育为例,在课程详情页中获取课程信息就是一个 Ajax 异步数据获取请求。 - 2. **B 可以理解为应用层** ,比如 Tomcat。应用层接受了从 A 过来的请求信息,进行最简单的,类似判断用户身份信息的操作。从图中可以看到,B 操作了 Redis,假如我们在做 Session 共享,此时就可以把 Redis 作为存储方案。 - 3. **C 同样可以理解为应用层** 。就目前微服务化的基础架构来讲,我们 **一般也会将接口层和业务逻辑层分开,之间通过 RPC 的方式通信** 。这样的好处在于 **耦合度降低** ,接口层只需要专注和各种端上的交互,业务逻辑层只需要专注业务处理,无须关心上层是哪里调用过来的,这也可以很好地进行代码复用。以拉勾教育为例,图中的这一部分就可以理解为 B 通过 Dubbo 框架,来调用业务逻辑层获取课程信息数据。 + 2. **B 可以理解为应用层**,比如 Tomcat。应用层接受了从 A 过来的请求信息,进行最简单的,类似判断用户身份信息的操作。从图中可以看到,B 操作了 Redis,假如我们在做 Session 共享,此时就可以把 Redis 作为存储方案。 + 3. **C 同样可以理解为应用层** 。就目前微服务化的基础架构来讲,我们 **一般也会将接口层和业务逻辑层分开,之间通过 RPC 的方式通信** 。这样的好处在于 **耦合度降低**,接口层只需要专注和各种端上的交互,业务逻辑层只需要专注业务处理,无须关心上层是哪里调用过来的,这也可以很好地进行代码复用。以拉勾教育为例,图中的这一部分就可以理解为 B 通过 Dubbo 框架,来调用业务逻辑层获取课程信息数据。 4. **D 为数据库层** 。我们的数据基本都是采用数据库的方式存储的,图中的 D 就可以理解为通过 ID 获取课程的详细数据后返回。 通过这样的一张图,你可以清晰地了解一个请求完整的执行链路: **从端上开始,到应用层后,通过各种组件的访问最终获取到数据** 。当然,这里只是一个简单的示例,我相信你的业务会比这个更复杂,但是它们的原理都是相通的。下面,我会对这一流程中的执行流程进行细化讲解。 #### 调用流程 -我用流程图来展现整体的执行流程,为了方便说明,我在图中标注了每一步消耗的时间, **这个时间不是固定的** ,可能会依据场景和性能的不同而变化。在这张图中,我标识了请求开始到结束的整个流程,从左到右代表时间, **字母** 和对应的“ **字母'** ”代表每个阶段的开始和结束。 +我用流程图来展现整体的执行流程,为了方便说明,我在图中标注了每一步消耗的时间,**这个时间不是固定的**,可能会依据场景和性能的不同而变化。在这张图中,我标识了请求开始到结束的整个流程,从左到右代表时间,**字母** 和对应的“ **字母'** ”代表每个阶段的开始和结束。 ![图片9.png](assets/Ciqc1F9E7EKAApQXAACTV0JM4jg352.png) @@ -34,7 +34,7 @@ 1. B 在接受请求后,分别去请求了 Redis 和 C 应用(Redis 在这里我用了 R 来代表),在请求时又分别花费了 10ms 和 20ms。Redis 和 C 应用在接收请求后,又分别使用了 10ms 和 210ms 来处理业务逻辑并返回数据。 1. C 和上面一样,通过 10ms 来进行 D 的调用和处理,这个流程总共花费了 210ms。 -从这张图中我们可以清楚地看到每一个应用和别的应用进行交互时的总耗时和自身耗时,进而了解它们之间通信和自身处理的耗时、流程和数据走向。但是这还存在着一些问题,就是我们 **怎样将这个图以数字化的形式展现出来,然后通过这种形式去定位问题** ?这个问题的解决方式就是“ **链路图** ”。 +从这张图中我们可以清楚地看到每一个应用和别的应用进行交互时的总耗时和自身耗时,进而了解它们之间通信和自身处理的耗时、流程和数据走向。但是这还存在着一些问题,就是我们 **怎样将这个图以数字化的形式展现出来,然后通过这种形式去定位问题**?这个问题的解决方式就是“ **链路图** ”。 在介绍链路图之前,我要先来带你了解一下 Span,看看 Span 中包含的内容,好让你在理解链路图的时候更为轻松。 @@ -54,7 +54,7 @@ Span就代表我在流程图中的 **字母** 和对应的“ **字母'** ”, #### Span 之间关系 -介绍了 Span 的内容后,我再来讲一下 Span 之间的依赖关系。从上面的介绍中,我们知道, **链路在大多数的情况下会是一个树形结构** 。因此,我们在日常开发过程中,一个入口下面一般会有多个出口操作,比如我们通过 Kafka 发送消息、Redis 写入缓存、MySQL 查询数据。那么会不会有多个父节点的情况呢? +介绍了 Span 的内容后,我再来讲一下 Span 之间的依赖关系。从上面的介绍中,我们知道,**链路在大多数的情况下会是一个树形结构** 。因此,我们在日常开发过程中,一个入口下面一般会有多个出口操作,比如我们通过 Kafka 发送消息、Redis 写入缓存、MySQL 查询数据。那么会不会有多个父节点的情况呢? 其实是有的。比如 Kafka 在消费的时候,可能会一次性消费一个 Topic 下面的多个链路信息,由此就可以将这个消费的链路理解为是一个“森林”。因为它会同时拥有多个父节点,并且每个父节点对应着具体的某个链路。 @@ -66,14 +66,14 @@ Span就代表我在流程图中的 **字母** 和对应的“ **字母'** ”, 链路图 -在图中,每一行的长方形都可以理解为是一个操作的基本单元,在链路中也叫作 **Span** (跨度)。 **链路由一个 Span 的集合构成** 。其中 Span 中包含 4 个信息,在长方形中,从左到右依次是: **SpanID** 、 **父级 SpanID** 、 **当前开始时间** (从 0 开始)和 **当前 Span 的耗时** 。下面,我对图中所表示的流程做了一个简单的介绍: +在图中,每一行的长方形都可以理解为是一个操作的基本单元,在链路中也叫作 **Span** (跨度)。**链路由一个 Span 的集合构成** 。其中 Span 中包含 4 个信息,在长方形中,从左到右依次是: **SpanID** 、 **父级 SpanID** 、 **当前开始时间** (从 0 开始)和 **当前 Span 的耗时** 。下面,我对图中所表示的流程做了一个简单的介绍: -1. 假设第一行的 Span 代表在网页中发出请求,可以认定为是出口请求,所以 A 的 Span 是出口类型的操作。SpanID 从 1 开始,没有父级 Span, 所以 parentID 认定为 0,并且开始时间是 0ms,当前 Span 的总共耗时是 320ms。在真实的场景中, **当前开始时间是系统当前的时间戳** , **耗时也会根据场景和性能的不同而变化** ,这里使用 0ms 和 320ms 只是为了说明。 +1. 假设第一行的 Span 代表在网页中发出请求,可以认定为是出口请求,所以 A 的 Span 是出口类型的操作。SpanID 从 1 开始,没有父级 Span, 所以 parentID 认定为 0,并且开始时间是 0ms,当前 Span 的总共耗时是 320ms。在真实的场景中,**当前开始时间是系统当前的时间戳**,**耗时也会根据场景和性能的不同而变化**,这里使用 0ms 和 320ms 只是为了说明。 1. 在第二行中,B 接收到 A 传递来的请求,所以是入口类型的操作。由于网络损耗导致 B 在 50ms 时才接收到请求,所以当前操作的开始时间是 50ms。并且根据层级可以得知 B 是 A 的子节点,所以 B 的父级 ID 对应 A 的 ID,因此 B 的 parentID 是 1,并且 ID 是自增的,所以 B 的 ID 为 2。 -1. 在第三行中,因为 B 进行了一次 Redis 操作,而 Redis 需要连接别的数据源,所以这里的 Span 算为出口类型的操作。因为网络耗时和 Redis 处理各花了 10ms,所以总共的耗时是 20ms。当然, **如果再细一步的话,同样可以在当前行的下面画出 Redis 中的入口 Span,这个取决于链路追踪系统的能力,是否可以细粒度到 Redis 组件内部** 。 +1. 在第三行中,因为 B 进行了一次 Redis 操作,而 Redis 需要连接别的数据源,所以这里的 Span 算为出口类型的操作。因为网络耗时和 Redis 处理各花了 10ms,所以总共的耗时是 20ms。当然,**如果再细一步的话,同样可以在当前行的下面画出 Redis 中的入口 Span,这个取决于链路追踪系统的能力,是否可以细粒度到 Redis 组件内部** 。 1. 第四行则代表 B 向 C 应用发出了一个请求,所以同样是出口操作类型。这里需要注意的是,第三行和第四行的父级 SpanID 是一致的,这代表了它们的父级应用是一样的,都是 B 入口下面的操作。又由于它们开始的时间是相同的,所以代表它们分别启动了两个线程来进行操作。 -为什么不是同步执行的呢? **如果是同步执行,那么这张图就需要改变为先完成了 Redis 操作,在 Redis 结束后,才会开始第四行的操作** 。 +为什么不是同步执行的呢?**如果是同步执行,那么这张图就需要改变为先完成了 Redis 操作,在 Redis 结束后,才会开始第四行的操作** 。 图中后面的内容和前面我所讲的类似,就不再赘述了。 @@ -89,15 +89,15 @@ Span就代表我在流程图中的 **字母** 和对应的“ **字母'** ”, 通过对链路追踪基本概念和流程的讲解,相信你对链路追踪整体的概念有了一个整体的认识。链路追踪是可观测性中不可缺少的一环,因为它会监控我们服务运行中的每一步。我们可以通过链路追踪,快速了解程序真实的执行状态。那么,链路追踪具体有哪些作用呢? -1. **链路查询** :就算你接手了一个全新的项目,对这个项目一无所知,你也可以 **通过某个入口查看链路,快速了解当前程序的运行情况** ,并且可以通过很直观的图来展现:到底是哪里比较耗时,出现错误时是哪个操作导致的,等等。 -2. **性能分析** :通过聚合链路中的数据,我们可以结合操作名称, **快速得知系统的运行容量、耗时情况等** 。 -3. **拓扑图** :通过对链路信息的 **聚合分析** ,我们可以分析得到的数据,形成拓扑图。 **拓扑图可以使你直观地了解整个系统的构成** 。 +1. **链路查询** :就算你接手了一个全新的项目,对这个项目一无所知,你也可以 **通过某个入口查看链路,快速了解当前程序的运行情况**,并且可以通过很直观的图来展现:到底是哪里比较耗时,出现错误时是哪个操作导致的,等等。 +2. **性能分析** :通过聚合链路中的数据,我们可以结合操作名称,**快速得知系统的运行容量、耗时情况等** 。 +3. **拓扑图** :通过对链路信息的 **聚合分析**,我们可以分析得到的数据,形成拓扑图。**拓扑图可以使你直观地了解整个系统的构成** 。 4. **依赖关系** :同样是链路的聚合分析,你还可以了解到操作之间的依赖关系,从而快速感知操作之间的重要等级。如果将依赖关系与限流熔断技术相结合,可以帮助更快地构建一个企业级的链路保护措施。 5. **跨应用/语言** :像我上面所说的每个内容,它们都是 **不限制语言** 的,每个语言都有它自己的实现。在一个大型的企业中,几乎不可能保证所有的系统都使用同样的语言,利用链路追踪不限语言的特点,我们可以将不同语言的代码串联到一起。 #### 采样率 -我相信你一定有过这样的疑问: **监控这么大的数据量,岂不是会很消耗系统资源?** 是的。所以现在已有的大量的链路追踪中,都会存在 **采样率** 的设定,其作用就是 **只采集一部分的链路信息,从而提升程序性能** 。在真正的环境中,没有必要采集 100%的链路信息,因为很多时候,大量的链路信息是相同的,可能需要你关注的只是其中 **相对耗时较高** 或者 **出错次数较多** 的。当然,有些时候也会为了防止漏抓错误而进行全量的链路追踪。 **是否需要进行全面的链路追踪,就看你在观察成本和性能中如何权衡了** 。 +我相信你一定有过这样的疑问: **监控这么大的数据量,岂不是会很消耗系统资源?** 是的。所以现在已有的大量的链路追踪中,都会存在 **采样率** 的设定,其作用就是 **只采集一部分的链路信息,从而提升程序性能** 。在真正的环境中,没有必要采集 100%的链路信息,因为很多时候,大量的链路信息是相同的,可能需要你关注的只是其中 **相对耗时较高** 或者 **出错次数较多** 的。当然,有些时候也会为了防止漏抓错误而进行全量的链路追踪。**是否需要进行全面的链路追踪,就看你在观察成本和性能中如何权衡了** 。 ### 结语 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25409\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25409\350\256\262.md" index d6ae60fa9..7ba158567 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25409\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25409\350\256\262.md" @@ -7,7 +7,7 @@ 在上一课时我提到,链路追踪中最小的单位是 Span,每一个 Span 代表一个操作,但这样存在一个问题,粒度还是太粗了。如何解决粒度太粗的问题呢?在链路追踪的实现中,我们一般会有 2 种方式。 1. **代码埋点** :在代码中预埋点,通过侵入式的方式记录链路数据。 - 2. **字节码增强** :在字节码生成之后,再对其进行修改,从而给它功能。比如像 Java 这类语言,就可以通过字节码增强,而不是人工侵入式的方式记录链路数据。 **这两点的区别在于,是否是侵入式的** 。埋点的方式虽然灵活, **但是依赖性较高** ;字节码增强的形式不需要代码层介入,但仅能支持一部分应用框架。这两种方式能够实现链路追踪,并且可以大面积的使用 ,因为框架都是通用的。 **但通用的链路监控方案的实现方式都逃不过面向切面编程,因为它们只能做到框架级别** ,而这又会导致我们可能会遇到程序执行缓慢或者不稳定的情况,却无法查询到原因。我之前在链路监控时,发现一个接口内部有很长的一段时间内没有 Span 信息。虽然我知道了这个情况,但还是无能为力。 + 2. **字节码增强** :在字节码生成之后,再对其进行修改,从而给它功能。比如像 Java 这类语言,就可以通过字节码增强,而不是人工侵入式的方式记录链路数据。**这两点的区别在于,是否是侵入式的** 。埋点的方式虽然灵活,**但是依赖性较高** ;字节码增强的形式不需要代码层介入,但仅能支持一部分应用框架。这两种方式能够实现链路追踪,并且可以大面积的使用 ,因为框架都是通用的。**但通用的链路监控方案的实现方式都逃不过面向切面编程,因为它们只能做到框架级别**,而这又会导致我们可能会遇到程序执行缓慢或者不稳定的情况,却无法查询到原因。我之前在链路监控时,发现一个接口内部有很长的一段时间内没有 Span 信息。虽然我知道了这个情况,但还是无能为力。 这时候我们一般只能通过 **在业务中手动增加埋点** 的方式来进行更细粒度的 Span 开发,这种方法也有几个缺点: @@ -21,10 +21,10 @@ 那么对于以上的 3 个问题,要怎样解决呢?其实你可以换个角度去思考,不要被链路追踪中 Span 的思想框架给限制住。我在这里给出 2 点建议: -1. **在编程语言中,基本所有的代码都是运行在线程中的,并且大多数的情况下都是单线程** ,比如 HTTP 或者 RPC 等框架接收到请求之后都会交给单独的线程去处理。 -2. **大多数的编程模型是基于线程的这一个概念去实现很多功能的** ,无论是现成的框架,还是底层的 JDK,比如 Dubbo 中的 RPCContext、Java 中线程安全的随机数生成器 ThreadLocalRandom。 +1. **在编程语言中,基本所有的代码都是运行在线程中的,并且大多数的情况下都是单线程**,比如 HTTP 或者 RPC 等框架接收到请求之后都会交给单独的线程去处理。 +2. **大多数的编程模型是基于线程的这一个概念去实现很多功能的**,无论是现成的框架,还是底层的 JDK,比如 Dubbo 中的 RPCContext、Java 中线程安全的随机数生成器 ThreadLocalRandom。 -既然都是基于线程的,而线程中基本会伴随着方法栈,即每进入一个方法都会通过压入一个方法栈帧的情况来保存。 **那我们是不是可以定期查看方法栈的情况来确认问题呢?答案是肯定的** 。比如我们经常使用到的 jstack,其实就是实时地对所有线程的堆栈进行快照操作,来查看当前线程的执行情况。 +既然都是基于线程的,而线程中基本会伴随着方法栈,即每进入一个方法都会通过压入一个方法栈帧的情况来保存。**那我们是不是可以定期查看方法栈的情况来确认问题呢?答案是肯定的** 。比如我们经常使用到的 jstack,其实就是实时地对所有线程的堆栈进行快照操作,来查看当前线程的执行情况。 利用我上面提到的 2 点,再结合链路中的上下文信息,我们可以通过 **周期性地对执行中的线程进行快照操作,并聚合所有的快照,来获得应用线程在生命周期中的执行情况,从而估算代码的执行速度,查看出具体的原因** 。 @@ -32,15 +32,15 @@ ![Drawing 1.png](assets/Ciqc1F9HZ0uASChgAABQpC64934541.png) -在这张图中, **第一行代表线程进行快照的周期情况** ,每一个周期都可以认为是一段时间,比如 10ms、100ms。周期的时间长短,决定了对程序性能影响的大小。因为在进行线程快照时程序会暂停,当快照完成后才会继续进行操作。 **第二行则代表我们需要进行观测的方法的执行时间** ,线程快照只能做到周期性的快照获取。虽然可能并不能完全匹配,但通过这种方式,相对来说已经很精准了。 +在这张图中,**第一行代表线程进行快照的周期情况**,每一个周期都可以认为是一段时间,比如 10ms、100ms。周期的时间长短,决定了对程序性能影响的大小。因为在进行线程快照时程序会暂停,当快照完成后才会继续进行操作。**第二行则代表我们需要进行观测的方法的执行时间**,线程快照只能做到周期性的快照获取。虽然可能并不能完全匹配,但通过这种方式,相对来说已经很精准了。 性能剖析与埋点相比,有以下几个优势: -1. **整个过程中不涉及任何的埋点,所以性能损耗是相对稳定可控的** ,不用再担心因为埋点过多导致的业务系统的压力和性能风险。同时因为不涉及埋点,所以不再需要重复的增加或者删除埋点,大大节省了人力开发和上线的时间。 -2. **无须再担心是底层 JDK 还是框架,或是业务代码** ,在运行时它们都是代码,不需要对它们进行区分。 -3\. 结合当前链路中的上下文信息, **只需要对指定的链路执行时间之内进行性能剖析** ,而不用对每个线程定时进行线程快照操作,因此减少了程序性能的损耗。 +1. **整个过程中不涉及任何的埋点,所以性能损耗是相对稳定可控的**,不用再担心因为埋点过多导致的业务系统的压力和性能风险。同时因为不涉及埋点,所以不再需要重复的增加或者删除埋点,大大节省了人力开发和上线的时间。 +2. **无须再担心是底层 JDK 还是框架,或是业务代码**,在运行时它们都是代码,不需要对它们进行区分。 +3\. 结合当前链路中的上下文信息,**只需要对指定的链路执行时间之内进行性能剖析**,而不用对每个线程定时进行线程快照操作,因此减少了程序性能的损耗。 4\. 通过 **精准到代码行级别** 的方式,可以定位执行缓慢的原因,减少研发定位问题的时间。 -5. **只在需要的时候才使用** ,平时不会使用到这样的功能,因此性能损耗也是稳定的。 +5. **只在需要的时候才使用**,平时不会使用到这样的功能,因此性能损耗也是稳定的。 ### 性能剖析展现方式 @@ -50,7 +50,7 @@ #### 火焰图 -火焰图,顾名思义,是和火焰一样的图片。 **火焰图是在 Linux 环境中比较常见的一种性能剖析展现方式** 。火焰图有很多种展现形式,这里我就以我们会用到的 CPU 火焰图为例: +火焰图,顾名思义,是和火焰一样的图片。**火焰图是在 Linux 环境中比较常见的一种性能剖析展现方式** 。火焰图有很多种展现形式,这里我就以我们会用到的 CPU 火焰图为例: ![Drawing 2.png](assets/Ciqc1F9HZ2KAXgC0AAaJrTEo0uQ972.png) @@ -80,15 +80,15 @@ CPU 火焰图 但树形图也有一些自己的问题: -- **内容显示相对较长** ,在方法栈相对复杂的情况下,这一问题会更为突出。 +- **内容显示相对较长**,在方法栈相对复杂的情况下,这一问题会更为突出。 - **栈帧深度较多时,容易显示不全信息** 。 所以在树形图中一般会配合前端的展示,比如只显示 topN 中耗时较高的内容,或者支持搜索功能。 火焰图和树形图没有绝对的好坏之分,只是相对应的 **侧重点不同** : -1. **火焰图更擅长快速展现出问题所在** ,能够最快速地找到影响最大的问题的原因。 -2. **树形图则更倾向于具体展示出栈的执行流程** ,通过执行流程和耗时统计指标来定位问题的原因。 +1. **火焰图更擅长快速展现出问题所在**,能够最快速地找到影响最大的问题的原因。 +2. **树形图则更倾向于具体展示出栈的执行流程**,通过执行流程和耗时统计指标来定位问题的原因。 ### 结语 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25410\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25410\350\256\262.md" index 5d45f91c4..59fab77c2 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25410\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25410\350\256\262.md" @@ -4,7 +4,7 @@ ### 指标聚合 -我在“ **08 课时** ”有讲到, **链路追踪会跟踪应用中的请求数据。我们可以将这些数据聚合,获取到相关的数据指标。** 这便是链路分析除了观测链路之外的第一个功能。通过分析链路,部分指标无须再通过手动埋点的方式进行统计,比手动埋点获取更具优势,这个优势体现在如下 3 点。 +我在“ **08 课时** ”有讲到,**链路追踪会跟踪应用中的请求数据。我们可以将这些数据聚合,获取到相关的数据指标。** 这便是链路分析除了观测链路之外的第一个功能。通过分析链路,部分指标无须再通过手动埋点的方式进行统计,比手动埋点获取更具优势,这个优势体现在如下 3 点。 1. **更精准** :链路追踪中的数据,会比手动在各个业务代码中编写的计算时间更加精准,因为它面向的是框架内部,相比在业务代码中编写,这样的方式覆盖面更广,也更精准。 2. **更动态** :在传统方式中,开发人员需要对每个开发的功能都进行埋点。随着功能迭代,开发人员在编写时肯定会有遗漏。分析链路不再需要开发人员手动埋点,程序可以自动解析链路中的数据信息,实现动态化。 @@ -16,7 +16,7 @@ - **实例** :指 **服务在进行多进程、多机器部署时的运行实例** 。现在是微服务的时代,为了提高服务的吞吐率和服务在灰度上线时的稳定性,一般服务都不会单独部署,而是采用集群的形式。因此,一个服务往往对应两个或者更多的实例。 - **端点** :是 **与实例平级的一个概念,一个服务下会有多个端点** 。这里的端点可以理解为我们在 span 中定义的操作名称,每个操作的操作名称就是一个端点,大多时候端点都是入口操作。 -当然,通过链路来分析统计指标会有一些局限性。 **由于这些指标来源于链路数据,所以这个方法只能观测到通用的数据信息,而不能对指标进行定制化的统计** ,定制化的指标还是需要开发人员去代码层通过埋点统计。 **链路分析可以使你获取到通用的数据信息,代码埋点则可以帮助你收集定制化的指标数据** ,合理地使用这两种方式可以让你可观测的维度最大化,可以丰富你在分析数据、查看问题时的内容参考。 +当然,通过链路来分析统计指标会有一些局限性。**由于这些指标来源于链路数据,所以这个方法只能观测到通用的数据信息,而不能对指标进行定制化的统计**,定制化的指标还是需要开发人员去代码层通过埋点统计。**链路分析可以使你获取到通用的数据信息,代码埋点则可以帮助你收集定制化的指标数据**,合理地使用这两种方式可以让你可观测的维度最大化,可以丰富你在分析数据、查看问题时的内容参考。 那我们在链路分析中可以获取到哪些数据呢?一共有 8 个,分别是 QPS、SLA、耗时、Apdex、Percentile、Histogram、延迟和 topN,虽然我在前面的课程中已经对它们做过讲解,但这里我会介绍它们在链路追踪里是如何使用的。 @@ -33,13 +33,13 @@ ![图片1.png](assets/Ciqc1F9ODg6AUziZAAA_naf0ZIY270.png) -1. **可视化** :通过可视化的形式, **你可以以一个全局的视角来审视你的系统,能帮助我们更好地分析数据之间的依赖关系和数据走向** 。哪些点是可以优化的,系统的瓶颈可能在哪里,等等,这都是可以通过拓扑图了解到的。 -2. **数据化** : **将拓扑图中的数据和统计指标相互结合,可以将两者的数据放在同一个位置去展示** 。比如通过 QPS 就可以看出哪些服务可能产生的请求更多,这些请求又是来自哪里;当出现问题时,我们还可以通过 SLA 看出来受影响服务的范围有多大。 **通过服务、实例、端点之间的相互引用,我们可以快速分析出相应的依赖占比** 。通过一个接口中的依赖占比,快速分析出哪些是强依赖,哪些是弱依赖,从而更好地进行熔断降级。 +1. **可视化** :通过可视化的形式,**你可以以一个全局的视角来审视你的系统,能帮助我们更好地分析数据之间的依赖关系和数据走向** 。哪些点是可以优化的,系统的瓶颈可能在哪里,等等,这都是可以通过拓扑图了解到的。 +2. **数据化** : **将拓扑图中的数据和统计指标相互结合,可以将两者的数据放在同一个位置去展示** 。比如通过 QPS 就可以看出哪些服务可能产生的请求更多,这些请求又是来自哪里;当出现问题时,我们还可以通过 SLA 看出来受影响服务的范围有多大。**通过服务、实例、端点之间的相互引用,我们可以快速分析出相应的依赖占比** 。通过一个接口中的依赖占比,快速分析出哪些是强依赖,哪些是弱依赖,从而更好地进行熔断降级。 3. **动态性** : **在功能迭代时,拓扑图会通过链路数据进行动态分析,所以无须担心它是否是最新的** 。拓扑图的这一特性也允许我们通过时间维度来查看演进的过程。 与指标一样,我们也会将拓扑图中的数据分为3个维度,分别是服务、实例和操作。每个维度的数据显示的内容不同,作用也相对不同。 -- **服务** :你可以通过服务关系拓扑图 **了解整个系统的架构、服务和服务之间的依赖** ,还可以通过全局的数据内容 **提供“下钻点”** ,从大范围的一个点切入,直到发现问题的根源。通过拓扑图,我们还能以全局的视角来 **优先优化相对依赖度高,耗时更高,也更为重要的服务** 。 +- **服务** :你可以通过服务关系拓扑图 **了解整个系统的架构、服务和服务之间的依赖**,还可以通过全局的数据内容 **提供“下钻点”**,从大范围的一个点切入,直到发现问题的根源。通过拓扑图,我们还能以全局的视角来 **优先优化相对依赖度高,耗时更高,也更为重要的服务** 。 - **实例** :当两个服务之间存在依赖关系时,我们可以再往下跟踪,查看具体的进程和进程之前的依赖关系。再在实例之间加上统计指标数据,我们还可以看到两者在相互通信时的关系是怎么样的。通过实例之间的拓扑图,你可以确认你的实例是否都有在正常的工作,出现问题时,也可以根据这张图快速定位到是哪一个实例可能存在异常。比如我们在 Dubbo 请求调用时,可以通过依赖和指标数据,查看负载均衡器是否正常工作。 - **操作** :操作和端点是一样的,它与实例是一个级别。通过拓扑图,你可以了解到操作之间的业务逻辑依赖,并且可以根据统计指标了解到延迟和依赖的程度。通过这部分数据,你能够看到具体的某个接口在业务逻辑上依赖了多少个下游操作,如果依赖数据越多,程序出现错误的概率也会越大。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25411\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25411\350\256\262.md" index 7034140e1..190690423 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25411\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25411\350\256\262.md" @@ -4,10 +4,10 @@ ### 什么是黑/白盒监控 -**黑盒监控指的是监控外部用户可见的系统行为** , **白盒监控指的是监控内部暴露出来的指标信息** 。它们一个对外,一个对内。二者在功能上有 2 点区别。 +**黑盒监控指的是监控外部用户可见的系统行为**,**白盒监控指的是监控内部暴露出来的指标信息** 。它们一个对外,一个对内。二者在功能上有 2 点区别。 -1. **监控角度不同** : **黑盒更偏向于外侧** ,你可以理解为是通过某个功能或者具体的某个接口来观察,它并不清楚内部是如何实现的; **而白盒则更倾向于从内侧监控** ,它是代码层面的,从内部的视角来解读整个系统。 -2. **面向对象不同** : **黑盒更倾向于看到这个问题的现象** ,比如某个端口挂掉了无法再提供服务,它面向的是当下所发生的故障; **白盒则更加倾向于面向产生问题的原因** ,比如我们在日志中可以通过堆栈信息分析出故障的根源。 +1. **监控角度不同** : **黑盒更偏向于外侧**,你可以理解为是通过某个功能或者具体的某个接口来观察,它并不清楚内部是如何实现的; **而白盒则更倾向于从内侧监控**,它是代码层面的,从内部的视角来解读整个系统。 +2. **面向对象不同** : **黑盒更倾向于看到这个问题的现象**,比如某个端口挂掉了无法再提供服务,它面向的是当下所发生的故障; **白盒则更加倾向于面向产生问题的原因**,比如我们在日志中可以通过堆栈信息分析出故障的根源。 这里有一点我需要说明一下: **白盒监控有时是面向问题,有时是面向问题的原因的,它的监控对象会根据监控的内容改变** 。假设白盒在接口层反映接口耗时比较长,此时可以认定白盒面向的是“耗时长”这一问题。但同时,白盒监控也会监控到与这个接口相关联的数据库出现了严重的慢查询。慢查询是接口耗时较长的原因,这时候我们就可以认定白盒是面向原因的。 @@ -21,8 +21,8 @@ 1. **端口状态** : **通过程序检测具体业务的端口是否存活** 。可以简单确定程序是否有在提供服务,如果端口都无法连接,那么肯定是系统出现了问题。通常我们也会结合进程检测使用,如果进程存活,但是端口不存在,则说明可能程序存在某些问题,没有将服务暴露出来。 2. **证书检测** : **通过检测证书是否有效,确认用户是否可以正常访问** 。现在的网站服务基本都是使用的 HTTPS,如果证书出现了问题,则可能是浏览器认定为不安全,阻止了用户访问。 -3. **探活** : **通过心跳检测来判定服务是否存活** ,比如定期通过具体的某个接口检测我们的服务是否运行正常。如果在这里出现异常,则说明我们的系统存在问题。在 Dubbo 中就有心跳机制来保证双方的链接,这也算是一种探活。 -4. **端到端功能检测** :这个就相对复杂一些。 **通常是通过定期进行端到端的测试,结合业务流程感知业务是否在执行正常** ,比如我们可以通过 UI 或者接口自动化测试工具,来确认页面中返回的数据或者数据是否是正确的。 +3. **探活** : **通过心跳检测来判定服务是否存活**,比如定期通过具体的某个接口检测我们的服务是否运行正常。如果在这里出现异常,则说明我们的系统存在问题。在 Dubbo 中就有心跳机制来保证双方的链接,这也算是一种探活。 +4. **端到端功能检测** :这个就相对复杂一些。**通常是通过定期进行端到端的测试,结合业务流程感知业务是否在执行正常**,比如我们可以通过 UI 或者接口自动化测试工具,来确认页面中返回的数据或者数据是否是正确的。 了解了黑盒监控的内容后,不难看出,它其实更偏向确认功能是否可用。黑盒监控的理解门槛相对较低,即便是一个从来没参与过这个项目的开发人员,都可以对这些数据进行验证、确认。 @@ -38,7 +38,7 @@ ### 黄金指标 -下面我要引入一个 _ **Google SRE** _ 中提出的一个概念: **黄金指标** 。 **黄金指标是 Google 针对大量分布式监控的经验总结,它可以在服务级别帮助衡量终端用户体验、服务中断、业务影响等层面的问题** ,有 4 类指标信息,分为 **错误** 、 **延迟** 、 **流量** 和 **饱和度** 。无论你监控的数据再复杂、再令人眼花缭乱,都可以套用在这 4 类上。 +下面我要引入一个 _ **Google SRE** _ 中提出的一个概念: **黄金指标** 。**黄金指标是 Google 针对大量分布式监控的经验总结,它可以在服务级别帮助衡量终端用户体验、服务中断、业务影响等层面的问题**,有 4 类指标信息,分为 **错误** 、 **延迟** 、 **流量** 和 **饱和度** 。无论你监控的数据再复杂、再令人眼花缭乱,都可以套用在这 4 类上。 下面我会把每个指标信息按 @@ -49,7 +49,7 @@ 下面,我会对这 4 类依次说明。 -### 错误 **错误指的是当前系统所有发生过的错误请求** ,我们可以通过错误请求个数计算出相应的错误率。这个我想应该很容易理解,只要是程序运行,就肯定会因为某些原因而导致错误,可能是其他系统的组件导致的,也有可能是程序代码中自身的问题 +### 错误 **错误指的是当前系统所有发生过的错误请求**,我们可以通过错误请求个数计算出相应的错误率。这个我想应该很容易理解,只要是程序运行,就肯定会因为某些原因而导致错误,可能是其他系统的组件导致的,也有可能是程序代码中自身的问题 有 3 种比较常见的错误类型,我们这里以 HTTP 的接口举例: @@ -57,21 +57,21 @@ 2. **隐式错误** :指 **表面上显示完全正确,但是在数据中已经出现异常的错误** 。我在 **04 课时** 讲解 SLA 时提到,响应状态码为 200 可以认定为成功,但如果业务在返回的数据结构中被认定为是错误,那这个错误就是隐式的。 3. **策略导致错误** :与第二种类似,都是 **在表面上显示正确,但是可能因为某种策略被认定为错误** 。比如某个接口在 1s 时返回,因为已经接收到了数据,所以被认定为成功。但我们的策略限制 500ms 内才算完成,因此这个数据会被记录为错误,这就是策略导致的。在 RPC 的提供者消费者模式中这个情况也很常见。 -那么从基础和业务的两个维度,我们如何来监控错误呢? **在基础层中** ,我们可以把系统宕机、进程或者端口挂掉、网络丢包这样的情况认定为是故障。 **在业务层中** ,监控的内容也就相对比较多了,比如 Dubbo 调用出错、日志中出现的错误,都是可以被认定为“错误”的指标。具体的内容我在介绍日志、统计指标和链路追踪时都有介绍过。 +那么从基础和业务的两个维度,我们如何来监控错误呢?**在基础层中**,我们可以把系统宕机、进程或者端口挂掉、网络丢包这样的情况认定为是故障。**在业务层中**,监控的内容也就相对比较多了,比如 Dubbo 调用出错、日志中出现的错误,都是可以被认定为“错误”的指标。具体的内容我在介绍日志、统计指标和链路追踪时都有介绍过。 #### 延迟 **延迟指的是服务在处理请求时花费的时间。** 我们经常说的接口耗时、响应时长指的就是延迟。在这里需要注意一点: **一般在统计延迟时,并不会把成功或者错误的信息分开统计** 。这样的统计方式会使我们更难了解到真实的情况,所以在统计时常常需要把它们区分开。以一个 HTTP 接口为例,响应状态码正确的(200)和错误的(500)请求,它们的耗时一定会有差别,因为正确的请求走完了全流程,而错误的可能只进行了某一部分流程,我们就需要把这两个请求的耗时在统计时分别记录。 -**在基础层中** ,我们可以监控 I/O 等待、网络延迟等信息。 **在业务层中** ,则可以监控接口调用的耗时、MySQL 查询的耗时等信息。 +**在基础层中**,我们可以监控 I/O 等待、网络延迟等信息。**在业务层中**,则可以监控接口调用的耗时、MySQL 查询的耗时等信息。 延迟在系统中是一个十分关键的指标,很多时候我们的服务并不会产生错误,但很可能会有延迟,延迟在 HTTP 层会影响用户体验,在数据库中出现高延迟会导致请求错误,这是我们需要着重关注的。 -#### 流量 **流量是表现系统负载情况的数据** ,比如我们常见的 QPS、UV。通过这个指标我们能确切了解到服务当前承受了多大的压力,请求量和处理量有多大。 **我们常说的容量规划,其实是流量规划** 。通过流量,我们可以得知当前系统运行的状况,是否到达了它的负荷上限。 **在基础层中** ,常见的监控指标有磁盘的读写速度、网络 I/O 情况等。 **在业务层中** 则有请求量、MySQL 查询次数等等 +#### 流量 **流量是表现系统负载情况的数据**,比如我们常见的 QPS、UV。通过这个指标我们能确切了解到服务当前承受了多大的压力,请求量和处理量有多大。**我们常说的容量规划,其实是流量规划** 。通过流量,我们可以得知当前系统运行的状况,是否到达了它的负荷上限。**在基础层中**,常见的监控指标有磁盘的读写速度、网络 I/O 情况等。**在业务层中** 则有请求量、MySQL 查询次数等等 通过观察流量相关的指标,可以了解到是否存在突增突降的情况,从而判断是否遭受到了攻击或者是否在进行某些活动。 -#### 饱和度 **饱和度通常是指某个资源的使用率** 。通常指的是我们通过容量的最大值和现在的使用量,来判断这个容量是否“满”了。某些程序,如果资源饱和度过高,可能会导致执行缓慢,甚至无法使用。比如 CPU 使用率如果达到 100%,会出现执行缓慢;Dubbo 进行 RPC 调用时,如果线程池没有可用的线程数,则会使业务受到阻碍。 **饱和度一般也会配合其他指标一起使用** ,比如在使用网络 I/O 时,网卡都是有流量上限的,我们通过流量上限值和当前网络 I/O 的使用情况,可以得知相应的饱和度。 **饱和度是衡量我们这个系统是否到达瓶颈的关键** 。如饱和度过高,这时候就需要考虑扩容、减少数据量或是其他降低饱和度的操作了。 **在基础层中** ,需要监控的指标有 CPU 使用率、I/O 使用率、句柄使用情况等。 **在业务层中** 则会有线程池的线程使用数、JVM 中的堆内存使用率等信息 +#### 饱和度 **饱和度通常是指某个资源的使用率** 。通常指的是我们通过容量的最大值和现在的使用量,来判断这个容量是否“满”了。某些程序,如果资源饱和度过高,可能会导致执行缓慢,甚至无法使用。比如 CPU 使用率如果达到 100%,会出现执行缓慢;Dubbo 进行 RPC 调用时,如果线程池没有可用的线程数,则会使业务受到阻碍。**饱和度一般也会配合其他指标一起使用**,比如在使用网络 I/O 时,网卡都是有流量上限的,我们通过流量上限值和当前网络 I/O 的使用情况,可以得知相应的饱和度。**饱和度是衡量我们这个系统是否到达瓶颈的关键** 。如饱和度过高,这时候就需要考虑扩容、减少数据量或是其他降低饱和度的操作了。**在基础层中**,需要监控的指标有 CPU 使用率、I/O 使用率、句柄使用情况等。**在业务层中** 则会有线程池的线程使用数、JVM 中的堆内存使用率等信息 ### 结语 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25412\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25412\350\256\262.md" index fe2ed8225..3e7cab01c 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25412\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25412\350\256\262.md" @@ -4,15 +4,15 @@ ### 告警的作用 -在没有告警的时候,我们一般是人工定期地查看相关的指标或者链路数据,再去程序上确认。 **虽然人工也能监控,但有时还是难以判定是否真的出现了问题** ,因为某一个单独的指标的上升或下降并不代表系统出现了错误。 +在没有告警的时候,我们一般是人工定期地查看相关的指标或者链路数据,再去程序上确认。**虽然人工也能监控,但有时还是难以判定是否真的出现了问题**,因为某一个单独的指标的上升或下降并不代表系统出现了错误。 -**即便是确认了问题,没有一个完善的处理流程,也很难规定这种问题具体该如何处理** ,比如针对某个具体的问题应该做什么;出现问题后,运维人员又应该怎么通知开发人员去寻找问题的原因。除此之外,还有 2 个人工监控无法避免的难题。 **一个是无法保证 24 小时都有人监控** 。因为涉及人工,所以肯定不可能保证 24 小时都有人盯着指标。会存在有事情走开,或者忘记查看的情况。 **另一个是人工成本高** 。项目上线后,让开发人员定时定点地观看业务指标数据是不现实的,因为开发人员还会涉及其他业务的开发工作。 +**即便是确认了问题,没有一个完善的处理流程,也很难规定这种问题具体该如何处理**,比如针对某个具体的问题应该做什么;出现问题后,运维人员又应该怎么通知开发人员去寻找问题的原因。除此之外,还有 2 个人工监控无法避免的难题。**一个是无法保证 24 小时都有人监控** 。因为涉及人工,所以肯定不可能保证 24 小时都有人盯着指标。会存在有事情走开,或者忘记查看的情况。**另一个是人工成本高** 。项目上线后,让开发人员定时定点地观看业务指标数据是不现实的,因为开发人员还会涉及其他业务的开发工作。 -基于以上的问题,告警就是一个有效的解决方案, **它可以早于用户发现且及时地定位、反馈问题,再通过告警的规则和流程规范进行有效的处理** 。我们通过以下 4 点来看告警的主要作用。 +基于以上的问题,告警就是一个有效的解决方案,**它可以早于用户发现且及时地定位、反馈问题,再通过告警的规则和流程规范进行有效的处理** 。我们通过以下 4 点来看告警的主要作用。 1. **通过监控数据,可以早于用户发现问题** 。告警系统通过对数据进行监控,可以在出现问题时,第一时间告知给相关项目的开发人员。开发人员可以在问题反馈之前,通过告警查看问题的原因,然后将问题解决。这样的方式能极大地提高了用户的使用体验,降低用户流失的风险。 2. **通过聚合相关的指标快速定位问题** 。运维人员发现问题后,可以通过告警将指标等相关的内容,聚合显示给开发人员,让开发人员快速定位到问题产生的根本原因,而不是像无头苍蝇一样在数据海里翻找。 -3. **制定个性化的告警规则** 。通过告警系统,无论你是业务的开发人员还是运维人员,都可以去根据数据来源制定自己的告警规则,而不仅是参考现有数据。 **开发人员可以自定义指标和内容进行告警,让告警和真正的实际业务结合起来** ; **运维人员也可以更好地管理和制定符合公司的服务指标监控** 。 +3. **制定个性化的告警规则** 。通过告警系统,无论你是业务的开发人员还是运维人员,都可以去根据数据来源制定自己的告警规则,而不仅是参考现有数据。**开发人员可以自定义指标和内容进行告警,让告警和真正的实际业务结合起来** ; **运维人员也可以更好地管理和制定符合公司的服务指标监控** 。 4. **制定告警规范** 。告警的流程和规范是在告警之后运维和开发人员共同协定完成的,比如告警后的处理流程怎么样规范化,如何查看历史的错误情况以避免问题的再次发生。当数据确实产生了告警之后,运维和开发人员只需要遵循这样的规范,问题的处理也会变得快捷高效。 ### 告警数据来源 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25413\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25413\350\256\262.md" index 5e64e602a..f3922e1b8 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25413\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25413\350\256\262.md" @@ -8,12 +8,12 @@ ![图片13.png](assets/CgqCHl9Z5miAWj4jAAA4Voz8NqM535.png) -**其一是简化数据** 。将告警的数据聚合成指标后,我们不再需要关心数据的内容,通过指标表现出来的数字即可进行告警。比如在查看异常日志时,如果根据日志文本确认内容后再进行告警,就会相对来说复杂一些,因为此时还需要解析日志。不同的数据源会有不同的数据内容,如果告警前需要理解日志的每行文本,告警的成本也会增加。 **其二是易于告警** 。指标可以统一需要告警的资源数据的格式,告警时的规则也更加规范,易于去管理。格式统一后,既不会再因为格式的问题浪费时间,也可以在跨团队沟通时提高沟通的效率。 +**其一是简化数据** 。将告警的数据聚合成指标后,我们不再需要关心数据的内容,通过指标表现出来的数字即可进行告警。比如在查看异常日志时,如果根据日志文本确认内容后再进行告警,就会相对来说复杂一些,因为此时还需要解析日志。不同的数据源会有不同的数据内容,如果告警前需要理解日志的每行文本,告警的成本也会增加。**其二是易于告警** 。指标可以统一需要告警的资源数据的格式,告警时的规则也更加规范,易于去管理。格式统一后,既不会再因为格式的问题浪费时间,也可以在跨团队沟通时提高沟通的效率。 -通过数字可量化的形式,无论你的数据源来自哪里,都可以及时、有效地告警。以白盒中的 3 个来源为例,你可以通过以下的几种形式来构建。 **首先是日志。** 在日志中,我们通常是按照 2 种方法统计数据。 +通过数字可量化的形式,无论你的数据源来自哪里,都可以及时、有效地告警。以白盒中的 3 个来源为例,你可以通过以下的几种形式来构建。**首先是日志。** 在日志中,我们通常是按照 2 种方法统计数据。 1. **等级区分计数** : **按照不同的日志等级分别计数** 。比如通过统计 error 级别日志的数量,来了解当前系统中有多少已经存在的错误信息。 -2. **具体规则匹配计数** : **按照具体的匹配规则统计日志中指定信息的数量** 。比如可以将访问日志中的访问路径,按照指定 URL 来获取其执行的次数;或者是在支付场景中,统计支付成功的次数。这种情况通常用于一些无法统计业务指标的特殊场景。 **其次是统计指标** 。这里不用多说,我们所要做的就是将数据聚合成统计指标。 **最后是链路追踪。** 在链路中,可以按照具体 Span 中的数据分析出更详细的指标,比如依据 Span 名称来统计调用次数,或是根据这个 Span 中的具体标签信息来做更细维度的指标分析。 +2. **具体规则匹配计数** : **按照具体的匹配规则统计日志中指定信息的数量** 。比如可以将访问日志中的访问路径,按照指定 URL 来获取其执行的次数;或者是在支付场景中,统计支付成功的次数。这种情况通常用于一些无法统计业务指标的特殊场景。**其次是统计指标** 。这里不用多说,我们所要做的就是将数据聚合成统计指标。**最后是链路追踪。** 在链路中,可以按照具体 Span 中的数据分析出更详细的指标,比如依据 Span 名称来统计调用次数,或是根据这个 Span 中的具体标签信息来做更细维度的指标分析。 ### 告警规则与设定 @@ -23,10 +23,10 @@ 在讲解规则的设定之前,我会先介绍一些目前互联网中比较通用的指标存储方式,这可以帮助你更好地理解后面的内容。 -我们一般会将指标数据存储在时序数据库中,比如常见的 Prometheus(在最后的实践篇我会做专门的讲解)、OpenTSDB。 **这类数据库基本都是以时间作为维度进行数据索引的** 。数据库主要由以下 3 个部分构成。 +我们一般会将指标数据存储在时序数据库中,比如常见的 Prometheus(在最后的实践篇我会做专门的讲解)、OpenTSDB。**这类数据库基本都是以时间作为维度进行数据索引的** 。数据库主要由以下 3 个部分构成。 1. **时间点** :信息被记录的时间,每条记录都会有一个具体的时间戳。 -2. **主体** :对数据内容打标签,类似于在 Span 中增加自定义数据。 **键值对可以方便我们标记数据,确认是什么数据内容产生了值** 。 +2. **主体** :对数据内容打标签,类似于在 Span 中增加自定义数据。**键值对可以方便我们标记数据,确认是什么数据内容产生了值** 。 3. **测量值** :上面 2 点内容最终会对应到一个值上去,这个值也是我们后面在进行阈值设置和告警判断时的关键。 我们可以通过这 3 个部分检索指标数据。同时,时序数据库也具有和传统数据库一样的表的概念,方便我们针对不同的指标来进行相互数据隔离。通过这 3 个指标,你就可以聚合出任意数据。 @@ -76,7 +76,7 @@ #### 告警简化 -我们先来看告警简化。顾名思义, **告警简化就是简化系统中需要告警的指标,让告警更精简** 。 +我们先来看告警简化。顾名思义,**告警简化就是简化系统中需要告警的指标,让告警更精简** 。 如果你将系统中所有的指标都增加了告警,那你的系统可能并不能和你想的一样,针对每个地方都进行告警。给所有的指标都加上告警虽然看似全面,但真正发生错误时,反而会无法快速找到导致告警的原因,让处理变得复杂。 @@ -91,7 +91,7 @@ #### 告警频率 -其次是告警的频率。 **在告警时,我们要保证告警是有一定的频率的,适当的告警频率可以提高处理的效率** 。一般当系统出现了告警后,开发人员都会停下手上的事情去处理。如果在告警处理时,又收到了告警,可能会让开发人员顾此失彼。 +其次是告警的频率。**在告警时,我们要保证告警是有一定的频率的,适当的告警频率可以提高处理的效率** 。一般当系统出现了告警后,开发人员都会停下手上的事情去处理。如果在告警处理时,又收到了告警,可能会让开发人员顾此失彼。 结合我在工作中遇到的情况,我会给出 2 点建议,帮助你更好地设置告警的频率。 @@ -100,7 +100,7 @@ #### 黄金指标 -最后是黄金指标。你是否还记得我在“ **11 课时** ”讲的黄金指标, **黄金指标也是我们在配置告警时的重要参考** 。如果你可以将黄金指标按照告警规则处理,那就已经做得很不错了。 +最后是黄金指标。你是否还记得我在“ **11 课时** ”讲的黄金指标,**黄金指标也是我们在配置告警时的重要参考** 。如果你可以将黄金指标按照告警规则处理,那就已经做得很不错了。 错误、延迟、流量这 3 个指标可以帮助你更好地了解系统当下发生的状况,饱和度则可以让你了解系统可能快出现故障时的资源使用情况。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25414\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25414\350\256\262.md" index d9e9dddb8..dfd26065c 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25414\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25414\350\256\262.md" @@ -130,7 +130,7 @@ on-call 的开发或者运维人员,主要负责对生产环境中当前出现 ##### 重要级别告警 -对于重要级别的告警,首先要记住的就是, **不要慌乱** 。如果一直保持在紧张的状态会影响你处理问题的效率。 +对于重要级别的告警,首先要记住的就是,**不要慌乱** 。如果一直保持在紧张的状态会影响你处理问题的效率。 此时,相关的项目负责人、on-call 值班人员和运维会聚集在一起,参与问题的处理。明确好每个参与人员在此次故障恢复中的作用是很重要的,一般可以有以下的两个比较关键的角色。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25415\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25415\350\256\262.md" index 4cb2190d5..d9413b844 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25415\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25415\350\256\262.md" @@ -1,6 +1,6 @@ # 15 日志收集:ELK 如何更高效地收集日志? -在 **模块二** ,我介绍了如何将可观测性和告警体系相结合。从 **模块三** 开始,我将带你了解如何实现可观测性的理念。本节课作为模块三的第一节,我将从日志模块开始,介绍如何在实践中应用。 +在 **模块二**,我介绍了如何将可观测性和告警体系相结合。从 **模块三** 开始,我将带你了解如何实现可观测性的理念。本节课作为模块三的第一节,我将从日志模块开始,介绍如何在实践中应用。 ### 作用 @@ -14,8 +14,8 @@ 1. **日志聚合** : **将不同系统之间的日志组合到同一个系统中,不用再登录到每台机器上查看日志内容** 。你只需要连接到日志系统即可查看所有日志,而并非只是一个服务的日志。 2. **统一格式** : **将所有收集到的日志按统一的格式汇总** 。服务和服务之间的日志格式可能并不相同,但是日志系统收集处理后,能以相同的数据风格存储和查询,解决用户在进行数据检索时的内容不一致问题。 - 3. **日志归档** :日志收集后同样便于我们进行日志归档。 **日志归档以后,除了能看到实时的内容,我们还能通过日志系统检索,查询到历史数据** ,极大地节省了我们的日志检索成本。 - 4. **日志串联** :通过日志与链路追踪中 trace 的概念相结合, **我们在查看问题时,只需要找到相关的 trace 信息,就可以找到这个链路在所有层面中的日志内容** ,帮助我们聚焦问题。 + 3. **日志归档** :日志收集后同样便于我们进行日志归档。**日志归档以后,除了能看到实时的内容,我们还能通过日志系统检索,查询到历史数据**,极大地节省了我们的日志检索成本。 + 4. **日志串联** :通过日志与链路追踪中 trace 的概念相结合,**我们在查看问题时,只需要找到相关的 trace 信息,就可以找到这个链路在所有层面中的日志内容**,帮助我们聚焦问题。 ### 原理 @@ -28,7 +28,7 @@ 1. **socket** : **在程序中使用 socket 链接将日志内容发送到日志收集器中** 。这种方式存在一个弊端,就是因为日志收集器非高可用,当收集器出现问题时,日志可能丢失。 2. **agent** : **程序将日志内容写入本地磁盘中,在这个程序所属的机器或者容器中部署日志收集程序,当日志文件变化时,将日志变化的部分收集并发送到收集器中** 。这也是目前比较常见的一种做法。 -#### 日志解析 **通过 socket 和 agent 收集日志后,都会根据日志中规定好的格式解析出相对应的数据** ,比如使用正则的方式,解析数据组中的日志等级、时间戳、日志内容、异常等信息 +#### 日志解析 **通过 socket 和 agent 收集日志后,都会根据日志中规定好的格式解析出相对应的数据**,比如使用正则的方式,解析数据组中的日志等级、时间戳、日志内容、异常等信息 此时,我们也可以对日志中的内容做更细化的处理,比如在解析到 IP 地址信息时,同时增加其所属地区等信息,让数据更具有可辨识度。 @@ -53,7 +53,7 @@ ELK 是一套完整的日志系统解决方案,它提供了从日志收集、 #### 系统架构 -部署 ELK 时, **Logstach 就充当了日志采集和日志解析工作,Elasticsearch 用于数据存储,Kibana 用于数据检索** 。 +部署 ELK 时,**Logstach 就充当了日志采集和日志解析工作,Elasticsearch 用于数据存储,Kibana 用于数据检索** 。 现在也会使用 Filebeat 来代替 Logstash 完成数据采集工作。Filebeat 可以轻量化地部署在每一个服务容器中,使用较少的资源就可以实现数据采集的工作,并将其通过自定的协议发送到 Logstash 中对数据进行更细致的处理。 @@ -61,7 +61,7 @@ ELK 是一套完整的日志系统解决方案,它提供了从日志收集、 ELK 部署架构图 -这张图展现了一个比较主流的部署架构图,其中最左侧的就是在每台机器上部署的 Filebeat。 **收集到数据之后,它会将数据发送至 Kafka 集群中** 。Kafka 是一个开源流处理平台,它提供了高效率的发布与订阅功能,并且在消费不过来时,它还可以充当生产者与消费者数据处理之间的缓冲。 +这张图展现了一个比较主流的部署架构图,其中最左侧的就是在每台机器上部署的 Filebeat。**收集到数据之后,它会将数据发送至 Kafka 集群中** 。Kafka 是一个开源流处理平台,它提供了高效率的发布与订阅功能,并且在消费不过来时,它还可以充当生产者与消费者数据处理之间的缓冲。 接下来 Logstash 就可以启动一个集群来消费 Kafka 集群中的日志信息,这里的 Logstash 主要负责解析日志,并且将解析后的内容发送到 ElasticSearch 集群。如果 Kafka 中的 Lag 数量不断增加,则说明 Logstash 集群的消费能力不足以处理日志内容,这时需要动态地增减 Logstash 集群机器,从而实现发送与消费方的平衡。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25416\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25416\350\256\262.md" index 563006931..13a7ce875 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25416\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25416\350\256\262.md" @@ -9,7 +9,7 @@ 对于监控系统而言,指标体系是其中不可缺少的一环。那么,在一个比较完整的监控系统中,指标究竟发挥了哪些作用呢? 1. **通过查看指标数据了解指标的走向,从而更好地优化系统、流量等内容** 。以流量为例,假如有人在爬取你的数据,此时请求量指标可能会出现有规律的突增突降,这时你就可以结合访问日志分析出对方爬取的方式,然后结合具体的业务场景来处理。 - 2. **通过导出或者查询已有数据,将一段时间内的数据以 dashboard 的形式进行展示,这个也是目前主流的一个使用场景** ,比如“双十一”淘宝的销售额。 + 2. **通过导出或者查询已有数据,将一段时间内的数据以 dashboard 的形式进行展示,这个也是目前主流的一个使用场景**,比如“双十一”淘宝的销售额。 3. **基于预先配置好的告警规则,定时检测数据是否符合规则,并对其进行告警处理** 。这也是我们在指标监控中最为重要的一个目的。 ### 原理 @@ -36,17 +36,17 @@ 收集指标数据之后,收集器就可以对已有的数据内容聚合,然后保存。我在 **13 | 告警质量:如何更好地创建告警规则和质量?** 这一课时中,介绍了时序数据库的基本概念。 -指标聚合也是 **将数据按照主体部分,以时间维度进行按照秒或者分钟级别来聚合,然后根据不同的数据类型计算相应的数值** ,比如仪表盘类型的数据只保存最近一次的值。 **最后将得到的数据存储到数据库中** 。 +指标聚合也是 **将数据按照主体部分,以时间维度进行按照秒或者分钟级别来聚合,然后根据不同的数据类型计算相应的数值**,比如仪表盘类型的数据只保存最近一次的值。**最后将得到的数据存储到数据库中** 。 #### 指标查询 **数据存储到了相对应的数据库之后,就可以通过界面或者类似 SQL 的形式查询数据** -通常在查询时还会涉及时间范围和指标计算的工作。一个指标监控系统中最为重要的,是其 **数据查询的灵活度** 和 **支持聚合函数的种类** ,比如常见的分位值、最大值、平均值等方式的数值计算。 +通常在查询时还会涉及时间范围和指标计算的工作。一个指标监控系统中最为重要的,是其 **数据查询的灵活度** 和 **支持聚合函数的种类**,比如常见的分位值、最大值、平均值等方式的数值计算。 我们可以把这些数值与图表结合,定义自己的 dashboard,从而更快了解系统当前的运行情况。 #### 规则告警 -最后一个关键点是告警规则。 **监控系统可以通过统一的配置来进行告警** ,其中包含以下几个维度的数据信息。 +最后一个关键点是告警规则。**监控系统可以通过统一的配置来进行告警**,其中包含以下几个维度的数据信息。 - **检测时间** : **多长时间检测一次告警内容,时间越短说明检测的周期越密集** 。通常可以指定为 1 分钟,如果业务系统对于数据比较敏感,则可以适当降低时间,比如 30 秒。 - **告警规则** : **进行告警规则的配置,通常会包含统计指标的名称、查询方式和阈值构成** 。检测的时候会判断当前指标查询出的结果值是否符合告警规则,如果符合则说明达到了告警条件。 @@ -157,7 +157,7 @@ description: "{{ labels.instance }} 实例中的请求错误数超过 20 个( 这张图是官方 demo 所提供的展示内容,可以点击[这里](http://demo.robustperception.io:3000/d/KyOBFkuik/host-stats-prometheus-node-exporter?orgId=1)访问。 -Grafana是依赖于 Prometheus 提供的数据搭建而成的。其中, **最上面一行分别展示了当前的展示模板和对应查询时间范围** ,这个和我介绍的 Kibana 十分类似。 **下方的每一个模块展示的是通过 PromQL 所查询出数据的结果** ,这个部分可以选择不同的展示方式,例如柱状图、折线图、列表等。 +Grafana是依赖于 Prometheus 提供的数据搭建而成的。其中,**最上面一行分别展示了当前的展示模板和对应查询时间范围**,这个和我介绍的 Kibana 十分类似。**下方的每一个模块展示的是通过 PromQL 所查询出数据的结果**,这个部分可以选择不同的展示方式,例如柱状图、折线图、列表等。 Grafana 还支持导入和导出展示模板,你可以下载一些已经很成熟的模板信息来使用。· diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25417\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25417\350\256\262.md" index b17269213..14e75dc0b 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25417\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25417\350\256\262.md" @@ -19,7 +19,7 @@ #### 链路采集 **链路采集是指从业务系统或者组件中采集实时的流量数据,将这些数据汇聚成统一的格式,然后发送到链路收集服务中** -在这里需要关注的是如何进行链路采集。目前主流的实现方式可以分为 2 种: **埋点** 和 **字节码增强** ,这两部分我在“ **09 | 性能剖析:如何补足分布式追踪短板?** ”中有过简单的介绍,大家可以回顾一下。 +在这里需要关注的是如何进行链路采集。目前主流的实现方式可以分为 2 种: **埋点** 和 **字节码增强**,这两部分我在“ **09 | 性能剖析:如何补足分布式追踪短板?** ”中有过简单的介绍,大家可以回顾一下。 我们可以通过埋点和字节码增强在采集器中的应用来看一下这两者的区别。 @@ -40,9 +40,9 @@ #### 数据收集 -**从链路采集到数据之后,我们就可以对这些数据进行解析、分析等工作,并最终存储到相应的存储引擎中** ,常见的引擎有 ElasticSearch、HBase、MySQL 等。 +**从链路采集到数据之后,我们就可以对这些数据进行解析、分析等工作,并最终存储到相应的存储引擎中**,常见的引擎有 ElasticSearch、HBase、MySQL 等。 -通常这时候数据会分为两类,统计数据和链路数据。统计数据可以让你了解数据的走向,链路数据则可以让你清晰地看到链路中的每一个细节。 **统计数据通常指通过链路分析得到的数据** ,我会在“ **18 | 观测分析:SkyWalking 如何把观测和分析结合起来?** ”这一课时做详细介绍。通常这类数据与时间维度有关,这有点儿类似于我们在上一节所讲的 Prometheus,将其存储到时序数据库上更为合适。 **链路数据通常指我们采集后解析成指定格式,进行链路展示的数据** 。通常这部分数据会采用唯一的数据 ID 来存储。我们在检索时需要采用链路 ID 或者相关的链路数据信息的方式,此时则可以考虑使用支持全文检索的存储引擎。 +通常这时候数据会分为两类,统计数据和链路数据。统计数据可以让你了解数据的走向,链路数据则可以让你清晰地看到链路中的每一个细节。**统计数据通常指通过链路分析得到的数据**,我会在“ **18 | 观测分析:SkyWalking 如何把观测和分析结合起来?** ”这一课时做详细介绍。通常这类数据与时间维度有关,这有点儿类似于我们在上一节所讲的 Prometheus,将其存储到时序数据库上更为合适。**链路数据通常指我们采集后解析成指定格式,进行链路展示的数据** 。通常这部分数据会采用唯一的数据 ID 来存储。我们在检索时需要采用链路 ID 或者相关的链路数据信息的方式,此时则可以考虑使用支持全文检索的存储引擎。 #### 数据查看 **数据已经存储到数据库后,我们就可以进行数据查询、基于这些数据进行告警以及其他的操作** @@ -73,13 +73,13 @@ Zipkin 是一款开源的链路追踪系统,它是基于我们之前提到的\ 这一部分对应我在原理中讲到的链路采集、数据收集和数据查看的步骤,我们从上往下依次来看。 -**首先是链路采集** 。紫色的部分代表业务系统和组件,图中是以一个典型的 RPC 请求作为所需要追踪的链路,其中 client 为请求的发起方,分别请求了两个服务端。其中被观测的客户端和服务端会在启动的实例中增加数据上报的功能,这里的数据上报就是指从本实例中观测到的链路数据,一并上报到 Zipkin 中,传输工具常见的有 Kafka 或者 HTTP 请求。 **数据传输到 Zipkin 的收集器后,会经过 Zipkin 的存储模块,存储到数据库中** 。目前支持的数据库有 MySQL、ElasticSearch、Cassandra 这几种类型,具体的数据库选择可以根据公司内部运维的实力评估出最适合的。 **最后是数据查看** 。Zipkin 提供了一套完整的 UI 界面来查询,这套 UI 界面依赖于一整套完整的 API 来处理请求。 +**首先是链路采集** 。紫色的部分代表业务系统和组件,图中是以一个典型的 RPC 请求作为所需要追踪的链路,其中 client 为请求的发起方,分别请求了两个服务端。其中被观测的客户端和服务端会在启动的实例中增加数据上报的功能,这里的数据上报就是指从本实例中观测到的链路数据,一并上报到 Zipkin 中,传输工具常见的有 Kafka 或者 HTTP 请求。**数据传输到 Zipkin 的收集器后,会经过 Zipkin 的存储模块,存储到数据库中** 。目前支持的数据库有 MySQL、ElasticSearch、Cassandra 这几种类型,具体的数据库选择可以根据公司内部运维的实力评估出最适合的。**最后是数据查看** 。Zipkin 提供了一套完整的 UI 界面来查询,这套 UI 界面依赖于一整套完整的 API 来处理请求。 下面我会对 Zipkin 中的一些常用功能做说明。 #### 链路采集 -Zipkin 提供了多语言的支持,在官方提供的版本中,提供了目前比较主流语言的支持,比如 C#、Go、Java、JavaScript、Scala 等语言的支持。同时它支持社区提供的其他语言,具体可以参考[官网说明](https://zipkin.io/pages/tracers_instrumentation.html)。 **在链路采集上,Zipkin 使用数据埋点的方式来进行观测** 。 +Zipkin 提供了多语言的支持,在官方提供的版本中,提供了目前比较主流语言的支持,比如 C#、Go、Java、JavaScript、Scala 等语言的支持。同时它支持社区提供的其他语言,具体可以参考[官网说明](https://zipkin.io/pages/tracers_instrumentation.html)。**在链路采集上,Zipkin 使用数据埋点的方式来进行观测** 。 在Java中,我们的项目一般都会集成 Spring,所以这里也可以通过spring-cloud-starter-zipkin 快速集成。通过简单的项目配置,我们就可以让项目拥有 Zipkin 的基础能力。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25418\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25418\350\256\262.md" index 414cb65f7..0f69bb773 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25418\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25418\350\256\262.md" @@ -10,7 +10,7 @@ SkyWalking 则是一个完整的 APM(Application Performance Management)系统,链路追踪只是其中的一部分。 -SkyWalking 和 Zipkin 的定位不同,决定了它们不是相同类型的产品。 **SkyWalking 中提供的组件更加偏向业务应用层面** ,并没有涉及过多的组件级别的观测; **Zipkin 提供了更多组件级别的链路观测** ,但并没有提供太多的链路分析能力。你可以根据两者的侧重点来选择合适的产品。 +SkyWalking 和 Zipkin 的定位不同,决定了它们不是相同类型的产品。**SkyWalking 中提供的组件更加偏向业务应用层面**,并没有涉及过多的组件级别的观测; **Zipkin 提供了更多组件级别的链路观测**,但并没有提供太多的链路分析能力。你可以根据两者的侧重点来选择合适的产品。 ### 系统架构 @@ -18,7 +18,7 @@ SkyWalking 和 Zipkin 的定位不同,决定了它们不是相同类型的产 ![1.png](assets/CgqCHl9xYUSAMdjMAAM5S7oWJck457.png) -从中间往上看, **首先是 Receiver Cluster,它代表接收器集群,是整个后端服务的接入入口,专门用来收集各个指标,链路信息,相当于我在上一节所讲的链路收集器。** **再往后面走是 Aggregator Cluster,代表聚合服务器,它会汇总接收器集群收集到的所有数据,并且最终存储至数据库,并进行对应的告警通知** 。右侧标明了它支持的多种不同的存储方式,比如常见的 ElasticSearch、MySQL,我们可以根据需要来选择。 +从中间往上看,**首先是 Receiver Cluster,它代表接收器集群,是整个后端服务的接入入口,专门用来收集各个指标,链路信息,相当于我在上一节所讲的链路收集器。** **再往后面走是 Aggregator Cluster,代表聚合服务器,它会汇总接收器集群收集到的所有数据,并且最终存储至数据库,并进行对应的告警通知** 。右侧标明了它支持的多种不同的存储方式,比如常见的 ElasticSearch、MySQL,我们可以根据需要来选择。 图的左上方表示,我们可以使用 CLI 和 GUI,通过 HTTP 的形式向集群服务器发送请求来读取数据。 @@ -38,7 +38,7 @@ SkyWalking 和 Zipkin 的定位不同,决定了它们不是相同类型的产 service_resp_time = from(Service.latency).longAvg(); ``` -我们先来看右侧的部分, **from 代表数据从哪里来** 。在这个示例中,数据来自 Service.latency,代表服务的响应耗时。 **longAvg 函数会对服务中的所有响应时长求平均值,最后赋值给左侧的 servce_resp_time 字段** ,这个字段也就是我们最终生成的统计名称。 +我们先来看右侧的部分,**from 代表数据从哪里来** 。在这个示例中,数据来自 Service.latency,代表服务的响应耗时。**longAvg 函数会对服务中的所有响应时长求平均值,最后赋值给左侧的 servce_resp_time 字段**,这个字段也就是我们最终生成的统计名称。 通过这样简单的方式我们就可以定义一个统计指标,在系统运行时,就会根据定义生成数据。 @@ -46,7 +46,7 @@ service_resp_time = from(Service.latency).longAvg(); 我们先来看一下有哪些数据源。 -在“ **10 课时** ”中,我介绍过在 **每一个 Trace 数据中,都有 3 个维度的实体,分别是服务、实例、端点** 。 **我们在进行链路分析时,不仅可以通过这 3 个实体计算得出数据** ,比如基于响应时间来计算平均响应耗时、基于调用次数计算 QPS; **还可以依据三者之间的依赖,统计数据与数据之间的关系** ,比如基于端点和端点之间的调用次数,统计两者的调用频次。 +在“ **10 课时** ”中,我介绍过在 **每一个 Trace 数据中,都有 3 个维度的实体,分别是服务、实例、端点** 。**我们在进行链路分析时,不仅可以通过这 3 个实体计算得出数据**,比如基于响应时间来计算平均响应耗时、基于调用次数计算 QPS; **还可以依据三者之间的依赖,统计数据与数据之间的关系**,比如基于端点和端点之间的调用次数,统计两者的调用频次。 计算得出数据之后,我们可以对同一个实体中的数据进行聚合,聚合之后由 SkyWalking 按照实体将数据分组。 @@ -70,7 +70,7 @@ service_resp_time = from(Service.latency).longAvg(); 在分布式系统中,RPC 的请求数量可能非常巨大,如果使用传统的拓扑检测,虽然也能完成,但是会导致高延迟和高内存使用。同时由于是基于时间窗口模式,如果提供者的数据上报事件超过了时间窗口规定的时间,就会出现无法匹配的问题。 -SkyWalking 为了解决上面提到的延迟和内存问题,引入了一个新的分析方式来进行拓扑检测,这种方式叫作 STAM(Streaming Topology Analysis Method)。 **STAM 通过在消息传递的内容中注入更多的链路上下文信息,解决了传统拓扑检测中高延迟和高内存的问题。** Zipkin 和 SkyWalking 在 OkHttp 框架的消息传递时,都会将链路信息放置在请求头中。 **无论它们的采集器是如何实现的,在进行消息传递时,都会通过某种方式将链路信息设置到请求中** 。如下图所示: +SkyWalking 为了解决上面提到的延迟和内存问题,引入了一个新的分析方式来进行拓扑检测,这种方式叫作 STAM(Streaming Topology Analysis Method)。**STAM 通过在消息传递的内容中注入更多的链路上下文信息,解决了传统拓扑检测中高延迟和高内存的问题。** Zipkin 和 SkyWalking 在 OkHttp 框架的消息传递时,都会将链路信息放置在请求头中。**无论它们的采集器是如何实现的,在进行消息传递时,都会通过某种方式将链路信息设置到请求中** 。如下图所示: ![1.png](assets/Ciqc1F9xYgmAfvCDAAEDn5gIMBo308.png) diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25419\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25419\350\256\262.md" index 2ad9be7bb..9c85932dc 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25419\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25419\350\256\262.md" @@ -25,17 +25,17 @@ ARMS 提供的功能主要分为 6 个部分:前端监控、App 监控、应用监控、自定义监控、大盘展示、报警。我们依次来看。 -![Lark20200929-154947.png](assets/CgqCHl9y50CAXyttAAEVMU02lVM306.png) **前端监控** **前端监控指的是通过在页面中埋入脚本的形式,让阿里云接管前端的数据上报** 。其中就包含我们比较常见的脚本错误次数、接口请求次数、PV、UV 等统计数据,也包含页面中脚本错误、API 访问等数据信息。通过统计数据你能快速了解前端用户的访问情况;脚本错误、API 访问等数据,则可以帮助你了解页面出现错误或者接口访问时的详细信息。 **App 监控** **App 监控的接入方式与前端监控方式类似,都是通过增加相关 SDK 的方式添加监控** 。其中主要包含崩溃、性能分析和远程日志获取。 +![Lark20200929-154947.png](assets/CgqCHl9y50CAXyttAAEVMU02lVM306.png) **前端监控** **前端监控指的是通过在页面中埋入脚本的形式,让阿里云接管前端的数据上报** 。其中就包含我们比较常见的脚本错误次数、接口请求次数、PV、UV 等统计数据,也包含页面中脚本错误、API 访问等数据信息。通过统计数据你能快速了解前端用户的访问情况;脚本错误、API 访问等数据,则可以帮助你了解页面出现错误或者接口访问时的详细信息。**App 监控** **App 监控的接入方式与前端监控方式类似,都是通过增加相关 SDK 的方式添加监控** 。其中主要包含崩溃、性能分析和远程日志获取。 - **崩溃指标和日志** 可以帮助移动端研发人员了解相关崩溃率,可以及时掌握崩溃时的堆栈信息,用于快速定位问题。 - **性能分析** 可以帮助研发人员了解页面中的统计指标,比如卡顿率、启动时间,从而得知当前 App 的性能处在什么水平。 -- **远程日志** 则可以收集存储在每个用户手机中的 App 操作日志。研发人员能够根据这部分日志分析复杂场景下用户使用的问题,并深入到具体的用户维度查看问题。 **应用监控** ARMS 的应用监控和我之前讲的链路追踪的内容十分相似,其中就包含链路数据查询,与业务系统的日志结合,统计指标、拓扑图等信息。阿里云的应用监控还提供了根据参数查询链路的功能,可以在链路中增加业务属性,让你在查询问题链路时更加个性化。 +- **远程日志** 则可以收集存储在每个用户手机中的 App 操作日志。研发人员能够根据这部分日志分析复杂场景下用户使用的问题,并深入到具体的用户维度查看问题。**应用监控** ARMS 的应用监控和我之前讲的链路追踪的内容十分相似,其中就包含链路数据查询,与业务系统的日志结合,统计指标、拓扑图等信息。阿里云的应用监控还提供了根据参数查询链路的功能,可以在链路中增加业务属性,让你在查询问题链路时更加个性化。 -将链路追踪放到云平台的有一个好处,那就是它可以和内部系统做完整的集成,比如结合阿里云提供的全生态组件,查看相互之间的全链路。 **自定义监控** 自定义监控提供多种场景的数据内容的自定义解析、数据清洗、聚合、保存到统计指标中,进行监控告警。 +将链路追踪放到云平台的有一个好处,那就是它可以和内部系统做完整的集成,比如结合阿里云提供的全生态组件,查看相互之间的全链路。**自定义监控** 自定义监控提供多种场景的数据内容的自定义解析、数据清洗、聚合、保存到统计指标中,进行监控告警。 -一般数据源都是通过日志的方式输入,我们可以根据日志中统一的规定,比如限定具体用户 ID 字段的解析位置,去计算异常次数、访问次数等数据。这样的方式可以方便你进行业务层的数据分析,与业务结合起来可以让你不再局限于技术层面去思考问题。 **大盘展示** 根据指标数据内容定制显示大盘数据。通过定制化的方式,你可以将系统中已有的统计指标内容,通过定制化的图表展示。 +一般数据源都是通过日志的方式输入,我们可以根据日志中统一的规定,比如限定具体用户 ID 字段的解析位置,去计算异常次数、访问次数等数据。这样的方式可以方便你进行业务层的数据分析,与业务结合起来可以让你不再局限于技术层面去思考问题。**大盘展示** 根据指标数据内容定制显示大盘数据。通过定制化的方式,你可以将系统中已有的统计指标内容,通过定制化的图表展示。 -你可以能够通过页面的方式,快速了解关心的业务的实时情况。阿里巴巴“双十一”时的交易额页面上会显示实时的交易额,也会显示一些国内外的主要指标数据,这个就是大盘展示最典型的应用场景。 **报警** 与我在“ **模块二** ”中讲到的创建规则与告警十分相似。阿里云 ARMS 所提供的告警功能,会提供一个界面,让你十分方便地通过这个界面去集成各个端中的数据和所有统计指标。同时,它也支持短信、钉钉、邮件等通知方式。 **端到端监控** 下面,我们依次来看一下如何在 ARMS 上实践上面说到的前端监控、APP 监控和应用监控。 **前端监控** 如上文所说,前端监控是通过在代码中增加脚本的方式来实现数据监控的。代码如下: +你可以能够通过页面的方式,快速了解关心的业务的实时情况。阿里巴巴“双十一”时的交易额页面上会显示实时的交易额,也会显示一些国内外的主要指标数据,这个就是大盘展示最典型的应用场景。**报警** 与我在“ **模块二** ”中讲到的创建规则与告警十分相似。阿里云 ARMS 所提供的告警功能,会提供一个界面,让你十分方便地通过这个界面去集成各个端中的数据和所有统计指标。同时,它也支持短信、钉钉、邮件等通知方式。**端到端监控** 下面,我们依次来看一下如何在 ARMS 上实践上面说到的前端监控、APP 监控和应用监控。**前端监控** 如上文所说,前端监控是通过在代码中增加脚本的方式来实现数据监控的。代码如下: ```javascript !(function(c,b,d,a){c[a]||(c[a]={});c[a].config={pid:"xx",AppType:"web",imgUrl:"https://arms-retcode.aliyuncs.com/r.png?",sendResource:true,enableLinkTrace:true,behavior:true}; @@ -43,18 +43,18 @@ ARMS 提供的功能主要分为 6 个部分:前端监控、App 监控、应 })(window,document,"https://retcode.alicdn.com/retcode/bl.js","__bl"); ``` -在页面中,通过 script 标签引入一个 JavaScript 文件来进行任务处理,然后通过 pid 参数设定的应用 ID,保证数据只会上传到你的服务中。 **网页运行时就会自动下载 bl.js 文件,下载完成后,代码会自动执行** 。当页面处理各种事件时,会通过异步的形式,上报当前的事件信息,从而实现对前端运行环境、执行情况的监控。常见的事件有:页面启动加载、页面加载完成、用户操作行为、页面执行时出现错误、离开页面。 **页面加载完成之后,会发送 HEAD 请求来上报数据** 。其中我们可以清楚的看到,在请求参数中包含 DNS、TCP、SSL、DOM、LOAD 等信息,分别代表 DNS 寻找、TCP 建立连接、SSL 握手这类,我在“ **05 | 监控指标:如何通过分析数据快速定位系统隐患?(上)** ”中讲到的通用指标,也包含 DOM 元素加载时间这类网页中的统计指标信息。如下所示: +在页面中,通过 script 标签引入一个 JavaScript 文件来进行任务处理,然后通过 pid 参数设定的应用 ID,保证数据只会上传到你的服务中。**网页运行时就会自动下载 bl.js 文件,下载完成后,代码会自动执行** 。当页面处理各种事件时,会通过异步的形式,上报当前的事件信息,从而实现对前端运行环境、执行情况的监控。常见的事件有:页面启动加载、页面加载完成、用户操作行为、页面执行时出现错误、离开页面。**页面加载完成之后,会发送 HEAD 请求来上报数据** 。其中我们可以清楚的看到,在请求参数中包含 DNS、TCP、SSL、DOM、LOAD 等信息,分别代表 DNS 寻找、TCP 建立连接、SSL 握手这类,我在“ **05 | 监控指标:如何通过分析数据快速定位系统隐患?(上)** ”中讲到的通用指标,也包含 DOM 元素加载时间这类网页中的统计指标信息。如下所示: ![1.png](assets/CgqCHl9xdOCAJwMaAAFLBfVQzpI118.png) -数据上报后,ARMS 就会接收到相对应事件中的完整数据信息,从而通过聚合的方式,存储和展示数据。在 ARMS 中,针对应用有访问速度、JS 错误、API 请求这些统计指标和错误信息的数据,ARMS 可以依据不同维度的数据了解到更详细的内容,包含页面、地理、终端、网络这 4 类。通过不同的数据维度,你也可以更有针对性地了解问题。 **App 监控** App 的监控方式与前端监控十分类似,都需要通过增加代码的方式进行。以 iOS 为例,如果我们想要接入性能分析功能,除了要引入相关依赖,还需要在代码中进行如下的声明: +数据上报后,ARMS 就会接收到相对应事件中的完整数据信息,从而通过聚合的方式,存储和展示数据。在 ARMS 中,针对应用有访问速度、JS 错误、API 请求这些统计指标和错误信息的数据,ARMS 可以依据不同维度的数据了解到更详细的内容,包含页面、地理、终端、网络这 4 类。通过不同的数据维度,你也可以更有针对性地了解问题。**App 监控** App 的监控方式与前端监控十分类似,都需要通过增加代码的方式进行。以 iOS 为例,如果我们想要接入性能分析功能,除了要引入相关依赖,还需要在代码中进行如下的声明: ```plaintext [[AlicloudAPMProvider alloc] autoInitWithAppVersion:AppVersion channel:channel nick:nick]; [AlicloudHAProvider start]; ``` -这段代码会创建 AlicloudAPMProvider 对象,并且传入相关的参数,然后通过 start 方法启动监控功能。 **应用监控** 对于服务端监控来说,ARMS 支持目前主流的 Java、PHP、Go 等语言,这里我以 Java 语言为例说明。 +这段代码会创建 AlicloudAPMProvider 对象,并且传入相关的参数,然后通过 start 方法启动监控功能。**应用监控** 对于服务端监控来说,ARMS 支持目前主流的 Java、PHP、Go 等语言,这里我以 Java 语言为例说明。 在 Java 中,主要通过字节码增强的形式采集数据。项目启动后,会采集机器中 JVM 中的统计指标、链路数据等信息,然后结合链路,分析出统计指标、拓扑图的信息,以及应用与各个组件之间的交互细节,比如数据库查询、消息 MQ 发送量等数据信息。 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25420\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25420\350\256\262.md" index bd7bd2341..0bd412fe7 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25420\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25420\350\256\262.md" @@ -4,26 +4,26 @@ ### 作用 -很多公司内部,都有属于自己的一套 OSS(Operation Support System),操作支持系统。 **它管理着系统内部的众多系统,比如我之前提过的 CMDB 系统,也会包含一些运营人员的业务后台系统;也负责将内部中的各种数据的收集汇总和管理控制。** OSS 系统可以帮助我们做到 2 件事情。 +很多公司内部,都有属于自己的一套 OSS(Operation Support System),操作支持系统。**它管理着系统内部的众多系统,比如我之前提过的 CMDB 系统,也会包含一些运营人员的业务后台系统;也负责将内部中的各种数据的收集汇总和管理控制。** OSS 系统可以帮助我们做到 2 件事情。 -1. **统一入口:统一所有子系统的全局访问入口** ,让寻找相关项目变得简单,方便研发和运维人员查看、管理相关的系统数据; **使用统一的一套 UI 和用户系统** ,统一用户身份和展现方式,让用户使用起来体验更好。 -2. **系统集成:子系统集成时,提供统一的 API 接口** ,比如通过全局的 API 接口获取用户名称、组织架构等。 **需要对用户进行信息通知时,也可以通过 OSS 提供的接口进行更方便的处理** 。通过 OSS 让系统集成起来更加简单,便捷。 +1. **统一入口:统一所有子系统的全局访问入口**,让寻找相关项目变得简单,方便研发和运维人员查看、管理相关的系统数据; **使用统一的一套 UI 和用户系统**,统一用户身份和展现方式,让用户使用起来体验更好。 +2. **系统集成:子系统集成时,提供统一的 API 接口**,比如通过全局的 API 接口获取用户名称、组织架构等。**需要对用户进行信息通知时,也可以通过 OSS 提供的接口进行更方便的处理** 。通过 OSS 让系统集成起来更加简单,便捷。 ### 常见功能 通过对 OSS 系统及其作用的介绍,你应该对它有了一个感性的认识。接下来,我再来带你了解它的功能,有哪些常见的系统会被集成到其中。 -1. **CMDB 系统** :这个系统我在“ **14 | 告警处理:怎样才能更好地解决问题?** ”中讲过, **它主要负责存储我们的服务、实例、机器配置等信息** 。当研发人员进行服务的申请,机器资源的扩容或者缩容时,可以在这个平台中进行申请,并且交由审批处理。 -2. **运营平台** : **该系统主要用于处理技术运营中常见的功能** ,比如我在“ **14 课时** ”讲到的 on-call 值班,就是在这里进行管理的。研发如果需要创建数据库、在 Git 上进行项目操作或是其他的行为,也可以通过运营平台进行相关的操作。 +1. **CMDB 系统** :这个系统我在“ **14 | 告警处理:怎样才能更好地解决问题?** ”中讲过,**它主要负责存储我们的服务、实例、机器配置等信息** 。当研发人员进行服务的申请,机器资源的扩容或者缩容时,可以在这个平台中进行申请,并且交由审批处理。 +2. **运营平台** : **该系统主要用于处理技术运营中常见的功能**,比如我在“ **14 课时** ”讲到的 on-call 值班,就是在这里进行管理的。研发如果需要创建数据库、在 Git 上进行项目操作或是其他的行为,也可以通过运营平台进行相关的操作。 3. **上线系统** : **主要是研发将系统上线时使用** 。通过上线系统,研发可以很方便地将程序逐步发送到线上环境,简化与统一上线流程,减少与运维的沟通成本。 -4. **观测系统** : **就是与可观测性的三大支柱,日志、统计指标、链路追踪相关的系统** ,比如 Kibana、Grafana、SkyWalking。除了外部可以看到的实现方式,有些系统内部可能还会有自己的,将可观测性中的内容相互结合展示的实现方式。 +4. **观测系统** : **就是与可观测性的三大支柱,日志、统计指标、链路追踪相关的系统**,比如 Kibana、Grafana、SkyWalking。除了外部可以看到的实现方式,有些系统内部可能还会有自己的,将可观测性中的内容相互结合展示的实现方式。 5. **告警系统** : **收集所有观测的数据,通过在告警平台中配置相关告警规则,实现告警** 。结合我在“ **14 课时** ”中讲的内容,如统一告警处理流程,通过界面化的形式查询等。 ### 告警 在可观测性与内部 OSS 系统集成时,其中最关键的就是 **如何收集数据内容,配置告警规则,完成最终的告警全流程** 。我带你依次了解它们。 -#### 数据收集 **数据收集指将各种观测到的数据内容,通过内容解析,然后进行聚合,最终组装成统计指标数据,并对其进行存储** ,以便在后面进行告警处理。在这里我会从链路追踪和日志这两个方面来说。因为数据最终都是存储到统计指标系统中的,所以无需介绍统计指标方面的内容 +#### 数据收集 **数据收集指将各种观测到的数据内容,通过内容解析,然后进行聚合,最终组装成统计指标数据,并对其进行存储**,以便在后面进行告警处理。在这里我会从链路追踪和日志这两个方面来说。因为数据最终都是存储到统计指标系统中的,所以无需介绍统计指标方面的内容 - **日志** :在日志中,我们可能需要统计各个系统中的 error 或者 warning 级别日志的次数,比如 error 级别的数据一段时间内超过 5 次就进行告警。 @@ -41,29 +41,29 @@ #### 配置告警规则 -有了数据,我们再来看如何根据这些数据进行告警规则的配置。我在“ **05 | 监控指标:如何通过分析数据快速定位系统隐患?(上)** ”和“ **06 | 监控指标:如何通过分析数据快速定位系统隐患?(下)** ”中,把监控指标分为 4 个端,分别是 **端上访问** 、 **应用程序** 、 **组件** 和 **机器信息** 。在这 2 个课时中,我介绍了这 4 个端分别有哪些指标,但不是所有的指标都适合在 OSS 系统中配置告警规则。这里我将带你了解,根据不同的端,应该如何去配置告警规则。我们首先来看端上访问。 **端上访问** **端上访问指通过 App 或者浏览器进行的访问操作,这个层面的告警比较关注用户的使用体验** 。在告警配置中,App 的统计指标内容更多的是“查看”,因为浏览器更易于去迭代更新,所以一般会更多针对浏览器进行告警配置。 +有了数据,我们再来看如何根据这些数据进行告警规则的配置。我在“ **05 | 监控指标:如何通过分析数据快速定位系统隐患?(上)** ”和“ **06 | 监控指标:如何通过分析数据快速定位系统隐患?(下)** ”中,把监控指标分为 4 个端,分别是 **端上访问** 、 **应用程序** 、 **组件** 和 **机器信息** 。在这 2 个课时中,我介绍了这 4 个端分别有哪些指标,但不是所有的指标都适合在 OSS 系统中配置告警规则。这里我将带你了解,根据不同的端,应该如何去配置告警规则。我们首先来看端上访问。**端上访问** **端上访问指通过 App 或者浏览器进行的访问操作,这个层面的告警比较关注用户的使用体验** 。在告警配置中,App 的统计指标内容更多的是“查看”,因为浏览器更易于去迭代更新,所以一般会更多针对浏览器进行告警配置。 在浏览器的告警配置中,主要会关注两个方面:页面元素和接口访问。 - **页面元素** :页面中的资源加载和页面的访问情况,比如常见的 JS 脚本错误数、资源错误数等。通过对页面元素的监控,我们可以快速感知页面的出现错误的可能性,比如大面积的脚本错误就可能导致用户无法与页面完成正常的交互操作。 -- **接口访问** :与后端服务的接口访问交互情况,比如调用耗时时长、请求错误数。通过接口访问,可以感知用户操作时的体验。如果调用耗时较长则会出现等待现象。 **应用程序** **应用程序指请求流量到达服务器后端后,应用进行请求处理时的操作。这个层面我们会比较关注服务之间调用的情况、服务本身耗时情况、是否有异常产生等问题。** 在应用程序的告警配置中,通常会关注以下 4 点。 +- **接口访问** :与后端服务的接口访问交互情况,比如调用耗时时长、请求错误数。通过接口访问,可以感知用户操作时的体验。如果调用耗时较长则会出现等待现象。**应用程序** **应用程序指请求流量到达服务器后端后,应用进行请求处理时的操作。这个层面我们会比较关注服务之间调用的情况、服务本身耗时情况、是否有异常产生等问题。** 在应用程序的告警配置中,通常会关注以下 4 点。 1. **服务调用** :服务之间的 RPC 调用在微服务架构中可以说是最常见的,因此监控其中的调用关系就会变得至关重要。这一部分,我们通常会监控 **调用次数** 、 **出现错误次数** 、 **响应耗时** 等信息,并且通过生产者与消费者之间的关联关系,聚焦到具体的调用依赖上。如果响应耗时持续出现错误,则说明服务处理时出现超时或者业务异常等问题,要根据模块的重要程度及时反馈。 2. **数据库操作** :对数据库进行监控也是有必要的,因为我们的数据最终都会将其存储至数据库中,比如常见的 MySQL、Redis、ElasticSearch 等。我们一般会对 **调用次数** 、 **执行耗时** 进行监控。如果出现执行耗时相对较长的情况,则可能会有接口响应缓慢,甚至于接口出错的问题。 3. **JVM** : **在 Java 语言中,代码都是运行在 JVM 平台上的,JVM 性能的好坏决定着程序的运行效率** 。我们都知道,Java 程序在出现 Full GC 时会先进行内存回收再恢复业务线程执行,因此会造成业务程序停顿。所以此时我们一般会监控 **堆空间使用占比** 、 **GC 次数** 、 **GC 耗时** 。当堆空间内存使用占比到达 90%甚至更高时,需要多加关注,防止其朝着不好的方向发展。 -4. **限流熔断** :当系统请求量到达一定的阶段后,限流熔断可以对应用程序起到很好的保护作用。但我们仍要对限流熔断的次数进行监控。如果大量的请求都触发了限流熔断的保护措施,用户的使用体验就会受到影响。此时,我们可以统计 **触发限流或者熔断的次数与占比** ,比如占比超过 10%时,研发人员可以通过告警来确认,是否要限流或者调整熔断的规则,如果是程序引发的错误,则需要根据具体的业务场景来查询问题的原因。 **组件** **组件指我们经常使用到的中间件** ,比如 Nginx、Kafka、Redis。 **这里的监控更偏向于运维层面** , **通过监控这部分数据** , **快速了解组件的整体运行情况** 。 +4. **限流熔断** :当系统请求量到达一定的阶段后,限流熔断可以对应用程序起到很好的保护作用。但我们仍要对限流熔断的次数进行监控。如果大量的请求都触发了限流熔断的保护措施,用户的使用体验就会受到影响。此时,我们可以统计 **触发限流或者熔断的次数与占比**,比如占比超过 10%时,研发人员可以通过告警来确认,是否要限流或者调整熔断的规则,如果是程序引发的错误,则需要根据具体的业务场景来查询问题的原因。**组件** **组件指我们经常使用到的中间件**,比如 Nginx、Kafka、Redis。**这里的监控更偏向于运维层面**,**通过监控这部分数据**,**快速了解组件的整体运行情况** 。 在配置告警时,我们一般会按照 **网关层** 、 **数据库** 、 **队列** 、 **缓存** 4 个类型进行相关告警的配置。 -1. 网关层中有我们常见的 Nginx 等, **在这个组件中我们更加关注于请求的耗时与响应时的状态** 。当请求中具体的某一个接口出现超时的情况,要进行告警,告知接口存在缓慢情况,然后进行及时的优化,减少对用户使用体验的伤害。如果响应状态码出现大面积的 500,相对而言,这一问题的重要级别就会很高,因为这代表有很多用户在使用程序时都出现了严重的问题。 -1. 数据库比较常见的有 MySQL、MongoDB。 **在应用程序中我讲到,需要关注其相应耗时等信息** 。在组件中我们则会更加关注 **其他服务与本服务的链接情况** , **本身所产生的慢查询情况** ,也会关注 **常见主从架构中的主从延迟** 数。如果主从延迟数较高时,业务方在数据查询方面可能会有一些影响。 -1. 队列中常见的则有 Kafka、RocketMQ。 **在队列的监控中** , **我们更关注生产者与消费者之间的队列的待消费数量** ,从而获取到数据的堆积情况。比如出现长时间的堆积,则可能导致业务受阻,严重时会影响用户的使用体验。 -1. 缓存中有我们常见的 Redis。由于缓存一般都会将数据存储至内存中加速读取的效率,所以 **内存的使用情况便是缓存中关注的重点** 。通过监控内存的使用占比,我们可以快速得知内存的使用量,从而确定对缓存是否足够使用。 **我们还会关注缓存的命中率** ,如果长期存在命中率不高的情况,则要告知业务方,让业务方确认是否存在缓存穿透的问题。 **机器信息** **机器是应用程序和组件的运行基础。通过对机器信息进行深度告警配置,可以让我们感知到业务系统是否会出现错误。** 在配置告警时,我们一般会关注 **CPU** 、 **内存** 、 **磁盘** 和 **网络** 这 4 个方向。 +1. 网关层中有我们常见的 Nginx 等,**在这个组件中我们更加关注于请求的耗时与响应时的状态** 。当请求中具体的某一个接口出现超时的情况,要进行告警,告知接口存在缓慢情况,然后进行及时的优化,减少对用户使用体验的伤害。如果响应状态码出现大面积的 500,相对而言,这一问题的重要级别就会很高,因为这代表有很多用户在使用程序时都出现了严重的问题。 +1. 数据库比较常见的有 MySQL、MongoDB。**在应用程序中我讲到,需要关注其相应耗时等信息** 。在组件中我们则会更加关注 **其他服务与本服务的链接情况**,**本身所产生的慢查询情况**,也会关注 **常见主从架构中的主从延迟** 数。如果主从延迟数较高时,业务方在数据查询方面可能会有一些影响。 +1. 队列中常见的则有 Kafka、RocketMQ。**在队列的监控中**,**我们更关注生产者与消费者之间的队列的待消费数量**,从而获取到数据的堆积情况。比如出现长时间的堆积,则可能导致业务受阻,严重时会影响用户的使用体验。 +1. 缓存中有我们常见的 Redis。由于缓存一般都会将数据存储至内存中加速读取的效率,所以 **内存的使用情况便是缓存中关注的重点** 。通过监控内存的使用占比,我们可以快速得知内存的使用量,从而确定对缓存是否足够使用。**我们还会关注缓存的命中率**,如果长期存在命中率不高的情况,则要告知业务方,让业务方确认是否存在缓存穿透的问题。**机器信息** **机器是应用程序和组件的运行基础。通过对机器信息进行深度告警配置,可以让我们感知到业务系统是否会出现错误。** 在配置告警时,我们一般会关注 **CPU** 、 **内存** 、 **磁盘** 和 **网络** 这 4 个方向。 1. **CPU** :CPU 是数据计算的关键,如果 **CPU 使用率** 较高,可能会导致业务程序执行缓慢,进而影响到业务的处理。 2. **内存** :内存代表我们程序可以操作的内存空间,我们会更加关注 **内存的使用占比** 。如果出现较高的内存占比并且保持持续地增速,此时就需要进行告警通知,防止系统检测到内存占用过高而关闭进程。 3. **磁盘** :磁盘在我们进行日志写入、业务临时文件使用时十分关键。我们关注 **磁盘的剩余空间** 、 **磁盘写入负载** 等。比如服务磁盘写入负载到达一定的占比,则可能会堵塞程序运行。 -4. **网络** :我们在进行系统之间的 RPC 或者是系统对接第三方时,通常会使用网络来通信,此时我们可以监控 **网卡流入和流出的数据量** ,如果超过了一定的占比,并且持续增长则可能会导致网络传输堵塞,影响程序执行。 +4. **网络** :我们在进行系统之间的 RPC 或者是系统对接第三方时,通常会使用网络来通信,此时我们可以监控 **网卡流入和流出的数据量**,如果超过了一定的占比,并且持续增长则可能会导致网络传输堵塞,影响程序执行。 #### 告警流程 @@ -74,7 +74,7 @@ 2\. 上线系统:在上线系统中我们可以快速找到指定服务最近是否有上线单,如果最近有上线单则同样可以提供给通知对象,来判定是否和上线相关。 3\. 观测系统:从观测系统中,我们可以了解到相关告警的数据信息,来更快的让用户进行查询数据内容。 2. **通过获取系统中组织架构的数据了解研发人员及其 TL** 。出现问题时,可以快速找到与项目相关的研发人员。 -3\. 很多公司内部都会使用协同软件来进行同事之间交流的平台,常见的有钉钉、企业微信、飞书等。 **通过对接协同软件的 API** , **在出现问题时** , **快速联系到相关的同事** , **共同协作** , **处理问题** 。 +3\. 很多公司内部都会使用协同软件来进行同事之间交流的平台,常见的有钉钉、企业微信、飞书等。**通过对接协同软件的 API**,**在出现问题时**,**快速联系到相关的同事**,**共同协作**,**处理问题** 。 ### 总结 diff --git "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25421\350\256\262.md" "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25421\350\256\262.md" index c5b032310..ca7bded3e 100644 --- "a/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25421\350\256\262.md" +++ "b/docs/Design/\345\210\206\345\270\203\345\274\217\351\223\276\350\267\257\350\277\275\350\270\252\345\256\236\346\210\230/\347\254\25421\350\256\262.md" @@ -4,7 +4,7 @@ ### 课程回顾 -在整个课程中,我带你认识了 **可观测性的重要性** 、 **如何将告警与可观测性相结合** ,以及 **如何将可观测性落地到项目中** 。这些内容,我将它们一共分成了 3 个模块: +在整个课程中,我带你认识了 **可观测性的重要性** 、 **如何将告警与可观测性相结合**,以及 **如何将可观测性落地到项目中** 。这些内容,我将它们一共分成了 3 个模块: 在“ **模块一:可观测性原理** ”,我介绍了可观测性中的 3 大支柱:日志、统计指标和链路追踪,以及如何将它们结合到你的业务代码开发中,让你的程序更加具有可观测性。高度定制化的可观测性,可以让你更加了解你的程序,出现问题时也可以根据观测中的数据,快速定位问题产生的原因,从而解决问题。 @@ -18,9 +18,9 @@ 随着分布式系统架构的普及,开发和运维人员对可观测性越来越重视。无论系统架构怎样变化,唯一不变的是需要对它们进行观测。我相信随着可观测性的普及,会有越来越多的人参与到可观测性系统的开发上。对于业务开发人员来说,了解可观测性中每一个概念的关键原理,并结合公司内部的可观测性框架来编写高度定制化、可观测的代码,让你更快解决问题。 -现在可观测中,因为目前没有一套完整的成体系化的内容,所以可以看到现在很多有较大规模的厂商都在做不同的尝试。 **在数据规范协定中** ,OpenTelemetry 就在尝试将日志、统计指标与链路这 3 个概念融合,让其拥有统一的数据格式; **在告警问题归因上** ,阿里巴巴的鹰眼就在尝试利用可观测性中的数据,做问题的归因判断,减少开发人员发现问题的时间。 +现在可观测中,因为目前没有一套完整的成体系化的内容,所以可以看到现在很多有较大规模的厂商都在做不同的尝试。**在数据规范协定中**,OpenTelemetry 就在尝试将日志、统计指标与链路这 3 个概念融合,让其拥有统一的数据格式; **在告警问题归因上**,阿里巴巴的鹰眼就在尝试利用可观测性中的数据,做问题的归因判断,减少开发人员发现问题的时间。 -我相信可观测性在未来肯定还会有更多变化与改进,但这些变化和改进的最终目的 **都是服务开发人员的** ,都是用来帮助开发人员定位和解决问题的。 **计算机再强大,也离不开开发人员。** +我相信可观测性在未来肯定还会有更多变化与改进,但这些变化和改进的最终目的 **都是服务开发人员的**,都是用来帮助开发人员定位和解决问题的。**计算机再强大,也离不开开发人员。** ### 总结 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25400\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25400\350\256\262.md" index 25e437c51..8c02f1b1b 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25400\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25400\350\256\262.md" @@ -8,7 +8,7 @@ ### 微服务架构的盛行,带来了新的机遇与挑战 -随着各行业应用的日益复杂化,产品为了适应不断变化的市场环境,就需要快速地迭代,而为了适应这种快速迭代的开发需求,主流的开发框架便由传统的单体应用架构转向微服务架构。 **技术快速更迭,守成显然不是正确选择,测试从业者同样需要跟上时代的步伐,如果满足于现状则很容易掉队,甚至被淘汰。** 比如,现阶段很多测试从业者还在项目中进行着“点点点”的测试工作,其实这样不但工作效率极低,而且难以积累实质经验,久而久之就会变成恶性循环。 +随着各行业应用的日益复杂化,产品为了适应不断变化的市场环境,就需要快速地迭代,而为了适应这种快速迭代的开发需求,主流的开发框架便由传统的单体应用架构转向微服务架构。**技术快速更迭,守成显然不是正确选择,测试从业者同样需要跟上时代的步伐,如果满足于现状则很容易掉队,甚至被淘汰。** 比如,现阶段很多测试从业者还在项目中进行着“点点点”的测试工作,其实这样不但工作效率极低,而且难以积累实质经验,久而久之就会变成恶性循环。 再比如,很多测试从业者积累的知识、经验和技能,往往只适用于自己当下的工作场景,这也导致他们不能轻易地换测试对象、换业务模块或者去换工作。因为不仅要重新学习新业务和适应新的协作方,还要变换测试方法和技术等。 @@ -30,7 +30,7 @@ 对业务发展来说,质量保障体系是企业内部系统的技术和管理手段,是为满足业务发展需要、生产出满足质量目标的产品而建立的,有计划的、系统的企业活动。它随着业务发展的阶段性规划和目标做调整,具有强烈的实用意义,不是单为建立体系而建立。 -对个人职业发展来说,质量保障体系指明了一个测试人员的终极目标。初阶测试人员的工作重点为“具体的测试工作”,中阶测试人员除了“具体的测试工作”,还需要能够参与“质量保障体系的建设”,而像测试架构师、测试专家、测试经理等高阶测试人员则需要能够规划、设计和主导“质量保障体系的建设”。可见, **工作中对“质量保障体系建设”的投入度体现了测试人员的职业发展阶段和核心竞争力,并且影响着测试人员的薪资待遇。** > 具体到实际工作场景中,假如没有搭建测试环境并建立提交测试的规范,测试活动无从开展;假如没有设置可量化的质量目标,测试活动都不知道应该在什么时候结束;假如不对质量指标进行定期分析和运营,就没有办法针对某类质量痛点做定向改进;假如没有引入丰富的测试技术和手段,测试活动的充分性和效率就无法保障……这些都是质量保障体系的范畴,可见质量保障体系,既是基础,又是核心。 **对于测试人员来说,一定要尽早树立测试策略分析和构建质量保障体系的意识,从全局视角理解所在业务中的质量保障体系。以终为始,有意识、有规划地补齐质量保障体系中的各种手段和技能,才能去体验不同的职业成长路径。** ### 课程设计 +对个人职业发展来说,质量保障体系指明了一个测试人员的终极目标。初阶测试人员的工作重点为“具体的测试工作”,中阶测试人员除了“具体的测试工作”,还需要能够参与“质量保障体系的建设”,而像测试架构师、测试专家、测试经理等高阶测试人员则需要能够规划、设计和主导“质量保障体系的建设”。可见,**工作中对“质量保障体系建设”的投入度体现了测试人员的职业发展阶段和核心竞争力,并且影响着测试人员的薪资待遇。** > 具体到实际工作场景中,假如没有搭建测试环境并建立提交测试的规范,测试活动无从开展;假如没有设置可量化的质量目标,测试活动都不知道应该在什么时候结束;假如不对质量指标进行定期分析和运营,就没有办法针对某类质量痛点做定向改进;假如没有引入丰富的测试技术和手段,测试活动的充分性和效率就无法保障……这些都是质量保障体系的范畴,可见质量保障体系,既是基础,又是核心。**对于测试人员来说,一定要尽早树立测试策略分析和构建质量保障体系的意识,从全局视角理解所在业务中的质量保障体系。以终为始,有意识、有规划地补齐质量保障体系中的各种手段和技能,才能去体验不同的职业成长路径。** ### 课程设计 借由这个微服务质量保障专栏,我希望能弥补市面上这部分知识的空白。现如今绝大多数服务端都是以微服务的形式存在,这也正是我围绕微服务的测试和质量保障展开介绍的关键所在。 @@ -47,4 +47,4 @@ 曾国藩说:“ **既往不恋,当下不杂,未来不迎** ”,表达了他对于过去、现在与未来的不同态度:对未来有规划,不过分留恋过去,走好当下的每一步,为达成目标持续努力。这句话同样适用于每一位测试从业者。 -虽然我们每个人入行的起点不同,但 **目标和终点往往是一致的** ,希望你能够通过持续的学习和努力打造核心竞争力,让自己在职业道路上有更多的选择。而这也就意味着,你不能一味重复几乎所有从业者人都会做的事情。 +虽然我们每个人入行的起点不同,但 **目标和终点往往是一致的**,希望你能够通过持续的学习和努力打造核心竞争力,让自己在职业道路上有更多的选择。而这也就意味着,你不能一味重复几乎所有从业者人都会做的事情。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25401\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25401\350\256\262.md" index 7e3461c80..9fe7ab396 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25401\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25401\350\256\262.md" @@ -6,7 +6,7 @@ ### 单体应用架构下的服务特性 -我第一份工作是网络游戏的测试保障工作,在功能测试之外做了很多服务端相关的工作,如编译后分发、配置、部署、发布等。那时候的服务端应用程序是几个独立的几十兆、上百兆的文件。 **每个文件是一个可执行文件,包含一个系统的所有功能,这些功能被打包成一体化的文件,几乎没有外部依赖,可以独立部署在装有 Linux 系统的硬件服务器上。** 这种应用程序通常被称为单体应用,单体应用的架构方法论,就是单体应用架构(Monolithic Architecture)。单体应用架构下,一个服务中包含了与用户交互的部分、业务逻辑处理层和数据访问层。如果存在数据库交互则与数据库直连,如下图所示。 +我第一份工作是网络游戏的测试保障工作,在功能测试之外做了很多服务端相关的工作,如编译后分发、配置、部署、发布等。那时候的服务端应用程序是几个独立的几十兆、上百兆的文件。**每个文件是一个可执行文件,包含一个系统的所有功能,这些功能被打包成一体化的文件,几乎没有外部依赖,可以独立部署在装有 Linux 系统的硬件服务器上。** 这种应用程序通常被称为单体应用,单体应用的架构方法论,就是单体应用架构(Monolithic Architecture)。单体应用架构下,一个服务中包含了与用户交互的部分、业务逻辑处理层和数据访问层。如果存在数据库交互则与数据库直连,如下图所示。 ![Drawing 0.png](assets/Ciqc1F8VMbWADRXPAABfysmIcFg665.png) @@ -74,7 +74,7 @@ #### 微服务的缺点 -当然, **事物都有两面性,任何一项技术都不可能十全十美,在解决一定问题的同时,也会引入新的问题。** 那么,微服务架构下服务有哪些缺点呢? +当然,**事物都有两面性,任何一项技术都不可能十全十美,在解决一定问题的同时,也会引入新的问题。** 那么,微服务架构下服务有哪些缺点呢? 从微服务架构设计角度来看。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25402\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25402\350\256\262.md" index fd9cb4317..a2cbf74e8 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25402\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25402\350\256\262.md" @@ -77,7 +77,7 @@ ### 总结 -上述,我介绍了这么多微服务架构对软件质量保障工作带来的诸多挑战,你肯定坐不住了吧?不要担心,这些挑战都有对策。 **任何新技术的引入和架构的演变都在解决当前痛点问题的同时引入新的问题,那么这些新的问题也将不断变成痛点被逐个解决,这是技术演化的必然,也是互联网革命的核心(唯一的不变是变化)** 。 +上述,我介绍了这么多微服务架构对软件质量保障工作带来的诸多挑战,你肯定坐不住了吧?不要担心,这些挑战都有对策。**任何新技术的引入和架构的演变都在解决当前痛点问题的同时引入新的问题,那么这些新的问题也将不断变成痛点被逐个解决,这是技术演化的必然,也是互联网革命的核心(唯一的不变是变化)** 。 任何时候挑战和机遇都是并存的,通过掌握恰当的测试策略和质量保障体系来应对这些挑战,那么你就比同行(横向比较)或过去的自己(纵向比较)具有更多的竞争力和优势,自然也会有更多的机遇。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25403\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25403\350\256\262.md" index 826c871d1..8b39d815a 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25403\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25403\350\256\262.md" @@ -18,7 +18,7 @@ 需要说明的是,传统意义下的测试金字塔,在微服务架构下不再完全奏效。因为微服务中最大的复杂性不在于服务本身,而在于微服务之间的交互方式,这一点值得特别注意。 -因此, **针对微服务架构,常见的测试策略模型** 有如下几种。 +因此,**针对微服务架构,常见的测试策略模型** 有如下几种。 (1) **微服务“测试金字塔”** 基于微服务架构的特点和测试金字塔的原理,Toby Clemson 有一篇关于[“微服务架构下的测试策略”](https://www.martinfowler.com/articles/microservice-testing/)的文章,其中通过分析阐述了微服务架构下的通用测试策略。 @@ -34,7 +34,7 @@ ![Drawing 5.png](assets/CgqCHl8ZQs-AByNAAACgJaZwyyU241.png) -我想,有多少个基于微服务架构的测试团队大概就有多少个测试策略模型吧。 **“测试金字塔”是一种测试策略模型和抽象框架** ,当技术架构、系统特点、质量痛点、团队阶段不同时,每种测试的比例也不尽相同,而且最关键的,并不一定必须是金字塔结构。 +我想,有多少个基于微服务架构的测试团队大概就有多少个测试策略模型吧。**“测试金字塔”是一种测试策略模型和抽象框架**,当技术架构、系统特点、质量痛点、团队阶段不同时,每种测试的比例也不尽相同,而且最关键的,并不一定必须是金字塔结构。 理解了测试策略模型的思考框架,我们看下应如何保障测试活动的全面性和有效性。 @@ -56,28 +56,28 @@ 确定了分层测试方法,我们应该如何选取每种测试方法的比例,来确保该测试策略的有效性呢? -首先必须要明确的是不存在普适性的测试组合比例。我们都知道,测试的目的是解决企业的质量痛点、交付高质量的软件。因此不能为了测试而测试,更不能为了质量而不惜一切代价,需要 **考虑资源的投入产出比。** 测试策略如同测试技术、技术架构一样,并不是一成不变,它会随着业务或项目所处的阶段,以及基于此的其他影响因素的变化而不断演进。但归根结底,还是要从质量保障的目标出发,制定出适合当时的测试策略,并阶段性地对策略进行评估和度量,进而不断改进和优化测试策略。因此, **选取测试策略一定要基于现实情况的痛点出发,结果导向,通过调整测试策略来解决痛点。** 比如,在项目早期阶段或某 MVP 项目中,业务的诉求是尽快发布到线上,对功能的质量要求不太高,但对发布的时间节点要求非常严格。那这种情况下快速地用端到端这种能模拟用户真实价值的测试方法保障项目质量也未尝不可;随着项目逐渐趋于平稳后,时间要求渐渐有了节奏,对功能的质量要求会逐渐变高,那么这时候可以再根据实际情况引入其他测试方法,如契约测试或组件测试等。 +首先必须要明确的是不存在普适性的测试组合比例。我们都知道,测试的目的是解决企业的质量痛点、交付高质量的软件。因此不能为了测试而测试,更不能为了质量而不惜一切代价,需要 **考虑资源的投入产出比。** 测试策略如同测试技术、技术架构一样,并不是一成不变,它会随着业务或项目所处的阶段,以及基于此的其他影响因素的变化而不断演进。但归根结底,还是要从质量保障的目标出发,制定出适合当时的测试策略,并阶段性地对策略进行评估和度量,进而不断改进和优化测试策略。因此,**选取测试策略一定要基于现实情况的痛点出发,结果导向,通过调整测试策略来解决痛点。** 比如,在项目早期阶段或某 MVP 项目中,业务的诉求是尽快发布到线上,对功能的质量要求不太高,但对发布的时间节点要求非常严格。那这种情况下快速地用端到端这种能模拟用户真实价值的测试方法保障项目质量也未尝不可;随着项目逐渐趋于平稳后,时间要求渐渐有了节奏,对功能的质量要求会逐渐变高,那么这时候可以再根据实际情况引入其他测试方法,如契约测试或组件测试等。 -你要永远记住, **适合自身项目阶段和团队的测试策略才是“完美”的策略。** ![Drawing 7.png](assets/CgqCHl8ZSvOAK06pAAVCHyjoRMg396.png) +你要永远记住,**适合自身项目阶段和团队的测试策略才是“完美”的策略。**![Drawing 7.png](assets/CgqCHl8ZSvOAK06pAAVCHyjoRMg396.png) ### 如何建立质量保障体系? -上述分层的测试策略只是尽可能地对微服务进行全面的测试,确保系统的所有层次都被覆盖到,它更多体现在测试活动本身的全面性和有效性方面。要想将质量保障内化为企业的组织能力,就需要通过技术和管理手段形成系统化、标准化和规范化的机制,这就需要建设质量保障体系。 **质量保障体系:通过一定的流程规范、测试技术和方法,借助于持续集成/持续交付等技术把质量保障活动有效组合,进而形成系统化、标准化和规范化的保障体系。** 同时,还需要相应的度量、运营手段以及组织能力的保障。 +上述分层的测试策略只是尽可能地对微服务进行全面的测试,确保系统的所有层次都被覆盖到,它更多体现在测试活动本身的全面性和有效性方面。要想将质量保障内化为企业的组织能力,就需要通过技术和管理手段形成系统化、标准化和规范化的机制,这就需要建设质量保障体系。**质量保障体系:通过一定的流程规范、测试技术和方法,借助于持续集成/持续交付等技术把质量保障活动有效组合,进而形成系统化、标准化和规范化的保障体系。** 同时,还需要相应的度量、运营手段以及组织能力的保障。 如下几个方面是质量保障体系的关键,后续课程也将按如下方式展开讲解。 - **流程规范** :没有规矩不成方圆,好的流程规范是保障质量中非常关键的一环。当出现交付质量差的情况时,过程质量也一定是差的。通常会出现某些关键动作未执行或执行不到位、对事情的不当处理等情况,而这些情况可以通过建立闭环、分工明确的流程规范完全可以避免。另外,研发过程中,过程质量跟执行人的质量意识、个人能力等直接相关,那么就需要建立易执行的流程规范,降低人员的执行门槛。同时需要特别注意,规范的不断完善是几乎所有团队的常态,但当规范执行效果不好时一定要及时跟进,分析其根本原因,必要时要进行简化。 - **测试技术** : 测试策略模型中的分层测试方法可以使面向微服务的测试活动具有一定的全面性和有效性,使得被测内容在功能方面符合预期。除功能性之外,软件质量还有其他很多属性,如可靠性、易用性、可维护性、可移植性等,而这些质量属性就需要通过各种专项测试技术来保障了。同时,还有许多测试技术的首要价值在于提升测试效率。因此合理地组合这些测试技术,形成测试技术矩阵,有利于最大化发挥它们的价值。 - **持续集成与持续交付** :微服务的优势需要通过持续集成和持续交付的支持才能充分发挥出来,这就要求在执行测试活动时提高反馈效率、尽快发现问题。一方面要明确各种“类生产环境”在交付流程中的位置和用途差异点,保证它们的稳定可用。另一方面需要将各种测试技术和自动化技术集成起来,使代码提交后能够自动进行构建、部署和测试等操作,形成工具链,实现真正意义上的持续集成和持续交付。 -- **度量与运营** :管理学大师德鲁克曾经说过“你如果无法度量它,就无法管理它(It you can’t measure it, you can’t manage it)”。要想能有效管理和改进,就难以绕开度量这个话题。对于研发过程来说,度量无疑是比较难的,它是一个脑力密集型的过程,指标多维度,且很多维度的内容难以清晰地度量。在质量保障体系中,我将基于质量、效率、价值等多维度视角建立起基础的度量体系,并结合定期运营做定向改进,形成 PDCA 正向循环,促使各项指标稳步提升。同时,需要特别警惕的是, **度量是一把双刃剑** ,这里我也会告诉一些我的经验教训和踩过的坑,避免走错方向。 +- **度量与运营** :管理学大师德鲁克曾经说过“你如果无法度量它,就无法管理它(It you can’t measure it, you can’t manage it)”。要想能有效管理和改进,就难以绕开度量这个话题。对于研发过程来说,度量无疑是比较难的,它是一个脑力密集型的过程,指标多维度,且很多维度的内容难以清晰地度量。在质量保障体系中,我将基于质量、效率、价值等多维度视角建立起基础的度量体系,并结合定期运营做定向改进,形成 PDCA 正向循环,促使各项指标稳步提升。同时,需要特别警惕的是,**度量是一把双刃剑**,这里我也会告诉一些我的经验教训和踩过的坑,避免走错方向。 - **组织保障** :产品的交付离不开组织中每个参与部门成员的努力。正如质量大师戴明所说,质量是设计出来的,不是测试出来的。因此在组织中树立起“质量文化”至关重要。在这部分内容里,我将介绍常见的参与方的角色、职责和协作过程中的常见问题、对策,以及如何营造质量文化等内容。 ### 总结 -在本课中,我谈到了基于微服务架构下的各种质量挑战,可以从两个方面有效且高效地保障微服务的质量: **确保面向微服务的测试活动具备全面性和有效性** , **质量保障需要内化为企业的组织能力。** 通过对测试金字塔原理和微服务的特点分析,引入单元测试、集成测试、组件测试、契约测试和端到端测试等分层测试类型来确保测试活动的全面性,通过自身项目阶段和团队情况来选取合适的测试策略模型,以保障测试活动的有效性。 +在本课中,我谈到了基于微服务架构下的各种质量挑战,可以从两个方面有效且高效地保障微服务的质量: **确保面向微服务的测试活动具备全面性和有效性**,**质量保障需要内化为企业的组织能力。** 通过对测试金字塔原理和微服务的特点分析,引入单元测试、集成测试、组件测试、契约测试和端到端测试等分层测试类型来确保测试活动的全面性,通过自身项目阶段和团队情况来选取合适的测试策略模型,以保障测试活动的有效性。 要想把质量保障内化为企业的组织能力,就需要通过系统的技术和管理手段形成机制,在流程规范、测试技术、持续集成与持续交付、度量与运营、组织保障等方面构建质量保障体系。 -你是否测试过微服务架构的项目和服务?如果有,欢迎在留言区评论,说说你所经历过的项目的测试策略和质量保障体系是怎样的,期间遇到了哪些困难和问题。同时欢迎你能把这篇文章分享给你的同学、朋友和同事,大家一起交流。 **相关链接** +你是否测试过微服务架构的项目和服务?如果有,欢迎在留言区评论,说说你所经历过的项目的测试策略和质量保障体系是怎样的,期间遇到了哪些困难和问题。同时欢迎你能把这篇文章分享给你的同学、朋友和同事,大家一起交流。**相关链接** > Testing Strategies in a Microservice Architecture(微服务架构下的测试策略): :[https://www.martinfowler.com/articles/microservice-testing/](https://www.martinfowler.com/articles/microservice-testing/) diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25404\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25404\350\256\262.md" index 278a204cf..f9a7df062 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25404\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25404\350\256\262.md" @@ -107,7 +107,7 @@ - **测试数据要求** :尽量使用生产环境的测试数据以保障有效性和多样性。 - **颗粒度要求** :要保证测试粒度足够小,有助于精确定位问题。单测粒度一般是方法级别,最好不要超过类级别。只有测试粒度小才能在出错时尽快定位到出错位置,一个待测试方法建议关联一个测试方法,如果待测试方法逻辑复杂分支较多,建议拆分为多个测试方法。 - **验证结果必须要符合预期** :简单来说就是单元测试必须执行通过,执行失败时要及时查明原因并修正问题。 -- **代码要遵守 BCDE 原则** ,以保证被测试模块的交付质量。 +- **代码要遵守 BCDE 原则**,以保证被测试模块的交付质量。 - B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。 - C:Correct,正确的输入,并得到预期的结果。 - D:Design,与设计文档相结合,来编写单元测试。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25405\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25405\350\256\262.md" index 3dae11ca6..232978e82 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25405\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25405\350\256\262.md" @@ -10,9 +10,9 @@ 即,集成测试(有时称为集成和测试,简称 I&T)是软件测试中的阶段,在该阶段中,将各个单独开发的软件模块组合在一起并进行整体测试,以便评估系统或组件是否符合指定的功能要求。 -微服务架构下也需要集成测试, **需要针对不同服务的不同方法之间的通信情况进行相关测试。** 因为在对微服务进行单元测试时,单元测试用例只会验证被测单元的内部逻辑,并不验证其依赖的模块。即使对于服务 A 和服务 B 的单元测试分别通过,并不能说明服务 A 和服务 B 的交互是正常的。 +微服务架构下也需要集成测试,**需要针对不同服务的不同方法之间的通信情况进行相关测试。** 因为在对微服务进行单元测试时,单元测试用例只会验证被测单元的内部逻辑,并不验证其依赖的模块。即使对于服务 A 和服务 B 的单元测试分别通过,并不能说明服务 A 和服务 B 的交互是正常的。 -对于微服务架构来说, **集成测试通常关注于验证那些与外部组件(例如数据存储或其他微服务)通信的子系统或模块。** 目标是验证这些子系统或模块是否可以正确地与外部组件进行通信,而不是测试外部组件是否正常工作。因此,微服务架构下的集成测试,应该验证要集成的子系统之间与外部组件之间的基本通信路径,包括正确路径和错误路径。 +对于微服务架构来说,**集成测试通常关注于验证那些与外部组件(例如数据存储或其他微服务)通信的子系统或模块。** 目标是验证这些子系统或模块是否可以正确地与外部组件进行通信,而不是测试外部组件是否正常工作。因此,微服务架构下的集成测试,应该验证要集成的子系统之间与外部组件之间的基本通信路径,包括正确路径和错误路径。 ### 微服务架构下的集成测试 @@ -78,7 +78,7 @@ serviceResponse: null, #### 服务不可用 -针对服务不可用的情况, **微服务虚拟化技术** 可以完美解决这种问题,它是避免与其他服务通信时出现意外的必要工具,在具有大量依赖项的企业环境中工作的时候更是如此。它可以用于在测试阶段消除对第三方服务的依赖,测试应用程序在遇到延迟或其他网络问题时的行为。它通过创建代理服务实现对依赖服务的模拟,特别适合测试服务之间的通信。常见的工具有 Wiremock、Hoverfly、Mountebank 等。 +针对服务不可用的情况,**微服务虚拟化技术** 可以完美解决这种问题,它是避免与其他服务通信时出现意外的必要工具,在具有大量依赖项的企业环境中工作的时候更是如此。它可以用于在测试阶段消除对第三方服务的依赖,测试应用程序在遇到延迟或其他网络问题时的行为。它通过创建代理服务实现对依赖服务的模拟,特别适合测试服务之间的通信。常见的工具有 Wiremock、Hoverfly、Mountebank 等。 以 Wiremock 为例,如下代码的效果是:当相对 URL 完全匹配 /api/json/cet/now 时,将返回状态 200,响应的主体类似于 [/api/json/cet/now](http://worldclockapi.com/api/json/cet/now)的返回值,Content-Type Header 的值为 text/plain。否则,当相对 URL 错误,比如访问 /api/json **111** /cet/now 时,则返回 404 的错误。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25406\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25406\350\256\262.md" index d249ef9cd..6dd608341 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25406\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25406\350\256\262.md" @@ -182,7 +182,7 @@ Wiremock 的模拟能力远远不止这些,足够你用它来模拟被测服 测试设计复杂、成本更高; 运行速度慢; 跨网络,运行环境不稳定; -对比两者,进程外组件测试的优势并不明显,因此, **实际项目测试过程中应首选进程内组件测试** 。如果微服务具有复杂的集成、持久性或启动逻辑,则进程外(out-of-process )方法可能更合适。 +对比两者,进程外组件测试的优势并不明显,因此,**实际项目测试过程中应首选进程内组件测试** 。如果微服务具有复杂的集成、持久性或启动逻辑,则进程外(out-of-process )方法可能更合适。 ### 总结 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25407\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25407\350\256\262.md" index 96a54e5b5..8f262f116 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25407\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25407\350\256\262.md" @@ -6,7 +6,7 @@ 在介绍契约测试之前,首先来看下什么是契约。现实世界中,契约是一种书面的约定,比如租房时需要跟房东签房屋租赁合同、买房时需要签署购房合同、换工作时你要跟公司签署劳动合同等。在信息世界中,契约也有很多使用场景,像 TCP/IP 协议簇、HTTP 协议等,只是这些协议已经成为一种技术标准,我们只需要按标准方式接入就可以实现特定的功能。 -具体到业务场景中,契约是研发人员在技术设计时达成的约定,它规定了服务提供者和服务消费者的交互内容。可见,无论是物理世界还是信息世界, **契约是双方或多方共识的一种约定,需要协同方共同遵守。** 在微服务架构中,服务与服务之间的交互内容更需要约定好。因为一个微服务可能与其他 N 个微服务进行交互,只有对交互内容达成共识并保持功能实现上的协同,才能实现业务功能。我们来看一个极简场景,比如我们要测试服务 A 的功能,然而需要服务 A 调用服务 B 才能完成,如图: +具体到业务场景中,契约是研发人员在技术设计时达成的约定,它规定了服务提供者和服务消费者的交互内容。可见,无论是物理世界还是信息世界,**契约是双方或多方共识的一种约定,需要协同方共同遵守。** 在微服务架构中,服务与服务之间的交互内容更需要约定好。因为一个微服务可能与其他 N 个微服务进行交互,只有对交互内容达成共识并保持功能实现上的协同,才能实现业务功能。我们来看一个极简场景,比如我们要测试服务 A 的功能,然而需要服务 A 调用服务 B 才能完成,如图: ![Drawing 0.png](assets/CgqCHl8rwdWARQ7JAAAlzqKNM8A650.png) @@ -14,7 +14,7 @@ ![Drawing 2.png](assets/CgqCHl8rwd2AHsPJAAAqXjJCb3o139.png) -需要特别注意的是,如果此时你针对内部系统的测试用例都执行通过了,可以说明你针对服务 A的测试是通过的吗?答案是否定的。因为这里面有个 **特别重要的假设是** ,服务虚拟化出来的Mock B 服务与真实的 B 服务是相等的。而事实是,它们可能只在你最初进行服务虚拟化时是相等的,随着时间的推移,它们很难保持相等。 +需要特别注意的是,如果此时你针对内部系统的测试用例都执行通过了,可以说明你针对服务 A的测试是通过的吗?答案是否定的。因为这里面有个 **特别重要的假设是**,服务虚拟化出来的Mock B 服务与真实的 B 服务是相等的。而事实是,它们可能只在你最初进行服务虚拟化时是相等的,随着时间的推移,它们很难保持相等。 ![Drawing 4.png](assets/CgqCHl8rweeAaDkdAABVWLFzSS8274.png) @@ -196,7 +196,7 @@ public class UserControllerProviderTest { ### 总结 -本节课我首先讲解了契约的定义,通俗地讲,它是双方或多方共识的一种约定,需要协同方共同遵守。而在微服务架构下, **契约(Contract)是指服务的消费者(Consumer)与服务的提供者(Provider)之间交互协作的约定,主要包括请求和响应两部分。** 紧接着讲解了微服务架构下跨服务测试的痛点和难点,因而引入了契约测试的概念,它的指导思想是 **通过“契约”来降低服务和服务之间的依赖** ,即,将契约作为中间标准,对消费者与提供者间的协作进行的验证。根据测试对象的不同,契约测试分为两种,但最常用的契约测试类型是 **消费者驱动的契约测试** (Consumer-Driven Contract Test,简称 CDC)。核心思想是 **从消费者业务实现的角度出发** ,由消费者端定义需要的数据格式以及交互细节,生成一份契约文件。然后提供者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证该实现结果是否正确。契约测试框架也有多种,但最常见的框架有 Spring Cloud Contract 和 Pact,其中 Pact 框架更为流行。 +本节课我首先讲解了契约的定义,通俗地讲,它是双方或多方共识的一种约定,需要协同方共同遵守。而在微服务架构下,**契约(Contract)是指服务的消费者(Consumer)与服务的提供者(Provider)之间交互协作的约定,主要包括请求和响应两部分。** 紧接着讲解了微服务架构下跨服务测试的痛点和难点,因而引入了契约测试的概念,它的指导思想是 **通过“契约”来降低服务和服务之间的依赖**,即,将契约作为中间标准,对消费者与提供者间的协作进行的验证。根据测试对象的不同,契约测试分为两种,但最常用的契约测试类型是 **消费者驱动的契约测试** (Consumer-Driven Contract Test,简称 CDC)。核心思想是 **从消费者业务实现的角度出发**,由消费者端定义需要的数据格式以及交互细节,生成一份契约文件。然后提供者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证该实现结果是否正确。契约测试框架也有多种,但最常见的框架有 Spring Cloud Contract 和 Pact,其中 Pact 框架更为流行。 最后给出了基于 Pact 框架的契约测试实例的大体步骤,并在文稿下方给出了示例代码地址,感兴趣的同学可以自行学习。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25408\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25408\350\256\262.md" index a62a0fa7d..b839809e5 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25408\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25408\350\256\262.md" @@ -10,7 +10,7 @@ ![image](assets/CgqCHl8yTcSAeDftAAB83pJdFxE154.png) -与其他类型的测试相反, **端到端测试是面向业务的** ,其目的是验证应用程序系统整体上是否符合业务目标。为了实现这一目标,该系统通常被视为 **黑盒子** :尽可能完整地部署系统中的微服务,并主要通过 GUI 和 API 等公共接口对其进行操作。 +与其他类型的测试相反,**端到端测试是面向业务的**,其目的是验证应用程序系统整体上是否符合业务目标。为了实现这一目标,该系统通常被视为 **黑盒子** :尽可能完整地部署系统中的微服务,并主要通过 GUI 和 API 等公共接口对其进行操作。 > GUI:Graphical User Interface,又称图形用户界面或图形用户接口。它是采用图形方式显示的计算机操作用户界面,是一种人与计算机通信的界面显示格式,允许用户使用鼠标等输入设备操纵屏幕上的图标或菜单选项,以选择命令、调用文件、启动程序或执行其他一些日常任务。 > @@ -20,7 +20,7 @@ #### 测试范围 -通过微服务的分层测试策略可知, **端到端测试的范围比其他类型的测试大得多。** +通过微服务的分层测试策略可知,**端到端测试的范围比其他类型的测试大得多。** ![分层测试策略-示例2.png](assets/Ciqc1F8yTdSAPYvnAAVCHyjoRMg047.png) @@ -38,8 +38,8 @@ 因为端到端测试是面向业务的,那么测试时要从真实用户的使用场景来进行测试,根据应用程序系统是否有 GUI,可以分为两种情况: -- **应用程序系统有 GUI** ,这种情况下用户可以直接操作 GUI 来使用系统,那么诸如 Selenium WebDriver 之类的工具可以帮助驱动 GUI 触发系统内的特定行为。 -- **应用程序系统没有 GUI** ,这种情况下,使用 HTTP 客户端通过其公共的 API 直接操作微服务。没有真实的 GUI,不能直观地看到业务功能行为,但可以通过后台数据来确定系统的正确性,比如 API 的返回结果、持久化数据的变化情况,等等。 +- **应用程序系统有 GUI**,这种情况下用户可以直接操作 GUI 来使用系统,那么诸如 Selenium WebDriver 之类的工具可以帮助驱动 GUI 触发系统内的特定行为。 +- **应用程序系统没有 GUI**,这种情况下,使用 HTTP 客户端通过其公共的 API 直接操作微服务。没有真实的 GUI,不能直观地看到业务功能行为,但可以通过后台数据来确定系统的正确性,比如 API 的返回结果、持久化数据的变化情况,等等。 #### 测试设计 @@ -80,7 +80,7 @@ REST-assured 库可以绕开 GUI 来测试 REST API 的服务,它用于针对A 由于可以通过较低级别的测试技术来获得微服务的质量信心,因此端到端测试的作用是确保一切都紧密联系在一起,从而实现业务目标。在端到端这个级别的测试上,全面地测试业务需求无疑是浪费的,尤其当微服务数量较多时,它的投入产出比必然很低。所以需要所有其他测试手段都用尽后,再进行端到端测试,并以此作为最终的质量保证。 -同时需要警惕的是, **端到端测试要尽可能地少,但绝不能省略它** 。因为微服务架构下的分层测试,每一层都有独特的作用,不可轻易省略某一层级的测试。对于端到端测试来说,无论如何也需要验证业务的核心链路和功能。微服务测试人员经常会犯的错误是,在充分进行了较低层次的测试后,会乐观地认为已不存在质量问题,结果问题被生产环境的真实用户发现后才追悔莫及。 +同时需要警惕的是,**端到端测试要尽可能地少,但绝不能省略它** 。因为微服务架构下的分层测试,每一层都有独特的作用,不可轻易省略某一层级的测试。对于端到端测试来说,无论如何也需要验证业务的核心链路和功能。微服务测试人员经常会犯的错误是,在充分进行了较低层次的测试后,会乐观地认为已不存在质量问题,结果问题被生产环境的真实用户发现后才追悔莫及。 - 分析缺陷产生的层次,推进分层测试的落地与完善 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25409\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25409\350\256\262.md" index 132ca63bf..340bcb704 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25409\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25409\350\256\262.md" @@ -8,7 +8,7 @@ 为什么会出现这种情况呢?这需要回归到“质量保障”和“体系”的定义上来。 -- **质量保障的定义** 通常情况下,对业务发展来说, **质量保障体系是企业内部系统的技术和管理手段,是有计划的、系统的企业活动,目的是满足业务发展需要,生产出满足质量目标的产品。** +- **质量保障的定义** 通常情况下,对业务发展来说,**质量保障体系是企业内部系统的技术和管理手段,是有计划的、系统的企业活动,目的是满足业务发展需要,生产出满足质量目标的产品。** 对应到微服务架构下,说得更接地气一点就是为了共同的目标,一群人在一块儿做事。总结如下: @@ -20,7 +20,7 @@ > PMBOK 是 Project Management Body Of Knowledge 的缩写, 指项目管理知识体系的意思,具体是美国项目管理协会(PMI)对项目管理所需的知识、技能和工具进行的概括性描述。它涵盖了五大过程组和十大知识领域,其中,五大过程组是启动过程、规划过程、执行过程、监控过程、收尾过程;十大知识领域是整合管理、范围管理、时间管理、成本管理、质量管理、人力资源管理、沟通管理、风险管理、采购管理、干系人管理。 -可见,当一个体系进行了比较合理的抽象和概括后,它能够把一系列的活动拆解成不同的方面,这些方面又相互协同形成一个有机的整体,做到以不变应万变。我想,这也是 **质量保障体系对每个测试从业者个人职业发展来讲最有价值的地方** ,因此一定要尽早建立质量保障体系的意识,从全局视角理解所在业务中的质量保障体系。 +可见,当一个体系进行了比较合理的抽象和概括后,它能够把一系列的活动拆解成不同的方面,这些方面又相互协同形成一个有机的整体,做到以不变应万变。我想,这也是 **质量保障体系对每个测试从业者个人职业发展来讲最有价值的地方**,因此一定要尽早建立质量保障体系的意识,从全局视角理解所在业务中的质量保障体系。 ### 建立质量保障体系的切入点 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25410\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25410\350\256\262.md" index 4377b0f69..52124ab1a 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25410\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25410\350\256\262.md" @@ -10,7 +10,7 @@ - 日常运营/运维阶段。是指产品发布上线后,通过各类运营手段和运维手段向客户提供符合需求的、高可用的产品与服务。其中,运营常见的活动有拉新、留存、促活等。运维常见的活动有容量规划与实施、服务集群维护、系统容错管理等。 - 售后服务阶段。它主要由客服人员或售后工程师主导,包括解答或解决用户在使用产品后产生的疑问和投诉等。 -产品研发阶段是指需求产生到需求上线的过程,这阶段是测试人员的“主战场”。但这三个阶段共同组成了整个业务流程,要做到全流程质量保障,需要具有全局思维。即 **积极影响产品研发阶段** ,推动流程规范的制定、建设和完善; **对日常运营/运维阶段和售后服务阶段保持关注** ,定期收集这两个阶段中遇到的问题,做好协同和配合,思考在产品研发阶段如何预防或闭环解决这类问题。 +产品研发阶段是指需求产生到需求上线的过程,这阶段是测试人员的“主战场”。但这三个阶段共同组成了整个业务流程,要做到全流程质量保障,需要具有全局思维。即 **积极影响产品研发阶段**,推动流程规范的制定、建设和完善; **对日常运营/运维阶段和售后服务阶段保持关注**,定期收集这两个阶段中遇到的问题,做好协同和配合,思考在产品研发阶段如何预防或闭环解决这类问题。 ![Drawing 0.png](assets/CgqCHl87m3iAXfC1AACy09bRN3E695.png) @@ -24,8 +24,8 @@ 流程规范是用来约束有关部门和人员的,在产品研发流程中主要有如下关键角色。 -- **项目经理(Project Manager,简称 PM)** :通常情况,如果业务或项目设置了项目经理的角色,那么像日常的项目管理、流程规范制定等工作一般由项目经理来主导,其他协同部门有义务配合。如果没有设置项目经理这样的角色,流程规范的制定由各协同方共同商议决定,其中产品研发流程的规范绝大多数都由测试部门主导制定,一般由测试部门编写初稿,与协同部门共同商议后确定。 **本文中默认项目中并未设置项目经理的角色。** - **产品经理(Product Manager,简称 PM)** :主要负责对需求进行分析、编写需求文档、组织需求文档的评审、协调项目资源、对交付结果进行验收等工作。 -- **研发人员(Research and Development engineer,简称 RD)** :负责编写技术设计方案、编码(包括与协同方联调和自测),最终把交付物提交给测试人员进行测试。测试完成后把交付物发布到线上(对于发布环节来说,不同的公司中该环节的操作人员不一样,比如可能的发布人员有 SRE、测试人员、研发人员等, **本文中,发布环节假定由研发人员操作** )。 +- **项目经理(Project Manager,简称 PM)** :通常情况,如果业务或项目设置了项目经理的角色,那么像日常的项目管理、流程规范制定等工作一般由项目经理来主导,其他协同部门有义务配合。如果没有设置项目经理这样的角色,流程规范的制定由各协同方共同商议决定,其中产品研发流程的规范绝大多数都由测试部门主导制定,一般由测试部门编写初稿,与协同部门共同商议后确定。**本文中默认项目中并未设置项目经理的角色。** - **产品经理(Product Manager,简称 PM)** :主要负责对需求进行分析、编写需求文档、组织需求文档的评审、协调项目资源、对交付结果进行验收等工作。 +- **研发人员(Research and Development engineer,简称 RD)** :负责编写技术设计方案、编码(包括与协同方联调和自测),最终把交付物提交给测试人员进行测试。测试完成后把交付物发布到线上(对于发布环节来说,不同的公司中该环节的操作人员不一样,比如可能的发布人员有 SRE、测试人员、研发人员等,**本文中,发布环节假定由研发人员操作** )。 - **质量保障人员(Qualtiy Assurance,简称 QA,很多时候通俗表达为测试人员)** :对于当前需求来说,主要负责确保该需求的交付物符合产品需求。 > SRE是指Site Reliability Engineer (网站可靠性工程师)。他是软件工程师和系统管理员的结合,一个SRE工程师基本上需要掌握很多知识:算法、数据结构、编程能力、网络编程、分布式系统、可扩展架构、故障排除等。 @@ -40,7 +40,7 @@ - 重评审和讨论,群策群力 -产品研发过程是一个脑力密集型的工作,复杂度高, **大量的实践统计表明,在大规模软件开发中超过 50% 的错误来自需求分析和技术设计阶段** 。为了最大程度地降低风险,在其流程中需要加大评审和讨论环节的投入,通过多方审查的机制来保证过程质量、提高研发效率,所以,需求阶段和研发阶段的早期流程应有好的规范。 +产品研发过程是一个脑力密集型的工作,复杂度高,**大量的实践统计表明,在大规模软件开发中超过 50% 的错误来自需求分析和技术设计阶段** 。为了最大程度地降低风险,在其流程中需要加大评审和讨论环节的投入,通过多方审查的机制来保证过程质量、提高研发效率,所以,需求阶段和研发阶段的早期流程应有好的规范。 - 前紧后松,提前应对风险 @@ -62,7 +62,7 @@ #### 规范如何呈现? -流程规范涉及多方协作,其呈现形式的第一要点应为 **通俗易懂** ,一图胜千言,建议采用流程图的方式来展现,比如使用泳道图。 +流程规范涉及多方协作,其呈现形式的第一要点应为 **通俗易懂**,一图胜千言,建议采用流程图的方式来展现,比如使用泳道图。 ![Drawing 2.png](assets/CgqCHl87m6-AB5XhAAw01YGdoL8801.png) @@ -96,13 +96,13 @@ 常见的需求表述问题有“同线上逻辑”“同已有逻辑”,或者一句话的概况描述,如“每种状态都需要处理”,却不说明一共有几种状态,这些都非常容易产生理解上的偏差,应该予以杜绝。 -其中, **测试人员尤其要重视需求的可测性。** 早期提出可测试性方面的问题和风险,可以及早应对,从而降低项目风险。否则,到了后续的环节才发现需求不可测,这可能会导致需求变更或技术实现方案的变更,这对质量和效率的影响就太大了。 **测试人员如何参与需求评审?** ![Drawing 3.png](assets/CgqCHl87m7qAQWKQAAE2Bwd7NNk811.png) +其中,**测试人员尤其要重视需求的可测性。** 早期提出可测试性方面的问题和风险,可以及早应对,从而降低项目风险。否则,到了后续的环节才发现需求不可测,这可能会导致需求变更或技术实现方案的变更,这对质量和效率的影响就太大了。**测试人员如何参与需求评审?**![Drawing 3.png](assets/CgqCHl87m7qAQWKQAAE2Bwd7NNk811.png) -对于测试人员来说,在要进行需求评审或技术设计评审时,通常情况下还在另外一个需求的测试执行过程当中。测试执行过程通常需要投入较高的专注度,所以很多测试人员最最容易出现的情况是, **弱化需求评审或技术设计评审环节,投入度较低,等其他需求测试完成了再花精力去熟悉它。** 殊不知,这就造成了长期的恶性循环。 **正确的做法是,强化需求评审或技术设计评审环节,投入较多的精力,前置思考好一个需求中的重点、难点、风险点,提前应对** 。如果与测试执行时间有一定的冲突,则可以优先投入更多的个人时间来化解,同时在后续的测试执行过程中留有一定的 buffer,几个需求过后,你就会进入一个良性循环。对其他关键的评审环节,如技术设计评审也同样适用。 +对于测试人员来说,在要进行需求评审或技术设计评审时,通常情况下还在另外一个需求的测试执行过程当中。测试执行过程通常需要投入较高的专注度,所以很多测试人员最最容易出现的情况是,**弱化需求评审或技术设计评审环节,投入度较低,等其他需求测试完成了再花精力去熟悉它。** 殊不知,这就造成了长期的恶性循环。**正确的做法是,强化需求评审或技术设计评审环节,投入较多的精力,前置思考好一个需求中的重点、难点、风险点,提前应对** 。如果与测试执行时间有一定的冲突,则可以优先投入更多的个人时间来化解,同时在后续的测试执行过程中留有一定的 buffer,几个需求过后,你就会进入一个良性循环。对其他关键的评审环节,如技术设计评审也同样适用。 #### (2)研发阶段:技术设计评审 -技术设计主要评审是否满足业务需求的功能和非功能质量属性,以及发布方案是否完备。 **评审要点** +技术设计主要评审是否满足业务需求的功能和非功能质量属性,以及发布方案是否完备。**评审要点** - 正确性:技术设计是否可以满足业务需求中的全部功能要求?对异常情况的考虑是否充分? - 可测性:技术设计是否可测性?预期结果是否稳定? @@ -114,7 +114,7 @@ 测试阶段主要分两部分,测试设计阶段和测试执行阶段,测试设计阶段主要是进行测试方案和用例的设计,测试执行阶段主要是在提测后,对测试方案或用例进行执行的过程。 -**测试用例评审** 同样地,测试用例的质量关系到测试执行的质量和测试工作本身的质量。提高测试用例质量,可以通过两种方式,一是尽量将测试用例模板进行标准化;二是对用例进行评审。测试用例评审时间过早和过晚都不好,一般应在提测前 2 天左右的时间完成为宜。 **评审要点** +**测试用例评审** 同样地,测试用例的质量关系到测试执行的质量和测试工作本身的质量。提高测试用例质量,可以通过两种方式,一是尽量将测试用例模板进行标准化;二是对用例进行评审。测试用例评审时间过早和过晚都不好,一般应在提测前 2 天左右的时间完成为宜。**评审要点** - 测试范围:测试用例是否覆盖了业务和技术的需求,对于已有功能是否进行了必要的回归? - 异常情况:用例是否考虑了非常规操作或其他异常情况? diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25412\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25412\350\256\262.md" index 351e97d01..c207ec313 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25412\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25412\350\256\262.md" @@ -36,7 +36,7 @@ #### 流量录制与回放 -随着业务和系统的不断迭代,回归测试的比重将越来越高。那么,快速地编写自动化的回归测试用例能极大提升测试效率。 **编写回归测试用例时,测试数据的准备是消耗测试时间的一大痛点** ,因此,如果能够较快地准备充分的测试数据,将会极大提升回归测试效率。 +随着业务和系统的不断迭代,回归测试的比重将越来越高。那么,快速地编写自动化的回归测试用例能极大提升测试效率。**编写回归测试用例时,测试数据的准备是消耗测试时间的一大痛点**,因此,如果能够较快地准备充分的测试数据,将会极大提升回归测试效率。 通常来说,整个研发的交付环境既有线上(生产)环境又有线下(测试)环境,线上环境数据量庞大,线下环境数据量贫瘠。因此,把采集线上环境的数据,作为用例。这样在迭代过程中,可以在测试环境进行用例的回放和结果的比对,这样就可以知道在迭代过程中,是否会对线上目前已有的 case 造成影响。 这就需要用到流量录制与回放技术。 @@ -63,7 +63,7 @@ CI/CD 示意图 -由上图可知,在研发人员提交代码后,CI 服务根据指定分支自动执行“编译-打包-部署”,之后执行一系列的自动化测试,每一个阶段的测试结果都反馈给开发人员,这样就可以实现“快速反馈、快速解决”的效果,提升研发和测试效率。可见, **自动化测试技术可以在持续集成中应用起来。** ### 认知 +由上图可知,在研发人员提交代码后,CI 服务根据指定分支自动执行“编译-打包-部署”,之后执行一系列的自动化测试,每一个阶段的测试结果都反馈给开发人员,这样就可以实现“快速反馈、快速解决”的效果,提升研发和测试效率。可见,**自动化测试技术可以在持续集成中应用起来。** ### 认知 下面是我对精准测试和自动化测试收益分析方面的认知和思考,供你参考。 @@ -71,7 +71,7 @@ CI/CD 示意图 看了常见的提效测试技术后,你可能会提到“精准测试”。初次知道精准测试是在书籍《不测的秘密:精准测试之路》中,它提供了一种新的思路——尽量做到“不测”,从而解放人力、弥补缺失、去除冗余。精准测试,在我看来,它不是一种特定的技术,更像是一种测试方法论或思想体系。 -对于测试人员来说,最理想的情况是,只对已更改的组件运行测试,而不是尝试进行大量的回归测试。精准测试的目标是在不降低质量标准的前提下,探寻缩减测试范围、减少测试独占时长。主要解决的是传统黑盒测试回归内容较多、耗时较长的问题,这与李小龙的截拳道如出一辙。在进行精准测试的过程中,会应用到各种其他的测试技术( 自动化测试技术、流量录制与回放技术、质量度量、代码覆盖率分析等 ),如果只是知道这种思想,缺乏对其他测试技术的纯熟运用和大量的实践,也很难达到精准的效果。因此, **从技术角度看,精准测试不是完美的,也不可能是完美的。** 其实在测试领域中似乎也没有看到对应精准测试的英文术语,也没有看到各个互联网大厂在这方面的实践经验,所以现阶段它只是一个新颖的理念,保持持续关注即可。 +对于测试人员来说,最理想的情况是,只对已更改的组件运行测试,而不是尝试进行大量的回归测试。精准测试的目标是在不降低质量标准的前提下,探寻缩减测试范围、减少测试独占时长。主要解决的是传统黑盒测试回归内容较多、耗时较长的问题,这与李小龙的截拳道如出一辙。在进行精准测试的过程中,会应用到各种其他的测试技术( 自动化测试技术、流量录制与回放技术、质量度量、代码覆盖率分析等 ),如果只是知道这种思想,缺乏对其他测试技术的纯熟运用和大量的实践,也很难达到精准的效果。因此,**从技术角度看,精准测试不是完美的,也不可能是完美的。** 其实在测试领域中似乎也没有看到对应精准测试的英文术语,也没有看到各个互联网大厂在这方面的实践经验,所以现阶段它只是一个新颖的理念,保持持续关注即可。 #### 自动化测试的收益分析 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25413\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25413\350\256\262.md" index 3a9c72938..ed81f129c 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25413\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25413\350\256\262.md" @@ -54,7 +54,7 @@ #### 如何测试“流程”?——灾难恢复测试 -**前面所说的各种测试,都是对微服务系统的测试。其实测试流程和人也同样重要** ,这种测试通常叫作灾难恢复测试,简称 DiRT。 +**前面所说的各种测试,都是对微服务系统的测试。其实测试流程和人也同样重要**,这种测试通常叫作灾难恢复测试,简称 DiRT。 DiRT(Disaster Recovery Testing,灾难恢复测试)是通过对系统故障进行预案和演练,看看各团队如何协同响应。它的目标是沉淀通用的故障模式,以可控的成本在线上生产环境进行重放,通过演练暴露问题,不断推动系统、工具、流程、人员能力的提升。生活中比较类似的例子则是防火演习和地震演习。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25414\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25414\350\256\262.md" index becf83d64..fdc4e3466 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25414\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25414\350\256\262.md" @@ -4,7 +4,7 @@ ### CI/CD & “测试”环境 -**CI/CD** 缩略词 CI/CD 具有几个不同的含义,CI/CD 中的“CI”始终指 **持续集成** (Continuous Integration),它代表研发人员工作的自动化流程, **目的是让正在开发的软件始终处于可工作状态,** 它主要关注代码是否可以编译成功,以及是否可通过单元测试和验收测试等。即,每次当开发人员提交了新代码,CI服务器会自动对这些代码的所属服务进行构建,并对其执行全面的自动化测试。根据测试的结果,可以确定新提交的代码和原有代码是否正确地集成在一起了。如果整个过程中出现了构建失败或测试失败,也需要立即让开发人员知道并修复。如此重复这个过程,就可以确保新代码能够持续地与原有代码正确地集成。 +**CI/CD** 缩略词 CI/CD 具有几个不同的含义,CI/CD 中的“CI”始终指 **持续集成** (Continuous Integration),它代表研发人员工作的自动化流程,**目的是让正在开发的软件始终处于可工作状态,** 它主要关注代码是否可以编译成功,以及是否可通过单元测试和验收测试等。即,每次当开发人员提交了新代码,CI服务器会自动对这些代码的所属服务进行构建,并对其执行全面的自动化测试。根据测试的结果,可以确定新提交的代码和原有代码是否正确地集成在一起了。如果整个过程中出现了构建失败或测试失败,也需要立即让开发人员知道并修复。如此重复这个过程,就可以确保新代码能够持续地与原有代码正确地集成。 ![Drawing 0.png](assets/CgqCHl9OBxOAOKByAAPwRmhK4Dg635.png) @@ -26,21 +26,21 @@ - 持续部署 -持续部署是指,在持续交付的基础上,由开发人员或运维人员自助式地向生产环境部署优质的构建版本,甚至每当开发人员提交代码变更时,就触发自动化部署到生产环境。 **可见,持续交付是持续部署的前提,就像持续集成是持续交付的前提条件一样。** 如下图所示。 +持续部署是指,在持续交付的基础上,由开发人员或运维人员自助式地向生产环境部署优质的构建版本,甚至每当开发人员提交代码变更时,就触发自动化部署到生产环境。**可见,持续交付是持续部署的前提,就像持续集成是持续交付的前提条件一样。** 如下图所示。 ![Drawing 2.png](assets/CgqCHl9OByaAPz2NAAFRgHQ4La4072.png) 持续部署示意图 -由上图可知,无论 CD 是持续部署还是持续交付, **CI/CD 都是将重复的、手工的工作用自动化的方式来代替** 。因为这样可以减少不同阶段之间等待的时间成本、降低手工操作的出错率、快速收到反馈并修改。久而久之,最终整个产品的交付周期就缩短了。下面,本课时中的 CD 统一表示持续交付。 **“测试”环境** 文章标题提到的“测试”环境,并非代表我们日常所说的测试环境(Test 环境),而是产品交付过程中的各种环境。因为在产品交付过程中,不同的环境有着不同的特性和作用,需要在其中进行不同类型、针对不同对象的测试,所以它们都能起到“支撑测试活动”的作用。 +由上图可知,无论 CD 是持续部署还是持续交付,**CI/CD 都是将重复的、手工的工作用自动化的方式来代替** 。因为这样可以减少不同阶段之间等待的时间成本、降低手工操作的出错率、快速收到反馈并修改。久而久之,最终整个产品的交付周期就缩短了。下面,本课时中的 CD 统一表示持续交付。**“测试”环境** 文章标题提到的“测试”环境,并非代表我们日常所说的测试环境(Test 环境),而是产品交付过程中的各种环境。因为在产品交付过程中,不同的环境有着不同的特性和作用,需要在其中进行不同类型、针对不同对象的测试,所以它们都能起到“支撑测试活动”的作用。 如上述的 **持续部署示意图** 或 **持续交付示意图** 所示,在产品交付过程中,从代码提交到发布到生产环境,会经历多个环境,如 Test 环境、Staging 环境和 Prod 环境等,这些环境在 CI/CD 方面发挥着“价值传递”的作用。 -例如,某个业务有一个名叫 Order 的微服务,研发人员对其进行开发后,需要先将代码提交到代码仓库。然后 CI 服务器从代码仓库中将代码拉取到 CI 服务器的特定目录,再通过提前配置好的编译命令对该服务进行编译,并将结果部署到 Test 环境中。如果 Test 环境测试通过,则会进一步部署到 Staging 环境中,Staging 环境测试通过后会以自动化或手工触发的方式在生产环境中部署。由此可见, **Test、Staging、Prod 三个环境对要发布的微服务进行着构建和测试,每前进一步,该微服务就离交付更近一步,离实现业务价值就更近一步。** ![Drawing 3.png](assets/Ciqc1F9OBzOABTF9AATo-qaOITQ554.png) +例如,某个业务有一个名叫 Order 的微服务,研发人员对其进行开发后,需要先将代码提交到代码仓库。然后 CI 服务器从代码仓库中将代码拉取到 CI 服务器的特定目录,再通过提前配置好的编译命令对该服务进行编译,并将结果部署到 Test 环境中。如果 Test 环境测试通过,则会进一步部署到 Staging 环境中,Staging 环境测试通过后会以自动化或手工触发的方式在生产环境中部署。由此可见,**Test、Staging、Prod 三个环境对要发布的微服务进行着构建和测试,每前进一步,该微服务就离交付更近一步,离实现业务价值就更近一步。**![Drawing 3.png](assets/Ciqc1F9OBzOABTF9AATo-qaOITQ554.png) 多环境实现价值传递 -我们知道,CI/CD 的本质是产品价值的传递。因此,当代码提交后会经历编译、部署,最终形成二进制包,这些软件包会流经不同的环境进行测试。可见, **环境是产品交付过程中价值传递的载体。** +我们知道,CI/CD 的本质是产品价值的传递。因此,当代码提交后会经历编译、部署,最终形成二进制包,这些软件包会流经不同的环境进行测试。可见,**环境是产品交付过程中价值传递的载体。** 为了快速交付产品价值,需要及时地在不同环境对产品进行测试,这不仅需要各自环境足够稳定,还需要在各种环境中进行各种类型的自动化测试。测试通过后,产品发布到线上,测试不通过,则快速将结果反馈给开发人员。这样便实现了 **“快速响应,快速反馈”** 的效果。这也是 CI/CD 的精髓。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25415\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25415\350\256\262.md" index 1f84e9771..478065671 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25415\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25415\350\256\262.md" @@ -94,7 +94,7 @@ 可以对持续交付过程的工具进行整合的工具有很多,最常用的是 Jenkins,其中尤为推崇 Jenkins 2.x 。 -jenkins 1.0 虽然也可以实现自动化构建,但 Jenkins 2.x 的精髓是 **Pipeline as Code** ,它能将 project 中的配置信息以 steps 的方式放在一个脚本里,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程,形成流水式发布。 +jenkins 1.0 虽然也可以实现自动化构建,但 Jenkins 2.x 的精髓是 **Pipeline as Code**,它能将 project 中的配置信息以 steps 的方式放在一个脚本里,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程,形成流水式发布。 Jenkins Pipeline 脚本示例: diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25416\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25416\350\256\262.md" index c9a60bf0f..acaee2086 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25416\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25416\350\256\262.md" @@ -22,11 +22,11 @@ #### 运营 -运营, **通常着眼于软件产品的全生命周期,以某一内容为核心,数据驱动,通过一系列的良性循环干预动作,最终提升该内容的某项或多项指标** 。 +运营,**通常着眼于软件产品的全生命周期,以某一内容为核心,数据驱动,通过一系列的良性循环干预动作,最终提升该内容的某项或多项指标** 。 -比如, **产品运营** 是通过一系列人为干预动作来提升产品各维度的指标; **内容运营** 是通过人为干预使内容从生产、加工、互动、消费直至输出形成一个良性的循环; **用户运营** 是通过人工干预使产品和用户产生联系,从拉新、留存、促活一直到商业变现形成一个良性的闭环; **活动运营** 是针对某一活动进行策划、执行、评估、改进的全过程项目管理。 +比如,**产品运营** 是通过一系列人为干预动作来提升产品各维度的指标; **内容运营** 是通过人为干预使内容从生产、加工、互动、消费直至输出形成一个良性的循环; **用户运营** 是通过人工干预使产品和用户产生联系,从拉新、留存、促活一直到商业变现形成一个良性的闭环; **活动运营** 是针对某一活动进行策划、执行、评估、改进的全过程项目管理。 -可见,运营的本质是 **发现问题、拆解问题、解决问题的过程** ,它强调其人为干预动作需要形成 PDCA 正向循环。 +可见,运营的本质是 **发现问题、拆解问题、解决问题的过程**,它强调其人为干预动作需要形成 PDCA 正向循环。 本课程中"度量与运营"中的运营,与上面的运营序列不同,它是用运营的思路深入到质量保障全过程中,通过 **数据驱动提升质量、效率、价值等多方面的度量指标,最终实现业务价值** 。 @@ -38,7 +38,7 @@ 我们知道,质量保障的目标是线上环境没有故障和缺陷,这是最终交付给真实用户的质量,即交付质量。那么,质量度量是不是只关注交付质量指标就足够了呢?答案显然是否定的。因为如果只关注交付质量,往往达不到提升交付质量的目的。比如,你每天关注线上交付质量,忙着一个又一个的项目,一段时间过后,发现线上环境的故障数和缺陷数未见减少,这时候你甚至不知道根因出在哪里,应该如何改进,现有的工作哪些要继续保持哪些要放弃,等等。 -这是因为交付质量是 **滞后性指标** ,当你知道它时,它已经发生了。要想避免此类情况,还需要多关注并改善 **引领性指标** 。生活中的减肥的场景更能说明问题。 +这是因为交付质量是 **滞后性指标**,当你知道它时,它已经发生了。要想避免此类情况,还需要多关注并改善 **引领性指标** 。生活中的减肥的场景更能说明问题。 有过减肥经历的同学应该知道,减肥过程中,你通常会特别关注体重本身,经常时不时地去称量体重。观察了一段时间体重后,发现效果不好,你渐渐放弃了后续的坚持,减肥计划再一次泡汤。在这里,体重就是一个滞后性指标,当你知道体重时,之前做的减肥的改进动作都已经发生,不能再改变。而减肥的过程中,卡路里的摄入量和能量的消耗量则属于引领性指标( **引领性指标通常具有两个特点:预见性和可控性** )。很显然,每天摄入的卡路里-燃烧的能量=每天最终摄入的能量。只有保持这个数值在一段时间里是负数,减肥成功才有希望。 @@ -151,7 +151,7 @@ PRD 的质量可以用如下指标来衡量。 ### 总结 -本节课我讲解了度量与运营的基本知识,包括度量的意义(使现状有客观的评判、对目标有统一的共识、使改进更聚焦和精准),以及运营的本质,它是 **发现问题、拆解问题、解决问题的过程** ,强调其人为干预动作需要形成 PDCA 正向循环。 +本节课我讲解了度量与运营的基本知识,包括度量的意义(使现状有客观的评判、对目标有统一的共识、使改进更聚焦和精准),以及运营的本质,它是 **发现问题、拆解问题、解决问题的过程**,强调其人为干预动作需要形成 PDCA 正向循环。 接着我讲解了产品研发过程中针对质量可度量的核心指标,包括交付质量和过程质量。过程质量又分为需求质量、开发质量、测试质量和发布质量。在每一个维度中又有若干个具体的度量指标和含义。同时,我还给出了进行质量度量时的一些实践认知。 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25417\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25417\350\256\262.md" index c6a6de364..e7309640d 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25417\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25417\350\256\262.md" @@ -38,13 +38,13 @@ 吞吐率是单个阶段的效率衡量,它表示单位时间内,团队能够交付多少产出。产出这个词听起来比较“虚”,软件产品交付不是计件工作制,因此很难完全标准化。比如,同一个人,一天时间编写了 500 行代码,第二天编写了 300 行代码,那么它哪一天的效率更高?两个人,一天的时间分别编写了 500 行代码和 300 行代码,哪个人的效率更高?很难判断。 -因此,建议从多维度来进行度量参考,比如对于开发人员来说,可以同时使用代码行数、实现功能点数、需求数等多个指标来度量。 **因为产品交付过程是以需求为单位进行价值传递,所以各个阶段可以以需求个数来作为度量单位,并且需要拉长周期来统计吞吐率,比如一个月或一个季度等。** #### 效率痛点分析 +因此,建议从多维度来进行度量参考,比如对于开发人员来说,可以同时使用代码行数、实现功能点数、需求数等多个指标来度量。**因为产品交付过程是以需求为单位进行价值传递,所以各个阶段可以以需求个数来作为度量单位,并且需要拉长周期来统计吞吐率,比如一个月或一个季度等。** #### 效率痛点分析 虽然交付效率是多个部门协同提高的,但在产品交付过程中,测试团队是最容易被吐槽存在效率问题的,常见的说辞有“测试效率不高”“测试人力不足”“测试资源阻塞”等。 为什么总是测试人员被吐槽效率存在问题呢?主要有两点,一是测试是交付前的最后一环,原因常常就近找,因此更容易被吐槽;二是测试人力不足或效率不高的确是存在的,但很可能不是根本原因。 -我来举个例子:很多人迟到的时候你问他为什么迟到,他很容易说是因为今天堵车、打车不好打、天气不好等,这太容易回答了,因为这只是表面答案。而事实上的原因不应该是今天出门晚、今天起得晚、昨天睡得晚等吗?另外,在进行根本原因分析时需要 5 Why,我个人比较好奇“测试团队的资源不足”这个结论是第几个 Why 得出来的?如果是第一个 why 就得出来的,那么后面四个 why 得出了哪些结论?所以,当出现类似的反馈时, **需要看结论是哪些人共识的,共识的逻辑是什么。** +我来举个例子:很多人迟到的时候你问他为什么迟到,他很容易说是因为今天堵车、打车不好打、天气不好等,这太容易回答了,因为这只是表面答案。而事实上的原因不应该是今天出门晚、今天起得晚、昨天睡得晚等吗?另外,在进行根本原因分析时需要 5 Why,我个人比较好奇“测试团队的资源不足”这个结论是第几个 Why 得出来的?如果是第一个 why 就得出来的,那么后面四个 why 得出了哪些结论?所以,当出现类似的反馈时,**需要看结论是哪些人共识的,共识的逻辑是什么。** > 5 why 分析法,又称“5 问法”,也就是对一个问题点连续以 5 个“为什么”来自问,以追究其根本原因。虽为 5 个为什么,但使用时不限定只做“5 次为什么的探讨”,主要是必须找到根本原因为止,有时可能只要 3 次,有时也许要 10 次,如古话所言:打破砂锅问到底。 > diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25418\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25418\350\256\262.md" index 172e51e28..17b946a27 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25418\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25418\350\256\262.md" @@ -24,7 +24,7 @@ 归结为一句话来说就是,PM 主要负责对需求进行分析、编写需求文档、组织需求文档的评审、协调项目资源、对交付结果进行验收等工作。 -**常见问题与对策** 在协作过程中,PM 的常见问题主要有需求质量差、临时需求多、倒排期需求多,针对这些问题,对策如下。 **常见问题:需求文档质量差** +**常见问题与对策** 在协作过程中,PM 的常见问题主要有需求质量差、临时需求多、倒排期需求多,针对这些问题,对策如下。**常见问题:需求文档质量差** 需求文档是产品经理日常最重要的输出,在产品交付过程中使用频率极高。需求文档是需求实现前用文档讲述需求的实现思路,实现过程中按需求文档进行技术设计、研发、测试设计、测试执行,需求上线后回顾需求文档进行复盘总结。因此,提升需求文档的质量,有助于保障需求整体质量,提升研发过程效率,降低质量成本。可以通过事前、事中、事后三个阶段来应对。 @@ -36,7 +36,7 @@ **常见问题:临时需求多,倒排期项目多** 这类问题属于需求规划类问题。比较建议的做法是定期与 PM 针对项目规划进行沟通,了解他的阶段性规划(季度和月度),重点项目的预期上线时间点,提前管理好预期,在合理的范围内引导 PM 把需求均匀分布,避免出现一段时间忙、一段时间闲的情况。 -#### (2)研发人员 **角色职责** 研发人员,就是我们通常所说的程序员或研发工程师,在一些公司也叫 RD(即 Research & Development engineer),主要负责某系统或平台的开发和维护,使其性能、稳定性满足业务要求。具体到需求层面,研发人员负责编写技术设计方案、编码(包括与协同方联调和自测),最终把交付物提交给测试人员进行测试。测试完成后再把交付物发布到线上环境。 **常见问题:协同项目易出问题** 研发涉及多个方向的需求或项目,比较容易出现各种各样的问题。比如多方需求理解不一致、项目排期未对齐、技术方案实现有误、因依赖服务问题导致测试阻塞,等等。上述这些问题的主要原因是,各方向的产品研发测试等人员都只明确负责所在方向的交付内容,对于需求关联处和需要协同的部分,看似都负责,实际上 **多人同时负责等同于没有人负责。** 这种情况比较推荐的做法是借鉴 RASCI 工具的思想。比如,有且仅有一个人为整个项目的完成负责,在各子方向需求的产品经理、研发人员、测试人员中也推选一个主 R,职责是在该职责角色内起到横向主导作用。在整个项目过程中,分职能主 R 向项目主 R 虚线汇报,如下为相应的 RASCI 矩阵 +#### (2)研发人员 **角色职责** 研发人员,就是我们通常所说的程序员或研发工程师,在一些公司也叫 RD(即 Research & Development engineer),主要负责某系统或平台的开发和维护,使其性能、稳定性满足业务要求。具体到需求层面,研发人员负责编写技术设计方案、编码(包括与协同方联调和自测),最终把交付物提交给测试人员进行测试。测试完成后再把交付物发布到线上环境。**常见问题:协同项目易出问题** 研发涉及多个方向的需求或项目,比较容易出现各种各样的问题。比如多方需求理解不一致、项目排期未对齐、技术方案实现有误、因依赖服务问题导致测试阻塞,等等。上述这些问题的主要原因是,各方向的产品研发测试等人员都只明确负责所在方向的交付内容,对于需求关联处和需要协同的部分,看似都负责,实际上 **多人同时负责等同于没有人负责。** 这种情况比较推荐的做法是借鉴 RASCI 工具的思想。比如,有且仅有一个人为整个项目的完成负责,在各子方向需求的产品经理、研发人员、测试人员中也推选一个主 R,职责是在该职责角色内起到横向主导作用。在整个项目过程中,分职能主 R 向项目主 R 虚线汇报,如下为相应的 RASCI 矩阵 ![image](assets/CgqCHl9ggNOAK7HQAABwTzbiGic297.png) @@ -50,7 +50,7 @@ 质量保障人员(Qualtiy Assurance, QA)很多时候通俗表达为测试人员,它 **是一种统称,在角色设置上不同的公司或项目有所区别。** 比如有些公司的功能测试人员和测试开发人员是两个不同的职位,有的公司则只有 QA 一种角色,还有的公司把 QA 和 QC 分开,他们对职位的命名方式也有所不同。我听过最极端的情况是一个对日外包的项目,执行用例、提交 Bug、维护用例、编写用例、设计测试计划的测试人员分别是独立的群体,互不干涉。这些人共同保障所在项目的质量。 -一般来说,QA 的工作涉及产品研发整个流程,且涉及每一位参与研发的人员(包括但不限于产品经理、各种开发人员、测试人员、UE/UI、运营人员、客服人员、SRE 等),但专职的质量保障工作本身不太涉及具体的软件研发细节,比较偏向于保障整个流程的质量。而 QC 则侧重于具体的测试活动,利用各种方法去检查某个功能是否满足业务需求。在之前的文章中也提到过“测试”和“质量保障”,QC 偏向于测试部分,QC+QA 则偏向于质量保障。 **在我看来,QA 应该是上述 QC 和 QA 角色职责的结合体,既要保证开发流程的质量,又要保证具体产品的质量。** 许多一二线的互联网大厂,无论叫作 QA, 还是叫做测试工程师,他们的工作职责都是质量保障,而非单单是测试本身。 **常见问题:有规范,执行效果不好** 在产品交付过程中,有各种各样的规范,但总有 QA 在执行的时候打折扣。比如,明确规定了“冒烟用例只要有 1 条执行不通过,则认定为提测失败,需要提测打回重新提测”,但依然有 QA遇到此类情况时,默默地按照提测通过处理。再比如,规定了“PM 需要在产品功能上线前完成功能验收,否则可以拒绝该需求上线”,但依然有 QA 抱着侥幸心理,默许需求上线。 +一般来说,QA 的工作涉及产品研发整个流程,且涉及每一位参与研发的人员(包括但不限于产品经理、各种开发人员、测试人员、UE/UI、运营人员、客服人员、SRE 等),但专职的质量保障工作本身不太涉及具体的软件研发细节,比较偏向于保障整个流程的质量。而 QC 则侧重于具体的测试活动,利用各种方法去检查某个功能是否满足业务需求。在之前的文章中也提到过“测试”和“质量保障”,QC 偏向于测试部分,QC+QA 则偏向于质量保障。**在我看来,QA 应该是上述 QC 和 QA 角色职责的结合体,既要保证开发流程的质量,又要保证具体产品的质量。** 许多一二线的互联网大厂,无论叫作 QA, 还是叫做测试工程师,他们的工作职责都是质量保障,而非单单是测试本身。**常见问题:有规范,执行效果不好** 在产品交付过程中,有各种各样的规范,但总有 QA 在执行的时候打折扣。比如,明确规定了“冒烟用例只要有 1 条执行不通过,则认定为提测失败,需要提测打回重新提测”,但依然有 QA遇到此类情况时,默默地按照提测通过处理。再比如,规定了“PM 需要在产品功能上线前完成功能验收,否则可以拒绝该需求上线”,但依然有 QA 抱着侥幸心理,默许需求上线。 偶尔一两次不严格执行规范,不一定会导致线上问题或故障,但这样的行为隐患太大了。因为 **它会让协同团队人员对流程规范缺少敬畏感,不利于其他规范的落地** 。而且虽然表面上你的“网开一面”让协同方更“便捷”了,但他们心里会认为这个 QA 不靠谱。 @@ -111,13 +111,13 @@ SRE 全称是Site Reliability Engineer,即网站可靠性工程师。在不同 ### 质量文化建设 -**什么是质量文化?** 文化是组织成员表现出来的共同的信念、价值观、态度、制度和行为模式。那么质量文化就是组成成员在质量方面表现出来的共同的价值观、态度和信念,并且每天都以这些为驱动力来对待日常工作。可见, **文化不是纸面上写了什么,喊了什么口号,而是大家信仰什么,比如日常如何思考、如何做事儿。** **为什么需要建立质量文化?** 可能你会问,已经有质量保障体系了,为什么还要推行质量文化建设?因为,在大多数质量保障体系推行过程中更多关注的是可见的质量标准、要求、操作程序等,这些内容给人的感觉是“组织要求我做好质量”,而忽略了不可见的质量意识、质量行为等精神层面的东西,这些内容给人的感觉是“我要为组织做好质量”,是主动的、自发的。 +**什么是质量文化?** 文化是组织成员表现出来的共同的信念、价值观、态度、制度和行为模式。那么质量文化就是组成成员在质量方面表现出来的共同的价值观、态度和信念,并且每天都以这些为驱动力来对待日常工作。可见,**文化不是纸面上写了什么,喊了什么口号,而是大家信仰什么,比如日常如何思考、如何做事儿。** **为什么需要建立质量文化?** 可能你会问,已经有质量保障体系了,为什么还要推行质量文化建设?因为,在大多数质量保障体系推行过程中更多关注的是可见的质量标准、要求、操作程序等,这些内容给人的感觉是“组织要求我做好质量”,而忽略了不可见的质量意识、质量行为等精神层面的东西,这些内容给人的感觉是“我要为组织做好质量”,是主动的、自发的。 -当然,质量文化建设是建立在质量保障体系之上的,没有完善的质量保障体系作基础,没有相应的质量标准和流程的约束,是无法推行好质量文化的。 **如何推行质量文化建设?** +当然,质量文化建设是建立在质量保障体系之上的,没有完善的质量保障体系作基础,没有相应的质量标准和流程的约束,是无法推行好质量文化的。**如何推行质量文化建设?** 推行质量文化建设主要有以下几个方面。 -- 领导重视:这是非常关键的一点,但也通常被忽略。因为大家会默认高层管理者肯定对质量重视啊,但要注意的是,这里的领导重视是领导层要意识到进行文化变革的必要性,意识到领导层的一言一行会影响员工对质量的态度,进而影响员工日常行为的质量。因此,在这一点上,务必跟业务负责人和各中级管理层达成共识,领导层要起到模范和支持的作用, **只有“上行”才能“下效”。** - 激励制度:质量文化的开展需要所在业务全体成员的共同参与,特别是一线成员。激励制度是要有机制去识别对产品或服务质量有益的行为。如质量改进方面的建议、单元测试覆盖度和稳定性达到一定标准、质量相关改进进步最大等,颁发一定的奖金和质量证书,精神奖励和物质奖励结合。 +- 领导重视:这是非常关键的一点,但也通常被忽略。因为大家会默认高层管理者肯定对质量重视啊,但要注意的是,这里的领导重视是领导层要意识到进行文化变革的必要性,意识到领导层的一言一行会影响员工对质量的态度,进而影响员工日常行为的质量。因此,在这一点上,务必跟业务负责人和各中级管理层达成共识,领导层要起到模范和支持的作用,**只有“上行”才能“下效”。** - 激励制度:质量文化的开展需要所在业务全体成员的共同参与,特别是一线成员。激励制度是要有机制去识别对产品或服务质量有益的行为。如质量改进方面的建议、单元测试覆盖度和稳定性达到一定标准、质量相关改进进步最大等,颁发一定的奖金和质量证书,精神奖励和物质奖励结合。 - 文化触达:一方面需要在某些会议场合宣导质量相关建设,另一方面针对在质量方面的 GoodCase 和 BadCase 进行信息触达,比如业务内部博客、阶段性的质量报告等。 ### 结语 diff --git "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25420\350\256\262.md" "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25420\350\256\262.md" index ed00a0249..c64e6e865 100644 --- "a/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25420\350\256\262.md" +++ "b/docs/Design/\345\276\256\346\234\215\345\212\241\350\264\250\351\207\217\344\277\235\351\232\234 20 \350\256\262/\347\254\25420\350\256\262.md" @@ -4,11 +4,11 @@ ### 怎样理解“核心竞争力”? -在讲解竞争力之前先看下什么是能力。能力是指一个人完成一个目标或者任务所体现出来的素质(如技能、知识、经验以及行为等)。解释中暗含了“能力是一个绝对值(正数)”的意思,显得比较学术,而在实际工作中,相对值才有意义: **在某些方面,当你具备一些素质,而其他人并不具备时,说明你有着相应的竞争力** 。即, **竞争力是参照于其他人高出的那部分能力** ,当然,这参照范围可以大到所有人、一个行业的从业人员,也可以小到一个公司的员工,甚至是几个人。 +在讲解竞争力之前先看下什么是能力。能力是指一个人完成一个目标或者任务所体现出来的素质(如技能、知识、经验以及行为等)。解释中暗含了“能力是一个绝对值(正数)”的意思,显得比较学术,而在实际工作中,相对值才有意义: **在某些方面,当你具备一些素质,而其他人并不具备时,说明你有着相应的竞争力** 。即,**竞争力是参照于其他人高出的那部分能力**,当然,这参照范围可以大到所有人、一个行业的从业人员,也可以小到一个公司的员工,甚至是几个人。 举例来说,无论是招聘网站职位描述还是简历上的描述,几乎不会出现“能熟练使用 Windows 操作系统、熟练使用 Android 系统、熟练使用 iOS 系统”等这样的要求和能力说明。因为,这些能力是底线,是基础中的基础,本就应该是测试人员都具备的,甚至都快成了网民大众的基本功。换句话说,具备了这些能力,在测试行业里没有任何优势。但 linux 则不同,它常常出现在测试职位的技能要求里。求职者也常常会把自己熟悉 linux 这一事实直白地体现在简历里,哪怕是不算太熟悉,也会表明自己有所了解。这意味着,熟练使用 linux 操作系统,甚至是简单地会用,在测试人员群体中,还算是稀缺的,是具备一定的竞争力的。 -通过这个现象可以得出一个结论,学习任何知识和技能时,不要害怕门槛高,学习成本高,因为门槛高,也是切切实实的好事儿。倘若门槛低,别人也能轻易获取和学习,那你就没有什么竞争力了。门槛高了(其实大部分情况下只是看起来门槛高),意味着许多人都会被排除在门槛外,那你就获得了足够的竞争力。总结一句话, **在培养核心的技能和能力时,应尽量选择有门槛的、稀缺的,这样才能让自己拥有持久的竞争优势,这就是核心竞争力。** +通过这个现象可以得出一个结论,学习任何知识和技能时,不要害怕门槛高,学习成本高,因为门槛高,也是切切实实的好事儿。倘若门槛低,别人也能轻易获取和学习,那你就没有什么竞争力了。门槛高了(其实大部分情况下只是看起来门槛高),意味着许多人都会被排除在门槛外,那你就获得了足够的竞争力。总结一句话,**在培养核心的技能和能力时,应尽量选择有门槛的、稀缺的,这样才能让自己拥有持久的竞争优势,这就是核心竞争力。** ### 核心竞争力的三个阶段 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25400\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25400\350\256\262.md" index 3a6415e93..e1e32d6b9 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25400\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25400\350\256\262.md" @@ -8,19 +8,19 @@ 而这与面试者的认知有很大关系。很多候选人认为,“架构设计”是应聘架构师或成为技术大牛后才会被问到的问题,觉得考察架构设计能力超出了岗位职责要求,并不重视。 -可实际情况是, **考察架构设计,是面试中高级研发工程师逃不开的一环。** 绝大多数面试官会看重候选人的架构设计能力,以此衡量候选人的技术深度和对技术的驾驭能力,挖掘你的技术亮点。 **如果你能在“如何设计系统架构”上回答得有条理、体现自己的思考,很容易得到认可,甚至掩盖个别技术问题上回答的不足** 。 +可实际情况是,**考察架构设计,是面试中高级研发工程师逃不开的一环。** 绝大多数面试官会看重候选人的架构设计能力,以此衡量候选人的技术深度和对技术的驾驭能力,挖掘你的技术亮点。**如果你能在“如何设计系统架构”上回答得有条理、体现自己的思考,很容易得到认可,甚至掩盖个别技术问题上回答的不足** 。 就算你是面试初级研发岗位,很多面试官也会站在你的能力上一层,继续问一些架构设计问题。以“Redis 是否可以作为分布式锁?”这个问题为例,面试官会站在中高级研发角度考察你的技术能力,问你用 Redis 实现分布式锁会存在哪些问题,以及为什么 Redis 会采用 AP 模型等。 应聘中高级研发时,面试官则会站在架构师的角度,扩展到分布式缓存系统的数据分布、复制,以及共识算法的问题上,还要考察你对在实际业务场景中应用分布式缓存的技术判断力。这些都是想挖掘你的能力边界,看看你的天花板有多高,未来能在团队中发挥多大价值。 -你可能会问: **就算我知道架构设计在面试中很重要,但我没有大厂经历,也没有机会做复杂的项目,又该怎么迎接面试呢** ? +你可能会问: **就算我知道架构设计在面试中很重要,但我没有大厂经历,也没有机会做复杂的项目,又该怎么迎接面试呢**? 很多同学都会面临这样的局面,切忌不要在网上搜索一些高性能高可用的架构设计方案,因为你没有实际踩过坑,很难分辨哪些技术场景下的设计仅仅是为了公关,很难落地,在面试中也很容易被面试官识破,怀疑你的技术能力的真实性。 当然,也有一些研发同学可能会遇到这样的情况: **技术明明可以满足应聘部门的岗位能力要求,但面不到想要的职位。** 这不是你的技术能力不足,而是你对技术的认知不够,达不到一个高级研发,或者是架构师该有的技术思维层次。于是你在面试大厂时,就会存在因为很难讲出自己的技术价值与亮点,导致竞争力不足的情况。 -这时,面试官更关注考察你解决问题的思维过程,那么如何阐述解决问题的思维能证明你的能力呢?这其实存在很多套路,比如回答问题的视角应该是什么样的?(我会在课程中为你举例说明)。 **所以,针对以上三点问题:** +这时,面试官更关注考察你解决问题的思维过程,那么如何阐述解决问题的思维能证明你的能力呢?这其实存在很多套路,比如回答问题的视角应该是什么样的?(我会在课程中为你举例说明)。**所以,针对以上三点问题:** - 没有设计经验,不了解面试前需要准备哪些架构设计问题? - 没有大厂经历,不知道如何回答面试官提出的架构设计问题? @@ -28,15 +28,15 @@ 我决定把自己多年的经验分享给你。 -这门课 **主要面向的是想准备面试的中高级后端研发,以及想提前掌握架构设计知识,从而在面试中增加亮点的初级研发** ,帮你摆脱面试中的架构设计误区,识别技术陷阱,掌握面试中关于架构设计问题的知识体系。 +这门课 **主要面向的是想准备面试的中高级后端研发,以及想提前掌握架构设计知识,从而在面试中增加亮点的初级研发**,帮你摆脱面试中的架构设计误区,识别技术陷阱,掌握面试中关于架构设计问题的知识体系。 面试官在面试候选人时,一般的形式是:假设一种场景,然后让候选人根据场景做技术设计,或者直接让候选人画出自己做过的最复杂的系统的架构图,再提具体设计问题。而 **这其中,100% 会涉及架构原理、分布式技术、中间件、数据库、缓存、业务系统架构 6 个方面,这几个方面也正是这门课的 6 个模块** 。 -**模块一:架构原理与技术认知** 我会以架构师视角解析研发同学在遇到系统设计问题时,应具备怎样的技术认知和解题思路。架构设计的底层思维逻辑是你的架构设计能否立足的根本,决定了你在面试时从什么角度来回答提问才更有价值,模块一是你学习后面内容的理论基础。 **模块二:分布式技术原理与设计** 有一句话叫“不懂分布式,别来面试互联网”,我会通过亿级商品的数据存储问题,解析在分布式系统技术架构中,面对热点问题该如何回答,比如用 etcd 如何解决数据共识问题?在这一模块中,我会深入原理并结合落地经验,让你抓住面试官的提问思路,给出被认可的答案。 **模块三:中间件常用组件的原理和设计问题** 我会结合大厂关注的考察点,讲解 RPC 远程调用和MQ(消息队列)的技术原理和实践,比如如何实现一个 RPC 框架?MQ 如何实现消息的不丢失、不重复消费,以及积压等问题。 **模块四:数据库原理与设计问题** 要想顺利回答出“数据库原理与设计”问题,你需要掌握 MySQL,但 MySQL 的知识点很零散,而我会整理出一套架构设计面试中必考的 MySQL 知识体系,并根据你应聘的职级,带你针对性学习。 **模块五:分布式缓存原理与设计问题** 面试者仅能熟练地使用 Redis 还不够,面试官还要求候选人能深入理解底层实现原理,并且具备解决常见问题的能力(尤其是在高并发场景下的缓存解决方案),我会结合分布式缓存的原理,并结合电商场景下 Redis 的设计案例解锁经典面试问题。 **模块六:互联网高性能高可用设计问题** 我会针对当系统遭遇百万并发时的技术瓶颈,以及优化思路,为你揭开大厂招聘必问的高性能、高可用问题背后的原理,比如如何判断你的系统是高可用的?并最终通过电商平台案例,解析面试中的高频架构设计问题。 +**模块一:架构原理与技术认知** 我会以架构师视角解析研发同学在遇到系统设计问题时,应具备怎样的技术认知和解题思路。架构设计的底层思维逻辑是你的架构设计能否立足的根本,决定了你在面试时从什么角度来回答提问才更有价值,模块一是你学习后面内容的理论基础。**模块二:分布式技术原理与设计** 有一句话叫“不懂分布式,别来面试互联网”,我会通过亿级商品的数据存储问题,解析在分布式系统技术架构中,面对热点问题该如何回答,比如用 etcd 如何解决数据共识问题?在这一模块中,我会深入原理并结合落地经验,让你抓住面试官的提问思路,给出被认可的答案。**模块三:中间件常用组件的原理和设计问题** 我会结合大厂关注的考察点,讲解 RPC 远程调用和MQ(消息队列)的技术原理和实践,比如如何实现一个 RPC 框架?MQ 如何实现消息的不丢失、不重复消费,以及积压等问题。**模块四:数据库原理与设计问题** 要想顺利回答出“数据库原理与设计”问题,你需要掌握 MySQL,但 MySQL 的知识点很零散,而我会整理出一套架构设计面试中必考的 MySQL 知识体系,并根据你应聘的职级,带你针对性学习。**模块五:分布式缓存原理与设计问题** 面试者仅能熟练地使用 Redis 还不够,面试官还要求候选人能深入理解底层实现原理,并且具备解决常见问题的能力(尤其是在高并发场景下的缓存解决方案),我会结合分布式缓存的原理,并结合电商场景下 Redis 的设计案例解锁经典面试问题。**模块六:互联网高性能高可用设计问题** 我会针对当系统遭遇百万并发时的技术瓶颈,以及优化思路,为你揭开大厂招聘必问的高性能、高可用问题背后的原理,比如如何判断你的系统是高可用的?并最终通过电商平台案例,解析面试中的高频架构设计问题。 总的来说,在面试中,互联网公司会把技术层层设卡,通过架构设计上的各类知识点将研发工程师进行分层。但是每个人的工作经历有限,很多人遇不到好的平台和好的机会,在平时工作中只做着 CRUD 的工作,这个问题对于很多中小型企业的研发工程师尤为明显,就导致他们应聘大厂的竞争力偏弱。 -而我会通过具体的面试场景入手,从案例背景、案例分析、原理剖析、解答方法等层面,由浅入深地把我的经验方法与实践总结分享给你,让你吃透了一个案例后,可灵活运用到其他案例中,让你为应对大场面做足准备。 **学习这个专栏的建议** +而我会通过具体的面试场景入手,从案例背景、案例分析、原理剖析、解答方法等层面,由浅入深地把我的经验方法与实践总结分享给你,让你吃透了一个案例后,可灵活运用到其他案例中,让你为应对大场面做足准备。**学习这个专栏的建议** 考虑到很多同学对架构设计掌握程度不同,在正式学习这门课之前,我给你几点建议。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25401\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25401\350\256\262.md" index 082f2cdf2..2d912cce5 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25401\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25401\350\256\262.md" @@ -18,7 +18,7 @@ 但实际上,很多研发同学对架构设计的掌握和理解是欠缺经验的,系统设计问题只能回答出表层的技术名词,落地没有实际经验,拔高没有理论支撑。那你怎么回答面试中的架构设计问题呢? -**关于架构设计的问题,一定要立足于点** 、 **连接成线** 、 **扩散成面** ,用这样的思路回答才能让面试官满意。下面我就通过一个例子,来帮你理解什么是回答架构设计问题该有的认知。 +**关于架构设计的问题,一定要立足于点** 、 **连接成线** 、 **扩散成面**,用这样的思路回答才能让面试官满意。下面我就通过一个例子,来帮你理解什么是回答架构设计问题该有的认知。 #### 例子 @@ -28,26 +28,26 @@ 系统拆分的架构设计问题,在面试中很常见,候选人给出了四个层面的回答。 -> **从订单系统层面来看** ,由于交易流程中的订单系统相对来说业务稳定,不存在很多的迭代需求,如果耦合到整个交易系统中,在其他功能发布上线的时候会影响订单系统,比如订单中心的稳定性。基于这样的考虑,需要拆分出一个独立的子系统。 +> **从订单系统层面来看**,由于交易流程中的订单系统相对来说业务稳定,不存在很多的迭代需求,如果耦合到整个交易系统中,在其他功能发布上线的时候会影响订单系统,比如订单中心的稳定性。基于这样的考虑,需要拆分出一个独立的子系统。 > -> **从促销系统层面来看** ,由于促销系统是交易流程中的非核心系统,出于保障交易流程稳定性的考虑,将促销系统单独拆分出来,在发生异常的时候能让促销系统具有可降级的能力。 +> **从促销系统层面来看**,由于促销系统是交易流程中的非核心系统,出于保障交易流程稳定性的考虑,将促销系统单独拆分出来,在发生异常的时候能让促销系统具有可降级的能力。 > -> **从报价系统层面来看** ,报价是业务交易流程中最为复杂和灵活的系统,出于专业化和快速迭代的考虑,拆分出一个独立的报价系统,目的就是为了快速响应需求的变化。 +> **从报价系统层面来看**,报价是业务交易流程中最为复杂和灵活的系统,出于专业化和快速迭代的考虑,拆分出一个独立的报价系统,目的就是为了快速响应需求的变化。 > -> **从复杂度评估层面来看** ,系统拆分虽然会导致系统交互更加复杂,但在规范了 API 的格式定义和调用方式后,系统的复杂度可以维持在可控的范围内。 +> **从复杂度评估层面来看**,系统拆分虽然会导致系统交互更加复杂,但在规范了 API 的格式定义和调用方式后,系统的复杂度可以维持在可控的范围内。 -这样的回答很好地表达了应聘者对系统设计的思考与理解。因为他说出了原有系统中关于订单、促销和报价功能耦合在一起带来的实际问题,这是 **立足于点** ,又从交易流程的角度做系统设计串联起三个系统的拆分逻辑, **这是连接成线** ,最后从复杂度和成本考量的方向夯实了设计的原则, **这是扩展成面** 。 +这样的回答很好地表达了应聘者对系统设计的思考与理解。因为他说出了原有系统中关于订单、促销和报价功能耦合在一起带来的实际问题,这是 **立足于点**,又从交易流程的角度做系统设计串联起三个系统的拆分逻辑,**这是连接成线**,最后从复杂度和成本考量的方向夯实了设计的原则,**这是扩展成面** 。 #### 案例分析 如果你是这名应聘者,会怎么回答呢?很多研发同学一提到架构设计就说要做拆分,将一个系统拆分成两个系统,将一个服务拆分成两个服务,甚至觉得架构就是做系统拆分,但其实并不理解拆分背后的深层原因,所以往往只能回答得比较表面,无法深入背后的底层设计逻辑,那这个问题的底层逻辑到底是什么呢?有这样四点。 -- **为什么做架构拆分** ?通常最直接目的就是做系统之间解耦、子系统之间解耦,或模块之间的解耦。 -- **为什么要做系统解耦** ?系统解耦后,使得原本错综复杂的调用逻辑能有序地分布到各个独立的系统中,从而使得拆封后的各个系统职责更单一,功能更为内聚。 -- **为什么要做职责单一** ?因为职责单一的系统功能逻辑的迭代速度会更快,会提高研发团队响应业务需求的速度,也就是提高了团队的开发效率。 -- **为什么要关注开发效率** ?研发迭代效率的提升是任何一家公司在业务发展期间都最为关注的问题, **所以从某种程度上看,架构拆分是系统提效最直接的手段** 。 +- **为什么做架构拆分**?通常最直接目的就是做系统之间解耦、子系统之间解耦,或模块之间的解耦。 +- **为什么要做系统解耦**?系统解耦后,使得原本错综复杂的调用逻辑能有序地分布到各个独立的系统中,从而使得拆封后的各个系统职责更单一,功能更为内聚。 +- **为什么要做职责单一**?因为职责单一的系统功能逻辑的迭代速度会更快,会提高研发团队响应业务需求的速度,也就是提高了团队的开发效率。 +- **为什么要关注开发效率**?研发迭代效率的提升是任何一家公司在业务发展期间都最为关注的问题,**所以从某种程度上看,架构拆分是系统提效最直接的手段** 。 -所以, **架构拆分其实是管理在技术上提效的一种手段** ,认识到这一点后,就不难理解为什么很多架构师在做系统架构时,会做系统设计上的拆分,甚至认为架构的本质就是拆分了。 +所以,**架构拆分其实是管理在技术上提效的一种手段**,认识到这一点后,就不难理解为什么很多架构师在做系统架构时,会做系统设计上的拆分,甚至认为架构的本质就是拆分了。 ### 对分析问题的认知 @@ -55,7 +55,7 @@ - 业务方的诉求是在技术升级后,系统有能力迭代功能来满足市场的要求,所以 **关注点在系统能力** 。 - 管理者的诉求是在技术升级后,系统研发团队的开发效能得到提升,所以 **关注点在人效管理** 。 -- 作为技术人员的你,需要找到自己做系统设计的立足点,来满足不同人对技术的诉求,而这个立足点通常就是 **系统设计原则。** 所以你应该认识到,系统的设计原则不是乱提出来的,而是 **针对系统现阶段业务发展带来的主要矛盾提出** , **才会更有价值且被认可** 。 +- 作为技术人员的你,需要找到自己做系统设计的立足点,来满足不同人对技术的诉求,而这个立足点通常就是 **系统设计原则。** 所以你应该认识到,系统的设计原则不是乱提出来的,而是 **针对系统现阶段业务发展带来的主要矛盾提出**,**才会更有价值且被认可** 。 #### 例子 @@ -67,7 +67,7 @@ 这个时期系统的主要矛盾就变成了:多人协作进行复杂业务,导致速度缓慢,但业务需求又快速迭代。说白了,就是 **研发效率不能匹配业务发展的速度,并且单靠加人不能解决问题** 。 -对于这样的一个系统, **此阶段的系统架构核心原则就不能随便定义为要保证高性能和高可用** 。 +对于这样的一个系统,**此阶段的系统架构核心原则就不能随便定义为要保证高性能和高可用** 。 那么应该怎么做呢?针对这样的问题,我们需要对原有系统进行合理的系统边界拆分,让研发人员有能力提速,来快速响应需求变化,这就要求架构师对业务领域和团队人员有足够的了解。 @@ -75,7 +75,7 @@ #### 案例分析 -面试中,研发人员在回答系统设计问题的时候,要根据系统所处阶段的主要矛盾来回答架构设计问题,在 20 世纪 60 年代,《人月神话》的作者就分析,软件复杂性来源于两点:本质复杂度和偶然复杂度。开发工具、开发框架、开发模式,以及高性能和高可用这些仅是偶然复杂性, **架构最重要的是要解决本质复杂性,这包括人的复杂性和业务的复杂性。** 技术是静态的,业务和用户是变化的,具体问题要从具体的业务领域出发。这时有人可能会说,我只想做技术,不想做业务,然而你会慢慢发现,在职业生涯中处理的最有价值的事情,一般都是利用技术解决了业务领域的某阶段的主要问题,这也是最复杂的。 +面试中,研发人员在回答系统设计问题的时候,要根据系统所处阶段的主要矛盾来回答架构设计问题,在 20 世纪 60 年代,《人月神话》的作者就分析,软件复杂性来源于两点:本质复杂度和偶然复杂度。开发工具、开发框架、开发模式,以及高性能和高可用这些仅是偶然复杂性,**架构最重要的是要解决本质复杂性,这包括人的复杂性和业务的复杂性。** 技术是静态的,业务和用户是变化的,具体问题要从具体的业务领域出发。这时有人可能会说,我只想做技术,不想做业务,然而你会慢慢发现,在职业生涯中处理的最有价值的事情,一般都是利用技术解决了业务领域的某阶段的主要问题,这也是最复杂的。 而一个优秀的应聘者,在回答中应该向面试官展现出这样的技术认知。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25402\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25402\350\256\262.md" index 95b54e42a..3831afe57 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25402\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25402\350\256\262.md" @@ -30,7 +30,7 @@ 在互联网系统设计方案如此透明的今天,随便在网上搜一下都会有大量类似的解决方案。以上回答不但不会让面试官满意,甚至有可能令人怀疑你的项目经历的真实性。 -作为研发工程师,正确的回答方式是要让面试官知道你解决问题的思维。相比一上来就说用了什么技术,阐述解决思维更能证明你的能力, **因为解决技术问题的方法有很多,这是“术”,但解决技术问题的底层思维逻辑是一样的,这是“道”** 。 +作为研发工程师,正确的回答方式是要让面试官知道你解决问题的思维。相比一上来就说用了什么技术,阐述解决思维更能证明你的能力,**因为解决技术问题的方法有很多,这是“术”,但解决技术问题的底层思维逻辑是一样的,这是“道”** 。 面对此类问题,我总结了如下四个层面的答案: @@ -53,9 +53,9 @@ ![5.png](assets/CgqCHl_-eVaAcKLPAAE9AeK1Y8k819.png) -复杂度评估 **从功能性复杂度方面来看** ,你可以从案例中得知,产品业务发展快速、系统越来越多、协作效率越来越低。作为系统负责人,你敏锐地发现问题根源在架构上各业务子系统强耦合。于是你引入消息队列解耦各系统,这是系统业务领域带来的本质上的复杂度,也就是功能性的复杂度,解决的是系统效率的问题。 +复杂度评估 **从功能性复杂度方面来看**,你可以从案例中得知,产品业务发展快速、系统越来越多、协作效率越来越低。作为系统负责人,你敏锐地发现问题根源在架构上各业务子系统强耦合。于是你引入消息队列解耦各系统,这是系统业务领域带来的本质上的复杂度,也就是功能性的复杂度,解决的是系统效率的问题。 -此外,对于互联网系统设计,还需要考虑非功能性的复杂度,例如高性能、高可用和扩展性等的复杂度的设计。 **从非功能性复杂度方面来看** ,我们假设系统用户每天发送 100 万条点评,那么点评的消息管道一天会产生 100 万条消息,再假设平均一条消息有 10 个子系统读取,那么每秒的处理数据,即点评消息队列系统的 TPS 和 QPS(Queries Per Second,每秒查询次数)就分别是 11(1000000/60_60_24)和 115(10000000/60_60_24)。 +此外,对于互联网系统设计,还需要考虑非功能性的复杂度,例如高性能、高可用和扩展性等的复杂度的设计。**从非功能性复杂度方面来看**,我们假设系统用户每天发送 100 万条点评,那么点评的消息管道一天会产生 100 万条消息,再假设平均一条消息有 10 个子系统读取,那么每秒的处理数据,即点评消息队列系统的 TPS 和 QPS(Queries Per Second,每秒查询次数)就分别是 11(1000000/60_60_24)和 115(10000000/60_60_24)。 不过系统的读写不是完全平均的,设计的目标应该以峰值来计算,即取平均值的 4 倍。于是点评消息队列系统的 TPS 变成了 44,QPS 变成了 460,这个量级的数据意味着并不需要设计高性能架构方案。 @@ -82,7 +82,7 @@ #### 评估标准 -设计完三套解决方案之后,摆在眼前的问题就是需要选择最合适的一个。 **这就需要一套评估标准了。** +设计完三套解决方案之后,摆在眼前的问题就是需要选择最合适的一个。**这就需要一套评估标准了。** 在互联网软件架构中,架构师常常会把一些通用的设计原则写到设计文档中,比如设计松耦合、系统可监控,这些原则似乎不常用,但好的架构师会通过设计原则来控制项目的技术风险。比如系统无单点,限制了系统技术方案不可出现单点服务的设计;再如系统可降级,限制了系统有具备降级的能力,进而约束了开发人员需要设计数据兜底的技术方案。 @@ -94,19 +94,19 @@ 点评系统的功能性复杂度问题,本质上是随着业务发展带来的系统开发效率问题。解决这个问题要试着站得更高一些,以部门负责人的视角,考虑现有研发团队的能力素质、IT 成本、资源投入周期等因素是否匹配上面三种架构解决方案。 -- **点评系统非功能性复杂度** 为了解决系统的高可用,可以参考三个设计原则。 **第一个是系统无单点原则** 。首先要保证系统各节点在部署的时候至少是冗余的,没有单点。很显然三种设计方案都支持无单点部署方式,都可以做到高可用。 **第二个是可水平扩展原则** 。对于水平扩展,MQ 和 Redis 都具有先天的优势,但内存队列 + MySQL 的方式则需要做分库分表的开发改造,并且还要根据业务提前考虑未来的容量预估。 **第三个是可降级原则** 。降级处理是当系统出现故障的时候,为了系统的可用性,选择有损的或者兜底的方式提供服务。 +- **点评系统非功能性复杂度** 为了解决系统的高可用,可以参考三个设计原则。**第一个是系统无单点原则** 。首先要保证系统各节点在部署的时候至少是冗余的,没有单点。很显然三种设计方案都支持无单点部署方式,都可以做到高可用。**第二个是可水平扩展原则** 。对于水平扩展,MQ 和 Redis 都具有先天的优势,但内存队列 + MySQL 的方式则需要做分库分表的开发改造,并且还要根据业务提前考虑未来的容量预估。**第三个是可降级原则** 。降级处理是当系统出现故障的时候,为了系统的可用性,选择有损的或者兜底的方式提供服务。 常用手段主要有三种。 -- **限流** ,即抛弃超出预估流量外的用户。 -- **降级** ,即抛弃部分不重要的功能,让系统提供有损服务,如商品详情页不展示宝贝收藏的数量,以确保核心功能不受影响。 -- **熔断** ,即抛弃对故障系统的调用。一般情况下熔断会伴随着降级处理,比如展示兜底数据。 +- **限流**,即抛弃超出预估流量外的用户。 +- **降级**,即抛弃部分不重要的功能,让系统提供有损服务,如商品详情页不展示宝贝收藏的数量,以确保核心功能不受影响。 +- **熔断**,即抛弃对故障系统的调用。一般情况下熔断会伴随着降级处理,比如展示兜底数据。 针对案例场景中三个解决方案的降级策略,在一般的情况下,我们默认数据库是不可降级的,MQ 和 Redis 都可以通过降级到数据库的方式做容灾处理。所以案例中的三个解决方案,MQ 和 Redis 要考虑降级到 MySQL 或其他方式,这里就还需要根据情况投入降级的开发成本。 -对于本节课的案例我不评价哪个更好,你在多个解决方案中做选择时, **不要陷入某个纯粹技术点的优劣之争,那样很难有结果,越大的项目越明显。** 通常来说,方案没有优劣之分,而是要看哪个更适合当下的问题,只要架构满足一定时期内的业务发展就可以。 **你要知道,作为技术人,考虑问题的方式要比具体的选型结果更为重要,这是面试的加分点。** #### 技术实现 +对于本节课的案例我不评价哪个更好,你在多个解决方案中做选择时,**不要陷入某个纯粹技术点的优劣之争,那样很难有结果,越大的项目越明显。** 通常来说,方案没有优劣之分,而是要看哪个更适合当下的问题,只要架构满足一定时期内的业务发展就可以。**你要知道,作为技术人,考虑问题的方式要比具体的选型结果更为重要,这是面试的加分点。** #### 技术实现 -在确定了具体的架构解决方案之后, **需要进一步说明技术上的落地实现方式和深层原理** ,如果你最终选择基于 Redis 来实现消息队列,那么可以有几种实现方式?各自的优缺点有哪些?对于这些问题,要做到心里有数。比如,基于 Redis List 的 LPUSH 和 RPOP 的实现方式、基于 Redis 的订阅或发布模式,或者基于 Redis 的有序集合(Sorted Set)的实现方式,你可以自行搜索,我不再赘述。 +在确定了具体的架构解决方案之后,**需要进一步说明技术上的落地实现方式和深层原理**,如果你最终选择基于 Redis 来实现消息队列,那么可以有几种实现方式?各自的优缺点有哪些?对于这些问题,要做到心里有数。比如,基于 Redis List 的 LPUSH 和 RPOP 的实现方式、基于 Redis 的订阅或发布模式,或者基于 Redis 的有序集合(Sorted Set)的实现方式,你可以自行搜索,我不再赘述。 ### 总结 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25403\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25403\350\256\262.md" index a54a4b662..515226bf5 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25403\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25403\350\256\262.md" @@ -12,7 +12,7 @@ CAP 理论是分布式系统中最核心的基础理论,虽然在面试中, 相信只要学习过分布式技术的相关知识,基本上都知道 CAP 理论指的是什么:C(Consistency)是数据一致性、A(Availability)是服务可用性、P(Partition tolerance)是分区容错性。C、A、P 只能同时满足两个目标,而由于在分布式系统中,P 是必须要保留的,所以要在 C 和 A 间进行取舍。假如要保证服务的可用性,就选择 AP 模型,而要保证一致性的话,就选择 CP 模型。 -很多候选者如果发现面试题(比如“为了数据容灾,我们会做数据的主从备份,那么主从节点的数据一致性对调用端有什么影响呢?”)涉及了对“CAP 的理解和思考”,会下意识地做出类似的答案:“ CAP 理论描述了在出现网络分区的情况下,要在 C 和 A 之间做取舍,所以会影响站在调用端的视角看系统是不可用的”。如果是我的话,大概会给个及格分,并认为这样的回答,只能证明你有准备,不能证明你有能力。 **因为在面试中遇到理论问题时,单纯做浮于表面的概念性阐述,很难向面试官证明你的技术能力。** 面试官会觉得你是一个刚接触分布式系统,或者对分布式系统理解不够深入的研发,如果这恰好是你第一个面试题,会直接影响面试官对你的第一印象,甚至影响你的定级。 +很多候选者如果发现面试题(比如“为了数据容灾,我们会做数据的主从备份,那么主从节点的数据一致性对调用端有什么影响呢?”)涉及了对“CAP 的理解和思考”,会下意识地做出类似的答案:“ CAP 理论描述了在出现网络分区的情况下,要在 C 和 A 之间做取舍,所以会影响站在调用端的视角看系统是不可用的”。如果是我的话,大概会给个及格分,并认为这样的回答,只能证明你有准备,不能证明你有能力。**因为在面试中遇到理论问题时,单纯做浮于表面的概念性阐述,很难向面试官证明你的技术能力。** 面试官会觉得你是一个刚接触分布式系统,或者对分布式系统理解不够深入的研发,如果这恰好是你第一个面试题,会直接影响面试官对你的第一印象,甚至影响你的定级。 从我的经验出发,如果你想答得更好,你需要先掌握 CAP 的原理、实践经验、技术认知,然后再结合具体的面试题具体分析。 @@ -30,7 +30,7 @@ CAP 理论是分布式系统中最核心的基础理论,虽然在面试中, 这里你要注意了,上面的例子有个大前提,就是系统出现了网络分区,但实际情况是,在绝大多数时间里并不存在网络分区(网络不会经常出现问题)。那么还要进行三选二吗(CP 或者 AP)? -其实,不同的分布式系统要根据业务场景和业务需求在 CAP 三者中进行权衡。 **CAP 理论用于指导在系统设计时需要衡量的因素,而非进行绝对地选择** 。 +其实,不同的分布式系统要根据业务场景和业务需求在 CAP 三者中进行权衡。**CAP 理论用于指导在系统设计时需要衡量的因素,而非进行绝对地选择** 。 当网络没有出现分区时,CAP 理论并没有给出衡量 A 和 C 的因素,但如果你做过实际的分布式系统设计,一定会发现系统数据同步的时延(Latency),即例子中节点 A 同步数据到节点 A1 的时间才是衡量 A 和 C 最重要的因素,此时就不会有绝对的 AP 模型还是 CP 模型了,而是源于对实际业务场景的综合考量。 @@ -42,7 +42,7 @@ PACELC **但理解到这个程度还不够,你还需要结合落地经验进 你要意识到,互联网分布式的设计方案是数据一致性和系统可用性的权衡,并不是非此即彼,这一点尤为重要。所以即使无法做到强一致性(简单来讲强一致性就是在任何时刻所有的用户查询到的数据都是最新的),也可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。 -这时就要引出 BASE 理论,它是 CAP 理论的延伸。BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)三个单词的简写,作用是保证系统的可用性,然后通过最终一致性来代替强一致性,它是目前分布式系统设计中最具指导意义的经验总结。 **那么在实际项目中,你如何通过 BASE 理论来指导设计实践呢?** +这时就要引出 BASE 理论,它是 CAP 理论的延伸。BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)三个单词的简写,作用是保证系统的可用性,然后通过最终一致性来代替强一致性,它是目前分布式系统设计中最具指导意义的经验总结。**那么在实际项目中,你如何通过 BASE 理论来指导设计实践呢?** BASE 中的基本可用指的是保障核心功能的基本可用,其实是做了“可用性”方面的妥协,比如: @@ -61,7 +61,7 @@ BASE 中的基本可用指的是保障核心功能的基本可用,其实是做 #### 技术认知 -如果你应聘的是高级研发工程师或架构师,在回答时, **还要尽可能地展示知识体系和技术判断力,这是这两个岗位的基本素质。** 因为分布式技术错综复杂,各种技术又相互耦合,在面试中,如果你能通过一个 CAP 理论的知识点,扩展出一个脉络清晰的分布式核心技术知识体系,就会与其他人拉开差距。 +如果你应聘的是高级研发工程师或架构师,在回答时,**还要尽可能地展示知识体系和技术判断力,这是这两个岗位的基本素质。** 因为分布式技术错综复杂,各种技术又相互耦合,在面试中,如果你能通过一个 CAP 理论的知识点,扩展出一个脉络清晰的分布式核心技术知识体系,就会与其他人拉开差距。 分布式系统看起来就像一个计算机。计算机包括五大体系结构(即冯诺依曼结构),它有五大部件:分别是控制器、运算器、存储器、输入及输出。你可以这么理解:一个分布式系统也包含这五大部件,其中最重要的是计算与存储。计算与存储由一系列网络节点组成,每个节点之间的通信就是输入与输出,各节点之间的调度管理就是控制器。 @@ -71,12 +71,12 @@ BASE 中的基本可用指的是保障核心功能的基本可用,其实是做 这么看来,分布式系统就像一个网络计算机,它的知识体系包括四个角度: -1. **存储器** ,即分布式存储系统,如 NoSQL 数据库存储; -2. **运算器** ,即分布式计算,如分布式并行计算; -3. **输入输出** ,即分布式系统通信,如同步 RPC 调用和异步消息队列; -4. **控制器** ,即调度管理,如流量调度、任务调度与资源调度。 +1. **存储器**,即分布式存储系统,如 NoSQL 数据库存储; +2. **运算器**,即分布式计算,如分布式并行计算; +3. **输入输出**,即分布式系统通信,如同步 RPC 调用和异步消息队列; +4. **控制器**,即调度管理,如流量调度、任务调度与资源调度。 -你可以从这四个角度来概括分布式系统的知识体系(每个分支的具体子知识体系和知识点,我会在后面的课程中一一为你讲解)。 **那么具体的解题思路是什么呢?** 还是以“Redis 是否可以作为分布式锁”为例,咱们一起来分析一下问题背后隐藏的分布式理论知识,以及作为高级研发工程师的解题思路。 +你可以从这四个角度来概括分布式系统的知识体系(每个分支的具体子知识体系和知识点,我会在后面的课程中一一为你讲解)。**那么具体的解题思路是什么呢?** 还是以“Redis 是否可以作为分布式锁”为例,咱们一起来分析一下问题背后隐藏的分布式理论知识,以及作为高级研发工程师的解题思路。 #### 解题思路 @@ -92,7 +92,7 @@ BASE 中的基本可用指的是保障核心功能的基本可用,其实是做 Redis 属于分布式存储系统,你的头脑里就要有对分布式存储系统领域的知识体系。思考它的数据存储、数据分布、数据复制,以及数据一致性都是怎么做的,用了哪些技术来实现,为什么要做这样的技术或算法选型。你要学会从多维度、多角度去对比、分析同一分布式问题的不同方法,然后综合权衡各种方法的优缺点,最终形成自己的技术认知和技术判断力。 -- **有技术的判断力** 比如通过 Redis,你能想到目前分布式缓存系统的发展现状以及技术实现,如果让你造一个“Redis”出来,你会考虑哪些问题等。 **虽然在实际工作中不推荐重复“造轮子”,但在面试中要表现出自己具备“造轮子”的能力** 。 +- **有技术的判断力** 比如通过 Redis,你能想到目前分布式缓存系统的发展现状以及技术实现,如果让你造一个“Redis”出来,你会考虑哪些问题等。**虽然在实际工作中不推荐重复“造轮子”,但在面试中要表现出自己具备“造轮子”的能力** 。 ### 总结 @@ -102,4 +102,4 @@ CAP 理论看似简单,但在面试中,对它的理解深度可以从侧面 - 展示理论深度。你可以从一个熟知的知识点出发,深入浅出地回答,比如它的工作原理、优劣势、适用场景等。 - 结合落地经验。你不能仅停留在理论理解,还要结合落地方案的技术实现,这样才能体现你的技术闭环思维。 -- 展示知识体系,这是任何一个程序员向上发展的基础能力。理论深度和落地经验体现了作为程序员的基本素质, **而知识体系和技术判断力则体现了你是否达到架构师的能力边界** 。 +- 展示知识体系,这是任何一个程序员向上发展的基础能力。理论深度和落地经验体现了作为程序员的基本素质,**而知识体系和技术判断力则体现了你是否达到架构师的能力边界** 。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25404\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25404\350\256\262.md" index 08711d114..eae12d252 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25404\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25404\350\256\262.md" @@ -25,7 +25,7 @@ 关于数据一致性,通常要考虑一致性强弱(即强一致性和最终一致性的问题)。而要解决一致性的问题,则要进行一系列的一致性协议:如两阶段提交协议(Two-Phrase Commit,2PC)、Paxos 协议选举、Raft 协议、Gossip 协议。 -所以分布式数据存储的问题可以分成: **数据分片** 、 **数据复制** ,以及 **数据一致性** 带来的相关问题。接下来,我会针对这些问题,提炼出面试中最为核心和高频的考察点。 +所以分布式数据存储的问题可以分成: **数据分片** 、 **数据复制**,以及 **数据一致性** 带来的相关问题。接下来,我会针对这些问题,提炼出面试中最为核心和高频的考察点。 ![Lark20210106-170227.png](assets/CgqCHl_1fLCAU01mAAGC5GguKkM382.png) @@ -33,13 +33,13 @@ 就如我开篇提到的,面试官往往会把“案例背景中”的四个问题串联到具体的场景中,以具体的场景设问,比如“假设你是一家电商网站的架构师,现在要将原有单点上百 G 的商品做数据重构,存储到多个节点上,你会如何设计存储策略 ?” -因为是商品存储扩容的设计问题,很容易想到做数据的分库分表,也就是重新设计数据的分片规则,常用的分片策略有两种,即 Hash(哈希)分片和 Range(范围)分片。 **从这一点出发会考察你Hash(哈希)分片的具体实现原理。** 商品表包括主键、商品 ID、商品名称、所属品类和上架时间等字段。如果以商品 ID 作为关键字进行分片,系统会通过一个 Hash 函数计算商品 ID 的 Hash 值,然后取模,就能得到对应的分片。模为 4 就表示系统一共有四个节点,每个节点作为一个分片。 +因为是商品存储扩容的设计问题,很容易想到做数据的分库分表,也就是重新设计数据的分片规则,常用的分片策略有两种,即 Hash(哈希)分片和 Range(范围)分片。**从这一点出发会考察你Hash(哈希)分片的具体实现原理。** 商品表包括主键、商品 ID、商品名称、所属品类和上架时间等字段。如果以商品 ID 作为关键字进行分片,系统会通过一个 Hash 函数计算商品 ID 的 Hash 值,然后取模,就能得到对应的分片。模为 4 就表示系统一共有四个节点,每个节点作为一个分片。 假设Hash 函数为 “商品 ID % 节点个数 4”,通过计算可以得到每个数据应该存入的节点:计算结果为 0 的数据存入节点 A;结果为 1 的数据存入节点 B;结果为 2 的数据存入节点 C;计算为 3 的数据存储节点 D。 ![12.png](assets/Cip5yF_-eayAB5wqAAJhp0sQN50761.png) 商品数据 Hash 存储 -可以看出,Hash 分片的优点在于可以保证数据非常均匀地分布到多个分片上,并且实现起来简单,但扩展性很差,因为分片的计算方式就是直接用节点取模,节点数量变动,就需要重新计算 Hash,就会导致大规模数据迁移的工作。 **这时,就会延伸出第二个问题,如何解决 Hash 分片的缺点,既保证数据均匀分布,又保证扩展性?** 答案就是一致性 Hash :它是指将存储节点和数据都映射到一个首尾相连的哈希环上。存储节点一般可以根据 IP 地址进行 Hash 计算,数据的存储位置是从数据映射在环上的位置开始,依照顺时针方向所找到的第一个存储节点。 +可以看出,Hash 分片的优点在于可以保证数据非常均匀地分布到多个分片上,并且实现起来简单,但扩展性很差,因为分片的计算方式就是直接用节点取模,节点数量变动,就需要重新计算 Hash,就会导致大规模数据迁移的工作。**这时,就会延伸出第二个问题,如何解决 Hash 分片的缺点,既保证数据均匀分布,又保证扩展性?** 答案就是一致性 Hash :它是指将存储节点和数据都映射到一个首尾相连的哈希环上。存储节点一般可以根据 IP 地址进行 Hash 计算,数据的存储位置是从数据映射在环上的位置开始,依照顺时针方向所找到的第一个存储节点。 在具体操作过程中,通常会选择带有虚拟节点的一致性 Hash。假设在这个案例中将虚拟节点的数量设定为 10 个,就形成 10 个分片,而这 10 个分片构成了整个 Hash 空间。现在让 A 节点对应虚拟节点 0 ~ 3,B 节点对应虚拟节点 4 ~ 6,C 节点对应虚拟节点 7 ~ 8,D 节点对应虚拟节点 9。 @@ -55,7 +55,7 @@ 一致性 Hash 分片的优点是数据可以较为均匀地分配到各节点,其并发写入性能表现也不错。如果你应聘的是初级研发工程师,面试官通常不会追问下去,但是应聘中高级别研发的话,这样的回答还不够,你还要进一步阐述对分布式数据存储的理解。 -要知道,虽然一致性 Hash 提升了稳定性,使节点的加入和退出不会造成大规模的数据迁移,但本质上 Hash 分片是一种静态的分片方式,必须要提前设定分片的最大规模, **而且无法避免单一热点问题,** 某一数据被海量并发请求后,不论如何进行 Hash,数据也只能存在一个节点上,这势必会带来热点请求问题。比如案例中的电商商品,如果某些商品卖得非常火爆,通过 Hash 分片的方式很难针对热点商品做单独的架构设计。 +要知道,虽然一致性 Hash 提升了稳定性,使节点的加入和退出不会造成大规模的数据迁移,但本质上 Hash 分片是一种静态的分片方式,必须要提前设定分片的最大规模,**而且无法避免单一热点问题,** 某一数据被海量并发请求后,不论如何进行 Hash,数据也只能存在一个节点上,这势必会带来热点请求问题。比如案例中的电商商品,如果某些商品卖得非常火爆,通过 Hash 分片的方式很难针对热点商品做单独的架构设计。 所以,如果面试官想深入考核你对分布式数据存储的架构设计,一般会追问你: **如何解决单一热点问题?** **答案是做 Range(范围)分片。** 与 Hash 分片不同的是,Range 分片能结合业务逻辑规则,例如,我们用 “Category(商品类目)” 作为关键字进行分片时,不是以统一的商品一级类目为标准,而是可以按照一、二、三级类目进行灵活分片。例如,对于京东强势的 3C 品类,可以按照 3C 的三级品类设置分片;对于弱势品类,可以先按照一级品类进行分片,这样会让分片间的数据更加平衡。 @@ -63,11 +63,11 @@ 按业务品类分片 -要达到这种灵活性,前提是要有能力控制数据流向哪个分区,一个简单的实现方式是:预先设定主键的生成规则,根据规则进行数据的分片路由,但这种方式会侵入商品各条线主数据的业务规则, **更好的方式是基于分片元数据** (不过架构设计没有好坏,只有适合与否,所以在面试场景中,我建议你用擅长的解决方案来回答问题)。 +要达到这种灵活性,前提是要有能力控制数据流向哪个分区,一个简单的实现方式是:预先设定主键的生成规则,根据规则进行数据的分片路由,但这种方式会侵入商品各条线主数据的业务规则,**更好的方式是基于分片元数据** (不过架构设计没有好坏,只有适合与否,所以在面试场景中,我建议你用擅长的解决方案来回答问题)。 基于分片元数据的方式,就是调用端在操作数据的时候,先问一下分片元数据系统数据在哪,然后在根据得到的地址操作数据。元数据中存储的是数据分片信息,分片信息就是数据分布情况。在一个分布式存储系统中,承担数据调度功能的节点是分片元数据,当客户端收到请求后,会请求分片元数据服务,获取分片对应的实际节点地址,才能访问真正的数据。而请求分片元数据获取的信息也不仅仅只有数据分片信息,还包括数据量、读写 QPS 和分片副本的健康状态等。 -这种方式的灵活性在于分片规则不固定,易扩展,但是高灵活性就会带来高复杂性,从存储的角度看,元数据也是数据,特殊之处在于它类似一个路由表,每一次请求都要访问它,所以分片元数据本身就要做到高可用。如果系统支持动态分片,那么分片信息的变更数据还要在节点之间进行同步,这又带来多副本之间的一致性问题, **以此延伸出如何保证分片元数据服务的可用性和数据一致性?** 最直接的方式是专门给元数据做一个服务集群,并通过一致性算法复制数据。在实现方式上,就是将元数据服务的高可用和数据一致性问题转嫁给外围协调组件,如 ETCD 集群,这样既保证了系统的可靠,数据同步的成本又比较低。知道了设计思路,那具体的架构实现上怎么做 ? +这种方式的灵活性在于分片规则不固定,易扩展,但是高灵活性就会带来高复杂性,从存储的角度看,元数据也是数据,特殊之处在于它类似一个路由表,每一次请求都要访问它,所以分片元数据本身就要做到高可用。如果系统支持动态分片,那么分片信息的变更数据还要在节点之间进行同步,这又带来多副本之间的一致性问题,**以此延伸出如何保证分片元数据服务的可用性和数据一致性?** 最直接的方式是专门给元数据做一个服务集群,并通过一致性算法复制数据。在实现方式上,就是将元数据服务的高可用和数据一致性问题转嫁给外围协调组件,如 ETCD 集群,这样既保证了系统的可靠,数据同步的成本又比较低。知道了设计思路,那具体的架构实现上怎么做 ? 1. 给分片元数据做集群服务,并通过 ETCD 存储数据分片信息。 1. 每个数据存储实例节点定时向元数据服务集群同步心跳和分片信息。 @@ -77,11 +77,11 @@ 元数据分片 -掌握了这些知识后,你基本可以应对大多数公司对于研发工程师在数据架构设计上考点了,但如果面试官想挖掘你的能力,还会深入聊到共识算法,在一致性共识算法和最终一致性共识算法方面提出类似的问题, **比如, ETCD 是如何解决数据共识问题的** ? **为什么要选择这种数据复制方式呢** ? +掌握了这些知识后,你基本可以应对大多数公司对于研发工程师在数据架构设计上考点了,但如果面试官想挖掘你的能力,还会深入聊到共识算法,在一致性共识算法和最终一致性共识算法方面提出类似的问题,**比如, ETCD 是如何解决数据共识问题的**?**为什么要选择这种数据复制方式呢**? 对于这类问题,你要从一致性算法原理层面解答,思路是:清楚 ETCD 的共识算法是什么,还有哪些常用的共识算法,以及为什么 ETCD 会做这样的选型。 -ETCD 的共识算法是基于 Raft 协议实现的强一致性算法,同类的强一致性算法还有 Paxos,在面试过程中,面试官很可能让你从自己的角度理解一下这两个算法,当然也会直接问:为什么没有选择 Paxos 而选择了 Raft ? **这个问题对应聘高级研发的同学来讲很常见,主要考核你对以下内容的理解:** +ETCD 的共识算法是基于 Raft 协议实现的强一致性算法,同类的强一致性算法还有 Paxos,在面试过程中,面试官很可能让你从自己的角度理解一下这两个算法,当然也会直接问:为什么没有选择 Paxos 而选择了 Raft ?**这个问题对应聘高级研发的同学来讲很常见,主要考核你对以下内容的理解:** - Paxos 算法解决了什么问题? - Basic Paxos 算法的工作流程是什么? @@ -89,7 +89,7 @@ ETCD 的共识算法是基于 Raft 协议实现的强一致性算法,同类的 在分布式系统中,造成系统不可用的场景很多,比如服务器硬件损坏、网络数据丢包等问题,解决这些问题的根本思路是多副本,副本是分布式系统解决高可用的唯一手段,也就是主从模式,那么如何在保证一致性的前提下,提高系统的可用性,Paxos 就被用来解决这样的问题,而 Paxos 又分为 Basic Paxos 和 Multi Paxos,然而因为它们的实现复杂,工业界很少直接采用 Paxos 算法,所以 ETCD 选择了 Raft 算法 **(在面试过程中,面试官容易在这里设置障碍,来对候选者做技术分层)。** Raft 是 Multi Paxos 的一种实现,是通过一切以领导者为准的方式,实现一系列值的共识,然而不是所有节点都能当选 Leader 领导者,Raft 算法对于 Leader 领导者的选举是有限制的,只有最全的日志节点才可以当选。正因为 ETCD 选择了 Raft,为工业界提供了可靠的工程参考,就有更多的工程实现选择基于 Raft,如 TiDB 就是基于 Raft 算法的优化。 -如果你应聘的部门非基础架构部,那么对于中高级别研发工程师来说, **掌握以上问题的主线知识基本可以应对面试了** (我没有过多涉及算法细节,因为每一个算法都可以单独花一讲,而我侧重讲解分析问题,答题的思维,你可以在课下夯实算法基础,并在留言区与我互动)。 +如果你应聘的部门非基础架构部,那么对于中高级别研发工程师来说,**掌握以上问题的主线知识基本可以应对面试了** (我没有过多涉及算法细节,因为每一个算法都可以单独花一讲,而我侧重讲解分析问题,答题的思维,你可以在课下夯实算法基础,并在留言区与我互动)。 如果把问题设计的极端一些,考察你对最终一致性算法的掌握,还可以有一种思路:分片元数据服务毕竟是一个中心化的设计思路,而且基于强一致性的共识机制还是可能存在性能的问题,有没有更好的架构思路呢? diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25405\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25405\350\256\262.md" index 20b56d30e..957e48fee 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25405\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25405\350\256\262.md" @@ -4,7 +4,7 @@ ### 案例背景 -在互联网分布式场景中,原本一个系统被拆分成多个子系统,要想完成一次写入操作,你需要同时协调多个系统,这就带来了分布式事务的问题(分布式事务是指:一次大的操作由多个小操作组成,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败)。 **那怎么设计才能实现系统之间的事务一致性呢?** 这就是咱们今天要讨论的问题,也是面试的高频问题。 +在互联网分布式场景中,原本一个系统被拆分成多个子系统,要想完成一次写入操作,你需要同时协调多个系统,这就带来了分布式事务的问题(分布式事务是指:一次大的操作由多个小操作组成,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败)。**那怎么设计才能实现系统之间的事务一致性呢?** 这就是咱们今天要讨论的问题,也是面试的高频问题。 这一讲,我先从“解决分布式事务”这个问题本身出发,讲解答题思路和你要掌握的知识点。然后再结合“高并发”场景,看在该场景下如何保证分布式系统事务一致性?希望通过这种方式,让你彻底掌握分布式系统事务一致性的解题思路和技术认知。 @@ -16,7 +16,7 @@ 所以当很多候选者听到“怎么实现系统之间的分布式一致性?”的问题之后,会信心满满地选择一个方案,回答说:方案很多,可以选择 2PC ,2PC 实现的流程是…… -这种答题思路犯了一个很明显的错误,因为在实际工作中,很少采用前几种方案,基本都是基于 MQ 的可靠消息投递的方式来实现。所以一上来就说 2PC、3PC 或者 TCC 会让我觉得你并没有实际做过。 **那答题的套路是什么呢?** 我建议你先介绍目前主流实现分布式系统事务一致性的方案(也就是基于 MQ 的可靠消息投递的机制)然后回答出可实现方案和关键知识点。另外,为了和面试官进一步交流,你可以提出 2PC 或 TCC (这是一种交流方案)。因为 2PC 或 TCC 在工业界落地代价很大,不适合互联网场景,所以只有少部分的强一致性业务场景(如金融支付领域)会基于这两种方案实现。而你可以围绕它们的解决思路和方案弊端与面试官讨论, **这会让你和面试官由不平等的“面试与被面试”变成平等且友好的“双方沟通”,是一种面试套路。** +这种答题思路犯了一个很明显的错误,因为在实际工作中,很少采用前几种方案,基本都是基于 MQ 的可靠消息投递的方式来实现。所以一上来就说 2PC、3PC 或者 TCC 会让我觉得你并没有实际做过。**那答题的套路是什么呢?** 我建议你先介绍目前主流实现分布式系统事务一致性的方案(也就是基于 MQ 的可靠消息投递的机制)然后回答出可实现方案和关键知识点。另外,为了和面试官进一步交流,你可以提出 2PC 或 TCC (这是一种交流方案)。因为 2PC 或 TCC 在工业界落地代价很大,不适合互联网场景,所以只有少部分的强一致性业务场景(如金融支付领域)会基于这两种方案实现。而你可以围绕它们的解决思路和方案弊端与面试官讨论,**这会让你和面试官由不平等的“面试与被面试”变成平等且友好的“双方沟通”,是一种面试套路。** 但要做到这几点,需要建立在你深入掌握分布式事务一致性问题的基础之上,所以接下来,我们就解析一下面试中最为常见的两种实现方案。 @@ -39,15 +39,15 @@ Spring事务管理 我们假设订单数据,商品数据和促销数据分别保存在数据库 D1,数据库 D2 和数据库 D3 上。 -- **准备阶段** ,事务管理器首先通知所有资源管理器开启事务,询问是否做好提交事务的准备。如资源管理器此时会将 undo 日志和 redo 日志计入事务日志中,并做出应答,当协调者接收到反馈 Yes 后,则准备阶段结束。 ![19.png](assets/Cip5yF_-ek-AeszEAAGVNQOE9EQ982.png) +- **准备阶段**,事务管理器首先通知所有资源管理器开启事务,询问是否做好提交事务的准备。如资源管理器此时会将 undo 日志和 redo 日志计入事务日志中,并做出应答,当协调者接收到反馈 Yes 后,则准备阶段结束。 ![19.png](assets/Cip5yF_-ek-AeszEAAGVNQOE9EQ982.png) 2PC 准备阶段 -- **提交阶段** ,当收到所有数据库实例的 Yes 后,事务管理器会发出提交指令。每个数据库接受指令进行本地操作,正式提交更新数据,然后向协调者返回 Ack 消息,事务结束。 ![20.png](assets/CgpVE1_-elyAMxAUAAGGnETIxqE263.png) +- **提交阶段**,当收到所有数据库实例的 Yes 后,事务管理器会发出提交指令。每个数据库接受指令进行本地操作,正式提交更新数据,然后向协调者返回 Ack 消息,事务结束。 ![20.png](assets/CgpVE1_-elyAMxAUAAGGnETIxqE263.png) 2PC 提交阶段 -- **中断阶段** ,如果任何一个参与者向协调者反馈了 No 响应,例如用户 B 在数据库 D3 上面的余额在执行其他扣款操作,导致数据库 D3 的数据无法锁定,则只能向事务管理器返回失败。此时,协调者向所有参与者发出 Rollback 请求,参与者接收 Rollback 请求后,会利用其在准备阶段中记录的 undo 日志来进行回滚操作,并且在完成事务回滚之后向协调者发送 Ack 消息,完成事务回滚操作。 +- **中断阶段**,如果任何一个参与者向协调者反馈了 No 响应,例如用户 B 在数据库 D3 上面的余额在执行其他扣款操作,导致数据库 D3 的数据无法锁定,则只能向事务管理器返回失败。此时,协调者向所有参与者发出 Rollback 请求,参与者接收 Rollback 请求后,会利用其在准备阶段中记录的 undo 日志来进行回滚操作,并且在完成事务回滚之后向协调者发送 Ack 消息,完成事务回滚操作。 ![21.png](assets/Cip5yF_-emuANRvWAAJkZ2BNZ00511.png) @@ -61,11 +61,11 @@ Spring事务管理 还是上面的例子,如果商品库存数据为 1,也就是数据库 D1 为 1,在准备阶段询问是否可以扣减库存,商品数据返回可以,此时如果不锁定数据,在提交阶段之前另外一个请求去扣减了数据库 D1 的数据,这时候,在提交阶段再去扣减库存时,数据库 D1 的数据就会超售变成了负 1。 -但正因为要加锁,会导致两阶段提交存在一系列问题, **最严重的就是死锁问题** ,一旦发生故障,数据库就会阻塞,尤其在提交阶段,如果发生故障,数据都还处于资源锁定状态,将无法完成后续的事务提交操作。 +但正因为要加锁,会导致两阶段提交存在一系列问题,**最严重的就是死锁问题**,一旦发生故障,数据库就会阻塞,尤其在提交阶段,如果发生故障,数据都还处于资源锁定状态,将无法完成后续的事务提交操作。 -其次是 **性能问题** ,数据库(如 MySQL )在执行过程中会对操作的数据行执行数据行锁,如果此时其他的事务刚好也要操作被锁定的数据行,那它们就只能阻塞等待,使分布式事务出现高延迟和性能低下。 +其次是 **性能问题**,数据库(如 MySQL )在执行过程中会对操作的数据行执行数据行锁,如果此时其他的事务刚好也要操作被锁定的数据行,那它们就只能阻塞等待,使分布式事务出现高延迟和性能低下。 -再有就是 **数据不一致性** ,在提交阶段,当事务管理器向参与者发送提交事务请求之后,如果此时出现了网络异常,只有部分数据库接收到请求,那么会导致未接收到请求的数据库无法提交事务,整个系统出现数据不一致性。 +再有就是 **数据不一致性**,在提交阶段,当事务管理器向参与者发送提交事务请求之后,如果此时出现了网络异常,只有部分数据库接收到请求,那么会导致未接收到请求的数据库无法提交事务,整个系统出现数据不一致性。 至此,我们就了解了基于 2PC 实现的分布式事务一致性的解决方案,你可以从这几点出发,与面试官进行友好的交流。 @@ -77,19 +77,19 @@ Spring事务管理 基于 MQ 的消息投递 -基于 MQ 的可靠消息投递的方案不仅可以解决由于业务流程的同步执行而造成的阻塞问题,还可以实现业务解耦合流量削峰。这种方案中的可选型的 MQ 也比较多,比如基于 RabbitMQ 或者 RocketMQ,但并不是引入了消息队列中间件就万事大吉了,通常情况下, **面试官会着重通过以下两个知识点来考察你对这种方案的掌握程度** 。 +基于 MQ 的可靠消息投递的方案不仅可以解决由于业务流程的同步执行而造成的阻塞问题,还可以实现业务解耦合流量削峰。这种方案中的可选型的 MQ 也比较多,比如基于 RabbitMQ 或者 RocketMQ,但并不是引入了消息队列中间件就万事大吉了,通常情况下,**面试官会着重通过以下两个知识点来考察你对这种方案的掌握程度** 。 - **MQ 自动应答机制导致的消息丢失** 订阅消息事件的优惠券服务在接收订单服务投递的消息后,消息中间件(如 RabbitMQ)默认是开启消息自动应答机制,当优惠券系统消费了消息,消息中间件就会删除这个持久化的消息。 但在优惠券系统执行的过程中,很可能因为执行异常导致流程中断,那这时候消息中间件中就没有这个数据了,进而会导致消息丢失。因此你要采取编程的方式手动发送应答,也就是当优惠券系统执行业务成功之后,消息中间件才能删除这条持久化消息。 -这个知识点很容易被忽略掉,但却很重要,会让面试官认为你切切实实的做过,另外还有一个高频的问题,就是在大促的时候,瞬时流量剧增,很多没能及时消费的消息积压在 MQ 队列中, **这个问题如何解决呢?** +这个知识点很容易被忽略掉,但却很重要,会让面试官认为你切切实实的做过,另外还有一个高频的问题,就是在大促的时候,瞬时流量剧增,很多没能及时消费的消息积压在 MQ 队列中,**这个问题如何解决呢?** - **高并发场景下的消息积压导致消息丢失** 分布式部署环境基于网络进行通信,而在网络通信的过程中,上下游可能因为各种原因而导致消息丢失。比如优惠券系统由于流量过大而触发限流,不能保证事件消息能够被及时地消费,这个消息就会被消息队列不断地重试,最后可能由于超过了最大重试次数而被丢弃到死信队列中。 但实际上,你需要人工干预处理移入死信队列的消息,于是在这种场景下,事件消息大概率会被丢弃。而这个问题源于订单系统作为事件的生产者进行消息投递后,无法感知它下游(即优惠券系统)的所有操作,那么优惠券系统作为事件的消费者,是消费成功还是消费失败,订单系统并不知道。 -顺着这个思路,如果让订单知道消费执行结果的响应,即使出现了消息丢失的情况,订单系统也还是可以通过定时任务扫描的方式,将未完成的消息重新投递来进行消息补偿。 **这是基于消息队列实现分布式事务的关键** ,是一种双向消息确认的机制。 +顺着这个思路,如果让订单知道消费执行结果的响应,即使出现了消息丢失的情况,订单系统也还是可以通过定时任务扫描的方式,将未完成的消息重新投递来进行消息补偿。**这是基于消息队列实现分布式事务的关键**,是一种双向消息确认的机制。 那么如何落地实现呢?你可以先让订单系统把要发送的消息持久化到本地数据库里,然后将这条消息记录的状态设置为代发送,紧接着订单系统再投递消息到消息队列,优惠券系统消费成功后,也会向消息队列发送一个通知消息。当订单系统接收到这条通知消息后,再把本地持久化的这条消息的状态设置为完成。 ![11.png](assets/Cip5yF_-epGAWFi1AAE-yrH59GA499.png) diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25406\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25406\350\256\262.md" index c4cfcc04d..487c2e495 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25406\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25406\350\256\262.md" @@ -25,7 +25,7 @@ 如果面试官觉得你回答问题的思路清晰有条理,给出的实现方案也可以落地,并且满足你的业务场景,那么他会认可你具备初中级研发工程师该具备的设计能力,但不要高兴得太早。 -因为有些面试官会继续追问:“分布式锁用 Zookeeper 实现行不行?”,“分布式锁用 etcd 实现行不行?” 借机考察你对分布式协调组件的掌握。你可能会觉得开源组件那么多,自己不可能每一个都用过,答不出来也无妨。 **但面试官提问的重点不是停留在组件的使用上,而是你对分布式锁的原理问题的掌握程度。** +因为有些面试官会继续追问:“分布式锁用 Zookeeper 实现行不行?”,“分布式锁用 etcd 实现行不行?” 借机考察你对分布式协调组件的掌握。你可能会觉得开源组件那么多,自己不可能每一个都用过,答不出来也无妨。**但面试官提问的重点不是停留在组件的使用上,而是你对分布式锁的原理问题的掌握程度。** 换句话说,“如果让借助第三方组件,你怎么设计分布式锁?” 这背后涉及了分布式锁的底层设计逻辑,是你需要掌握的。 @@ -66,7 +66,7 @@ select id from order where order_id = xxx for update - 可重复读(REPEATABLE READ); - 可串行化(SERIALIZABLE)。 -其中,可串行化操作就是按照事务的先后顺序,排队执行,然而一个事务操作可能要执行很久才能完成,这就没有并发效率可言了, **所以数据库隔离级别越高,系统的并发性能就越差。** +其中,可串行化操作就是按照事务的先后顺序,排队执行,然而一个事务操作可能要执行很久才能完成,这就没有并发效率可言了,**所以数据库隔离级别越高,系统的并发性能就越差。** - **基于乐观锁的方式实现分布式锁** 在数据库层面,select for update 是悲观锁,会一直阻塞直到事务提交,所以为了不产生锁等待而消耗资源,你可以基于乐观锁的方式来实现分布式锁,比如基于版本号的方式,首先在数据库增加一个 int 型字段 ver,然后在 SELECT 同时获取 ver 值,最后在 UPDATE 的时候检查 ver 值是否为与第 2 步或得到的版本值相同。 @@ -85,7 +85,7 @@ update order set ver = old_ver + 1, amount = yyy where order_id = xxx and ver = 我在开篇提到,因为数据库的性能限制了业务的并发量,所以针对“ 618 和双 11 大促”等请求量剧增的场景,你要引入基于缓存的分布式锁,这个方案可以避免大量请求直接访问数据库,提高系统的响应能力。 -基于缓存实现的分布式锁,就是将数据仅存放在系统的内存中,不写入磁盘,从而减少 I/O 读写。 **接下来,我以 Redis 为例讲解如何实现分布式锁。** +基于缓存实现的分布式锁,就是将数据仅存放在系统的内存中,不写入磁盘,从而减少 I/O 读写。**接下来,我以 Redis 为例讲解如何实现分布式锁。** 在加锁的过程中,实际上就是在给 Key 键设置一个值,为避免死锁,还要给 Key 键设置一个过期时间。 @@ -111,14 +111,14 @@ else end ``` -以上,就是基于 Redis 的 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁、解锁,不过在实际面试中, **你不能仅停留在操作上,因为这并不能满足应对面试需要掌握的知识深度,** 所以你还要清楚基于 Redis 实现分布式锁的优缺点;Redis 的超时时间设置问题;站在架构设计层面上 Redis 怎么解决集群情况下分布式锁的可靠性问题。 +以上,就是基于 Redis 的 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁、解锁,不过在实际面试中,**你不能仅停留在操作上,因为这并不能满足应对面试需要掌握的知识深度,** 所以你还要清楚基于 Redis 实现分布式锁的优缺点;Redis 的超时时间设置问题;站在架构设计层面上 Redis 怎么解决集群情况下分布式锁的可靠性问题。 需要注意的是,你不用一股脑全部将其说出来,而是要做好准备,以便跟上面试官的思路,同频沟通。 - **基于 Redis 实现分布式锁的优缺点** 基于数据库实现分布式锁的方案来说,基于缓存实现的分布式锁主要的优点主要有三点。 1. 性能高效(这是选择缓存实现分布式锁最核心的出发点)。 -1. 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。但是需要注意的是,在 Redis2.6.12 的之前的版本中,由于加锁命令和设置锁过期时间命令是两个操作(不是原子性的),当出现某个线程操作完成 setnx 之后,还没有来得及设置过期时间,线程就挂掉了,就会导致当前线程设置 key 一直存在,后续的线程无法获取锁,最终造成死锁的问题, **所以要选型 Redis 2.6.12 后的版本或通过 Lua 脚本执行加锁和设置超时时间** (Redis 允许将 Lua 脚本传到 Redis 服务器中执行, 脚本中可以调用多条 Redis 命令,并且 Redis 保证脚本的原子性)。 +1. 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。但是需要注意的是,在 Redis2.6.12 的之前的版本中,由于加锁命令和设置锁过期时间命令是两个操作(不是原子性的),当出现某个线程操作完成 setnx 之后,还没有来得及设置过期时间,线程就挂掉了,就会导致当前线程设置 key 一直存在,后续的线程无法获取锁,最终造成死锁的问题,**所以要选型 Redis 2.6.12 后的版本或通过 Lua 脚本执行加锁和设置超时时间** (Redis 允许将 Lua 脚本传到 Redis 服务器中执行, 脚本中可以调用多条 Redis 命令,并且 Redis 保证脚本的原子性)。 1. 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。 当然,基于 Redis 实现分布式锁也存在缺点,主要是不合理设置超时时间,以及 Redis 集群的数据同步机制,都会导致分布式锁的不可靠性。 @@ -129,7 +129,7 @@ end 锁超时导致的误操作 -所以,如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短,有可能业务阻塞没有处理完成, **能否合理设置超时时间,是基于缓存实现分布式锁很难解决的一个问题。** **那么如何合理设置超时时间呢?** 你可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可。 +所以,如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短,有可能业务阻塞没有处理完成,**能否合理设置超时时间,是基于缓存实现分布式锁很难解决的一个问题。** **那么如何合理设置超时时间呢?** 你可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可。 不过这种方式实现起来相对复杂,我建议你结合业务场景进行回答,所以针对超时时间的设置,要站在实际的业务场景中进行衡量。 @@ -139,9 +139,9 @@ end 但 03 讲我没有说怎么解决这个问题,其实 Redis 官方已经设计了一个分布式锁算法 Redlock 解决了这个问题。 -而如果你能基于 Redlock 原理回答出怎么解决 Redis 集群节点实现分布式锁的问题,会成为面试的加分项。 **那官方是怎么解决的呢?** 为了避免 Redis 实例故障导致锁无法工作的问题,Redis 的开发者 Antirez 设计了分布式锁算法 Redlock。Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。 +而如果你能基于 Redlock 原理回答出怎么解决 Redis 集群节点实现分布式锁的问题,会成为面试的加分项。**那官方是怎么解决的呢?** 为了避免 Redis 实例故障导致锁无法工作的问题,Redis 的开发者 Antirez 设计了分布式锁算法 Redlock。Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。 -这样一来,即使有某个 Redis 实例发生故障,因为锁的数据在其他实例上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。 **那 Redlock 算法是如何做到的呢?** **我们假设目前有 N 个独立的 Redis 实例,** 客户端先按顺序依次向 N 个 Redis 实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,但是需要注意的是,Redlock 算法设置了加锁的超时时间,为了避免因为某个 Redis 实例发生故障而一直等待的情况。 +这样一来,即使有某个 Redis 实例发生故障,因为锁的数据在其他实例上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。**那 Redlock 算法是如何做到的呢?** **我们假设目前有 N 个独立的 Redis 实例,** 客户端先按顺序依次向 N 个 Redis 实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,但是需要注意的是,Redlock 算法设置了加锁的超时时间,为了避免因为某个 Redis 实例发生故障而一直等待的情况。 当客户端完成了和所有 Redis 实例的加锁操作之后,如果有超过半数的 Redis 实例成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。 @@ -151,7 +151,7 @@ end 但是在面试时,你要分清楚面试官的考查点,并结合工作中的业务场景给出答案,面试官不侧重你是否能很快地给出结果,而是你思考的过程。 -对于分布式锁, **你要从“解决可用性、死锁、脑裂”等问题为出发点来展开回答各分布式锁的实现方案的优缺点和适用场景。** 另外,在设计分布式锁的时候,为了解决可用性、死锁、脑裂等问题,一般还会再考虑一下锁的四种设计原则。 +对于分布式锁,**你要从“解决可用性、死锁、脑裂”等问题为出发点来展开回答各分布式锁的实现方案的优缺点和适用场景。** 另外,在设计分布式锁的时候,为了解决可用性、死锁、脑裂等问题,一般还会再考虑一下锁的四种设计原则。 - **互斥性** :即在分布式系统环境下,对于某一共享资源,需要保证在同一时间只能一个线程或进程对该资源进行操作。 - **高可用** :也就是可靠性,锁服务不能有单点风险,要保证分布式锁系统是集群的,并且某一台机器锁不能提供服务了,其他机器仍然可以提供锁服务。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25407\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25407\350\256\262.md" index 2b04f46b0..b06189e93 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25407\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25407\350\256\262.md" @@ -71,13 +71,13 @@ App 商品详情页服务调用 然而我在面试候选人时发现,一些同学虽然做了准备,但只能说出个别 RPC 框架的大致流程,不能深刻理解每个环节的工作原理,所以整体给我的感觉就是: **应用层面通过,原理深度不够** 。 -而我对你的要求是:对于中间件等技术工具和框架,虽然在实际工作中不推荐重复“造轮子”, **但在面试中要证明自己具备“造轮子”的能力** ,因为要评价一个程序员是否对技术栈有全面的认识,考察其“造轮子”的能力是一个不错的切入点。 +而我对你的要求是:对于中间件等技术工具和框架,虽然在实际工作中不推荐重复“造轮子”,**但在面试中要证明自己具备“造轮子”的能力**,因为要评价一个程序员是否对技术栈有全面的认识,考察其“造轮子”的能力是一个不错的切入点。 接下来我们先理解一下完整的 RPC 会涉及哪些步骤,然后再解析其中的重要环节,搞懂 RPC 原理的考察点。 #### 一次完整的 RPC 流程 **因为 RPC 是远程调用,首先会涉及网络通信,** 又因为 RPC 用于业务系统之间的数据交互,要保证数据传输的可靠性,所以它一般默认采用 TCP 来实现网络数据传输 -网络传输的数据必须是二进制数据,可是在 RPC 框架中,调用方请求的出入参数都是对象,对象不能直接在网络中传输,所以需要提前把对象转成可传输的二进制数据,转换算法还要可逆, **这个过程就叫“序列化”和“反序列化”。** 另外,在网络传输中,RPC 不会把请求参数的所有二进制数据一起发送到服务提供方机器上,而是拆分成好几个数据包(或者把好几个数据包封装成一个数据包),所以服务提供方可能一次获取多个或半个数据包,这也就是网络传输中的粘包和半包问题。 **为了解决这个问题,需要提前约定传输数据的格式,即“RPC 协议”。** 大多数的协议会分成数据头和消息体: +网络传输的数据必须是二进制数据,可是在 RPC 框架中,调用方请求的出入参数都是对象,对象不能直接在网络中传输,所以需要提前把对象转成可传输的二进制数据,转换算法还要可逆,**这个过程就叫“序列化”和“反序列化”。** 另外,在网络传输中,RPC 不会把请求参数的所有二进制数据一起发送到服务提供方机器上,而是拆分成好几个数据包(或者把好几个数据包封装成一个数据包),所以服务提供方可能一次获取多个或半个数据包,这也就是网络传输中的粘包和半包问题。**为了解决这个问题,需要提前约定传输数据的格式,即“RPC 协议”。** 大多数的协议会分成数据头和消息体: - 数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息; - 消息体主要是请求的业务参数信息和扩展属性等。 @@ -100,7 +100,7 @@ RPC 通信流程 #### 如何选型序列化方式 -RPC 的调用过程会涉及网络数据(二进制数据)的传输, **从中延伸的问题是:如何选型序列化和反序列化方式?** +RPC 的调用过程会涉及网络数据(二进制数据)的传输,**从中延伸的问题是:如何选型序列化和反序列化方式?** 要想回答这一点,你需要先明确序列化方式,常见的方式有以下几种。 @@ -169,9 +169,9 @@ public class ServerTaskThread implements Runnable { BIO 网络模型 -所以,BIO 的网络模型中, **每当客户端发送一个连接请求给服务端,服务端都会启动一个新的线程去处理客户端连接的读写操作** ,即每个 Socket 都对应一个独立的线程,客户端 Socket 和服务端工作线程的数量是 1 比 1,这会导致服务器的资源不够用,无法实现高并发下的网络开发。所以 BIO 的网络模型只适用于 Socket 连接不多的场景,无法支撑几十甚至上百万的连接场景。 +所以,BIO 的网络模型中,**每当客户端发送一个连接请求给服务端,服务端都会启动一个新的线程去处理客户端连接的读写操作**,即每个 Socket 都对应一个独立的线程,客户端 Socket 和服务端工作线程的数量是 1 比 1,这会导致服务器的资源不够用,无法实现高并发下的网络开发。所以 BIO 的网络模型只适用于 Socket 连接不多的场景,无法支撑几十甚至上百万的连接场景。 -另外, **BIO 模型有两处阻塞的地方** 。 +另外,**BIO 模型有两处阻塞的地方** 。 - 服务端阻塞等待客户端发起连接。在第 11 行代码中,通过 serverSocket.accept() 方法服务端等待用户发连接请求过来。 - 连接成功后,工作线程阻塞读取客户端 Socket 发送数据。在第 27 行代码中,通过 in.readLine() 服务端从网络中读客户端发送过来的数据,这个地方也会阻塞。如果客户端已经和服务端建立了一个连接,但客户端迟迟不发送数据,那么服务端的 readLine() 操作会一直阻塞,造成资源浪费。 @@ -188,7 +188,7 @@ NIO 网络模型 这时就需要一个调度者去监控所有的客户端连接,比如当图中的客户端 A 的输入已经准备好后,就由这个调度者去通知服务端的工作线程,告诉它们由工作线程 1 去服务于客户端 A 的请求。这种思路就是 NIO 编程模型的基本原理,调度者就是 Selector 选择器。 -由此可见, **NIO 比 BIO 提高了服务端工作线程的利用率,并增加了一个调度者,来实现 Socket 连接与 Socket 数据读写之间的分离** 。 +由此可见,**NIO 比 BIO 提高了服务端工作线程的利用率,并增加了一个调度者,来实现 Socket 连接与 Socket 数据读写之间的分离** 。 在目前主流的 RPC 框架中,广泛使用的也是 I/O 多路复用模型,Linux 系统中的 select、poll、epoll等系统调用都是 I/O 多路复用的机制。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25408\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25408\350\256\262.md" index 10a90b5da..603b632f6 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25408\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25408\350\256\262.md" @@ -23,7 +23,7 @@ 这几年,很多电商平台为了方便流量运营,改造了传统秒杀场景,通过先预约再抢购的方式预热商品,并根据预约量调整运营策略。而且在预约抢购的活动中,为了增加商品售卖量,会允许抢购前,预约资格超过实际的库存数量。 -那么问题来了:如何在高并发量的情况下,让每个用户都能得到抢购资格呢? **这是预约抢购场景第一个技术考察点。** 那你可以基于“06 | 分布式系统中,如何回答锁的实现原理?”来控制抢购资格的发放。 +那么问题来了:如何在高并发量的情况下,让每个用户都能得到抢购资格呢?**这是预约抢购场景第一个技术考察点。** 那你可以基于“06 | 分布式系统中,如何回答锁的实现原理?”来控制抢购资格的发放。 我们基于 Redis 实现分布式锁(这是最常用的方式),在加锁的过程中,实际上是给 Key 键设置一个值,为避免死锁,还要给 Key 键设置一个过期时间。 @@ -55,7 +55,7 @@ end 用户预约成功之后,在商品详情页面中,会存在一个抢购倒计时,这个倒计时的初始时间是从服务端获取的,用户点击购买按钮时,系统还会去服务端验证是否已经到了抢购时间。 -在等待抢购阶段,流量突增,因为在抢购商品之前(尤其是临近开始抢购之前的一分钟内),大部分用户会频繁刷新商品详情页, **商品详情页面的读请求量剧增,** 如果商品详情页面没有做好流量控制,就容易成为整个预约抢购系统中的性能瓶颈点。 +在等待抢购阶段,流量突增,因为在抢购商品之前(尤其是临近开始抢购之前的一分钟内),大部分用户会频繁刷新商品详情页,**商品详情页面的读请求量剧增,** 如果商品详情页面没有做好流量控制,就容易成为整个预约抢购系统中的性能瓶颈点。 那么问题来了:如何解决等待抢购时间内的流量突增问题呢?有两个解决思路。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25409\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25409\350\256\262.md" index 26ff220ac..836c4ef28 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25409\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25409\350\256\262.md" @@ -8,7 +8,7 @@ 很多面试官考察候选人对“数据库知识”的掌握程度,会以“数据库的索引原理和优化方法”作为切入点。 -**假设面试官问你:** 在电商平台的订单中心系统中,通常要根据商品类型、订单状态筛选出需要的订单,并按照订单创建的时间进行排序, **那针对下面这条 SQL,你怎么通过索引来提高查询效率呢?** +**假设面试官问你:** 在电商平台的订单中心系统中,通常要根据商品类型、订单状态筛选出需要的订单,并按照订单创建的时间进行排序,**那针对下面这条 SQL,你怎么通过索引来提高查询效率呢?** ```sql select * from order where status = 1 order by create_time asc @@ -55,7 +55,7 @@ select * from order where status = 1 order by create_time asc 在创建表时,InnoDB 存储引擎默认使用表的主键作为主键索引,该主键索引就是聚簇索引(Clustered Index),如果表没有定义主键,InnoDB 就自己产生一个隐藏的 6 个字节的主键 ID 值作为主键索引,而创建的主键索引默认使用的是 B+Tree 索引。 -接下来我们通过一个简单的例子,说明一下 B+Tree 索引在存储数据中的具体实现, **为的是让你理解通过 B+Tree 做索引的原理。** +接下来我们通过一个简单的例子,说明一下 B+Tree 索引在存储数据中的具体实现,**为的是让你理解通过 B+Tree 做索引的原理。** 首先,我们创建一张商品表: @@ -100,7 +100,7 @@ select * from product where id = 15 那么问题来了,如果你当前查询数据时候,不是通过主键 ID,而是用商品编码查询商品,那么查询过程又是怎样的呢? -- **通过非主键(辅助索引)查询商品数据的过程** 如果你用商品编码查询商品(即使用辅助索引进行查询),会先检索辅助索引中的 B+Tree 的 商品编码,找到对应的叶子节点,获取主键值,然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。 **这个过程叫回表。** **以上就是索引的实现原理。** 掌握索引的原理是了解 MySQL 数据库的查询效率的基础,是每一个研发工程师都需要精通的知识点。 +- **通过非主键(辅助索引)查询商品数据的过程** 如果你用商品编码查询商品(即使用辅助索引进行查询),会先检索辅助索引中的 B+Tree 的 商品编码,找到对应的叶子节点,获取主键值,然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。**这个过程叫回表。** **以上就是索引的实现原理。** 掌握索引的原理是了解 MySQL 数据库的查询效率的基础,是每一个研发工程师都需要精通的知识点。 在面试时,面试官一般不会让你直接描述查询索引的过程,但是会通过考察你对索引优化方法的理解,来评估你对索引原理的掌握程度,比如为什么 MySQL InnoDB 选择 B+Tree 作为默认的索引数据结构?MySQL 常见的优化索引的方法有哪些? @@ -152,7 +152,7 @@ CREATE TABLE `product` ( 执行计划 -对于执行计划,参数有 possible_keys 字段表示可能用到的索引,key 字段表示实际用的索引,key_len 表示索引的长度,rows 表示扫描的数据行数。 **这其中需要你重点关注 type 字段,** 表示数据扫描类型,也就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为(考虑到查询效率问题,全表扫描和全索引扫描要尽量避免): +对于执行计划,参数有 possible_keys 字段表示可能用到的索引,key 字段表示实际用的索引,key_len 表示索引的长度,rows 表示扫描的数据行数。**这其中需要你重点关注 type 字段,** 表示数据扫描类型,也就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为(考虑到查询效率问题,全表扫描和全索引扫描要尽量避免): - ALL(全表扫描); - index(全索引扫描); @@ -195,7 +195,7 @@ CREATE TABLE `product` ( 我们可以建立一个组合索引,即商品ID、名称、价格作为一个组合索引。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。所以,使用覆盖索引的好处很明显,即不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。 -- **联合索引** 联合索引时,存在 **最左匹配原则** ,也就是按照最左优先的方式进行索引的匹配。比如联合索引 (userpin, username),如果查询条件是 WHERE userpin=1 AND username=2,就可以匹配上联合索引;或者查询条件是 WHERE userpin=1,也能匹配上联合索引,但是如果查询条件是 WHERE username=2,就无法匹配上联合索引。 +- **联合索引** 联合索引时,存在 **最左匹配原则**,也就是按照最左优先的方式进行索引的匹配。比如联合索引 (userpin, username),如果查询条件是 WHERE userpin=1 AND username=2,就可以匹配上联合索引;或者查询条件是 WHERE userpin=1,也能匹配上联合索引,但是如果查询条件是 WHERE username=2,就无法匹配上联合索引。 另外,建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25410\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25410\350\256\262.md" index 0b10b41be..7e11e63c8 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25410\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25410\350\256\262.md" @@ -4,7 +4,7 @@ ### 案例背景 -MySQL 的事务隔离级别(Isolation Level),是指:当多个线程操作数据库时,数据库要负责隔离操作,来保证各个线程在获取数据时的准确性。它分为四个不同的层次,按隔离水平高低排序, **读未提交 < 读已提交 < 可重复度 < 串行化** 。 +MySQL 的事务隔离级别(Isolation Level),是指:当多个线程操作数据库时,数据库要负责隔离操作,来保证各个线程在获取数据时的准确性。它分为四个不同的层次,按隔离水平高低排序,**读未提交 < 读已提交 < 可重复度 < 串行化** 。 ![幻灯片1.PNG](assets/CgpVE2AXOh2AEXQqAACGXaq3WXI045.PNG) @@ -41,13 +41,13 @@ MySQL 隔离级别 假设有 A 和 B 两个事务,在并发情况下,事务 A 先开始读取商品数据表中的数据,然后再执行更新操作,如果此时事务 A 还没有提交更新操作,但恰好事务 B 开始,然后也需要读取商品数据,此时事务 B 查询得到的是刚才事务 A 更新后的数据。 -如果接下来事务 A 触发了回滚,那么事务 B 刚才读到的数据就是过时的数据,这种现象就是脏读。 **“脏读”面试关注点:** 1. 脏读对应的隔离级别是“读未提交”,只有该隔离级别才会出现脏读。 -2\. 脏读的解决办法是升级事务隔离级别,比如“读已提交”。 **不可重复读:** 事务 A 先读取一条数据,然后执行逻辑的过程中,事务 B 更新了这条数据,事务 A 再读取时,发现数据不匹配,这个现象就是“不可重复读”。 +如果接下来事务 A 触发了回滚,那么事务 B 刚才读到的数据就是过时的数据,这种现象就是脏读。**“脏读”面试关注点:** 1. 脏读对应的隔离级别是“读未提交”,只有该隔离级别才会出现脏读。 +2\. 脏读的解决办法是升级事务隔离级别,比如“读已提交”。**不可重复读:** 事务 A 先读取一条数据,然后执行逻辑的过程中,事务 B 更新了这条数据,事务 A 再读取时,发现数据不匹配,这个现象就是“不可重复读”。 ![幻灯片3.PNG](assets/Cip5yGAXOmGAcCNlAABpFaU7YQ8179.PNG) 事务并发时的“不可重复读”现象 **“不可重复读”面试关注点:** 1. 简单理解是两次读取的数据中间被修改,对应的隔离级别是“读未提交”或“读已提交”。 -2\. 不可重复读的解决办法就是升级事务隔离级别,比如“可重复度”。 **幻读:** 在一个事务内,同一条查询语句在不同时间段执行,得到不同的结果集。 +2\. 不可重复读的解决办法就是升级事务隔离级别,比如“可重复度”。**幻读:** 在一个事务内,同一条查询语句在不同时间段执行,得到不同的结果集。 ![幻灯片4.PNG](assets/Cip5yGAXOnSASsgQAABza2XSHV0638.PNG) @@ -55,9 +55,9 @@ MySQL 隔离级别 事务 A 读了一次商品表,得到最后的 ID 是 3,事务 B 也同样读了一次,得到最后 ID 也是 3。接下来事务 A 先插入了一行,然后读了一下最新的 ID 是 4,刚好是前面 ID 3 加上 1,然后事务 B 也插入了一行,接着读了一下最新的 ID 发现是 5,而不是 3 加 1。 -这时,你发现在使用 ID 做判断或做关键数据时,就会出现问题,这种现象就像是让事务 B 产生了幻觉一样,读取到了一个意想不到的数据,所以叫幻读。当然,不仅仅是新增,删除、修改数据也会发生类似的情况。 **“幻读”面试关注点:** 1. 要想解决幻读不能升级事务隔离级别到“可串行化”,那样数据库也失去了并发处理能力。 +这时,你发现在使用 ID 做判断或做关键数据时,就会出现问题,这种现象就像是让事务 B 产生了幻觉一样,读取到了一个意想不到的数据,所以叫幻读。当然,不仅仅是新增,删除、修改数据也会发生类似的情况。**“幻读”面试关注点:** 1. 要想解决幻读不能升级事务隔离级别到“可串行化”,那样数据库也失去了并发处理能力。 2\. 行锁解决不了幻读,因为即使锁住所有记录,还是阻止不了插入新数据。 -3\. 解决幻读的办法是锁住记录之间的“间隙”,为此 MySQL InnoDB 引入了新的锁,叫 **间隙锁(Gap Lock)** ,所以在面试中,你也要掌握间隙锁,以及间隙锁与行锁结合的 next-key lock 锁。 +3\. 解决幻读的办法是锁住记录之间的“间隙”,为此 MySQL InnoDB 引入了新的锁,叫 **间隙锁(Gap Lock)**,所以在面试中,你也要掌握间隙锁,以及间隙锁与行锁结合的 next-key lock 锁。 #### 怎么理解死锁 @@ -69,7 +69,7 @@ MySQL 隔离级别 比如你有资源 1 和 2,以及线程 A 和 B,当线程 A 在已经获取到资源 1 的情况下,期望获取线程 B 持有的资源 2。与此同时,线程 B 在已经获取到资源 2 的情况下,期望获取现场 A 持有的资源 1。 -那么线程 A 和线程 B 就处理了相互等待的死锁状态,在没有外力干预的情况下,线程 A 和线程 B 就会一直处于相互等待的状态,从而不能处理其他的请求。 **死锁产生的四个必要条件** 。 +那么线程 A 和线程 B 就处理了相互等待的死锁状态,在没有外力干预的情况下,线程 A 和线程 B 就会一直处于相互等待的状态,从而不能处理其他的请求。**死锁产生的四个必要条件** 。 ![幻灯片6.PNG](assets/CgpVE2AXOpyAODwtAABTGjPzh6k531.PNG) @@ -87,7 +87,7 @@ MySQL 隔离级别 循环等待 **循环等待:** 发生死锁时,必然会存在一个线程,也就是资源的环形链。比如线程 A 已经获取了资源 1,但同时又请求获取资源 2。线程 B 已经获取了资源 2,但同时又请求获取资源 1,这就会形成一个线程和资源请求等待的环形图。 -死锁只有同时满足 **互斥** 、 **持有并等待** 、 **不可剥夺** 、 **循环等待** 时才会发生。并发场景下一旦死锁,一般没有特别好的方法,很多时候只能重启应用。 **因此,最好是规避死锁,那么具体怎么做呢?答案是:至少破坏其中一个条件** (互斥必须满足,你可以从其他三个条件出发)。 +死锁只有同时满足 **互斥** 、 **持有并等待** 、 **不可剥夺** 、 **循环等待** 时才会发生。并发场景下一旦死锁,一般没有特别好的方法,很多时候只能重启应用。**因此,最好是规避死锁,那么具体怎么做呢?答案是:至少破坏其中一个条件** (互斥必须满足,你可以从其他三个条件出发)。 - 持有并等待:我们可以一次性申请所有的资源,这样就不存在等待了。 - 不可剥夺:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可剥夺这个条件就破坏掉了。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25411\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25411\350\256\262.md" index 90f43e11c..f73fd5886 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25411\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25411\350\256\262.md" @@ -132,7 +132,7 @@ MySQL 做读写分离的前提,是把 MySQL 集群拆分成“主 + 从”结 #### 技术认知 -以上就是你在应聘初中级工程师时需要掌握的内容,如果你应聘的是高级研发工程师,在回答问题时, **还要尽可能地展示自己对 MySQL 数据复制的抽象能力** 。因为在网络分布式技术错综复杂的今天,如果你能将技术抽象成一个更高层次的理论体系,很容易在面试中脱颖而出。 +以上就是你在应聘初中级工程师时需要掌握的内容,如果你应聘的是高级研发工程师,在回答问题时,**还要尽可能地展示自己对 MySQL 数据复制的抽象能力** 。因为在网络分布式技术错综复杂的今天,如果你能将技术抽象成一个更高层次的理论体系,很容易在面试中脱颖而出。 ![2021-02-04](assets/Cgp9HWAbNDmAXznSAAFTEZJZjkc471.png) @@ -146,7 +146,7 @@ MySQL 做读写分离的前提,是把 MySQL 集群拆分成“主 + 从”结 > 如果客户端将要执行的命令发送给集群中的一台服务器,那么这台服务器就会以日志的方式记录这条命令,然后将命令发送给集群内其他的服务,并记录在其他服务器的日志文件中,注意,只要保证各个服务器上的日志是相同的,并且各服务器都能以相同的顺序执行相同的命令的话,那么集群中的每个节点的执行结果也都会是一样的。 -这种数据共识的机制就叫 **复制状态机** ,目的是通过日志复制和回放的方式来实现集群中所有节点内的状态一致性。 +这种数据共识的机制就叫 **复制状态机**,目的是通过日志复制和回放的方式来实现集群中所有节点内的状态一致性。 其实 MySQL 中的主从复制,通过 binlog 操作日志来实现从主库到从库的数据复制的,就是应用了这种复制状态机的机制。所以这种方式不是 MySQL 特有的。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25412\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25412\350\256\262.md" index eb15c1ed0..2a0cc8c8b 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25412\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25412\350\256\262.md" @@ -22,7 +22,7 @@ 我在面试候选人时发现,大部分研发工程师都能把分库分表策略熟练地回答出来,因为这个技术是常识,那你可能会问了:既然这个技术很普遍,大家都知道,面试官为什么还要问呢? -虽然分库分表技术方案很常见,但是在面试中回答好并不简单。因为面试官不会单纯浮于表面问你“分库分表的思路”,而是会站在业务场景中,当数据出现写多读少时, **考察你做分库分表的整体设计方案和技术实现的落地思路** 。一般会涉及这样几个问题: +虽然分库分表技术方案很常见,但是在面试中回答好并不简单。因为面试官不会单纯浮于表面问你“分库分表的思路”,而是会站在业务场景中,当数据出现写多读少时,**考察你做分库分表的整体设计方案和技术实现的落地思路** 。一般会涉及这样几个问题: - 什么场景该分库?什么场景该分表? - 复杂的业务如何选择分片策略? @@ -38,7 +38,7 @@ 当数据量过大造成事务执行缓慢时,就要考虑分表,因为减少每次查询数据总量是解决数据查询缓慢的主要原因。你可能会问:“查询可以通过主从分离或缓存来解决,为什么还要分表?”但这里的查询是指事务中的查询和更新操作。 -- **何时分库** 为了应对高并发,一个数据库实例撑不住,即单库的性能无法满足高并发的要求,就把并发请求分散到多个实例中去(这种应对高并发的思路我之前也说过)。 **总的来说,分库分表使用的场景不一样:** 分表是因为数据量比较大,导致事务执行缓慢;分库是因为单库的性能无法满足要求。 +- **何时分库** 为了应对高并发,一个数据库实例撑不住,即单库的性能无法满足高并发的要求,就把并发请求分散到多个实例中去(这种应对高并发的思路我之前也说过)。**总的来说,分库分表使用的场景不一样:** 分表是因为数据量比较大,导致事务执行缓慢;分库是因为单库的性能无法满足要求。 #### 如何选择分片策略? @@ -76,7 +76,7 @@ Range 分片 -但是同样的,由于不同“商品品类”的业务热点不同,对于商品数据存储也会存在热点数据问题,这个时候处理的手段有两个。 **1、垂直扩展** 由于 Range 分片是按照业务特性进行的分片策略,所以可以对热点数据做垂直扩展,即提升单机处理能力。在业务发展突飞猛进的初期,建议使用“增强单机硬件性能”的方式提升系统处理能力,因为此阶段,公司的战略往往是发展业务抢时间,“增强单机硬件性能”是最快的方法。 **2、分片元数据** +但是同样的,由于不同“商品品类”的业务热点不同,对于商品数据存储也会存在热点数据问题,这个时候处理的手段有两个。**1、垂直扩展** 由于 Range 分片是按照业务特性进行的分片策略,所以可以对热点数据做垂直扩展,即提升单机处理能力。在业务发展突飞猛进的初期,建议使用“增强单机硬件性能”的方式提升系统处理能力,因为此阶段,公司的战略往往是发展业务抢时间,“增强单机硬件性能”是最快的方法。**2、分片元数据** 单机性能总是有极限的,互联网分布式架构设计高并发终极解决方案还是水平扩展,所以结合业务的特性,就需要在 Range 的基础上引入“分片元数据”的概念:分片的规则记录在一张表里面,每次执行查询的时候,先去表里查一下要找的数据在哪个分片中。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25413\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25413\350\256\262.md" index fc273cf6c..db775c9cd 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25413\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25413\350\256\262.md" @@ -30,7 +30,7 @@ Redis 是单线程的,主要是指 Redis 的网络 I/O 线程,以及键值 最后,也是最重要的一点, Redis 采用了 I/O 多路复用机制(参考 07 讲,这里不再赘述)处理大量的客户端 Socket 请求,这让 Redis 可以高效地进行网络通信,因为基于非阻塞的 I/O 模型,就意味着 I/O 的读写流程不再阻塞。 -但是因为 Redis 不同版本的特殊性,所以对于 Redis 的线程模型要分版本来看。 **Redis 4.0 版本之前** ,使用单线程速度快的原因就是上述的几个原因; **Redis 4.0 版本之后** ,Redis 添加了多线程的支持,但这时的多线程主要体现在大数据的异步删除功能上,例如 unlink key、flushdb async、flushall async 等。 **Redis 6.0 版本之后** ,为了更好地提高 Redis 的性能,新增了多线程 I/O 的读写并发能力,但是在面试中,能把 Redis 6.0 中的多线程模型回答上来的人很少,如果你能在面试中补充 Redis 6.0 多线程的原理,势必会增加面试官对你的认可。 +但是因为 Redis 不同版本的特殊性,所以对于 Redis 的线程模型要分版本来看。**Redis 4.0 版本之前**,使用单线程速度快的原因就是上述的几个原因; **Redis 4.0 版本之后**,Redis 添加了多线程的支持,但这时的多线程主要体现在大数据的异步删除功能上,例如 unlink key、flushdb async、flushall async 等。**Redis 6.0 版本之后**,为了更好地提高 Redis 的性能,新增了多线程 I/O 的读写并发能力,但是在面试中,能把 Redis 6.0 中的多线程模型回答上来的人很少,如果你能在面试中补充 Redis 6.0 多线程的原理,势必会增加面试官对你的认可。 你可以在面试中这样补充: @@ -61,7 +61,7 @@ Redis 高性能和高可用的核心考点 - **RDB 快照(Redis DataBase)** :将某一个时刻的内存数据,以二进制的方式写入磁盘。 - **混合持久化方式** :Redis 4.0 新增了混合持久化的方式,集成了 RDB 和 AOF 的优点。 -接下来我们看一下这三种方式的实现原理。 **AOF 日志是如何实现的?** 通常情况下,关系型数据库(如 MySQL)的日志都是“写前日志”(Write Ahead Log, WAL),也就是说,在实际写数据之前,先把修改的数据记到日志文件中,以便当出现故障时进行恢复,比如 MySQL 的 redo log(重做日志),记录的就是修改后的数据。 +接下来我们看一下这三种方式的实现原理。**AOF 日志是如何实现的?** 通常情况下,关系型数据库(如 MySQL)的日志都是“写前日志”(Write Ahead Log, WAL),也就是说,在实际写数据之前,先把修改的数据记到日志文件中,以便当出现故障时进行恢复,比如 MySQL 的 redo log(重做日志),记录的就是修改后的数据。 而 AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的,不同的是,Redis 的 AOF 日志的记录顺序与传统关系型数据库正好相反,它是写后日志,“写后”是指 Redis 要先执行命令,把数据写入内存,然后再记录日志到文件。 @@ -69,7 +69,7 @@ Redis 高性能和高可用的核心考点 AOF 执行过程 -那么面试的考察点来了: **Reids 为什么先执行命令,在把数据写入日志呢** ?为了方便你理解,我整理了关键的记忆点: +那么面试的考察点来了: **Reids 为什么先执行命令,在把数据写入日志呢**?为了方便你理解,我整理了关键的记忆点: - 因为 ,Redis 在写入日志之前,不对命令进行语法检查; - 所以,只记录执行成功的命令,避免了出现记录错误命令的情况; @@ -80,7 +80,7 @@ AOF 执行过程 - **数据可能会丢失:** 如果 Redis 刚执行完命令,此时发生故障宕机,会导致这条命令存在丢失的风险。 - **可能阻塞其他操作:** 虽然 AOF 是写后日志,避免阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。 -又因为 Redis 的持久化离不开 AOF 和 RDB,所以我们就需要学习 RDB。 **那么 RDB 快照是如何实现的呢?** +又因为 Redis 的持久化离不开 AOF 和 RDB,所以我们就需要学习 RDB。**那么 RDB 快照是如何实现的呢?** 因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦日志非常多,势必会造成 Redis 的恢复操作缓慢。 @@ -99,7 +99,7 @@ AOF 执行过程 - **RDB 做快照的时候数据能修改吗?** 这个问题非常重要,考察候选人对 RDB 的技术掌握得够不够深。你可以思考一下,如果在执行快照的过程中,数据如果能被修改或者不能被修改都会带来什么影响? 1. 如果此时可以执行写操作:意味着 Redis 还能正常处理写操作,就可能出现正在执行快照的数据是已经被修改了的情况; -1. 如果此时不可以执行写操作:意味着 Redis 的所有写操作都得等到快照执行完成之后才能执行,那么就又出现了阻塞主线程的问题。 **那Redis 是如何解决这个问题的呢?** 它利用了 bgsave 的子进程,具体操作如下: +1. 如果此时不可以执行写操作:意味着 Redis 的所有写操作都得等到快照执行完成之后才能执行,那么就又出现了阻塞主线程的问题。**那Redis 是如何解决这个问题的呢?** 它利用了 bgsave 的子进程,具体操作如下: - 如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响; - 如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。 @@ -108,13 +108,13 @@ AOF 执行过程 Redis 是如何保证执行快照期间数据可修改 -要注意,Redis 对 RDB 的执行频率非常重要,因为这会影响快照数据的完整性以及 Redis 的稳定性,所以在 Redis 4.0 后, **增加了 AOF 和 RDB 混合的数据持久化机制:** 把数据以 RDB 的方式写入文件,再将后续的操作命令以 AOF 的格式存入文件,既保证了 Redis 重启速度,又降低数据丢失风险。 +要注意,Redis 对 RDB 的执行频率非常重要,因为这会影响快照数据的完整性以及 Redis 的稳定性,所以在 Redis 4.0 后,**增加了 AOF 和 RDB 混合的数据持久化机制:** 把数据以 RDB 的方式写入文件,再将后续的操作命令以 AOF 的格式存入文件,既保证了 Redis 重启速度,又降低数据丢失风险。 我们来总结一下,当面试官问你“Redis 是如何实现数据不丢失的”时,你首先要意识到这是在考察你对 Redis 数据持久化知识的掌握程度,那么你的回答思路是:先说明 Redis 有几种持久化的方式,然后分析 AOF 和 RDB 的原理以及存在的问题,最后分析一下 Redis 4.0 版本之后的持久化机制。 #### Redis 如何实现服务高可用? -另外,Redis 不仅仅可以用来当作缓存,很多时候也会直接作为数据存储,那么你就要一个高可用的 Redis 服务,来支撑和保证业务的正常运行。 **那么你怎么设计一个不宕机的 Redis 高可用服务呢?** +另外,Redis 不仅仅可以用来当作缓存,很多时候也会直接作为数据存储,那么你就要一个高可用的 Redis 服务,来支撑和保证业务的正常运行。**那么你怎么设计一个不宕机的 Redis 高可用服务呢?** 思考一下,解决数据高可用的手段是什么?是副本。那么要想设计一个高可用的 Redis 服务,一定要从 Redis 的多服务节点来考虑,比如 Redis 的主从复制、哨兵模式,以及 Redis 集群。这三点是你一定要在面试中回答出来的。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25414\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25414\350\256\262.md" index 742bef10e..5eb60f998 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25414\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25414\350\256\262.md" @@ -38,7 +38,7 @@ 假设在缓存失效的同时,出现多个客户端并发请求获取同一个 key 的情况,此时因为 key 已经过期了,所有请求在缓存数据库中查询 key 不命中,那么所有请求就会到数据库中去查询,然后当查询到数据之后,所有请求再重复将查询到的数据更新到缓存中。 -这里就会引发一个问题,所有请求更新的是同一条数据,这不仅会增加数据库的压力,还会因为反复更新缓存而占用缓存资源,这就叫缓存并发。 **那你怎么解决缓存并发呢?** ![image](assets/CioPOWAprzCAHp6VAABhPy4VZWw709.png) +这里就会引发一个问题,所有请求更新的是同一条数据,这不仅会增加数据库的压力,还会因为反复更新缓存而占用缓存资源,这就叫缓存并发。**那你怎么解决缓存并发呢?**![image](assets/CioPOWAprzCAHp6VAABhPy4VZWw709.png) 解决缓存并发 @@ -80,7 +80,7 @@ 1. 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。 1. 在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。 -前面的内容中,我们都是将缓存操作与业务代码耦合在一起,这样虽然在项目初期实现起来简单容易,但是随着项目的迭代,代码的可维护性会越来越差,并且也不符合架构的“高内聚,低耦合”的设计原则, **那么如何解决这个问题呢?** +前面的内容中,我们都是将缓存操作与业务代码耦合在一起,这样虽然在项目初期实现起来简单容易,但是随着项目的迭代,代码的可维护性会越来越差,并且也不符合架构的“高内聚,低耦合”的设计原则,**那么如何解决这个问题呢?** 回答的思路可以是这样:将缓存操作与业务代码解耦,实现方案上可以通过 MySQL Binlog + Canal + MQ 的方式。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25415\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25415\350\256\262.md" index 863d26f28..c02183cf6 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25415\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25415\350\256\262.md" @@ -46,13 +46,13 @@ SLA 的计算公式 给你 10 秒钟的时间思考一下,当面试官听到你按照时间指标度量系统可用性,会不会满意? -要知道,任何一家互联网公司,都有流量的低峰期和高峰期,你在低峰期停机 1 分钟和高峰期停机 1 分钟,对业务影响的结果完全不同。 **如果认识不到这一点,面试官很容易认为你很业余,并没有实践经验。** 所以,仅凭理论指标在有些情况下是不能满足实际需求的,那有没有更加科学的度量方式呢?答案就是基于一段时间(比如 1 年)的停机影响的请求量占比,进行评估,公式如下: +要知道,任何一家互联网公司,都有流量的低峰期和高峰期,你在低峰期停机 1 分钟和高峰期停机 1 分钟,对业务影响的结果完全不同。**如果认识不到这一点,面试官很容易认为你很业余,并没有实践经验。** 所以,仅凭理论指标在有些情况下是不能满足实际需求的,那有没有更加科学的度量方式呢?答案就是基于一段时间(比如 1 年)的停机影响的请求量占比,进行评估,公式如下: ![Drawing 2.png](assets/CioPOWAs3f-ARcGTAAAKDhhS0CU196.png) -这样一来,你就可以评估,业务在高峰期停机和在低峰期停机分别造成多少的损失了。 **所以,如果你再回答系统高可用指标的时候,我建议你可以遵循这样的套路:先摆明度量的两种方式,“N 个 9” 和 “影响请求量占比”,然后再结合实际业务场景表明第二种方式的科学性。** 总的来说,作为候选人,要立足业务价值去回答问题,不是仅停留于技术概念的堆砌,这才能体现你的思考。 +这样一来,你就可以评估,业务在高峰期停机和在低峰期停机分别造成多少的损失了。**所以,如果你再回答系统高可用指标的时候,我建议你可以遵循这样的套路:先摆明度量的两种方式,“N 个 9” 和 “影响请求量占比”,然后再结合实际业务场景表明第二种方式的科学性。** 总的来说,作为候选人,要立足业务价值去回答问题,不是仅停留于技术概念的堆砌,这才能体现你的思考。 -当然了,以上的内容基本可以满足你应聘初中级研发工程师的需求,如果你要面试高级研发工程师或者是架构师,你还要有一个思路的闭环。 **为了方便你的记忆,我把这个思路总结为:“可评估”“可监控”“可保证”。** +当然了,以上的内容基本可以满足你应聘初中级研发工程师的需求,如果你要面试高级研发工程师或者是架构师,你还要有一个思路的闭环。**为了方便你的记忆,我把这个思路总结为:“可评估”“可监控”“可保证”。** 所以,当你向面试官证明系统高可用时,其实是在回答这样几个问题: @@ -64,9 +64,9 @@ SLA 的计算公式 ### 案例解答 -我们以设计一个保证系统服务 SLA 等于 4 个 9 的监控报警体系为例。 **监控系统包括三个部分:基础设施监控报警、系统应用监控报警,以及存储服务监控报警。** 接下来,我就围绕这三个最核心的框架带你设计一个监控系统,并基于监控系统的设计,让你了解到系统哪些环节会影响系统整体的可用性,从而在面试中对系统高可用设计有更加清晰的掌握。 +我们以设计一个保证系统服务 SLA 等于 4 个 9 的监控报警体系为例。**监控系统包括三个部分:基础设施监控报警、系统应用监控报警,以及存储服务监控报警。** 接下来,我就围绕这三个最核心的框架带你设计一个监控系统,并基于监控系统的设计,让你了解到系统哪些环节会影响系统整体的可用性,从而在面试中对系统高可用设计有更加清晰的掌握。 -- **基础设施监控** 基础设施监控由三个部分组成:监控报警指标、监控工具以及报警策略。 **监控报警指标分为两种类型。** 1. 系统要素指标:主要有 CPU、内存,和磁盘。 +- **基础设施监控** 基础设施监控由三个部分组成:监控报警指标、监控工具以及报警策略。**监控报警指标分为两种类型。** 1. 系统要素指标:主要有 CPU、内存,和磁盘。 2. 网络要素指标:主要有带宽、网络 I/O、CDN、DNS、安全策略、和负载策略。 @@ -74,7 +74,7 @@ SLA 的计算公式 ![2.png](assets/Cgp9HWAuYWyATkxVAABau7vw5jQ035.png) -监控报警指标 **监控工具常用的有** ZABBIX(Alexei Vladishev 开源的监控系统,覆盖市场最多的老牌监控系统,资料很多)、Open-Falcon(小米开源的监控系统,小米、滴滴、美团等公司内部都在用)、Prometheus(SoundCloud 开源监控系统,对 K8S 的监控支持更好)。这些工具基本都能监控所有系统的 CPU、内存、磁盘、网络带宽、网络 I/O 等基础关键指标,再结合一些运营商提供的监控平台,就可以覆盖整个基础设施监控。 **监控报警策略一般由时间维度** 、 **报警级别** 、 **阈值设定三部分组成** 。 +监控报警指标 **监控工具常用的有** ZABBIX(Alexei Vladishev 开源的监控系统,覆盖市场最多的老牌监控系统,资料很多)、Open-Falcon(小米开源的监控系统,小米、滴滴、美团等公司内部都在用)、Prometheus(SoundCloud 开源监控系统,对 K8S 的监控支持更好)。这些工具基本都能监控所有系统的 CPU、内存、磁盘、网络带宽、网络 I/O 等基础关键指标,再结合一些运营商提供的监控平台,就可以覆盖整个基础设施监控。**监控报警策略一般由时间维度** 、 **报警级别** 、 **阈值设定三部分组成** 。 ![3.png](assets/CioPOWAuYX-APgb5AABCnrQ8zLc613.png) @@ -100,7 +100,7 @@ SLA 的计算公式 对于存储服务监的内容细节,我这里就不再一一介绍,在面试中,你只需要基于监控系统的三个核心组成部分(基础设施监控、系统应用监控、存储服务监控)来回答问题即可,比如,你可以回答:我为了确保系统的健康可靠,设计了一套监控体系,用于在生产环境对系统的可用性进行监控,具体的指标细节可以结合业务场景进行裁剪,比如你们是游戏领域,所以很关注流量和客户端连接数。 -总的来说, **让面试官认可你有一个全局的监控视角,比掌握很多监控指标更为重要。** +总的来说,**让面试官认可你有一个全局的监控视角,比掌握很多监控指标更为重要。** 当然,很多互联网公司都很重视系统服务稳定性的工作,因为服务的稳定性直接影响用户的体验和口碑,线上服务稳定性是研发工程师必须要重点关注的问题。所以当你回答完上述问题后,有的面试官为了考察候选人的责任意识,一般还会追问:“如果线上出现告警,你作为核心研发,该怎么做呢?” diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25416\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25416\350\256\262.md" index 56d04cc55..08ce34f3e 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25416\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25416\350\256\262.md" @@ -22,10 +22,10 @@ ### 案例分析 -这道面试题就涉及了高可用架构的设计,我们再来分析一下商品的调用链条。在电商平台的商品系统中,一次系统查询的流程经历了三次调用,从网关系统开始,然后依次调用商品系统、促销系统、积分系统的三个服务,如果此时积分系统的响应时间变长,那么整条请求的响应时间也会因此变长,整体服务甚至会发生宕机。 **这就是服务雪崩现象:即局部故障最终导致了全局故障。** 在分布式环境下,系统某一个服务或者组件响应缓慢,从而拖垮整个系统的情况随处可见。那你要怎么避免呢?这就涉及我们在 15 讲中的内容了。在 15 讲中我提到了,对于系统可用性,你要通过三个方面来解决:分别是“评估”“检测”和“保证”,具体如下。 +这道面试题就涉及了高可用架构的设计,我们再来分析一下商品的调用链条。在电商平台的商品系统中,一次系统查询的流程经历了三次调用,从网关系统开始,然后依次调用商品系统、促销系统、积分系统的三个服务,如果此时积分系统的响应时间变长,那么整条请求的响应时间也会因此变长,整体服务甚至会发生宕机。**这就是服务雪崩现象:即局部故障最终导致了全局故障。** 在分布式环境下,系统某一个服务或者组件响应缓慢,从而拖垮整个系统的情况随处可见。那你要怎么避免呢?这就涉及我们在 15 讲中的内容了。在 15 讲中我提到了,对于系统可用性,你要通过三个方面来解决:分别是“评估”“检测”和“保证”,具体如下。 1. 用科学的方法评估 **系统的可用性指标;** 2. 通过实时监控预警 **检测系统的可用性** ; -1. 通过系统架构设计 **保证系统的可用性。** 解决的思路是:在分布式系统中,当检测到某一个系统或服务响应时长出现异常时,要想办法停止调用该服务,让服务的调用快速返回失败,从而释放此次请求持有的资源。 **这就是架构设计中经常提到的降级和熔断机制。** +1. 通过系统架构设计 **保证系统的可用性。** 解决的思路是:在分布式系统中,当检测到某一个系统或服务响应时长出现异常时,要想办法停止调用该服务,让服务的调用快速返回失败,从而释放此次请求持有的资源。**这就是架构设计中经常提到的降级和熔断机制。** 对应到面试中,面试官一般会通过如下两个问题考察候选者: @@ -40,7 +40,7 @@ 形象一点儿说:熔断机制参考了电路中保险丝的保护原理,当电路出现短路、过载时,保险丝就会自动熔断,保证整体电路的安全。 -而在微服务架构中,服务的熔断机制是指:在服务 A 调用服务 B 时,如果 B 返回错误或超时的次数超过一定阈值,服务 A 的后续请求将不再调用服务 B。 **这种设计方式就是断路器模式。** 在这种模式下,服务调用方为每一个调用的服务维护一个有限状态机,在这个状态机中存在 **关闭** 、 **半打开** 和 **打开三种状态。** +而在微服务架构中,服务的熔断机制是指:在服务 A 调用服务 B 时,如果 B 返回错误或超时的次数超过一定阈值,服务 A 的后续请求将不再调用服务 B。**这种设计方式就是断路器模式。** 在这种模式下,服务调用方为每一个调用的服务维护一个有限状态机,在这个状态机中存在 **关闭** 、 **半打开** 和 **打开三种状态。** - 关闭:正常调用远程服务。 - 半打开:尝试调用远程服务。 @@ -125,7 +125,7 @@ if(breaker.isHalfOpen()) { #### 如何设计一个降级机制 -从架构设计的角度出发, **降级设计就是在做取舍,你要从服务降级** 和 **功能降级** 两方面来考虑。 +从架构设计的角度出发,**降级设计就是在做取舍,你要从服务降级** 和 **功能降级** 两方面来考虑。 在实现上,服务降级可以分为读操作降级和写操作降级。 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25417\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25417\350\256\262.md" index 931a5b8df..be4eac4b9 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25417\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25417\350\256\262.md" @@ -27,7 +27,7 @@ - **吞吐量** (系统处理请求的速率):反映单位时间内处理请求的能力(单位一般是TPS或QPS)。 - **延迟** (响应时间):从客户端发送请求到接收响应的时间(单位一般是ms、s)。 -一般来说, **延迟和吞吐量既互斥,又不绝对的互斥** ,你可以通过性能压测分别绘制吞吐量和延迟的曲线图: +一般来说,**延迟和吞吐量既互斥,又不绝对的互斥**,你可以通过性能压测分别绘制吞吐量和延迟的曲线图: ![image](assets/Cgp9HWA2_ZuAHdfgAAA1WOvYXBI512.png) @@ -35,7 +35,7 @@ 总体来看,随着压力增大,单位时间内系统被访问的次数增加。结合延迟和吞吐量观察的话,吞吐量曲线的最高点,往往是延迟曲线最低偏后的一个时间点,这意味着延迟已经开始增大一段时间了。那么 **对一些延迟要求比较高的系统来说,系统优化性能指标是要找到延迟趋向最低和吞吐量趋向最高的点** 。 -从图中你也可以看出,如果不做流量控制,在系统压力不断增大后,系统便什么也做不成。这也是一些不够健壮的系统,在压力较大的特殊业务场景下(比如一元秒杀、抢购、瞬时流量非常大的系统),直接崩溃,对所有用户拒绝服务的原因。 **除了吞吐量和延迟,TP(Top Percentile)也经常被提到。** 以 TP 99 为例,它是指请求中 99% 的请求能达到的性能,TP 是一个时间值,比如 TP 99 = 10ms,含义就是 99%的请求,在 10ms 之内可以得到响应。 +从图中你也可以看出,如果不做流量控制,在系统压力不断增大后,系统便什么也做不成。这也是一些不够健壮的系统,在压力较大的特殊业务场景下(比如一元秒杀、抢购、瞬时流量非常大的系统),直接崩溃,对所有用户拒绝服务的原因。**除了吞吐量和延迟,TP(Top Percentile)也经常被提到。** 以 TP 99 为例,它是指请求中 99% 的请求能达到的性能,TP 是一个时间值,比如 TP 99 = 10ms,含义就是 99%的请求,在 10ms 之内可以得到响应。 关于 TP 指标,你要掌握两个考点。 @@ -64,7 +64,7 @@ ``` -**步骤二:建立TCP连接** 由于 HTTP 是应用层协议,TCP 是传输层协议,所以 HTTP 是基于 TCP 协议基础上进行数据传输的。所以你要建立 TCP 请求连接,这里你也可以用 TCP的连接时间来衡量浏览器与 Web 服务器建立的请求连接时间。 **步骤三:服务器响应** +**步骤二:建立TCP连接** 由于 HTTP 是应用层协议,TCP 是传输层协议,所以 HTTP 是基于 TCP 协议基础上进行数据传输的。所以你要建立 TCP 请求连接,这里你也可以用 TCP的连接时间来衡量浏览器与 Web 服务器建立的请求连接时间。**步骤三:服务器响应** 这部分就是我们开篇讲到的最重要的性能指标了,即服务器端的延迟和吞吐能力。针对影响服务端性能的指标,还可以细分为基础设施性能指标、数据库性能指标,以及系统应用性能指标。 @@ -79,7 +79,7 @@ 由于浏览器自上而下显示 HTML,同时渲染顺序也是自上而下的,所以当用户在浏览器地址栏输入 URL 按回车,到他看到网页的第一个视觉标志为止,这段白屏时间可以作为一个性能的衡量指标(白屏时间越长,用户体验越差)。 -优化手段为减少首次文件的加载体积,比如用 gzip 算法压缩资源文件,调整用户界面的浏览行为(现在主流的Feed流也是一种减少白屏时间的方案)。 **步骤五:首屏时间** +优化手段为减少首次文件的加载体积,比如用 gzip 算法压缩资源文件,调整用户界面的浏览行为(现在主流的Feed流也是一种减少白屏时间的方案)。**步骤五:首屏时间** 用户端浏览界面的渲染,首屏时间也是一个重要的衡量指标,首屏时间是指:用户在浏览器地址输入 URL 按回车,然后看到当前窗口的区域显示完整页面的时间。一般情况下,一个页面总的白屏时间在 2 秒以内,用户会认为系统响应快,2 ~ 5 秒,用户会觉得响应慢,超过 5 秒很可能造成用户流失。 @@ -95,9 +95,9 @@ 也就是评审代码,代码包括应用程序源代码、环境参数配置、程序整个调用流程和处理逻辑。比如,用户在 App 中触发了“立即下单”按钮,服务端的应用程序从线程池里取得了线程来处理请求,然后查询了几次缓存和数据库,都读取和写入了什么数据,再把最终的响应返回给 App,响应的数据报文格式是什么,有哪些状态码和异常值…… -* **测试阶段,压测发现系统性能峰值** 一般来说,你要在系统上线前,对系统进行全方位的压力测试,绘制出系统吞吐量和延迟曲线,然后找到最佳性能点,并在超过最佳性能点时做限流,如果达不到最佳性能点(比如多数系统的吞吐量,随着压力增大,吞吐量上不去)就需要考虑出现延迟和吞吐量的这几种情况。 **1.定位延迟问题** 你要本着端到端的策略,大到整体流程,小到系统模块调用,逐一排查时间消耗在哪里。 +* **测试阶段,压测发现系统性能峰值** 一般来说,你要在系统上线前,对系统进行全方位的压力测试,绘制出系统吞吐量和延迟曲线,然后找到最佳性能点,并在超过最佳性能点时做限流,如果达不到最佳性能点(比如多数系统的吞吐量,随着压力增大,吞吐量上不去)就需要考虑出现延迟和吞吐量的这几种情况。**1.定位延迟问题** 你要本着端到端的策略,大到整体流程,小到系统模块调用,逐一排查时间消耗在哪里。 -你可以使用 kill -3 PID, jstack 等命令打印系统当前的线程执行的堆栈;还可以用一些性能分析工具,如 JProfiler 来监控系统的内存使用情况、垃圾回收、线程运行状况,比如你发现了运行的 100 个线程里面,有 80 个卡在某一个锁的释放上面,这时极有可能这把锁造成的延迟问题。 **2. 对于吞吐量问题的定位** +你可以使用 kill -3 PID, jstack 等命令打印系统当前的线程执行的堆栈;还可以用一些性能分析工具,如 JProfiler 来监控系统的内存使用情况、垃圾回收、线程运行状况,比如你发现了运行的 100 个线程里面,有 80 个卡在某一个锁的释放上面,这时极有可能这把锁造成的延迟问题。**2. 对于吞吐量问题的定位** 对于吞吐量指标要和 CPU使用率一起来看,在请求速率逐步增大时,经常会出现四种情况: diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25419\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25419\350\256\262.md" index 6305f8659..d8de90c29 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25419\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25419\350\256\262.md" @@ -4,7 +4,7 @@ 因为在工作中,不是每一个研发都有机会参与架构设计;很多公司也不会主动去培养你成为架构师。所以,有很多职场人在一家公司工作三年或五年之后并没有多大的提升。 -而很多的架构师都是研发自己在机遇巧合下,遇到大项目、参与其中、趟了坑、解决了问题,最终形成自己的知识体系和解决问题的能力之后才成长起来的。 **那么如果没有这些条件,你还有没有途径成为一名架构师呢?** 当然有,在我看来,你要先掌握架构师的知识体系,然后再通过实践进行检验,这样才能逐步成长为一名架构师。 +而很多的架构师都是研发自己在机遇巧合下,遇到大项目、参与其中、趟了坑、解决了问题,最终形成自己的知识体系和解决问题的能力之后才成长起来的。**那么如果没有这些条件,你还有没有途径成为一名架构师呢?** 当然有,在我看来,你要先掌握架构师的知识体系,然后再通过实践进行检验,这样才能逐步成长为一名架构师。 ![Drawing 0.png](assets/Cgp9HWA-_yKAS726AAFIB4WlQhs636.png) @@ -22,7 +22,7 @@ 1. **基础技术架构** :这部分是纯技术架构,所有非功能性的技术都是基础技术的范畴。 2. **业务架构** :在业务场景下对业务需求的抽象。 -3. **开发技能** :这是架构师落地架构的能力。 **你要怎么理解这个模型呢?** +3. **开发技能** :这是架构师落地架构的能力。**你要怎么理解这个模型呢?** 举个例子,我们在开发时会经历需求分析、架构设计、架构选型、架构落地几个阶段,这几个阶段对架构师的能力要求总结为一句话就是“架构师要把握系统技术”。 @@ -32,7 +32,7 @@ 你看,从需求分析、架构设计、到架构选型、再到架构落地,架构师都需要参与,而这些阶段体现出来的需求分析能力、架构设计能力、代码开发能力,最终都会作用在一个系统上,这就是所谓的“把握系统技术”。也就是说,你如果想成为架构师就要做到、做好系统开发各环节的技术把控! -那么在架构师能力模型的指引下, **你要掌握哪些知识体系呢?** +那么在架构师能力模型的指引下,**你要掌握哪些知识体系呢?** ### 架构师知识体系 diff --git "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25420\350\256\262.md" "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25420\350\256\262.md" index d906a4dd6..0bc1ad7c9 100644 --- "a/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25420\350\256\262.md" +++ "b/docs/Design/\346\236\266\346\236\204\350\256\276\350\256\241\351\235\242\350\257\225\347\262\276\350\256\262/\347\254\25420\350\256\262.md" @@ -4,7 +4,7 @@ 在更新课程的过程中,我看了很多同学的留言,有积极提问题的,比如 blossom、徐力辉同学等,也有分享自己方案的,比如 coder、Reiser同学。很开心,你们能在课程中有所收获。 -当然了,在这个过程中我也发现了一个很明显的问题,那就是:作为技术人员,很容易在学习的过程中,纠结于具体的形式(比如案例、代码)。 **在我看来,相比于形式,思维过程最为重要。** 比如我是怎么思考到某个点?有哪些合理或者不合理的地方?哪些能变成你自己的?哪些你只是看个热闹? +当然了,在这个过程中我也发现了一个很明显的问题,那就是:作为技术人员,很容易在学习的过程中,纠结于具体的形式(比如案例、代码)。**在我看来,相比于形式,思维过程最为重要。** 比如我是怎么思考到某个点?有哪些合理或者不合理的地方?哪些能变成你自己的?哪些你只是看个热闹? 你只有真正思考、转化、并积累,能力才能得到提升。 @@ -54,7 +54,7 @@ - **惊艳** 这一点其实是我对自己的要求,我会暗示自己,当作一件很有价值的事情时,评估的方式就是要惊艳到自己。 -因为只有惊艳到自己,你做的事情才会超出别人的预期,超出领导对你的期望。领导要的可能是 1,你要尽力做到 1.2,甚至 1.5,因为 1 谁都能做到,而超出的 0.2 / 0.5 就是你与其他人拉开的差距。 **事事有回应,件件有着落,回应和着落都要超出预期。** 千万不要当职场老油条,因为懒惰会变成习惯,最终会影响你的判断力。 +因为只有惊艳到自己,你做的事情才会超出别人的预期,超出领导对你的期望。领导要的可能是 1,你要尽力做到 1.2,甚至 1.5,因为 1 谁都能做到,而超出的 0.2 / 0.5 就是你与其他人拉开的差距。**事事有回应,件件有着落,回应和着落都要超出预期。** 千万不要当职场老油条,因为懒惰会变成习惯,最终会影响你的判断力。 - **认知** diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25400\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25400\350\256\262.md" index 08c8fba9e..520cf7fa0 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25400\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25400\350\256\262.md" @@ -1,6 +1,6 @@ # 003 领域驱动设计概览 -领域驱动设计(Domain Driven Design,DDD)是由 Eric Evans 最早提出的综合软件系统分析和设计的面向对象建模方法,如今已经发展成为了一种针对大型复杂系统的领域建模与分析方法。它完全改变了传统软件开发工程师针对数据库进行的建模方法,从而 **将要解决的业务概念和业务规则转换为软件系统中的类型以及类型的属性与行为,通过合理运用面向对象的封装、继承和多态等设计要素** ,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。 +领域驱动设计(Domain Driven Design,DDD)是由 Eric Evans 最早提出的综合软件系统分析和设计的面向对象建模方法,如今已经发展成为了一种针对大型复杂系统的领域建模与分析方法。它完全改变了传统软件开发工程师针对数据库进行的建模方法,从而 **将要解决的业务概念和业务规则转换为软件系统中的类型以及类型的属性与行为,通过合理运用面向对象的封装、继承和多态等设计要素**,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。 ### 领域驱动设计的开放性 @@ -24,7 +24,7 @@ 领域驱动设计的战略设计阶段是从下面两个方面来考量的: -- 问题域方面:针对问题域,引入 **限界上下文(Bounded Context)** 和 **上下文映射(Context Map)** 对问题域进行合理的分解,识别出 **核心领域(Core Domain)** 与 **子领域(SubDomain)** ,并确定领域的边界以及它们之间的关系,维持模型的完整性。 +- 问题域方面:针对问题域,引入 **限界上下文(Bounded Context)** 和 **上下文映射(Context Map)** 对问题域进行合理的分解,识别出 **核心领域(Core Domain)** 与 **子领域(SubDomain)**,并确定领域的边界以及它们之间的关系,维持模型的完整性。 - 架构方面:通过 **分层架构** 来隔离关注点,尤其是将领域实现独立出来,能够更利于领域模型的单一性与稳定性;引入 **六边形架构** 可以清晰地表达领域与技术基础设施的边界;CQRS 模式则分离了查询场景和命令场景,针对不同场景选择使用同步或异步操作,来提高架构的低延迟性与高并发能力。 Eric Evans 提出战略设计的初衷是要 **保持模型的完整性** 。限界上下文的边界可以保护上下文内部和其他上下文之间的领域概念互不冲突。然而,如果我们将领域驱动设计的战略设计模式引入到架构过程中,就会发现限界上下文不仅限于对领域模型的控制,而在于分离关注点之后,使得整个上下文可以成为独立部署的设计单元,这就是“微服务”的概念,上下文映射的诸多模式则对应了微服务之间的协作。因此在战略设计阶段,微服务扩展了领域驱动设计的内容,反过来领域驱动设计又能够保证良好的微服务设计。 @@ -50,7 +50,7 @@ Eric Evans 通过下图勾勒出了战术设计诸要素之间的关系: ![enter image description here](assets/41040a90-7854-11e8-9ada-255ab1257678) -领域驱动设计围绕着领域模型进行设计,通过 **分层架构(Layered Architecture)** 将领域独立出来。表示领域模型的对象包括: **实体** 、 **值对象** 和 **领域服务** , **领域逻辑都应该封装在这些对象中** 。这一严格的设计原则可以避免业务逻辑渗透到领域层之外,导致技术实现与业务逻辑的混淆。在领域驱动设计的演进中,又引入了 **领域事件** 来丰富领域模型。 **聚合** 是一种边界,它可以封装一到多个 **实体** 与 **值对象** ,并维持该边界范围之内的业务完整性。在聚合中,至少包含一个实体,且只有实体才能作为 **聚合根(Aggregate Root)** 。注意,在领域驱动设计中,没有任何一个类是单独的聚合,因为聚合代表的是边界概念,而非领域概念。在极端情况下,一个聚合可能有且只有一个实体。 **工厂** 和 **资源库** 都是对领域对象生命周期的管理。前者负责领域对象的创建,往往用于封装复杂或者可能变化的创建逻辑;后者则负责从存放资源的位置(数据库、内存或者其他 Web 资源)获取、添加、删除或者修改领域对象。领域模型中的资源库不应该暴露访问领域对象的技术实现细节。 +领域驱动设计围绕着领域模型进行设计,通过 **分层架构(Layered Architecture)** 将领域独立出来。表示领域模型的对象包括: **实体** 、 **值对象** 和 **领域服务**,**领域逻辑都应该封装在这些对象中** 。这一严格的设计原则可以避免业务逻辑渗透到领域层之外,导致技术实现与业务逻辑的混淆。在领域驱动设计的演进中,又引入了 **领域事件** 来丰富领域模型。**聚合** 是一种边界,它可以封装一到多个 **实体** 与 **值对象**,并维持该边界范围之内的业务完整性。在聚合中,至少包含一个实体,且只有实体才能作为 **聚合根(Aggregate Root)** 。注意,在领域驱动设计中,没有任何一个类是单独的聚合,因为聚合代表的是边界概念,而非领域概念。在极端情况下,一个聚合可能有且只有一个实体。**工厂** 和 **资源库** 都是对领域对象生命周期的管理。前者负责领域对象的创建,往往用于封装复杂或者可能变化的创建逻辑;后者则负责从存放资源的位置(数据库、内存或者其他 Web 资源)获取、添加、删除或者修改领域对象。领域模型中的资源库不应该暴露访问领域对象的技术实现细节。 #### 演进的领域驱动设计过程 diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25401\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25401\350\256\262.md" index 09265e9fa..816e0ebae 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25401\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25401\350\256\262.md" @@ -1,6 +1,6 @@ # 011 建立统一语言 -统一语言是提炼领域知识的产出物, **获得统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程。** 使用统一语言可以帮助我们将参与讨论的客户、领域专家与开发团队拉到同一个维度空间进行讨论,若没有达成这种一致性,那就是鸡同鸭讲,毫无沟通效率,相反还可能造成误解。因此, **在沟通需求时,团队中的每个人都应使用统一语言进行交流** 。 +统一语言是提炼领域知识的产出物,**获得统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程。** 使用统一语言可以帮助我们将参与讨论的客户、领域专家与开发团队拉到同一个维度空间进行讨论,若没有达成这种一致性,那就是鸡同鸭讲,毫无沟通效率,相反还可能造成误解。因此,**在沟通需求时,团队中的每个人都应使用统一语言进行交流** 。 一旦确定了统一语言,无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,清晰准确地定义领域知识。重要的是,当我们建立了符合整个团队皆认同的一套统一语言后,就可以在此基础上寻找正确的领域概念,为建立领域模型提供重要参考。 @@ -17,15 +17,15 @@ 某些领域术语是有行业规范的,例如财会领域就有标准的会计准则,对于账目、对账、成本、利润等概念都有标准的定义,在一定程度上避免了分歧。然而,标准并非绝对的,在某些行业甚至存在多种标准共存的现象。以民航业的运输统计指标为例,牵涉到与运量、运力以及周转量相关的术语,就存在 ICAO(International Civil Aviation Organization,国际民用航空组织)与IATA(International Air Transport Association,国际航空运输协会)两大体系,而中国民航局又有自己的中文解释,航空公司和各大机场亦有自己衍生的定义。 -例如,针对一次航空运输的运量,就要分为 **城市对** 与 **航段** 的运量统计。 **城市对运量** 统计的是出发城市到目的城市两点之间的旅客数量,机场将其称之为 **流向** 。ICAO 定义的领域术语为 **City-pair(OFOD)** ,而 IATA 则命名为 **O & D** 。 **航段运量** 又称为 **载客量** ,指某个特定航段上所承载的旅客总数量,ICAO将其定义为 **TFS(Traffic by flight stage)** ,而 IATA 则称为 **Segment Traffic** 。 +例如,针对一次航空运输的运量,就要分为 **城市对** 与 **航段** 的运量统计。**城市对运量** 统计的是出发城市到目的城市两点之间的旅客数量,机场将其称之为 **流向** 。ICAO 定义的领域术语为 **City-pair(OFOD)**,而 IATA 则命名为 **O & D** 。**航段运量** 又称为 **载客量**,指某个特定航段上所承载的旅客总数量,ICAO将其定义为 **TFS(Traffic by flight stage)**,而 IATA 则称为 **Segment Traffic** 。 -即使针对 **航段运量** 这个术语,我们还需要明确地定义这个运量究竟指的是 **载客量** ,还是包含了该航段上承载的全部旅客、货物与邮件数量;我们还需要明确 **城市对** 与 **航段** 之间的区别,它们在指标统计时,实则存在细微的差异,一不小心忽略,结果就可能谬以千里。以航班 CZ5724 为例,该航班从北京(目的港代码 PEK)出发,经停武汉(目的港代码 WUH)飞往广州(目的港代码 CAN)。假定从北京到武汉的旅客数为 105,从北京到广州的旅客数为 14,从武汉到广州的旅客数为 83,则统计该次航班的 **城市对运量** ,应该分为三个城市对分别统计,即统计 PEK-WUH、PEK-CAN、WUH-CAN。而 **航段运量** 的统计则仅仅分为两个航段 PEK-WUH 与 WUH-CAN,至于从北京到广州的 14 名旅客,这个数量值则被截分为了两段,分别计数,如下图所示: +即使针对 **航段运量** 这个术语,我们还需要明确地定义这个运量究竟指的是 **载客量**,还是包含了该航段上承载的全部旅客、货物与邮件数量;我们还需要明确 **城市对** 与 **航段** 之间的区别,它们在指标统计时,实则存在细微的差异,一不小心忽略,结果就可能谬以千里。以航班 CZ5724 为例,该航班从北京(目的港代码 PEK)出发,经停武汉(目的港代码 WUH)飞往广州(目的港代码 CAN)。假定从北京到武汉的旅客数为 105,从北京到广州的旅客数为 14,从武汉到广州的旅客数为 83,则统计该次航班的 **城市对运量**,应该分为三个城市对分别统计,即统计 PEK-WUH、PEK-CAN、WUH-CAN。而 **航段运量** 的统计则仅仅分为两个航段 PEK-WUH 与 WUH-CAN,至于从北京到广州的 14 名旅客,这个数量值则被截分为了两段,分别计数,如下图所示: ![enter image description here](assets/6f680680-7a79-11e8-ae51-d5fb97616c42) 显然,如果我们不明白城市对运量与航段运量的真正含义,就可能混淆这两种指标的统计计算规则。这种术语理解错误带来的缺陷往往难以发现,除非业务分析人员、开发人员与测试人员能就此知识达成一致的正确理解。 -在领域建模过程中,我们往往需要在文档中建立一个大家一致认可的术语表。术语表中需要包括整个团队精炼出来的术语概念,以及对该术语的清晰明白的解释。若有可能,可以为难以理解的术语提供具体的案例。该术语表是领域建模的关键,是模型的重要参考规范,能够真实地反应模型的领域意义。一旦发生变更,也需要及时地对其进行更新。 **在维护领域术语表时,一定需要给出对应的英文术语,否则可能直接影响到代码实现** 。在我们的一个产品开发中,根据需求识别出了“导入策略”的领域概念。由于这个术语非常容易理解,团队就此达成了一致,却没有明确给出英文名称,最后导致前端和后端在开发与“导入策略”有关的功能时,分别命名为 ImportingPolicy 与 ImportingStrategy,人为地制造了混乱。 +在领域建模过程中,我们往往需要在文档中建立一个大家一致认可的术语表。术语表中需要包括整个团队精炼出来的术语概念,以及对该术语的清晰明白的解释。若有可能,可以为难以理解的术语提供具体的案例。该术语表是领域建模的关键,是模型的重要参考规范,能够真实地反应模型的领域意义。一旦发生变更,也需要及时地对其进行更新。**在维护领域术语表时,一定需要给出对应的英文术语,否则可能直接影响到代码实现** 。在我们的一个产品开发中,根据需求识别出了“导入策略”的领域概念。由于这个术语非常容易理解,团队就此达成了一致,却没有明确给出英文名称,最后导致前端和后端在开发与“导入策略”有关的功能时,分别命名为 ImportingPolicy 与 ImportingStrategy,人为地制造了混乱。 即使术语的英语并不需要对外暴露给用户,我们仍然需要引起重视,就算不强调英文翻译的纯正,也必须保证概念的一致性,倘若认为英文表达不合理或者不标准,牵涉到对类、方法的重命名,则需要统一修改。在大数据分析领域中,针对“维度”与“指标”两个术语,我们在过去开发的产品中就曾不幸地衍生出了两套英文定义,分别为 Dimension 与 Metric,Category 与 Measure,这种混乱让整个团队的开发成员痛苦不堪,带来了沟通和交流的障碍。就我而言,我宁愿代码命名没有正确地表达领域概念,也不希望出现命名上的不一致性。倘若在建模之初就明确母语和英语的术语表达,就可以做到正本清源! @@ -52,7 +52,7 @@ * 每次对Sprint Backlog的分配都需要保存以便于查询 ``` -用户故事中的 **分配(assign)Sprint Backlog 给团队成员** 就是一种领域行为,这种行为是在特定上下文中由角色触发的动作,并由此产生的业务流程和操作结果。同时,这种领域行为还是一种 **契约** ,明确地表达了服务提供者与消费者之间的业务关系,即明确了领域行为的 **前置条件** 、执行 **主语** 和 **宾语** 以及行为的 **执行结果** ,这些描述丰富了该领域的统一语言,并直接影响了 API 的设计。例如,针对分配 Sprint Backlog 的行为,用户故事就明确了未关闭的 SprintBacklog 只能分配给一个团队成员,且不允许重复分配,这体现了分配行为的业务规则。验收标准中提出对分配的保存,实际上也帮助我们得到了一个领域概念 SprintBacklogAssignment,该行为的代码实现如下所示: +用户故事中的 **分配(assign)Sprint Backlog 给团队成员** 就是一种领域行为,这种行为是在特定上下文中由角色触发的动作,并由此产生的业务流程和操作结果。同时,这种领域行为还是一种 **契约**,明确地表达了服务提供者与消费者之间的业务关系,即明确了领域行为的 **前置条件** 、执行 **主语** 和 **宾语** 以及行为的 **执行结果**,这些描述丰富了该领域的统一语言,并直接影响了 API 的设计。例如,针对分配 Sprint Backlog 的行为,用户故事就明确了未关闭的 SprintBacklog 只能分配给一个团队成员,且不允许重复分配,这体现了分配行为的业务规则。验收标准中提出对分配的保存,实际上也帮助我们得到了一个领域概念 SprintBacklogAssignment,该行为的代码实现如下所示: ```java package practiceddd.projectmanager.scrumcontext.domain; @@ -89,7 +89,7 @@ public class SprintBacklog extends Entity { } ``` -基于“ **信息专家模式** ”,SprintBacklog 类的 assignTo() 方法只承担了它能够履行的职责。作为 SprintBacklog 对象自身,它 **知道** 自己的状态, **知道** 自己是否被分配过,分配给谁,也 **知道** 遵循不同的业务规则会导致产生不同的结果。但由于它不具备发送邮件的知识,针对邮件发送它就无能为力了,因此这里实现的 assignTo() 方法仅仅完成了部分领域行为,若要完成整个用户故事描述的业务场景,需要交给领域服务 AssignSprintBacklogService 来完成: +基于“ **信息专家模式** ”,SprintBacklog 类的 assignTo() 方法只承担了它能够履行的职责。作为 SprintBacklog 对象自身,它 **知道** 自己的状态,**知道** 自己是否被分配过,分配给谁,也 **知道** 遵循不同的业务规则会导致产生不同的结果。但由于它不具备发送邮件的知识,针对邮件发送它就无能为力了,因此这里实现的 assignTo() 方法仅仅完成了部分领域行为,若要完成整个用户故事描述的业务场景,需要交给领域服务 AssignSprintBacklogService 来完成: ```java package practiceddd.projectmanager.scrumcontext.domain; diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25402\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25402\350\256\262.md" index 73b8fc243..ee5d549b7 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25402\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25402\350\256\262.md" @@ -59,9 +59,9 @@ #### 分层的依据与原则 -我们之所以要以水平方式对整个系统进行分层,是我们下意识地确定了一个认知规则: **机器为本,用户至上** ,机器是运行系统的基础,而我们打造的系统却是为用户提供服务的。分层架构中的层次越往上,其抽象层次就越面向业务、面向用户;分层架构中的层次越往下,其抽象层次就变得越通用、面向设备。为什么经典分层架构为三层架构?正是源于这样的认知规则:其上,面向用户的体验与交互;居中,面向应用与业务逻辑;其下,面对各种外部资源与设备。在进行分层架构设计时,我们完全可以基于这个经典的三层架构,沿着水平方向进一步切分属于不同抽象层次的关注点。因此, **分层的第一个依据是基于关注点为不同的调用目的划分层次** 。以领域驱动设计的四层架构为例,之所以引入应用层(Application Layer),就是为了给调用者提供完整的业务用例。 +我们之所以要以水平方式对整个系统进行分层,是我们下意识地确定了一个认知规则: **机器为本,用户至上**,机器是运行系统的基础,而我们打造的系统却是为用户提供服务的。分层架构中的层次越往上,其抽象层次就越面向业务、面向用户;分层架构中的层次越往下,其抽象层次就变得越通用、面向设备。为什么经典分层架构为三层架构?正是源于这样的认知规则:其上,面向用户的体验与交互;居中,面向应用与业务逻辑;其下,面对各种外部资源与设备。在进行分层架构设计时,我们完全可以基于这个经典的三层架构,沿着水平方向进一步切分属于不同抽象层次的关注点。因此,**分层的第一个依据是基于关注点为不同的调用目的划分层次** 。以领域驱动设计的四层架构为例,之所以引入应用层(Application Layer),就是为了给调用者提供完整的业务用例。 -**分层的第二个依据是面对变化** 。分层时应针对不同的变化原因确定层次的边界,严禁层次之间互相干扰,或者至少把变化对各层带来的影响降到最低。例如,数据库结构的修改自然会影响到基础设施层的数据模型以及领域层的领域模型,但当我们仅需要修改基础设施层中数据库访问的实现逻辑时,就不应该影响到领域层了。 **层与层之间的关系应该是正交的** ,所谓“正交”,并非二者之间没有关系,而是垂直相交的两条直线,唯一相关的依赖点是这两条直线的相交点,即两层之间的协作点,正交的两条直线,无论哪条直线进行延伸,都不会对另一条直线产生任何影响(指直线的投影);如果非正交,即“斜交”,当一条直线延伸时,它总是会投影到另一条直线,这就意味着另一条直线会受到它变化的影响。 +**分层的第二个依据是面对变化** 。分层时应针对不同的变化原因确定层次的边界,严禁层次之间互相干扰,或者至少把变化对各层带来的影响降到最低。例如,数据库结构的修改自然会影响到基础设施层的数据模型以及领域层的领域模型,但当我们仅需要修改基础设施层中数据库访问的实现逻辑时,就不应该影响到领域层了。**层与层之间的关系应该是正交的**,所谓“正交”,并非二者之间没有关系,而是垂直相交的两条直线,唯一相关的依赖点是这两条直线的相交点,即两层之间的协作点,正交的两条直线,无论哪条直线进行延伸,都不会对另一条直线产生任何影响(指直线的投影);如果非正交,即“斜交”,当一条直线延伸时,它总是会投影到另一条直线,这就意味着另一条直线会受到它变化的影响。 在进行分层时,我们还应该 **保证同一层的组件处于同一个抽象层次** 。这是分层架构的设计原则,它借鉴了 Kent Beck 在 Smalltalk Best Practice Patterns 一书提出的“组合方法”模式,该模式要求一个方法中的所有操作处于相同的抽象层,这就是所谓的“单一抽象层次原则(SLAP)”,这一原则可以运用到分层架构中。例如,在一个基于元数据的多租户报表系统中,我们特别定义了一个引擎层(Engine Layer),这是一个隐喻,相当于为报表系统提供报表、实体与数据的驱动引擎。引擎层之下,是基础设施层,提供了多租户、数据库访问与元数据解析与管理等功能。在引擎层之上是一个控制层,通过该控制层的组件可以将引擎层的各个组件组合起来,分层架构的顶端是面向用户的用户展现层,如下图所示: @@ -71,7 +71,7 @@ 在我们固有的认识中,分层架构的依赖都是自顶向下传递的,这也符合大多数人对分层的认知模型。从抽象层次来看,层次越处于下端,就会变得越通用越公共,与具体的业务隔离得越远。出于重用的考虑,这些通用和公共的功能往往会被单独剥离出来形成平台或框架,在系统边界内的低层,除了面向高层提供足够的实现外,就都成了平台或框架的调用者。换言之,越是通用的层,越有可能与外部平台或框架形成强依赖。若依赖的传递方向仍然采用自顶向下,就会导致系统的业务对象也随之依赖于外部平台或框架。 -依赖倒置原则(Dependency Inversion Principle,DIP)提出了对这种自顶向下依赖的挑战,它要求“高层模块不应该依赖于低层模块,二者都应该依赖于抽象”,这个原则正本清源,给了我们严重警告——谁规定在分层架构中,依赖就一定要沿着自顶向下的方向传递?我们常常理解依赖,是因为被依赖方需要为依赖方(调用方)提供功能支撑,这是从功能重用的角度来考虑的。但我们不能忽略变化对系统产生的影响!与建造房屋一样,我们自然希望分层的模块“构建”在稳定的模块之上,谁更稳定?抽象更稳定。因此,依赖倒置原则隐含的本质是: **我们要依赖不变或稳定的元素(类、模块或层)** ,也就是该原则的第二句话: **抽象不应该依赖于细节,细节应该依赖于抽象** 。 +依赖倒置原则(Dependency Inversion Principle,DIP)提出了对这种自顶向下依赖的挑战,它要求“高层模块不应该依赖于低层模块,二者都应该依赖于抽象”,这个原则正本清源,给了我们严重警告——谁规定在分层架构中,依赖就一定要沿着自顶向下的方向传递?我们常常理解依赖,是因为被依赖方需要为依赖方(调用方)提供功能支撑,这是从功能重用的角度来考虑的。但我们不能忽略变化对系统产生的影响!与建造房屋一样,我们自然希望分层的模块“构建”在稳定的模块之上,谁更稳定?抽象更稳定。因此,依赖倒置原则隐含的本质是: **我们要依赖不变或稳定的元素(类、模块或层)**,也就是该原则的第二句话: **抽象不应该依赖于细节,细节应该依赖于抽象** 。 这一原则实际是“面向接口设计”原则的体现,即“针对接口编程,而不是针对实现编程”。高层模块对低层模块的实现是一无所知的,带来的好处是: diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25403\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25403\350\256\262.md" index 40da6770c..6c111751c 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25403\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25403\350\256\262.md" @@ -10,7 +10,7 @@ 通信边界、协作机制与上下文映射模式的选择息息相关。例如,通信边界采用进程内通信,就可能无需采用 **开放主机服务** 模式,甚至为了保证架构的简单性,也无需采用 **防腐层** 模式。如果采用命令和查询的协作机制,可能会采用 **客户方/供应方** 模式,如果采用事件的协作机制,则需要采用 **发布者/订阅者** 模式。 -在识别限界上下文协作关系的阶段,是否需要定义协作的接口呢?我认为是必要的。一方面接口的定义直接影响到协作模式,也属于架构中体现“组件关系”的设计内容;另一方面通过要求对协作接口的定义,可以强迫我们思考各种协作的业务场景,避免做出错误的上下文映射。如果在这个阶段还未做好框架的技术选型, **接口的设计就不应该与具体的框架技术绑定** ,而是给出体现业务价值的领域模型,换言之,就是定义好当前限界上下文的应用服务,因为应用服务恰好体现了用例的应用逻辑。 +在识别限界上下文协作关系的阶段,是否需要定义协作的接口呢?我认为是必要的。一方面接口的定义直接影响到协作模式,也属于架构中体现“组件关系”的设计内容;另一方面通过要求对协作接口的定义,可以强迫我们思考各种协作的业务场景,避免做出错误的上下文映射。如果在这个阶段还未做好框架的技术选型,**接口的设计就不应该与具体的框架技术绑定**,而是给出体现业务价值的领域模型,换言之,就是定义好当前限界上下文的应用服务,因为应用服务恰好体现了用例的应用逻辑。 ### 识别 EAS 的上下文映射 @@ -18,7 +18,7 @@ #### 根据用例识别协作关系 -为避免出现上下文映射的疏漏,我们应该根据业务场景来分析各种限界上下文协作的关系。这时,先启阶段领域场景分析获得的用例图就派上用场了。为了确保设计的严谨,我们 **应该“遍历”所有的主用例** ,理解用例的目标与流程,再结合我们已经识别出来的限界上下文判断它们之间的关系。 +为避免出现上下文映射的疏漏,我们应该根据业务场景来分析各种限界上下文协作的关系。这时,先启阶段领域场景分析获得的用例图就派上用场了。为了确保设计的严谨,我们 **应该“遍历”所有的主用例**,理解用例的目标与流程,再结合我们已经识别出来的限界上下文判断它们之间的关系。 由于用例图中的用例传递的信息量有限,我们在识别协作关系时,可以进一步确定详细的流程,绘制更为详细的用例图甚至活动图。用例的好处在于不会让你遗漏重要的业务场景,而用例图中的包含用例与扩展用例,往往是存在上下文协作的信号。当然,在识别上下文协作关系时,还需要注意其中的陷阱。正如在\[第 3-9 课:辨别限界上下文的协作关系(上)\]中提到的那样,要理解 **协作即依赖** 的本质,正确辨别这种依赖关系到底是领域行为或领域模型的依赖,还是数据导致的依赖,又或者与限界上下文的边界彻底无关。 @@ -26,15 +26,15 @@ ![enter image description here](assets/156bb3b0-d3a3-11e8-a9a3-fd92848cdb84) -主用例“创建需求订单”属于 **订单上下文** ,“指定客户需求承担者”属于 **客户上下文** ,“通知承担者”用例是“指定客户需求承担者”的扩展用例,但它实际上会通过 **OA 集成上下文** 发送消息通知。若满足于这样的表面现象,可得出上下文映射(图中使用了六边形图例来表达限界上下文,但并不说明该限界上下文一定为微服务): +主用例“创建需求订单”属于 **订单上下文**,“指定客户需求承担者”属于 **客户上下文**,“通知承担者”用例是“指定客户需求承担者”的扩展用例,但它实际上会通过 **OA 集成上下文** 发送消息通知。若满足于这样的表面现象,可得出上下文映射(图中使用了六边形图例来表达限界上下文,但并不说明该限界上下文一定为微服务): ![img](assets/24b48360-d3a3-11e8-83c4-93b72872a9ed) -然而事实上,在指定客户需求承担者时, **订单上下文** 并非该用例的真正发起者,而是市场人员通过用户界面获得客户信息,再将选择的客户 ID 传递给了订单, **订单上下文** 并不知道 **客户上下文** 。如此一来,消息通知的发送也将转为由订单上下文发起。于是,上下文映射变为: +然而事实上,在指定客户需求承担者时,**订单上下文** 并非该用例的真正发起者,而是市场人员通过用户界面获得客户信息,再将选择的客户 ID 传递给了订单,**订单上下文** 并不知道 **客户上下文** 。如此一来,消息通知的发送也将转为由订单上下文发起。于是,上下文映射变为: ![img](assets/39e7e970-d3a3-11e8-83c4-93b72872a9ed) -目前获得的上下文映射自然不会是最终方案。不同的用例代表不同的场景,产生的协作关系自然会有所不同。在“跟踪需求订单”用例中,需要在用户界面呈现需求订单状态,同时还将显示需求订单下所有客户需求的客户信息和承担者信息,这就需要分别求助于 **客户上下文** 和 **员工上下文** 。因此, **订单上下文** 的上下文映射就修改为: +目前获得的上下文映射自然不会是最终方案。不同的用例代表不同的场景,产生的协作关系自然会有所不同。在“跟踪需求订单”用例中,需要在用户界面呈现需求订单状态,同时还将显示需求订单下所有客户需求的客户信息和承担者信息,这就需要分别求助于 **客户上下文** 和 **员工上下文** 。因此,**订单上下文** 的上下文映射就修改为: ![img](assets/4ddae040-d3a3-11e8-b055-6fdf72668cfc) @@ -42,7 +42,7 @@ ![enter image description here](assets/5fd625c0-d3a3-11e8-abac-396c1f0bcec5) -除了需要在 **订单上下文** 中创建市场需求之外,还要通过 **文件共享上下文** 完成附件的上传。此外,操作订单时需要对用户进行身份认证。最终, **订单上下文** 的上下文映射就演变为: +除了需要在 **订单上下文** 中创建市场需求之外,还要通过 **文件共享上下文** 完成附件的上传。此外,操作订单时需要对用户进行身份认证。最终,**订单上下文** 的上下文映射就演变为: ![img](assets/71be3160-d3a3-11e8-b055-6fdf72668cfc) @@ -67,15 +67,15 @@ - 通知该员工已成为项目组的项目成员 - 将当前项目的信息追加到项目成员的项目经历中 -注意,列出员工清单的功能属于 **员工上下文** ,但该操作是通过用户界面发起对 **员工上下文** 的调用, **组织上下文** 并不需要获取员工清单,而是用户界面传递给它的。在员工加入到当前项目组后, **组织上下文** 需要通过 **OA 集成** 发送通知消息,还要通过 **员工上下文** 来追加项目经历功能。基于这样的流程,得到的上下文映射为: +注意,列出员工清单的功能属于 **员工上下文**,但该操作是通过用户界面发起对 **员工上下文** 的调用,**组织上下文** 并不需要获取员工清单,而是用户界面传递给它的。在员工加入到当前项目组后,**组织上下文** 需要通过 **OA 集成** 发送通知消息,还要通过 **员工上下文** 来追加项目经历功能。基于这样的流程,得到的上下文映射为: ![img](assets/b74bac80-d3a3-11e8-abac-396c1f0bcec5) -然而考虑 **认证上下文** ,它又需要调用 **组织上下文** 提供的服务来判断用户是否属于某个部门或团队,这就在二者之间产生了上下游关系。由于 **认证上下文** 比较特殊,如果系统没有采用 API 网关,则作为通用子领域的限界上下文,会被多个核心子领域的限界上下文调用,其中也包括 **员工上下文** 与 **项目上下文** ,于是上下文映射就变为: +然而考虑 **认证上下文**,它又需要调用 **组织上下文** 提供的服务来判断用户是否属于某个部门或团队,这就在二者之间产生了上下游关系。由于 **认证上下文** 比较特殊,如果系统没有采用 API 网关,则作为通用子领域的限界上下文,会被多个核心子领域的限界上下文调用,其中也包括 **员工上下文** 与 **项目上下文**,于是上下文映射就变为: ![img](assets/c59052a0-d3a3-11e8-abac-396c1f0bcec5) -为了更好地体现协作关系,我在上图增加了箭头,加粗了相关连线。可以清晰地看到,上图粗线部分形成了认证、组织与员工三个限界上下文之间的循环依赖,这是设计上的“坏味道”。导致这种循环依赖的原因,是因为与项目成员有关的用例被放到了 **组织上下文** 中,从而导致了它与 **员工上下文** 产生协作关系,这充分说明了之前识别的限界上下文仍有不足之处。组织结构是一种领域,管理的是部门、部门层次、角色等更为普适性的特性。换言之,即使不是在 EAS 系统,只要存在组织结构的需求,仍然需要该限界上下文。如此看来,项目成员的管理应属于更加特定的业务领域。在添加项目成员时,领域逻辑仍然属于 **项目上下文** ,但建立成员与项目组之间的关系,则应交给更为通用的 **组织上下文** ,形成二者的上下游关系。经过这样的更改后,“追加项目成员的项目经历”用例就由 **项目上下文** 向 **员工上下文** 直接发起调用请求: +为了更好地体现协作关系,我在上图增加了箭头,加粗了相关连线。可以清晰地看到,上图粗线部分形成了认证、组织与员工三个限界上下文之间的循环依赖,这是设计上的“坏味道”。导致这种循环依赖的原因,是因为与项目成员有关的用例被放到了 **组织上下文** 中,从而导致了它与 **员工上下文** 产生协作关系,这充分说明了之前识别的限界上下文仍有不足之处。组织结构是一种领域,管理的是部门、部门层次、角色等更为普适性的特性。换言之,即使不是在 EAS 系统,只要存在组织结构的需求,仍然需要该限界上下文。如此看来,项目成员的管理应属于更加特定的业务领域。在添加项目成员时,领域逻辑仍然属于 **项目上下文**,但建立成员与项目组之间的关系,则应交给更为通用的 **组织上下文**,形成二者的上下游关系。经过这样的更改后,“追加项目成员的项目经历”用例就由 **项目上下文** 向 **员工上下文** 直接发起调用请求: ![img](assets/4bb9bc90-d796-11e8-a846-1515ba7379c6) @@ -85,7 +85,7 @@ 要确定上下文协作模式,首先需要明确限界上下文的通信边界,即确定为进程内通信还是进程间通信。采用进程间通信的限界上下文就是一个微服务。在\[第 4-8 课:代码模型的架构决策\]中,我总结了微服务的优势与不足。EAS 系统作为一个企业的内部系统,对并发访问与低延迟的要求并不高,可用性固然是一个系统该有的特质,但毕竟它不是“生死攸关”的一线生产系统,短时间出现故障不会给企业带来致命的打击或难以估量的损失。整体来看,在质量属性方面,除了安全与可维护性之外,系统并无特别高的要求。综上所述,我看不到需要建立微服务架构的任何理由。既然无需创建微服务架构,就不必遵守一个限界上下文一个数据库的约束,满足架构的简单原则,可以为整个 EAS 系统创建一个集中的数据库。 -这一设计决策直接影响到 **决策分析上下文** 的实现方案。就目前的需求而言,我们似乎没有必要为实现该上下文的功能专门引入数据仓库。 **决策分析上下文** 具有如下特征: +这一设计决策直接影响到 **决策分析上下文** 的实现方案。就目前的需求而言,我们似乎没有必要为实现该上下文的功能专门引入数据仓库。**决策分析上下文** 具有如下特征: - 访问的数据涵盖所有的核心子领域 - 决策分析仅针对数据执行查询统计操作 @@ -111,7 +111,7 @@ 既然采用了进程内通信,且针对这样的企业系统,演变为微服务架构的可能性较低,为了架构的简单性,针对以上限界上下文之间的协作,并无必要引入间接的防腐层。至于它与外部的 OA 系统之间的协作,已经由 **OA 集成上下文** 提供了“防腐”功能。 -我们是否需要采用“遵奉者”模式实现限界上下文之间的模型重用呢?同样是设计的取舍,简单还是灵活,重用还是清晰,这是一个问题!限界上下文的边界控制力会在架构中产生无与伦比的价值,它可以有效地保证系统架构的清晰度。如果为了简单与重用而纵容对模型的“滥用”,可能会导致系统变得越来越糟糕。对于采用进程内通信的限界上下文,运用“遵奉者”模式重用领域模型,就会失去限界上下文存在的意义,使之与战术设计中的模块(Module)没有什么区别了。说好的限界上下文保证领域概念的一致性呢?例如, **合同上下文** 、 **项目上下文** 、 **订单上下文** 都需要通过 **员工上下文** 获得员工的联系信息,那么最好的方式不是直接重用 **员工上下文** 中的 Employee 模型对象,而是各自建立自己的模型对象 Employee 或 TeamMember,除了具有 EmployeeId 之外,可以只包含一个 Contact 属性: +我们是否需要采用“遵奉者”模式实现限界上下文之间的模型重用呢?同样是设计的取舍,简单还是灵活,重用还是清晰,这是一个问题!限界上下文的边界控制力会在架构中产生无与伦比的价值,它可以有效地保证系统架构的清晰度。如果为了简单与重用而纵容对模型的“滥用”,可能会导致系统变得越来越糟糕。对于采用进程内通信的限界上下文,运用“遵奉者”模式重用领域模型,就会失去限界上下文存在的意义,使之与战术设计中的模块(Module)没有什么区别了。说好的限界上下文保证领域概念的一致性呢?例如,**合同上下文** 、 **项目上下文** 、 **订单上下文** 都需要通过 **员工上下文** 获得员工的联系信息,那么最好的方式不是直接重用 **员工上下文** 中的 Employee 模型对象,而是各自建立自己的模型对象 Employee 或 TeamMember,除了具有 EmployeeId 之外,可以只包含一个 Contact 属性: ![enter image description here](assets/edd10a70-d3a3-11e8-abac-396c1f0bcec5) @@ -121,7 +121,7 @@ #### 定义协作接口 -定义协作接口的重要性在于 **保证开发不同限界上下文的特性团队能够并行开发** ,这相当于为团队规定了合作的契约。集成是痛苦的,无论团队成员能力有多么强,只要没有规定好彼此之间协作的接口,就有可能导致系统模块无法正确地集成,或者隐藏的缺陷无法及时发现,最严重的是破坏了限界上下文的边界。我们需要像保卫疆土一样去守护限界上下文的边界,如果不加以控制,任何风吹草动都可能酿成“边疆”的风云突变。 +定义协作接口的重要性在于 **保证开发不同限界上下文的特性团队能够并行开发**,这相当于为团队规定了合作的契约。集成是痛苦的,无论团队成员能力有多么强,只要没有规定好彼此之间协作的接口,就有可能导致系统模块无法正确地集成,或者隐藏的缺陷无法及时发现,最严重的是破坏了限界上下文的边界。我们需要像保卫疆土一样去守护限界上下文的边界,如果不加以控制,任何风吹草动都可能酿成“边疆”的风云突变。 注意,现在定义的是限界上下文之间协作的接口,并非限界上下文所有的服务接口,也不包括限界上下文对外部资源的访问接口。协作接口完全可以根据之前确定的上下文映射获得。在上下文映射图中,每个协作关系都意味着一个接口,不同的上下文映射模式可能会影响到对这些接口的设计。例如,如果下游限界上下文通过 **开放主机服务** 模式与上游协作,就需要定义 RESTful 或 RPC 接口;如果下游限界上下文直接调用上游,意味着需要定义应用服务接口;如果限界上下文之间采用 **发布者/订阅者** 模式,需要定义的接口其实是事件(Event)。 @@ -145,7 +145,7 @@ - **描述,** 对操作用户进行身份认证 - **命名空间,** paracticeddd.eas.authcontext.application - **方法,** authenticate(userId): AuthenticatedResult -- **模式** ,客户方/供应方模式 +- **模式**,客户方/供应方模式 - **接口类型,** 命令 协作接口定义的格式不是重要的,关键还是在战略设计阶段需要重视对它们的定义。只有这样才能更好地保证限界上下文的边界,因为除了这些协作接口,限界上下文之间是不允许直接协作的。协作接口的定义也是上下文映射的一种落地实践,要避免上下文映射在战略设计中沦为一幅幅中看不中用的设计图。同时,通过它还可以更好地遵循统一语言,保证设计模型与领域模型的一致性。 diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25404\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25404\350\256\262.md" index a56b55281..94b677b2d 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25404\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25404\350\256\262.md" @@ -106,7 +106,7 @@ CourseProvider.subscribe(subscription) { } ``` -倘若客户端发送的请求消息 **缺少** 了 manufacturer\\homePage 的值, **多余增加** 了一个 manufacturer\\address 值,又或者 manufacturer 的属性值并未按照指定的 **顺序** 发送,服务端在接收这样的响应消息时,同样应该正确地执行。当然,这种放松并非完全不做任何约束,如果协议规定 id、name、releaseDate 及 manufacturer\\name 是必须提供的值,服务实现时就需要验证这些值是否存在,如果不存在,应该返回 404 状态码,表示一个非法请求。 +倘若客户端发送的请求消息 **缺少** 了 manufacturer\\homePage 的值,**多余增加** 了一个 manufacturer\\address 值,又或者 manufacturer 的属性值并未按照指定的 **顺序** 发送,服务端在接收这样的响应消息时,同样应该正确地执行。当然,这种放松并非完全不做任何约束,如果协议规定 id、name、releaseDate 及 manufacturer\\name 是必须提供的值,服务实现时就需要验证这些值是否存在,如果不存在,应该返回 404 状态码,表示一个非法请求。 相反,服务契约的实现在处理完服务功能后,返回的响应消息却应该严格按照标准定义。例如服务资源契约就要求响应消息必须提供正确的 HTTP 状态码,如果涉及到状态迁移,也必须给出指向下一个资源的链接。如果是 JSON 格式的响应消息,也必须遵守当前契约版本规定的标准,提供正确的属性值内容。 diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25405\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25405\350\256\262.md" index c8f0bb440..1c3e04c37 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25405\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25405\350\256\262.md" @@ -18,7 +18,7 @@ > 对于你自己的工作,看看是否有和模式相近的,如果有,用模式试试看。即使你相信自己的解决方案更好,也要使用模式并找出你的方案为什么更适合的原因。我发现这样可以更好地理解问题。对于其他人的工作也同样如此。如果你找到一个相近的模式,把它当作一个起点来向你正在回顾的工作发问:它和模式相比强在哪里?模式是否包含该工作中没有的东西?如果有,重要吗? -当然,分析模式并非万能的灵药。即使已经为该领域建立了成熟的分析模式,也需要随着需求的变化不断地维护这个核心模式。注意, **模式并非模型** ,它的抽象层次要高于模型,故而具有一定通用性。正因为此,它无法真实传递完整的领域知识。分析模式是领域分析模式的参考,利用一些模式与建模原则,可以帮助我们进一步精炼领域分析模型,使得该模型能够变得稳定而又具有足够的扩展能力。 +当然,分析模式并非万能的灵药。即使已经为该领域建立了成熟的分析模式,也需要随着需求的变化不断地维护这个核心模式。注意,**模式并非模型**,它的抽象层次要高于模型,故而具有一定通用性。正因为此,它无法真实传递完整的领域知识。分析模式是领域分析模式的参考,利用一些模式与建模原则,可以帮助我们进一步精炼领域分析模型,使得该模型能够变得稳定而又具有足够的扩展能力。 接下来,我将尝试运用分析模式中提到的建模原则与建模实践针对电商网站的促销领域进行分析建模。通过分析促销领域的业务背景,逐步地对促销领域分析模型进行精炼。这个精炼的过程运用了如下建模原则: @@ -79,7 +79,7 @@ 或许市场人员在现实中就是这样来谈论促销规则,但领域分析模型并不一定就是现实世界模型的概念映射。在领域分析建模时,我们需要精确的概念。 -事实上,这一描述并非 **促销规则** ,而是一次完整的 **促销** !分析描述中的字词:“指定图书”属于促销活动中对适用商品(品种)的配置,“2018 年 12 月 12 日当天有效”是该促销的有效时段属性。唯有描述“满 100 元减 20 元,满 200 元减 40 元”,才是所谓的 **规则** 。该规则又包含了两条金额阈值的条件(Criterion)。描述中的促销活动与促销规则组成了促销产品,类别为“券(Coupon)”,券的类型为现金券(若描述中为满额折扣,就是折扣券)。诸多概念合起来,最终形成了一次促销。这个促销模型如下所示: +事实上,这一描述并非 **促销规则**,而是一次完整的 **促销**!分析描述中的字词:“指定图书”属于促销活动中对适用商品(品种)的配置,“2018 年 12 月 12 日当天有效”是该促销的有效时段属性。唯有描述“满 100 元减 20 元,满 200 元减 40 元”,才是所谓的 **规则** 。该规则又包含了两条金额阈值的条件(Criterion)。描述中的促销活动与促销规则组成了促销产品,类别为“券(Coupon)”,券的类型为现金券(若描述中为满额折扣,就是折扣券)。诸多概念合起来,最终形成了一次促销。这个促销模型如下所示: ![img](assets/3b0dfeb0-b125-11e9-8032-55077247240b) @@ -93,7 +93,7 @@ - 验证(Validation):需要根据确定的目标获得满足条件的合适对象 - 按需构造(Construction-to-order):需要描述对象应该做什么而无需解释对象执行的细节,这样就可以构造一个候选对象来满足需求 -**解决方案** 创建一个规格(Specification)对象,它能够辨别候选对象是否满足某些条件。规格对象定义了方法 isSatisfiedBy(anObject),如果 anObject 的所有条件均满足,则返回值 true。 **结果** +**解决方案** 创建一个规格(Specification)对象,它能够辨别候选对象是否满足某些条件。规格对象定义了方法 isSatisfiedBy(anObject),如果 anObject 的所有条件均满足,则返回值 true。**结果** - 解除需求设计、实现与验证之间的耦合 - 提供清晰的声明式的系统定义 @@ -153,7 +153,7 @@ Cosmos 作为一个医疗保健系统,需要对医药行业的测量和观察 一个促销并不会对应某一个具体的买家。促销面向商品和店铺,通过类别来说明它的使用范围。提供给买家的其实是促销产品。例如,一个买家获得了一张现金券或者礼品卡。为了避免买家无限次地享受促销福利,促销产品也需要标记其状态,包括未使用、已使用和过期状态。其中,已使用和过期状态都表现了该促销产品的无效状态,说明该促销产品对应的促销策略已经失效。为了区分促销活动与促销产品的状态,需要用限定修饰符说明,分别为“活动状态”和“产品状态”。 -**促销活动概念的引入对于促销而言具有重大意义** ,某种程度上,它根据变化频率的不同,将与促销相关的概念分成了完全独立的两部分。例如,一旦促销确定了优先级和类别,就不会轻易进行调整;而有效时段与促销状态则经常发生变化,如果作为促销的属性,就会受到时间和状态的限制,让促销无法被重用。促销活动与促销之间的分离,使得促销更加稳定,在保证重用的同时,还能避免促销被无限使用带来的潜在风险。 +**促销活动概念的引入对于促销而言具有重大意义**,某种程度上,它根据变化频率的不同,将与促销相关的概念分成了完全独立的两部分。例如,一旦促销确定了优先级和类别,就不会轻易进行调整;而有效时段与促销状态则经常发生变化,如果作为促销的属性,就会受到时间和状态的限制,让促销无法被重用。促销活动与促销之间的分离,使得促销更加稳定,在保证重用的同时,还能避免促销被无限使用带来的潜在风险。 通过引入分析模式的建模原则与模式,我们对最初的模型进行了精炼,最终获得了如下的领域分析模型: @@ -165,7 +165,7 @@ Cosmos 作为一个医疗保健系统,需要对医药行业的测量和观察 ![36166933.png](assets/e2fe1740-b125-11e9-8032-55077247240b) -上图给出了两种 **促销类别(Label)** :联合促销活动与玩具元旦特惠。以玩具元旦优惠类别为例, **促销(Promotion)** 为“跨店铺满减”。该促销的 **活动类型(Activity Type)** 包括适用店铺(值为“跨店铺”)、适用品种(值为“玩具”)。 **促销产品(Promotion Product)** 为优惠(Special Offer), **促销产品类型(Product Type)** 为满减(Reward), **规则(Rule)** 为金额阈值规则, **规格(Specification)** 为满99.00元减。 **促销活动(Promotion Activity)** 的 **有效时段(Valid Period)** 为 2019 年 1 月 1 日。图中的两种玩具都属于同一个促销类别,因此在计算满减时,这两个商品是可以叠加的。对应的分析模型为: +上图给出了两种 **促销类别(Label)** :联合促销活动与玩具元旦特惠。以玩具元旦优惠类别为例,**促销(Promotion)** 为“跨店铺满减”。该促销的 **活动类型(Activity Type)** 包括适用店铺(值为“跨店铺”)、适用品种(值为“玩具”)。**促销产品(Promotion Product)** 为优惠(Special Offer),**促销产品类型(Product Type)** 为满减(Reward),**规则(Rule)** 为金额阈值规则,**规格(Specification)** 为满99.00元减。**促销活动(Promotion Activity)** 的 **有效时段(Valid Period)** 为 2019 年 1 月 1 日。图中的两种玩具都属于同一个促销类别,因此在计算满减时,这两个商品是可以叠加的。对应的分析模型为: ![39618493.png](assets/f3704bc0-b125-11e9-8032-55077247240b) diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25406\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25406\350\256\262.md" index b3a132aec..696e90a1a 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25406\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25406\350\256\262.md" @@ -10,7 +10,7 @@ 在针对不同领域、不同限界上下文进行领域建模时,注意不要被看似相同的领域概念误导,以为概念相同就要遵循相同的设计。任何设计都不能脱离具体业务的上下文。例如钞票 Money,在多数领域中,我们都只需要关心它的面值与货币单位。如果都是人民币,面值都为 100,则此 100 元与彼 100 元并没有任何实质上的区别,可以认为其值相等,定义为值对象类型。然而,在印钞厂的生产领域,管理者关心的就不仅仅是每张钞票的面值和货币单位,而是要区分每张钞票的具体身份,即印在钞票上的唯一标识。此时,钞票 Money 就应该被定义为实体类型。 -总而言之, **是否拥有唯一的身份标识才是实体与值对象的根本区别** 。正是因为实体拥有身份标识,才能够让资源库更好地管理和控制它的生命周期;正是因为值对象没有身份标识,我们才不能直接管理值对象,使得它成为了实体的附庸,用以表达主体对象的属性。至于值对象的不变性,则主要是从优化、测试、并发访问等非业务因素去考量的,并非领域设计建模的领域需求。 +总而言之,**是否拥有唯一的身份标识才是实体与值对象的根本区别** 。正是因为实体拥有身份标识,才能够让资源库更好地管理和控制它的生命周期;正是因为值对象没有身份标识,我们才不能直接管理值对象,使得它成为了实体的附庸,用以表达主体对象的属性。至于值对象的不变性,则主要是从优化、测试、并发访问等非业务因素去考量的,并非领域设计建模的领域需求。 ### 不变性 @@ -53,7 +53,7 @@ public final class Money { ### 领域行为 -值对象与实体对象的领域行为并无本质区别。Eric Evans 之所以将其命名为”值对象(Value Object)”,是因为我们在理解领域概念时,关注的重点在于值。例如,我们在谈论温度时,关心的是多少度,以及单位是摄氏度还是华氏度;我们在谈论钞票时,关心的是面值,以及货币是人民币还是美元。 **但是,这并不意味着值对象不能拥有领域行为** 。不仅如此,我们还要依据“合理分配职责”的原则,力求将实体对象的领域行为按照关联程度的强弱分配给对应的值对象。这实际上也是面向对象“分治”思想的体现。 +值对象与实体对象的领域行为并无本质区别。Eric Evans 之所以将其命名为”值对象(Value Object)”,是因为我们在理解领域概念时,关注的重点在于值。例如,我们在谈论温度时,关心的是多少度,以及单位是摄氏度还是华氏度;我们在谈论钞票时,关心的是面值,以及货币是人民币还是美元。**但是,这并不意味着值对象不能拥有领域行为** 。不仅如此,我们还要依据“合理分配职责”的原则,力求将实体对象的领域行为按照关联程度的强弱分配给对应的值对象。这实际上也是面向对象“分治”思想的体现。 在讲解实体时,我提到需要“实体专注于身份”的设计原则。分配给实体的领域逻辑,应该是符合它身份的领域行为。身份是什么?就是实体作为主体对象具有自己的身份特征,属于实体的主要属性值。例如,一个酒店预订(Reservation)实体,它的身份与预订有关,就应该包含预订时间、预订周期、预订房型与客户等属性。现在有一个领域行为,需要检查预订周期是否满足预订规则,该规则为: @@ -82,7 +82,7 @@ public class Reservation { } ``` -checkReservationDuration() 方法专注于 Reservation 实体的身份了吗?显然没有,它操作的并非一次 **预订** ,而是一段 **预订周期** 。预订周期是一个高内聚的细粒度领域概念,因为它既离不开 checkInDate,也离不开 checkOutDate,只有两个属性都具备时,预订周期这个概念才完整。这是一个值对象。一旦封装为值对象类型,则检查预订周期的领域行为也应该“推”向它: +checkReservationDuration() 方法专注于 Reservation 实体的身份了吗?显然没有,它操作的并非一次 **预订**,而是一段 **预订周期** 。预订周期是一个高内聚的细粒度领域概念,因为它既离不开 checkInDate,也离不开 checkOutDate,只有两个属性都具备时,预订周期这个概念才完整。这是一个值对象。一旦封装为值对象类型,则检查预订周期的领域行为也应该“推”向它: ```java public class ReservationDuration { diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25407\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25407\350\256\262.md" index 0d7bceeaa..50ecc898a 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25407\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25407\350\256\262.md" @@ -172,4 +172,4 @@ Methodful Role 与 Methodless Role 的分离不会影响角色的定义,因为 即使不遵循 DCI 模式,我们也应尽量遵循“角色接口”的设计思想。角色、职责、协作本身就是场景驱动设计分配职责过程的三要素。区别在于二者对角色的定义不同。场景驱动设计的角色构造型属于设计角度的角色定义,它来自于职责驱动设计对角色的分类,也参考了领域驱动设计的设计模式。不同的角色构造型承担不同的职责,但并不包含任何业务含义。DCI 模式的角色是直接参与领域场景的对象,如 Martin Fowler 对角色接口的阐述,他认为是从供应者与消费者之间协作的角度来定义的接口,代表了业务场景中与其他类型协作的角色。 -在场景驱动设计过程中,当我们将职责分配给聚合时,可以借鉴 DCI 模式,从领域服务的角度去思考抽象的角色交互,引入的角色接口可以在重用性和扩展性方面改进领域设计模型。当然,这在一定程度上要考究面向对象的设计能力,没有足够的抽象与概括能力,可能难以识别出正确的角色。例如,在薪资管理系统的支付薪资场景中,该为计算薪资上下文引入什么样的角色呢?与转账上下文不同,计算薪资上下文并没有两个不同的角色参与交互,这时的角色就应该体现为 **数据类在上下文中的能力** ,故而可以获得 PayrollCalculable 角色。数据类 Employee 只有实现了该角色接口,才有“能力”被上下文计算薪资。 +在场景驱动设计过程中,当我们将职责分配给聚合时,可以借鉴 DCI 模式,从领域服务的角度去思考抽象的角色交互,引入的角色接口可以在重用性和扩展性方面改进领域设计模型。当然,这在一定程度上要考究面向对象的设计能力,没有足够的抽象与概括能力,可能难以识别出正确的角色。例如,在薪资管理系统的支付薪资场景中,该为计算薪资上下文引入什么样的角色呢?与转账上下文不同,计算薪资上下文并没有两个不同的角色参与交互,这时的角色就应该体现为 **数据类在上下文中的能力**,故而可以获得 PayrollCalculable 角色。数据类 Employee 只有实现了该角色接口,才有“能力”被上下文计算薪资。 diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25408\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25408\350\256\262.md" index 9cc3f06d1..a4106bb21 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25408\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25408\350\256\262.md" @@ -106,7 +106,7 @@ public class PaymentFailed extends ApplicationEvent { 既然限界上下文的协作方式发生了变化,意味着应用服务之间的调用方式也将随之改变。 -在买家下订单的业务场景中,考虑订单上下文与支付上下文之间的协作关系。如果采用 **开放主机模式** ,则订单上下文将作为下游发起对支付服务的调用。支付成功后,订单状态被修改为“已支付”,按照流程就需要发送邮件通知买家订单已创建成功,同时通知卖家发货。这时,订单上下文会作为下游发起对通知服务的调用。显然,在这个业务场景中,订单上下文成为了整个协作过程的“枢纽站”: +在买家下订单的业务场景中,考虑订单上下文与支付上下文之间的协作关系。如果采用 **开放主机模式**,则订单上下文将作为下游发起对支付服务的调用。支付成功后,订单状态被修改为“已支付”,按照流程就需要发送邮件通知买家订单已创建成功,同时通知卖家发货。这时,订单上下文会作为下游发起对通知服务的调用。显然,在这个业务场景中,订单上下文成为了整个协作过程的“枢纽站”: ![70835542.png](assets/f26b0090-f87d-11e9-85a1-8d79b502b71a) diff --git "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25409\350\256\262.md" "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25409\350\256\262.md" index f018fce8a..a2ee29c8c 100644 --- "a/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25409\350\256\262.md" +++ "b/docs/Design/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\345\256\236\350\267\265/\347\254\25409\350\256\262.md" @@ -12,9 +12,9 @@ #### 通信协议 -为了保障分布式通信的可靠性,在传输层需要采用 **TCP 协议** ,它能够可靠地把数据在不同的地址空间上搬运。在传输层之上的应用层,往往选择 HTTP 协议,如 REST 架构风格的框架,又或者采用二进制协议的 HTTP/2,如 Google 的 RPC 框架 gRPC。 +为了保障分布式通信的可靠性,在传输层需要采用 **TCP 协议**,它能够可靠地把数据在不同的地址空间上搬运。在传输层之上的应用层,往往选择 HTTP 协议,如 REST 架构风格的框架,又或者采用二进制协议的 HTTP/2,如 Google 的 RPC 框架 gRPC。 -可靠传输还要建立在网络传输的低延迟基础上,如果服务端如果无法在更短时间内处理完请求,又或者处理并发请求的能力较弱,就会导致服务器资源被阻塞,影响数据的传输。数据传输的能力取决于操作系统的 **I/O 模型** ,因为分布式节点之间的数据传输本质就是两个操作系统之间通过 Socket 实现的数据输入与输出。传统的 I/O 模式属于阻塞 I/O,它与线程池的线程模型相结合。由于一个系统内部可使用的线程数量是有限的,一旦线程池没有可用线程资源,当工作线程都阻塞在 I/O 上时,服务器响应客户端通信请求的能力就会下降,导致通信的阻塞。因此,分布式通信一般会采用 I/O 多路复用或异步 I/O,如 Netty 就采用了 I/O 多路复用的模型。 +可靠传输还要建立在网络传输的低延迟基础上,如果服务端如果无法在更短时间内处理完请求,又或者处理并发请求的能力较弱,就会导致服务器资源被阻塞,影响数据的传输。数据传输的能力取决于操作系统的 **I/O 模型**,因为分布式节点之间的数据传输本质就是两个操作系统之间通过 Socket 实现的数据输入与输出。传统的 I/O 模式属于阻塞 I/O,它与线程池的线程模型相结合。由于一个系统内部可使用的线程数量是有限的,一旦线程池没有可用线程资源,当工作线程都阻塞在 I/O 上时,服务器响应客户端通信请求的能力就会下降,导致通信的阻塞。因此,分布式通信一般会采用 I/O 多路复用或异步 I/O,如 Netty 就采用了 I/O 多路复用的模型。 #### 数据协议 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25400\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25400\350\256\262.md" index dfa5500ce..d7ec5ef68 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25400\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25400\350\256\262.md" @@ -28,7 +28,7 @@ ### 不要囿于公司现有的业务场景,你的能力,绝不止于此 -那你可能会说:“我在小公司工作,小公司的系统并发不高,流量也不大,学习高并发系统设计似乎有些多此一举。”但我想说的是, **公司业务流量平稳,并不表示不会遇到一些高并发的需求场景。** 就拿电商系统中的下单流程设计技术方案为例。在每秒只有一次调用的系统中,你只需要关注业务逻辑本身就好了:查询库存是否充足,如果充足,就可以到数据库中生成订单,成功后锁定库存,然后进入支付流程。 +那你可能会说:“我在小公司工作,小公司的系统并发不高,流量也不大,学习高并发系统设计似乎有些多此一举。”但我想说的是,**公司业务流量平稳,并不表示不会遇到一些高并发的需求场景。** 就拿电商系统中的下单流程设计技术方案为例。在每秒只有一次调用的系统中,你只需要关注业务逻辑本身就好了:查询库存是否充足,如果充足,就可以到数据库中生成订单,成功后锁定库存,然后进入支付流程。 ![img](assets/2d95823d39676e18a43ab3328ce0d0f3.jpg) @@ -36,9 +36,9 @@ 10000 次请求同时查询库存,是否会把库存系统拖垮?如果请求全部通过,那么就要同时生成 10000 次订单,数据库能否抗住?如果抗不住,我们要如何做?这些问题都可能出现,并让之前的方案不再适用,此时你就需要设计新的方案。 -除此之外,同样是缓存的使用,在低并发下你只需要了解基本的使用方式,但在高并发场景下你需要关注缓存命中率,如何应对缓存穿透,如何避免雪崩,如何解决缓存一致性等问题,这就增加了设计方案的复杂度,对设计者能力的要求也会更高。 **所以,为了避免遇到问题时手忙脚乱,你有必要提前储备足够多的高并发知识,从而具备随时应对可能出现的高并发需求场景的能力。** 我身边有很多在小公司打拼闯荡,小有建树的朋友,他们无一不经历过低谷期,又一一开拓了一片天地,究其原因,是因为他们没有将目光放在现有的业务场景中,而是保持着对于新技术的好奇心,时刻关注业界新技术的实现原理,思考如何使用技术来解决业务上的问题。 +除此之外,同样是缓存的使用,在低并发下你只需要了解基本的使用方式,但在高并发场景下你需要关注缓存命中率,如何应对缓存穿透,如何避免雪崩,如何解决缓存一致性等问题,这就增加了设计方案的复杂度,对设计者能力的要求也会更高。**所以,为了避免遇到问题时手忙脚乱,你有必要提前储备足够多的高并发知识,从而具备随时应对可能出现的高并发需求场景的能力。** 我身边有很多在小公司打拼闯荡,小有建树的朋友,他们无一不经历过低谷期,又一一开拓了一片天地,究其原因,是因为他们没有将目光放在现有的业务场景中,而是保持着对于新技术的好奇心,时刻关注业界新技术的实现原理,思考如何使用技术来解决业务上的问题。 -他们虽然性格很不同,但不甘于现状,突破自己的信念却是一致的。我相信,你也一定如此。 **所以完成业务需求,解决产品问题不应该是你最终的目标,提升技术能力和技术视野才应是你始终不变的追求。** +他们虽然性格很不同,但不甘于现状,突破自己的信念却是一致的。我相信,你也一定如此。**所以完成业务需求,解决产品问题不应该是你最终的目标,提升技术能力和技术视野才应是你始终不变的追求。** ### 计算机领域里虽然知识点庞杂,但很多核心思想都是相通的 @@ -50,7 +50,7 @@ 所以,高并发系统设计无论是对于初入职场的工程师了解基本系统设计思想,还是对于有一定工作经验的同学完善自身技能树,为未来可能遇见的系统问题做好技术储备,都有很大的帮助。 -也许你会担心知识点不成体系;担心只讲理论,没有实际的场景;担心只有空洞的介绍,没有干货。放心!我同样考虑了这些问题并在反复思考之后, **决定以一个虚拟的系统为主线,讲解在流量和并发不断提升的情况下如何一步步地优化它,** 并在这个过程中穿插着讲解知识点,这样通过场景、原理、实践相结合的方式,来帮助你更快、更深入地理解和消化。 +也许你会担心知识点不成体系;担心只讲理论,没有实际的场景;担心只有空洞的介绍,没有干货。放心!我同样考虑了这些问题并在反复思考之后,**决定以一个虚拟的系统为主线,讲解在流量和并发不断提升的情况下如何一步步地优化它,** 并在这个过程中穿插着讲解知识点,这样通过场景、原理、实践相结合的方式,来帮助你更快、更深入地理解和消化。 总体来说,学完这次课程,你会有三个收获: @@ -62,7 +62,7 @@ 我将课程划分了三个模块来讲解,分别是:基础篇、演进篇和实战篇。 -**基础篇** 主要是一些基本的高并发架构设计理念,你可以把它看作整个课程的一个总纲,建立对高并发系统的初步认识。 **演进篇** 是整个课程的核心,主要讲解系统支持高并发的方法。我会用一个虚拟的系统,带你分析当随着前端并发增加,这个系统的变化,以及你会遇到的一系列痛点问题。比如数据查询的性能瓶颈,缓存的高可用问题,然后从数据库、缓存、消息队列、分布式服务和维护这五个角度来展开,针对问题寻找解决方案, **让你置身其中,真真切切地走一遍系统演进的道路。** **实战篇** 将以两个实际案例,带你应用学到的知识应对高并发大流量的冲击。 +**基础篇** 主要是一些基本的高并发架构设计理念,你可以把它看作整个课程的一个总纲,建立对高并发系统的初步认识。**演进篇** 是整个课程的核心,主要讲解系统支持高并发的方法。我会用一个虚拟的系统,带你分析当随着前端并发增加,这个系统的变化,以及你会遇到的一系列痛点问题。比如数据查询的性能瓶颈,缓存的高可用问题,然后从数据库、缓存、消息队列、分布式服务和维护这五个角度来展开,针对问题寻找解决方案,**让你置身其中,真真切切地走一遍系统演进的道路。** **实战篇** 将以两个实际案例,带你应用学到的知识应对高并发大流量的冲击。 一个案例是 **如何设计承担每秒几十万次用户未读数请求的系统。** 之所以选择它,是因为在大部分的系统中未读数都会是请求量最大、并发最高的服务,在微博时 QPS 会达到每秒 50 万次。同时,未读数系统的业务逻辑比较简单,在你了解设计方案的时候也不需要预先对业务逻辑有深入了解; **另一个例子是信息流系统的设计,** 它是社区社交产品中的核心系统,业务逻辑复杂且请求量大,方案中几乎涉及高并发系统设计的全部内容。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25401\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25401\350\256\262.md" index 3367b1a71..e75f1a7c3 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25401\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25401\350\256\262.md" @@ -58,7 +58,7 @@ Web 2.0 是缓存的时代,这一点毋庸置疑。缓存遍布在系统设计 异步调用恰恰相反,调用方不需要等待方法逻辑执行完成就可以返回执行其他的逻辑,在被调用方法执行完毕后再通过回调、事件通知等方式将结果反馈给调用方。 -异步调用在大规模高并发系统中被大量使用, **比如我们熟知的 12306 网站。** 当我们订票时,页面会显示系统正在排队,这个提示就代表着系统在异步处理我们的订票请求。在 12306 系统中查询余票、下单和更改余票状态都是比较耗时的操作,可能涉及多个内部系统的互相调用,如果是同步调用就会像 12306 刚刚上线时那样,高峰期永远不可能下单成功。 +异步调用在大规模高并发系统中被大量使用,**比如我们熟知的 12306 网站。** 当我们订票时,页面会显示系统正在排队,这个提示就代表着系统在异步处理我们的订票请求。在 12306 系统中查询余票、下单和更改余票状态都是比较耗时的操作,可能涉及多个内部系统的互相调用,如果是同步调用就会像 12306 刚刚上线时那样,高峰期永远不可能下单成功。 而采用异步的方式,后端处理时会把请求丢到消息队列中,同时快速响应用户,告诉用户我们正在排队处理,然后释放出资源来处理更多的请求。订票请求处理完之后,再通知用户订票成功或者失败。 @@ -68,7 +68,7 @@ Web 2.0 是缓存的时代,这一点毋庸置疑。缓存遍布在系统设计 既然我们了解了这三种方法,那么是不是意味着在高并发系统设计中,开发一个系统时要把这些方法都用上呢?当然不是,系统的设计是不断演进的。 -**罗马不是一天建成的,系统的设计也是如此。** 不同量级的系统有不同的痛点,也就有不同的架构设计的侧重点。 **如果都按照百万、千万并发来设计系统,电商一律向淘宝看齐,IM 全都学习微信和 QQ,那么这些系统的命运一定是灭亡。** +**罗马不是一天建成的,系统的设计也是如此。** 不同量级的系统有不同的痛点,也就有不同的架构设计的侧重点。**如果都按照百万、千万并发来设计系统,电商一律向淘宝看齐,IM 全都学习微信和 QQ,那么这些系统的命运一定是灭亡。** 因为淘宝、微信的系统虽然能够解决同时百万、千万人同时在线的需求,但其内部的复杂程度也远非我们能够想象的。盲目地追从只能让我们的架构复杂不堪,最终难以维护。就拿从单体架构往服务化演进来说,淘宝也是在经历了多年的发展后,发现系统整体的扩展能力出现问题时,开始启动服务化改造项目的。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25402\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25402\350\256\262.md" index 6fbb62bdf..eff55a594 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25402\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25402\350\256\262.md" @@ -98,10 +98,10 @@ Linux 文件系统也是分层设计的,从下图你可以清晰地看出文 另外一个可能的缺陷是,如果我们把每个层次独立部署,层次间通过网络来交互,那么多层的架构在性能上会有损耗。这也是为什么服务化架构性能要比单体架构略差的原因,也就是所谓的“多一跳”问题。 -## 那我们是否要选择分层的架构呢? **答案当然是肯定的。** 你要知道,任何的方案架构都是有优势有缺陷的,天地尚且不全何况我们的架构呢?分层架构固然会增加系统复杂度,也可能会有性能的损耗,但是相比于它能带给我们的好处来说,这些都是可以接受的,或者可以通过其它的方案解决的。 **我们在做决策的时候切不可以偏概全,因噎废食。** 课程小结 +## 那我们是否要选择分层的架构呢?**答案当然是肯定的。** 你要知道,任何的方案架构都是有优势有缺陷的,天地尚且不全何况我们的架构呢?分层架构固然会增加系统复杂度,也可能会有性能的损耗,但是相比于它能带给我们的好处来说,这些都是可以接受的,或者可以通过其它的方案解决的。**我们在做决策的时候切不可以偏概全,因噎废食。** 课程小结 今天我带着你了解了分层架构的优势和不足,以及我们在实际工作中如何来对架构做分层。我想让你了解的是,分层架构是软件设计思想的外在体现,是一种实现方式。我们熟知的一些软件设计原则都在分层架构中有所体现。 -比方说, **单一职责原则** 规定每个类只有单一的功能,在这里可以引申为每一层拥有单一职责,且层与层之间边界清晰; **迪米特法则** 原意是一个对象应当对其它对象有尽可能少的了解,在分层架构的体现是数据的交互不能跨层,只能在相邻层之间进行;而 **开闭原则** 要求软件对扩展开放,对修改关闭。它的含义其实就是将抽象层和实现层分离,抽象层是对实现层共有特征的归纳总结,不可以修改,但是具体的实现是可以无限扩展,随意替换的。 +比方说,**单一职责原则** 规定每个类只有单一的功能,在这里可以引申为每一层拥有单一职责,且层与层之间边界清晰; **迪米特法则** 原意是一个对象应当对其它对象有尽可能少的了解,在分层架构的体现是数据的交互不能跨层,只能在相邻层之间进行;而 **开闭原则** 要求软件对扩展开放,对修改关闭。它的含义其实就是将抽象层和实现层分离,抽象层是对实现层共有特征的归纳总结,不可以修改,但是具体的实现是可以无限扩展,随意替换的。 掌握这些设计思想会自然而然地明白分层架构设计的妙处,同时也能帮助我们做出更好的设计方案。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25403\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25403\350\256\262.md" index 17915470a..bfcc1047b 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25403\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25403\350\256\262.md" @@ -14,7 +14,7 @@ 另一个耳熟能详的名词叫 **“可扩展性”,** 它同样是高并发系统设计需要考虑的因素。为什么呢?我来举一个具体的例子。 -流量分为平时流量和峰值流量两种,峰值流量可能会是平时流量的几倍甚至几十倍,在应对峰值流量的时候,我们通常需要在架构和方案上做更多的准备。 **这就是淘宝会花费大半年的时间准备双十一,也是在面对“明星离婚”等热点事件时,看起来无懈可击的微博系统还是会出现服务不可用的原因。** 而易于扩展的系统能在短时间内迅速完成扩容,更加平稳地承担峰值流量。 +流量分为平时流量和峰值流量两种,峰值流量可能会是平时流量的几倍甚至几十倍,在应对峰值流量的时候,我们通常需要在架构和方案上做更多的准备。**这就是淘宝会花费大半年的时间准备双十一,也是在面对“明星离婚”等热点事件时,看起来无懈可击的微博系统还是会出现服务不可用的原因。** 而易于扩展的系统能在短时间内迅速完成扩容,更加平稳地承担峰值流量。 高性能、高可用和可扩展,是我们在做高并发系统设计时追求的三个目标,我会用三节课的时间,带你了解在高并发大流量下如何设计高性能、高可用和易于扩展的系统。 @@ -36,7 +36,7 @@ ## 性能的度量指标 -性能优化的第三点原则中提到,对于性能我们需要有度量的标准,有了数据才能明确目前存在的性能问题,也能够用数据来评估性能优化的效果。 **所以明确性能的度量指标十分重要。** +性能优化的第三点原则中提到,对于性能我们需要有度量的标准,有了数据才能明确目前存在的性能问题,也能够用数据来评估性能优化的效果。**所以明确性能的度量指标十分重要。** 一般来说,度量性能的指标是系统接口的响应时间,但是单次的响应时间是没有意义的,你需要知道一段时间的性能情况是什么样的。所以,我们需要收集这段时间的响应时间数据,然后依据一些统计方法计算出特征值,这些特征值就能够代表这段时间的性能情况。我们常见的特征值有以下几类。 @@ -44,7 +44,7 @@ 顾名思义,平均值是把这段时间所有请求的响应时间数据相加,再除以总请求数。平均值可以在一定程度上反应这段时间的性能,但它敏感度比较差,如果这段时间有少量慢请求时,在平均值上并不能如实的反应。 -举个例子,假设我们在 30s 内有 10000 次请求,每次请求的响应时间都是 1ms,那么这段时间响应时间平均值也是 1ms。这时,当其中 100 次请求的响应时间变成了 100ms,那么整体的响应时间是 (100 *100 + 9900* 1) / 10000 = 1.99ms。你看,虽然从平均值上来看仅仅增加了不到 1ms,但是实际情况是有 1% 的请求(100/10000)的响应时间已经增加了 100 倍。 **所以,平均值对于度量性能来说只能作为一个参考。** +举个例子,假设我们在 30s 内有 10000 次请求,每次请求的响应时间都是 1ms,那么这段时间响应时间平均值也是 1ms。这时,当其中 100 次请求的响应时间变成了 100ms,那么整体的响应时间是 (100 *100 + 9900* 1) / 10000 = 1.99ms。你看,虽然从平均值上来看仅仅增加了不到 1ms,但是实际情况是有 1% 的请求(100/10000)的响应时间已经增加了 100 倍。**所以,平均值对于度量性能来说只能作为一个参考。** - 最大值 @@ -96,7 +96,7 @@ 所以我们在评估系统性能时通常需要做压力测试,目的就是找到系统的“拐点”,从而知道系统的承载能力,也便于找到系统的瓶颈,持续优化系统性能。 -说完了提升并行能力,我们再看看优化性能的另一种方式:减少单次任务响应时间。 **2. 减少单次任务响应时间** +说完了提升并行能力,我们再看看优化性能的另一种方式:减少单次任务响应时间。**2. 减少单次任务响应时间** 想要减少任务的响应时间,首先要看你的系统是 CPU 密集型还是 IO 密集型的,因为不同类型的系统性能优化方式不尽相同。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25404\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25404\350\256\262.md" index 58b48ecac..72959166d 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25404\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25404\350\256\262.md" @@ -78,7 +78,7 @@ 既然要做超时控制,那么我们怎么来确定超时时间呢?这是一个比较困难的问题。 -超时时间短了,会造成大量的超时错误,对用户体验产生影响;超时时间长了,又起不到作用。 **我建议你通过收集系统之间的调用日志,统计比如说 99% 的响应时间是怎样的,然后依据这个时间来指定超时时间。** 如果没有调用的日志,那么你只能按照经验值来指定超时时间。不过,无论你使用哪种方式,超时时间都不是一成不变的,需要在后面的系统维护过程中不断地修改。 +超时时间短了,会造成大量的超时错误,对用户体验产生影响;超时时间长了,又起不到作用。**我建议你通过收集系统之间的调用日志,统计比如说 99% 的响应时间是怎样的,然后依据这个时间来指定超时时间。** 如果没有调用的日志,那么你只能按照经验值来指定超时时间。不过,无论你使用哪种方式,超时时间都不是一成不变的,需要在后面的系统维护过程中不断地修改。 超时控制实际上就是不让请求一直保持,而是在经过一定时间之后让请求失败,释放资源给接下来的请求使用。这对于用户来说是有损的,但是却是必要的,因为它牺牲了少量的请求却保证了整体系统的可用性。而我们还有另外两种有损的方案能保证系统的高可用,它们就是降级和限流。 @@ -98,7 +98,7 @@ 你应该知道,在业务平稳运行过程中,系统是很少发生故障的,90% 的故障是发生在上线变更阶段的。比方说,你上了一个新的功能,由于设计方案的问题,数据库的慢请求数翻了一倍,导致系统请求被拖慢而产生故障。 -如果没有变更,数据库怎么会无缘无故地产生那么多的慢请求呢?因此,为了提升系统的可用性,重视变更管理尤为重要。而除了提供必要回滚方案,以便在出现问题时快速回滚恢复之外, **另一个主要的手段就是灰度发布。** 灰度发布指的是系统的变更不是一次性地推到线上的,而是按照一定比例逐步推进的。一般情况下,灰度发布是以机器维度进行的。比方说,我们先在 10% 的机器上进行变更,同时观察 Dashboard 上的系统性能指标以及错误日志。如果运行了一段时间之后系统指标比较平稳并且没有出现大量的错误日志,那么再推动全量变更。 +如果没有变更,数据库怎么会无缘无故地产生那么多的慢请求呢?因此,为了提升系统的可用性,重视变更管理尤为重要。而除了提供必要回滚方案,以便在出现问题时快速回滚恢复之外,**另一个主要的手段就是灰度发布。** 灰度发布指的是系统的变更不是一次性地推到线上的,而是按照一定比例逐步推进的。一般情况下,灰度发布是以机器维度进行的。比方说,我们先在 10% 的机器上进行变更,同时观察 Dashboard 上的系统性能指标以及错误日志。如果运行了一段时间之后系统指标比较平稳并且没有出现大量的错误日志,那么再推动全量变更。 灰度发布给了开发和运维同学绝佳的机会,让他们能在线上流量上观察变更带来的影响,是保证系统高可用的重要关卡。 @@ -108,7 +108,7 @@ 一个复杂的高并发系统依赖了太多的组件,比方说磁盘,数据库,网卡等,这些组件随时随地都可能会发生故障,而一旦它们发生故障,会不会如蝴蝶效应一般造成整体服务不可用呢?我们并不知道,因此,故障演练尤为重要。 -在我来看, **故障演练和时下比较流行的“混沌工程”的思路如出一辙,** 作为混沌工程的鼻祖,Netfix 在 2010 年推出的“Chaos Monkey”工具就是故障演练绝佳的工具。它通过在线上系统上随机地关闭线上节点来模拟故障,让工程师可以了解,在出现此类故障时会有什么样的影响。 +在我来看,**故障演练和时下比较流行的“混沌工程”的思路如出一辙,** 作为混沌工程的鼻祖,Netfix 在 2010 年推出的“Chaos Monkey”工具就是故障演练绝佳的工具。它通过在线上系统上随机地关闭线上节点来模拟故障,让工程师可以了解,在出现此类故障时会有什么样的影响。 当然,这一切是以你的系统可以抵御一些异常情况为前提的。如果你的系统还没有做到这一点,那么 **我建议你** 另外搭建一套和线上部署结构一模一样的线下系统,然后在这套系统上做故障演练,从而避免对生产系统造成影响。 @@ -125,4 +125,4 @@ **你还需要注意的是,** 提高系统的可用性有时候是以牺牲用户体验或者是牺牲系统性能为前提的,也需要大量人力来建设相应的系统,完善机制。所以我们要把握一个度,不该做过度的优化。就像我在文中提到的,核心系统四个九的可用性已经可以满足需求,就没有必要一味地追求五个九甚至六个九的可用性。 -另外,一般的系统或者组件都是追求极致的性能的,那么有没有不追求性能,只追求极致的可用性的呢?答案是有的。比如配置下发的系统,它只需要在其它系统启动时提供一份配置即可,所以秒级返回也可,十秒钟也 OK,无非就是增加了其它系统的启动速度而已。但是,它对可用性的要求是极高的,甚至会到六个九,原因是配置可以获取的慢,但是不能获取不到。 **我给你举这个例子是想让你了解,** 可用性和性能有时候是需要做取舍的,但如何取舍就要视不同的系统而定,不能一概而论了。 +另外,一般的系统或者组件都是追求极致的性能的,那么有没有不追求性能,只追求极致的可用性的呢?答案是有的。比如配置下发的系统,它只需要在其它系统启动时提供一份配置即可,所以秒级返回也可,十秒钟也 OK,无非就是增加了其它系统的启动速度而已。但是,它对可用性的要求是极高的,甚至会到六个九,原因是配置可以获取的慢,但是不能获取不到。**我给你举这个例子是想让你了解,** 可用性和性能有时候是需要做取舍的,但如何取舍就要视不同的系统而定,不能一概而论了。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25405\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25405\350\256\262.md" index f99582574..a88483690 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25405\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25405\350\256\262.md" @@ -20,13 +20,13 @@ 其实,无状态的服务和组件更易于扩展,而像 MySQL 这种存储服务是有状态的,就比较难以扩展。因为向存储集群中增加或者减少机器时,会涉及大量数据的迁移,而一般传统的关系型数据库都不支持。这就是为什么提升系统扩展性会很复杂的主要原因。 -除此之外,从例子中你可以看到,我们需要站在整体架构的角度,而不仅仅是业务服务器的角度来考虑系统的扩展性 。 **所以说,数据库、缓存、依赖的第三方、负载均衡、交换机带宽等等** 都是系统扩展时需要考虑的因素。我们要知道系统并发到了某一个量级之后,哪一个因素会成为我们的瓶颈点,从而针对性地进行扩展。 +除此之外,从例子中你可以看到,我们需要站在整体架构的角度,而不仅仅是业务服务器的角度来考虑系统的扩展性 。**所以说,数据库、缓存、依赖的第三方、负载均衡、交换机带宽等等** 都是系统扩展时需要考虑的因素。我们要知道系统并发到了某一个量级之后,哪一个因素会成为我们的瓶颈点,从而针对性地进行扩展。 针对这些复杂的扩展性问题,我提炼了一些系统设计思路,供你了解。 ## 高可扩展性的设计思路 -**拆分** 是提升系统扩展性最重要的一个思路,它会把庞杂的系统拆分成独立的,有单一职责的模块。相对于大系统来说,考虑一个一个小模块的扩展性当然会简单一些。 **将复杂的问题简单化,这就是我们的思路。** +**拆分** 是提升系统扩展性最重要的一个思路,它会把庞杂的系统拆分成独立的,有单一职责的模块。相对于大系统来说,考虑一个一个小模块的扩展性当然会简单一些。**将复杂的问题简单化,这就是我们的思路。** 但对于不同类型的模块,我们在拆分上遵循的原则是不一样的。我给你举一个简单的例子,假如你要设计一个社区,那么社区会有几个模块呢?可能有 5 个模块。 @@ -42,11 +42,11 @@ #### 1. 存储层的扩展性 -无论是存储的数据量,还是并发访问量,不同的业务模块之间的量级相差很大,比如说成熟社区中,关系的数据量是远远大于用户数据量的,但是用户数据的访问量却远比关系数据要大。所以假如存储目前的瓶颈点是容量,那么我们只需要针对关系模块的数据做拆分就好了,而不需要拆分用户模块的数据。 **所以存储拆分首先考虑的维度是业务维度。** 拆分之后,这个简单的社区系统就有了用户库、内容库、评论库、点赞库和关系库。这么做还能隔离故障,某一个库“挂了”不会影响到其它的数据库。 +无论是存储的数据量,还是并发访问量,不同的业务模块之间的量级相差很大,比如说成熟社区中,关系的数据量是远远大于用户数据量的,但是用户数据的访问量却远比关系数据要大。所以假如存储目前的瓶颈点是容量,那么我们只需要针对关系模块的数据做拆分就好了,而不需要拆分用户模块的数据。**所以存储拆分首先考虑的维度是业务维度。** 拆分之后,这个简单的社区系统就有了用户库、内容库、评论库、点赞库和关系库。这么做还能隔离故障,某一个库“挂了”不会影响到其它的数据库。 ![img](assets/5ee6e1350e2d4d5514a05032b10bd3b6.jpg) -按照业务拆分,在一定程度上提升了系统的扩展性,但系统运行时间长了之后,单一的业务数据库在容量和并发请求量上仍然会超过单机的限制。 **这时,我们就需要针对数据库做第二次拆分。** +按照业务拆分,在一定程度上提升了系统的扩展性,但系统运行时间长了之后,单一的业务数据库在容量和并发请求量上仍然会超过单机的限制。**这时,我们就需要针对数据库做第二次拆分。** 这次拆分是按照数据特征做水平的拆分,比如说我们可以给用户库增加两个节点,然后按照某些算法将用户的数据拆分到这三个库里面,具体的算法我会在后面讲述数据库分库分表时和你细说。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25407\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25407\350\256\262.md" index c93240a59..a914bf7ab 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25407\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25407\350\256\262.md" @@ -4,7 +4,7 @@ 那么从这一讲开始,我们正式进入演进篇,我会再从局部出发,带你逐一了解完成这些目标会使用到的一些方法,这些方法会针对性地解决高并发系统设计中出现的问题。比如,在 15 讲中我会提及布隆过滤器,这个组件就是为了解决存在大量缓存穿透的情况下,如何尽量提升缓存命中率的问题。 -当然,单纯地讲解理论,讲解方案会比较枯燥,所以我将用一个虚拟的系统作为贯穿整个课程的主线,说明当这个系统到达某一个阶段时,我们会遇到什么问题,然后要采用什么样的方案应对,应对的过程中又涉及哪些技术点。通过这样的讲述方式,力求以案例引出问题,能够让你了解遇到不同问题时,解决思路是怎样的, **当然,在这个过程中,我希望你能多加思考,然后将学到的知识活学活用到实际的项目中。** **接下来,让我们正式进入课程。** 来想象这样一个场景,一天,公司 CEO 把你叫到会议室,告诉你公司看到了一个新的商业机会,希望你能带领一名兄弟,迅速研发出一套面向某个垂直领域的电商系统。 +当然,单纯地讲解理论,讲解方案会比较枯燥,所以我将用一个虚拟的系统作为贯穿整个课程的主线,说明当这个系统到达某一个阶段时,我们会遇到什么问题,然后要采用什么样的方案应对,应对的过程中又涉及哪些技术点。通过这样的讲述方式,力求以案例引出问题,能够让你了解遇到不同问题时,解决思路是怎样的,**当然,在这个过程中,我希望你能多加思考,然后将学到的知识活学活用到实际的项目中。** **接下来,让我们正式进入课程。** 来想象这样一个场景,一天,公司 CEO 把你叫到会议室,告诉你公司看到了一个新的商业机会,希望你能带领一名兄弟,迅速研发出一套面向某个垂直领域的电商系统。 在人手紧张,时间不足的情况下,为了能够完成任务,你毫不犹豫地采用了最简单的架构:前端一台 Web 服务器运行业务代码,后端一台数据库服务器存储业务数据。 @@ -14,7 +14,7 @@ 再说回我们的垂直电商系统,系统一开始上线之后,虽然用户量不大,但运行平稳,你很有成就感,不过 CEO 觉得用户量太少了,所以紧急调动运营同学做了一次全网的流量推广。 -这一推广很快带来了一大波流量, **但这时,系统的访问速度开始变慢。** 分析程序的日志之后,你发现系统慢的原因出现在和数据库的交互上。因为你们数据库的调用方式是先获取数据库的连接,然后依靠这条连接从数据库中查询数据,最后关闭连接释放数据库资源。这种调用方式下,每次执行 SQL 都需要重新建立连接,所以你怀疑,是不是频繁地建立数据库连接耗费时间长导致了访问慢的问题。 **那么为什么频繁创建连接会造成响应时间慢呢?来看一个实际的测试。** +这一推广很快带来了一大波流量,**但这时,系统的访问速度开始变慢。** 分析程序的日志之后,你发现系统慢的原因出现在和数据库的交互上。因为你们数据库的调用方式是先获取数据库的连接,然后依靠这条连接从数据库中查询数据,最后关闭连接释放数据库资源。这种调用方式下,每次执行 SQL 都需要重新建立连接,所以你怀疑,是不是频繁地建立数据库连接耗费时间长导致了访问慢的问题。**那么为什么频繁创建连接会造成响应时间慢呢?来看一个实际的测试。** 我用"tcpdump -i bond0 -nn -tttt port 4490"命令抓取了线上 MySQL 建立连接的网络包来做分析,从抓包结果来看,整个 MySQL 的连接过程可以分为两部分: @@ -32,7 +32,7 @@ 虽然短时间解决了问题,不过你还是想彻底搞明白解决问题的核心原理,于是又开始补课。 -其实,在开发过程中我们会用到很多的连接池,像是数据库连接池、HTTP 连接池、Redis 连接池等等。而连接池的管理是连接池设计的核心, **我就以数据库连接池为例,来说明一下连接池管理的关键点。** +其实,在开发过程中我们会用到很多的连接池,像是数据库连接池、HTTP 连接池、Redis 连接池等等。而连接池的管理是连接池设计的核心,**我就以数据库连接池为例,来说明一下连接池管理的关键点。** 数据库连接池有两个最重要的配置: **最小连接数和最大连接数,** 它们控制着从连接池中获取连接的流程: @@ -62,7 +62,7 @@ 那么,作为按摩椅店老板,你怎么保证你启动着的按摩椅一定是可用的呢? -\\1. 启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”的命令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用, **也是我比较推荐的方式。** \\2. 在获取到连接之后,先校验连接是否可用,如果可用才会执行 SQL 语句。比如 DBCP 连接池的 testOnBorrow 配置项,就是控制是否开启这个验证。这种方式在获取连接时会引入多余的开销, **在线上系统中还是尽量不要开启,在测试服务上可以使用。** +\\1. 启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”的命令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用,**也是我比较推荐的方式。** \\2. 在获取到连接之后,先校验连接是否可用,如果可用才会执行 SQL 语句。比如 DBCP 连接池的 testOnBorrow 配置项,就是控制是否开启这个验证。这种方式在获取连接时会引入多余的开销,**在线上系统中还是尽量不要开启,在测试服务上可以使用。** 至此,你彻底搞清楚了连接池的工作原理。可是,当你刚想松一口气的时候,CEO 又提出了一个新的需求。你分析了一下这个需求,发现在一个非常重要的接口中,你需要访问 3 次数据库。根据经验判断,你觉得这里未来肯定会成为系统瓶颈。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25408\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25408\350\256\262.md" index 4f9a47dc5..d10d9ddad 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25408\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25408\350\256\262.md" @@ -40,7 +40,7 @@ MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所 ![img](assets/575ef1a6dc6463e4c5a60a3752d8554d.jpg) -你会发现,基于性能的考虑,主库的写入流程并没有等待主从同步完成就会返回结果,那么在极端的情况下,比如说主库上 binlog 还没有来得及刷新到磁盘上就出现了磁盘损坏或者机器掉电,就会导致 binlog 的丢失,最终造成主从数据的不一致。 **不过,这种情况出现的概率很低,对于互联网的项目来说是可以容忍的。** +你会发现,基于性能的考虑,主库的写入流程并没有等待主从同步完成就会返回结果,那么在极端的情况下,比如说主库上 binlog 还没有来得及刷新到磁盘上就出现了磁盘损坏或者机器掉电,就会导致 binlog 的丢失,最终造成主从数据的不一致。**不过,这种情况出现的概率很低,对于互联网的项目来说是可以容忍的。** 做了主从复制之后,我们就可以在写入时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响到读请求的执行。同时呢,在读流量比较大的情况下,我们可以部署多个从库共同承担读流量,这就是所说的“一主多从”部署方式,在你的垂直电商项目中就可以通过这种方式来抵御较高的并发读流量。另外,从库也可以当成一个备库来使用,以避免主库故障导致数据丢失。 @@ -72,7 +72,7 @@ MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所 #### 2. 如何访问数据库 -我们已经使用主从复制的技术将数据复制到了多个节点,也实现了数据库读写的分离,这时,对于数据库的使用方式发生了变化。以前只需要使用一个数据库地址就好了,现在需要使用一个主库地址和多个从库地址,并且需要区分写入操作和查询操作,如果结合下一节课中要讲解的内容“分库分表”,复杂度会提升更多。 **为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。** 第一类以淘宝的 TDDL( Taobao Distributed Data Layer)为代表,以代码形式内嵌运行在应用程序内部。你可以把它看成是一种数据源的代理,它的配置管理着多个数据源,每个数据源对应一个数据库,可能是主库,可能是从库。当有一个数据库请求时,中间件将 SQL 语句发给某一个指定的数据源来处理,然后将处理结果返回。 +我们已经使用主从复制的技术将数据复制到了多个节点,也实现了数据库读写的分离,这时,对于数据库的使用方式发生了变化。以前只需要使用一个数据库地址就好了,现在需要使用一个主库地址和多个从库地址,并且需要区分写入操作和查询操作,如果结合下一节课中要讲解的内容“分库分表”,复杂度会提升更多。**为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。** 第一类以淘宝的 TDDL( Taobao Distributed Data Layer)为代表,以代码形式内嵌运行在应用程序内部。你可以把它看成是一种数据源的代理,它的配置管理着多个数据源,每个数据源对应一个数据库,可能是主库,可能是从库。当有一个数据库请求时,中间件将 SQL 语句发给某一个指定的数据源来处理,然后将处理结果返回。 这一类中间件的优点是简单易用,没有多余的部署成本,因为它是植入到应用程序内部,与应用程序一同运行的,所以比较适合运维能力较弱的小团队使用;缺点是缺乏多语言的支持,目前业界这一类的主流方案除了 TDDL,还有早期的网易 DDB,它们都是 Java 语言开发的,无法支持其他的语言。另外,版本升级也依赖使用方更新,比较困难。 @@ -84,7 +84,7 @@ MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所 ![img](assets/e7e9430cbcb104764529ca5e01e6b3ff.jpg) -这些中间件,对你而言,可能并不陌生,但是我想让你注意到是, **在使用任何中间件的时候一定要保证对于中间件有足够深入的了解,否则一旦出了问题没法快速地解决就悲剧了。** **我之前的一个项目中,** 一直使用自研的一个组件来实现分库分表,后来发现这套组件有一定几率会产生对数据库多余的连接,于是团队讨论后决定替换成 Sharding-JDBC。原本以为是一次简单的组件切换,结果上线后发现两个问题:一是因为使用姿势不对,会偶发地出现分库分表不生效导致扫描所有库表的情况,二是偶发地出现查询延时达到秒级别。由于缺少对于 Sharding-JDBC 足够的了解,这两个问题我们都没有很快解决,后来不得已只能切回原来的组件,在找到问题之后再进行切换。 +这些中间件,对你而言,可能并不陌生,但是我想让你注意到是,**在使用任何中间件的时候一定要保证对于中间件有足够深入的了解,否则一旦出了问题没法快速地解决就悲剧了。** **我之前的一个项目中,** 一直使用自研的一个组件来实现分库分表,后来发现这套组件有一定几率会产生对数据库多余的连接,于是团队讨论后决定替换成 Sharding-JDBC。原本以为是一次简单的组件切换,结果上线后发现两个问题:一是因为使用姿势不对,会偶发地出现分库分表不生效导致扫描所有库表的情况,二是偶发地出现查询延时达到秒级别。由于缺少对于 Sharding-JDBC 足够的了解,这两个问题我们都没有很快解决,后来不得已只能切回原来的组件,在找到问题之后再进行切换。 ## 课程小结 @@ -98,6 +98,6 @@ MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所 其实,我们可以把主从复制引申为存储节点之间互相复制存储数据的技术,它可以实现数据的冗余,以达到备份和提升横向扩展能力的作用。在使用主从复制这个技术点时,你一般会考虑两个问题: -\\1. 主从的一致性和写入性能的权衡,如果你要保证所有从节点都写入成功,那么写入性能一定会受影响;如果你只写入主节点就返回成功,那么从节点就有可能出现数据同步失败的情况,从而造成主从不一致, **而在互联网的项目中,我们一般会优先考虑性能而不是数据的强一致性。** \\2. 主从的延迟问题,很多诡异的读取不到数据的问题都可能会和它有关,如果你遇到这类问题不妨先看看主从延迟的数据。 +\\1. 主从的一致性和写入性能的权衡,如果你要保证所有从节点都写入成功,那么写入性能一定会受影响;如果你只写入主节点就返回成功,那么从节点就有可能出现数据同步失败的情况,从而造成主从不一致,**而在互联网的项目中,我们一般会优先考虑性能而不是数据的强一致性。** \\2. 主从的延迟问题,很多诡异的读取不到数据的问题都可能会和它有关,如果你遇到这类问题不妨先看看主从延迟的数据。 -我们采用的很多组件都会使用到这个技术,比如,Redis 也是通过主从复制实现读写分离;Elasticsearch 中存储的索引分片也可以被复制到多个节点中;写入到 HDFS 中文件也会被复制到多个 DataNode 中。只是不同的组件对于复制的一致性、延迟要求不同,采用的方案也不同。 **但是这种设计的思想是通用的,是你需要了解的,这样你在学习其他存储组件的时候就能够触类旁通了。** \ No newline at end of file +我们采用的很多组件都会使用到这个技术,比如,Redis 也是通过主从复制实现读写分离;Elasticsearch 中存储的索引分片也可以被复制到多个节点中;写入到 HDFS 中文件也会被复制到多个 DataNode 中。只是不同的组件对于复制的一致性、延迟要求不同,采用的方案也不同。**但是这种设计的思想是通用的,是你需要了解的,这样你在学习其他存储组件的时候就能够触类旁通了。** \ No newline at end of file diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25409\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25409\350\256\262.md" index 99c9b3c17..b40ee10f0 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25409\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25409\350\256\262.md" @@ -8,7 +8,7 @@ 这时,公司 CEO 突然传来一个好消息,运营推广持续带来了流量,你所设计的电商系统的订单量突破了五千万,订单数据都是单表存储的,你的压力倍增,因为无论是数据库的查询还是写入性能都在下降,数据库的磁盘空间也在报警。所以,你主动分析现阶段自己需要考虑的问题,并寻求高效的解决方式,以便系统能正常运转下去。你考虑的问题主要有以下几点: -\\1. 系统正在持续不断地的发展,注册的用户越来越多,产生的订单越来越多,数据库中存储的数据也越来越多,单个表的数据量超过了千万甚至到了亿级别。这时即使你使用了索引,索引占用的空间也随着数据量的增长而增大,数据库就无法缓存全量的索引信息,那么就需要从磁盘上读取索引数据,就会影响到查询的性能了。 **那么这时你要如何提升查询性能呢?** \\2. 数据量的增加也占据了磁盘的空间,数据库在备份和恢复的时间变长, **你如何让数据库系统支持如此大的数据量呢?** \\3. 不同模块的数据,比如用户数据和用户关系数据,全都存储在一个主库中,一旦主库发生故障,所有的模块儿都会受到影响, **那么如何做到不同模块的故障隔离呢?** \\4. 你已经知道了,在 4 核 8G 的云服务器上对 MySQL5.7 做 Benchmark,大概可以支撑 500TPS 和 10000QPS,你可以看到数据库对于写入性能要弱于数据查询的能力,那么随着系统写入请求量的增长, **数据库系统如何来处理更高的并发写入请求呢?** 这些问题你可以归纳成,数据库的写入请求量大造成的性能和可用性方面的问题,要解决这些问题,你所采取的措施就是对数据进行分片,对数据进行分片,可以很好地分摊数据库的读写压力,也可以突破单机的存储瓶颈,而常见的一种方式是对数据库做“分库分表”。 +\\1. 系统正在持续不断地的发展,注册的用户越来越多,产生的订单越来越多,数据库中存储的数据也越来越多,单个表的数据量超过了千万甚至到了亿级别。这时即使你使用了索引,索引占用的空间也随着数据量的增长而增大,数据库就无法缓存全量的索引信息,那么就需要从磁盘上读取索引数据,就会影响到查询的性能了。**那么这时你要如何提升查询性能呢?** \\2. 数据量的增加也占据了磁盘的空间,数据库在备份和恢复的时间变长,**你如何让数据库系统支持如此大的数据量呢?** \\3. 不同模块的数据,比如用户数据和用户关系数据,全都存储在一个主库中,一旦主库发生故障,所有的模块儿都会受到影响,**那么如何做到不同模块的故障隔离呢?** \\4. 你已经知道了,在 4 核 8G 的云服务器上对 MySQL5.7 做 Benchmark,大概可以支撑 500TPS 和 10000QPS,你可以看到数据库对于写入性能要弱于数据查询的能力,那么随着系统写入请求量的增长,**数据库系统如何来处理更高的并发写入请求呢?** 这些问题你可以归纳成,数据库的写入请求量大造成的性能和可用性方面的问题,要解决这些问题,你所采取的措施就是对数据进行分片,对数据进行分片,可以很好地分摊数据库的读写压力,也可以突破单机的存储瓶颈,而常见的一种方式是对数据库做“分库分表”。 分库分表是一个很常见的技术方案,你应该有所了解。那你会说了:“既然这个技术很普遍,而我又有所了解,那你为什么还要提及这个话题呢?”因为以我过往的经验来看,不少人会在“分库分表”这里踩坑,主要体现在: @@ -30,7 +30,7 @@ 垂直拆分,顾名思义就是对数据库竖着拆分,也就是将数据库的表拆分到多个不同的数据库中。 -垂直拆分的原则一般是按照业务类型来拆分,核心思想是专库专用,将业务耦合度比较高的表拆分到单独的库中。举个形象的例子就是在整理衣服的时候,将羽绒服、毛衣、T 恤分别放在不同的格子里。这样可以解决我在开篇提到的第三个问题:把不同的业务的数据分拆到不同的数据库节点上,这样一旦数据库发生故障时只会影响到某一个模块的功能,不会影响到整体功能,从而实现了数据层面的故障隔离。 **我还是以微博系统为例来给你说明一下。** 在微博系统中有和用户相关的表,有和内容相关的表,有和关系相关的表,这些表都存储在主库中。在拆分后,我们期望用户相关的表分拆到用户库中,内容相关的表分拆到内容库中,关系相关的表分拆到关系库中。 +垂直拆分的原则一般是按照业务类型来拆分,核心思想是专库专用,将业务耦合度比较高的表拆分到单独的库中。举个形象的例子就是在整理衣服的时候,将羽绒服、毛衣、T 恤分别放在不同的格子里。这样可以解决我在开篇提到的第三个问题:把不同的业务的数据分拆到不同的数据库节点上,这样一旦数据库发生故障时只会影响到某一个模块的功能,不会影响到整体功能,从而实现了数据层面的故障隔离。**我还是以微博系统为例来给你说明一下。** 在微博系统中有和用户相关的表,有和内容相关的表,有和关系相关的表,这些表都存储在主库中。在拆分后,我们期望用户相关的表分拆到用户库中,内容相关的表分拆到内容库中,关系相关的表分拆到关系库中。 ![img](assets/7774c9393a6295b2d5e0f1a9fa7a5940.jpg) @@ -40,7 +40,7 @@ ## 如何对数据库做水平拆分 -和垂直拆分的关注点不同,垂直拆分的关注点在于业务相关性,而水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点。 **拆分的规则有下面这两种:** +和垂直拆分的关注点不同,垂直拆分的关注点在于业务相关性,而水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点。**拆分的规则有下面这两种:** \\1. 按照某一个字段的哈希值做拆分,这种拆分规则比较适用于实体表,比如说用户表,内容表,我们一般按照这些实体表的 ID 字段来拆分。比如说我们想把用户表拆分成 16 个库,64 张表,那么可以先对用户 ID 做哈希,哈希的目的是将 ID 尽量打散,然后再对 16 取余,这样就得到了分库后的索引值;对 64 取余,就得到了分表后的索引值。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25410\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25410\350\256\262.md" index 711e808e4..7dda81ad4 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25410\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25410\350\256\262.md" @@ -36,7 +36,7 @@ 没错,UUID(Universally Unique Identifier,通用唯一标识码)不依赖于任何第三方系统,所以在性能和可用性上都比较好,我一般会使用它生成 Request ID 来标记单次请求,但是如果用它来作为数据库主键,它会存在以下几点问题。 -首先,生成的 ID 做好具有单调递增性,也就是有序的,而 UUID 不具备这个特点。为什么 ID 要是有序的呢? **因为在系统设计时,ID 有可能成为排序的字段。** 我给你举个例子。 +首先,生成的 ID 做好具有单调递增性,也就是有序的,而 UUID 不具备这个特点。为什么 ID 要是有序的呢?**因为在系统设计时,ID 有可能成为排序的字段。** 我给你举个例子。 比如,你要实现一套评论的系统时,你一般会设计两个表,一张评论表,存储评论的详细信息,其中有 ID 字段,有评论的内容,还有评论人 ID,被评论内容的 ID 等等,以 ID 字段作为分区键;另一个是评论列表,存储着内容 ID 和评论 ID 的对应关系,以内容 ID 为分区键。 @@ -68,21 +68,21 @@ Snowflake 的核心思想是将 64bit 的二进制数字分成若干部分,每 如果你的系统部署在多个机房,那么 10 位的机器 ID 可以继续划分为 2~3 位的 IDC 标示(可以支撑 4 个或者 8 个 IDC 机房)和 7~8 位的机器 ID(支持 128-256 台机器);12 位的序列号代表着每个节点每毫秒最多可以生成 4096 的 ID。 -不同公司也会依据自身业务的特点对 Snowflake 算法做一些改造,比如说减少序列号的位数增加机器 ID 的位数以支持单 IDC 更多的机器,也可以在其中加入业务 ID 字段来区分不同的业务。 **比方说我现在使用的发号器的组成规则就是:** 1 位兼容位恒为 0 + 41 位时间信息 + 6 位 IDC 信息(支持 64 个 IDC)+ 6 位业务信息(支持 64 个业务)+ 10 位自增信息(每毫秒支持 1024 个号) +不同公司也会依据自身业务的特点对 Snowflake 算法做一些改造,比如说减少序列号的位数增加机器 ID 的位数以支持单 IDC 更多的机器,也可以在其中加入业务 ID 字段来区分不同的业务。**比方说我现在使用的发号器的组成规则就是:** 1 位兼容位恒为 0 + 41 位时间信息 + 6 位 IDC 信息(支持 64 个 IDC)+ 6 位业务信息(支持 64 个业务)+ 10 位自增信息(每毫秒支持 1024 个号) 我选择这个组成规则,主要是因为我在单机房只部署一个发号器的节点,并且使用 KeepAlive 保证可用性。业务信息指的是项目中哪个业务模块使用,比如用户模块生成的 ID,内容模块生成的 ID,把它加入进来,一是希望不同业务发出来的 ID 可以不同,二是因为在出现问题时可以反解 ID,知道是哪一个业务发出来的 ID。 -那么了解了 Snowflake 算法的原理之后,我们如何把它工程化,来为业务生成全局唯一的 ID 呢? **一般来说我们会有两种算法的实现方式:** **一种是嵌入到业务代码里,也就是分布在业务服务器中。** 这种方案的好处是业务代码在使用的时候不需要跨网络调用,性能上会好一些,但是就需要更多的机器 ID 位数来支持更多的业务服务器。另外,由于业务服务器的数量很多,我们很难保证机器 ID 的唯一性,所以就需要引入 ZooKeeper 等分布式一致性组件来保证每次机器重启时都能获得唯一的机器 ID。 +那么了解了 Snowflake 算法的原理之后,我们如何把它工程化,来为业务生成全局唯一的 ID 呢?**一般来说我们会有两种算法的实现方式:** **一种是嵌入到业务代码里,也就是分布在业务服务器中。** 这种方案的好处是业务代码在使用的时候不需要跨网络调用,性能上会好一些,但是就需要更多的机器 ID 位数来支持更多的业务服务器。另外,由于业务服务器的数量很多,我们很难保证机器 ID 的唯一性,所以就需要引入 ZooKeeper 等分布式一致性组件来保证每次机器重启时都能获得唯一的机器 ID。 -**另外一个部署方式是作为独立的服务部署,这也就是我们常说的发号器服务。** 业务在使用发号器的时候就需要多一次的网络调用,但是内网的调用对于性能的损耗有限,却可以减少机器 ID 的位数,如果发号器以主备方式部署,同时运行的只有一个发号器,那么机器 ID 可以省略,这样可以留更多的位数给最后的自增信息位。即使需要机器 ID,因为发号器部署实例数有限,那么就可以把机器 ID 写在发号器的配置文件里,这样即可以保证机器 ID 唯一性,也无需引入第三方组件了。 **微博和美图都是使用独立服务的方式来部署发号器的,性能上单实例单 CPU 可以达到两万每秒。** Snowflake 算法设计的非常简单且巧妙,性能上也足够高效,同时也能够生成具有全局唯一性、单调递增性和有业务含义的 ID,但是它也有一些缺点,其中最大的缺点就是它依赖于系统的时间戳,一旦系统时间不准,就有可能生成重复的 ID。所以如果我们发现系统时钟不准,就可以让发号器暂时拒绝发号,直到时钟准确为止。 +**另外一个部署方式是作为独立的服务部署,这也就是我们常说的发号器服务。** 业务在使用发号器的时候就需要多一次的网络调用,但是内网的调用对于性能的损耗有限,却可以减少机器 ID 的位数,如果发号器以主备方式部署,同时运行的只有一个发号器,那么机器 ID 可以省略,这样可以留更多的位数给最后的自增信息位。即使需要机器 ID,因为发号器部署实例数有限,那么就可以把机器 ID 写在发号器的配置文件里,这样即可以保证机器 ID 唯一性,也无需引入第三方组件了。**微博和美图都是使用独立服务的方式来部署发号器的,性能上单实例单 CPU 可以达到两万每秒。** Snowflake 算法设计的非常简单且巧妙,性能上也足够高效,同时也能够生成具有全局唯一性、单调递增性和有业务含义的 ID,但是它也有一些缺点,其中最大的缺点就是它依赖于系统的时间戳,一旦系统时间不准,就有可能生成重复的 ID。所以如果我们发现系统时钟不准,就可以让发号器暂时拒绝发号,直到时钟准确为止。 -另外,如果请求发号器的 QPS 不高,比如说发号器每毫秒只发一个 ID,就会造成生成 ID 的末位永远是 1,那么在分库分表时如果使用 ID 作为分区键就会造成库表分配的不均匀。 **这一点,也是我在实际项目中踩过的坑,而解决办法主要有两个:** \\1. 时间戳不记录毫秒而是记录秒,这样在一个时间区间里可以多发出几个号,避免出现分库分表时数据分配不均。 +另外,如果请求发号器的 QPS 不高,比如说发号器每毫秒只发一个 ID,就会造成生成 ID 的末位永远是 1,那么在分库分表时如果使用 ID 作为分区键就会造成库表分配的不均匀。**这一点,也是我在实际项目中踩过的坑,而解决办法主要有两个:** \\1. 时间戳不记录毫秒而是记录秒,这样在一个时间区间里可以多发出几个号,避免出现分库分表时数据分配不均。 \\2. 生成的序列号的起始号可以做一下随机,这一秒是 21,下一秒是 30,这样就会尽量的均衡了。 我在开头提到,自己的实际项目中采用的是变种的 Snowflake 算法,也就是说对 Snowflake 算法进行了一定的改造,从上面的内容中你可以看出,这些改造:一是要让算法中的 ID 生成规则符合自己业务的特点;二是为了解决诸如时间回拨等问题。 -其实,大厂除了采取 Snowflake 算法之外,还会选用一些其他的方案,比如滴滴和美团都有提出基于数据库生成 ID 的方案。这些方法根植于公司的业务,同样能解决分布式环境下 ID 全局唯一性的问题。对你而言,可以多角度了解不同的方法,这样能够寻找到更适合自己业务目前场景的解决方案,不过我想说的是, **方案不在多,而在精,方案没有最好,只有最适合,真正弄懂方法背后的原理,并将它落地,才是你最佳的选择。** +其实,大厂除了采取 Snowflake 算法之外,还会选用一些其他的方案,比如滴滴和美团都有提出基于数据库生成 ID 的方案。这些方法根植于公司的业务,同样能解决分布式环境下 ID 全局唯一性的问题。对你而言,可以多角度了解不同的方法,这样能够寻找到更适合自己业务目前场景的解决方案,不过我想说的是,**方案不在多,而在精,方案没有最好,只有最适合,真正弄懂方法背后的原理,并将它落地,才是你最佳的选择。** ## 课程小结 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25411\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25411\350\256\262.md" index 835a6b1b9..b34a0579f 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25411\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25411\350\256\262.md" @@ -76,7 +76,7 @@ MemTable 在累积到一定规模时,它会被刷新生成一个新的文件 你发现这类语句并不是都能使用到索引,只有后模糊匹配的语句才能使用索引。比如语句“select *from product where name like ‘% 电冰箱’”就没有使用到字段“name”上的索引,而“select* from product where name like ‘索尼 %’”就使用了“name”上的索引。而一旦没有使用索引就会扫描全表的数据,在性能上是无法接受的。 -于是你在谷歌上搜索了一下解决方案,发现大家都在使用开源组件 Elasticsearch 来支持搜索的请求,它本身是基于“倒排索引”来实现的, **那么什么是倒排索引呢?** +于是你在谷歌上搜索了一下解决方案,发现大家都在使用开源组件 Elasticsearch 来支持搜索的请求,它本身是基于“倒排索引”来实现的,**那么什么是倒排索引呢?** 倒排索引是指将记录中的某些列做分词,然后形成的分词与记录 ID 之间的映射关系。比如说,你的垂直电商项目里面有以下记录: @@ -88,7 +88,7 @@ MemTable 在累积到一定规模时,它会被刷新生成一个新的文件 这样,如果用户搜索电冰箱,就可以给他展示商品 ID 为 1 和 3 的两件商品了。 -而 Elasticsearch 作为一种常见的 NoSQL 数据库, **就以倒排索引作为核心技术原理,为你提供了分布式的全文搜索服务,这在传统的关系型数据库中使用 SQL 语句是很难实现的。** 所以你看,NoSQL 可以在某些业务场景下代替传统数据库提供数据存储服务。 +而 Elasticsearch 作为一种常见的 NoSQL 数据库,**就以倒排索引作为核心技术原理,为你提供了分布式的全文搜索服务,这在传统的关系型数据库中使用 SQL 语句是很难实现的。** 所以你看,NoSQL 可以在某些业务场景下代替传统数据库提供数据存储服务。 ## 提升扩展性 @@ -96,7 +96,7 @@ MemTable 在累积到一定规模时,它会被刷新生成一个新的文件 但是评论系统上线之后,存储量级增长的异常迅猛,你不得不将数据库拆分成更多的库表,而数据也要重新迁移到新的库表中,过程非常痛苦,而且数据迁移的过程也非常容易出错。 -这时,你考虑是否可以考虑使用 NoSQL 数据库来彻底解决扩展性的问题,经过调研你发现它们在设计之初就考虑到了分布式和大数据存储的场景, **比如像 MongoDB 就有三个扩展性方面的特性。** +这时,你考虑是否可以考虑使用 NoSQL 数据库来彻底解决扩展性的问题,经过调研你发现它们在设计之初就考虑到了分布式和大数据存储的场景,**比如像 MongoDB 就有三个扩展性方面的特性。** - 其一是 Replica,也叫做副本集,你可以理解为主从分离,也就是通过将数据拷贝成多份来保证当主挂掉后数据不会丢失。同时呢,Replica 还可以分担读请求。Replica 中有主节点来承担写请求,并且把对数据变动记录到 oplog 里(类似于 binlog);从节点接收到 oplog 后就会修改自身的数据以保持和主节点的一致。一旦主节点挂掉,MongoDB 会从从节点中选取一个节点成为主节点,可以继续提供写数据服务。 - 其二是 Shard,也叫做分片,你可以理解为分库分表,即将数据按照某种规则拆分成多份,存储在不同的机器上。MongoDB 的 Sharding 特性一般需要三个角色来支持,一个是 Shard Server,它是实际存储数据的节点,是一个独立的 Mongod 进程;二是 Config Server,也是一组 Mongod 进程,主要存储一些元信息,比如说哪些分片存储了哪些数据等;最后是 Route Server,它不实际存储数据,仅仅作为路由使用,它从 Config Server 中获取元信息后,将请求路由到正确的 Shard Server 中。 @@ -119,6 +119,6 @@ MemTable 在累积到一定规模时,它会被刷新生成一个新的文件 \\3. 在扩展性方面,NoSQL 数据库天生支持分布式,支持数据冗余和数据分片的特性。 -这些都让它成为传统关系型数据库的良好的补充,你需要了解的是, **NoSQL 可供选型的种类很多,每一个组件都有各自的特点。你在做选型的时候需要对它的实现原理有比较深入的了解,最好在运维方面对它有一定的熟悉,这样在出现问题时才能及时找到解决方案。** 否则,盲目跟从地上了一个新的 NoSQL 数据库,最终可能导致会出了故障无法解决,反而成为整体系统的拖累。 +这些都让它成为传统关系型数据库的良好的补充,你需要了解的是,**NoSQL 可供选型的种类很多,每一个组件都有各自的特点。你在做选型的时候需要对它的实现原理有比较深入的了解,最好在运维方面对它有一定的熟悉,这样在出现问题时才能及时找到解决方案。** 否则,盲目跟从地上了一个新的 NoSQL 数据库,最终可能导致会出了故障无法解决,反而成为整体系统的拖累。 -我在之前的项目中曾经使用 Elasticsearch 作为持久存储,支撑社区的 feed 流功能,初期开发的时候确实很爽,你可以针对 feed 中的任何字段做灵活高效地查询,业务功能迭代迅速,代码也简单易懂。可是到了后期流量上来之后,由于缺少对于 Elasticsearch 成熟的运维能力,造成故障频出,尤其到了高峰期就会出现节点不可用的问题,而由于业务上的巨大压力又无法分出人力和精力对 Elasticsearch 深入的学习和了解,最后不得不做大的改造切回熟悉的 MySQL。 **所以,对于开源组件的使用,不能只停留在只会“hello world”的阶段,而应该对它有足够的运维上的把控能力。** \ No newline at end of file +我在之前的项目中曾经使用 Elasticsearch 作为持久存储,支撑社区的 feed 流功能,初期开发的时候确实很爽,你可以针对 feed 中的任何字段做灵活高效地查询,业务功能迭代迅速,代码也简单易懂。可是到了后期流量上来之后,由于缺少对于 Elasticsearch 成熟的运维能力,造成故障频出,尤其到了高峰期就会出现节点不可用的问题,而由于业务上的巨大压力又无法分出人力和精力对 Elasticsearch 深入的学习和了解,最后不得不做大的改造切回熟悉的 MySQL。**所以,对于开源组件的使用,不能只停留在只会“hello world”的阶段,而应该对它有足够的运维上的把控能力。** \ No newline at end of file diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25412\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25412\350\256\262.md" index 81477e46f..a9308530e 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25412\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25412\350\256\262.md" @@ -6,7 +6,7 @@ ![img](assets/c14a816c828434fe1695220b7abdbc20.jpg) -从整体上看,数据库分了主库和从库,数据也被切分到多个数据库节点上。但随着并发的增加,存储数据量的增多,数据库的磁盘 IO 逐渐成了系统的瓶颈,我们需要一种访问更快的组件来降低请求响应时间,提升整体系统性能。这时我们就会使用缓存。 **那么什么是缓存,我们又该如何将它的优势最大化呢?** **本节课是缓存篇的总纲,** 我将从缓存定义、缓存分类和缓存优势劣势三个方面全方位带你掌握缓存的设计思想和理念,再用剩下 4 节课的时间,带你针对性地掌握使用缓存的正确姿势,以便让你在实际工作中能够更好地使用缓存提升整体系统的性能。 +从整体上看,数据库分了主库和从库,数据也被切分到多个数据库节点上。但随着并发的增加,存储数据量的增多,数据库的磁盘 IO 逐渐成了系统的瓶颈,我们需要一种访问更快的组件来降低请求响应时间,提升整体系统性能。这时我们就会使用缓存。**那么什么是缓存,我们又该如何将它的优势最大化呢?** **本节课是缓存篇的总纲,** 我将从缓存定义、缓存分类和缓存优势劣势三个方面全方位带你掌握缓存的设计思想和理念,再用剩下 4 节课的时间,带你针对性地掌握使用缓存的正确姿势,以便让你在实际工作中能够更好地使用缓存提升整体系统的性能。 接下来,让我们进入今天的课程吧! @@ -62,13 +62,13 @@ Linux 内存管理是通过一个叫做 MMU(Memory Management Unit)的硬件 **所以我们的解决思路是** 每篇文章在录入的时候渲染成静态页面,放置在所有的前端 Nginx 或者 Squid 等 Web 服务器上,这样用户在访问的时候会优先访问 Web 服务器上的静态页面,在对旧的文章执行一定的清理策略后,依然可以保证 99% 以上的缓存命中率。 -这种缓存只能针对静态数据来缓存,对于动态请求就无能为力了。那么我们如何针对动态请求做缓存呢? **这时你就需要分布式缓存了。** 分布式缓存的大名可谓是如雷贯耳了,我们平时耳熟能详的 Memcached、Redis 就是分布式缓存的典型例子。它们性能强劲,通过一些分布式的方案组成集群可以突破单机的限制。所以在整体架构中,分布式缓存承担着非常重要的角色(接下来的课程我会专门针对分布式缓存,带你了解分布式缓存的使用技巧以及高可用的方案,让你能在工作中对分布式缓存运用自如)。 +这种缓存只能针对静态数据来缓存,对于动态请求就无能为力了。那么我们如何针对动态请求做缓存呢?**这时你就需要分布式缓存了。** 分布式缓存的大名可谓是如雷贯耳了,我们平时耳熟能详的 Memcached、Redis 就是分布式缓存的典型例子。它们性能强劲,通过一些分布式的方案组成集群可以突破单机的限制。所以在整体架构中,分布式缓存承担着非常重要的角色(接下来的课程我会专门针对分布式缓存,带你了解分布式缓存的使用技巧以及高可用的方案,让你能在工作中对分布式缓存运用自如)。 -对于静态的资源的缓存你可以选择静态缓存,对于动态的请求你可以选择分布式缓存, **那么什么时候要考虑热点本地缓存呢?** **答案是当我们遇到极端的热点数据查询的时候。** 热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。 +对于静态的资源的缓存你可以选择静态缓存,对于动态的请求你可以选择分布式缓存,**那么什么时候要考虑热点本地缓存呢?** **答案是当我们遇到极端的热点数据查询的时候。** 热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。 比如某一位明星在微博上有了热点话题,“吃瓜群众”会到他 (她) 的微博首页围观,这就会引发这个用户信息的热点查询。这些查询通常会命中某一个缓存节点或者某一个数据库分区,短时间内会形成极高的热点查询。 -那么我们会在代码中使用一些本地缓存方案,如 HashMap,Guava Cache 或者是 Ehcache 等,它们和应用程序部署在同一个进程中,优势是不需要跨网络调度,速度极快,所以可以来阻挡短时间内的热点查询。 **来看个例子。** **比方说** 你的垂直电商系统的首页有一些推荐的商品,这些商品信息是由编辑在后台录入和变更。你分析编辑录入新的商品或者变更某个商品的信息后,在页面的展示是允许有一些延迟的,比如说 30 秒的延迟,并且首页请求量最大,即使使用分布式缓存也很难抗住,所以你决定使用 Guava Cache 来将所有的推荐商品的信息缓存起来,并且设置每隔 30 秒重新从数据库中加载最新的所有商品。 +那么我们会在代码中使用一些本地缓存方案,如 HashMap,Guava Cache 或者是 Ehcache 等,它们和应用程序部署在同一个进程中,优势是不需要跨网络调度,速度极快,所以可以来阻挡短时间内的热点查询。**来看个例子。** **比方说** 你的垂直电商系统的首页有一些推荐的商品,这些商品信息是由编辑在后台录入和变更。你分析编辑录入新的商品或者变更某个商品的信息后,在页面的展示是允许有一些延迟的,比如说 30 秒的延迟,并且首页请求量最大,即使使用分布式缓存也很难抗住,所以你决定使用 Guava Cache 来将所有的推荐商品的信息缓存起来,并且设置每隔 30 秒重新从数据库中加载最新的所有商品。 首先,我们初始化 Guava 的 Loading Cache: diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25413\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25413\350\256\262.md" index 9fc469d97..30bceddf5 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25413\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25413\350\256\262.md" @@ -28,7 +28,7 @@ ![img](assets/661da5a2b55b7d6e1575a3241247eec4.jpg) -这个策略就是我们使用缓存最常见的策略,Cache Aside 策略(也叫旁路缓存策略),这个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策略, **其中读策略的步骤是:** +这个策略就是我们使用缓存最常见的策略,Cache Aside 策略(也叫旁路缓存策略),这个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策略,**其中读策略的步骤是:** - 从缓存中读取数据; - 如果缓存命中,则直接返回数据; @@ -40,7 +40,7 @@ - 更新数据库中的记录; - 删除缓存记录。 -你也许会问了,在写策略中,能否先删除缓存,后更新数据库呢? **答案是不行的,** 因为这样也有可能出现缓存数据不一致的问题,我以用户表的场景为例解释一下。 +你也许会问了,在写策略中,能否先删除缓存,后更新数据库呢?**答案是不行的,** 因为这样也有可能出现缓存数据不一致的问题,我以用户表的场景为例解释一下。 假设某个用户的年龄是 20,请求 A 要更新用户年龄为 21,所以它会删除缓存中的内容。这时,另一个请求 B 要读取这个用户的年龄,它查询缓存发现未命中后,会从数据库中读取到年龄为 20,并且写入到缓存中,然后请求 A 继续更改数据库,将用户的年龄更新为 21,这就造成了缓存和数据库的不一致。 @@ -56,7 +56,7 @@ **而解决这个问题的办法** 恰恰是在插入新数据到数据库之后写入缓存,这样后续的读请求就会从缓存中读到数据了。并且因为是新注册的用户,所以不会出现并发更新用户信息的情况。 -Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。 **如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:** +Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。**如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:** \\1. 一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响; diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25414\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25414\350\256\262.md" index a0ba1dcc6..553b88cfd 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25414\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25414\350\256\262.md" @@ -14,7 +14,7 @@ 命中率仅仅下降 1% 造成的影响就如此可怕,更不要说缓存节点故障了。而图中单点部署的缓存节点就成了整体系统中最大的隐患,那我们要如何来解决这个问题,提升缓存的可用性呢? -我们可以通过部署多个节点,同时设计一些方案让这些节点互为备份。这样,当某个节点故障时,它的备份节点可以顶替它继续提供服务。 **而这些方案就是我们本节课的重点:分布式缓存的高可用方案。** 在我的项目中,我主要选择的方案有 **客户端方案、中间代理层方案和服务端方案** 三大类: +我们可以通过部署多个节点,同时设计一些方案让这些节点互为备份。这样,当某个节点故障时,它的备份节点可以顶替它继续提供服务。**而这些方案就是我们本节课的重点:分布式缓存的高可用方案。** 在我的项目中,我主要选择的方案有 **客户端方案、中间代理层方案和服务端方案** 三大类: - **客户端方案** 就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。 - **中间代理层方案** 是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。 @@ -33,7 +33,7 @@ **1. 缓存数据如何分片** 单一的缓存节点受到机器内存、网卡带宽和单节点请求量的限制,不能承担比较高的并发,因此我们考虑将数据分片,依照分片算法将数据打散到多个不同的节点上,每个节点上存储部分数据。 -这样在某个节点故障的情况下,其他节点也可以提供服务,保证了一定的可用性。这就好比不要把鸡蛋放在同一个篮子里,这样一旦一个篮子掉在地上,摔碎了,别的篮子里还有没摔碎的鸡蛋,不至于一个不剩。 **一般来讲,分片算法常见的就是 Hash 分片算法和一致性 Hash 分片算法两种。** +这样在某个节点故障的情况下,其他节点也可以提供服务,保证了一定的可用性。这就好比不要把鸡蛋放在同一个篮子里,这样一旦一个篮子掉在地上,摔碎了,别的篮子里还有没摔碎的鸡蛋,不至于一个不剩。**一般来讲,分片算法常见的就是 Hash 分片算法和一致性 Hash 分片算法两种。** Hash 分片的算法就是对缓存的 Key 做哈希计算,然后对总的缓存节点个数取余。你可以这么理解: @@ -41,11 +41,11 @@ Hash 分片的算法就是对缓存的 Key 做哈希计算,然后对总的缓 ![img](assets/720f7e4543d45fdc71056de280caff55.jpg) -这个算法最大的优点就是简单易理解,缺点是当增加或者减少缓存节点时,缓存总的节点个数变化造成计算出来的节点发生变化,从而造成缓存失效不可用。 **所以我建议你,** 如果采用这种方法,最好建立在你对于这组缓存命中率下降不敏感,比如下面还有另外一层缓存来兜底的情况下。  **当然了,用一致性 Hash 算法可以很好地解决增加和删减节点时,命中率下降的问题。** 在这个算法中,我们将整个 Hash 值空间组织成一个虚拟的圆环,然后将缓存节点的 IP 地址或者主机名做 Hash 取值后,放置在这个圆环上。当我们需要确定某一个 Key 需要存取到哪个节点上的时候,先对这个 Key 做同样的 Hash 取值,确定在环上的位置,然后按照顺时针方向在环上“行走”,遇到的第一个缓存节点就是要访问的节点。比方说下面这张图里面,Key 1 和 Key 2 会落入到 Node 1 中,Key 3、Key 4 会落入到 Node 2 中,Key 5 落入到 Node 3 中,Key 6 落入到 Node 4 中。 +这个算法最大的优点就是简单易理解,缺点是当增加或者减少缓存节点时,缓存总的节点个数变化造成计算出来的节点发生变化,从而造成缓存失效不可用。**所以我建议你,** 如果采用这种方法,最好建立在你对于这组缓存命中率下降不敏感,比如下面还有另外一层缓存来兜底的情况下。  **当然了,用一致性 Hash 算法可以很好地解决增加和删减节点时,命中率下降的问题。** 在这个算法中,我们将整个 Hash 值空间组织成一个虚拟的圆环,然后将缓存节点的 IP 地址或者主机名做 Hash 取值后,放置在这个圆环上。当我们需要确定某一个 Key 需要存取到哪个节点上的时候,先对这个 Key 做同样的 Hash 取值,确定在环上的位置,然后按照顺时针方向在环上“行走”,遇到的第一个缓存节点就是要访问的节点。比方说下面这张图里面,Key 1 和 Key 2 会落入到 Node 1 中,Key 3、Key 4 会落入到 Node 2 中,Key 5 落入到 Node 3 中,Key 6 落入到 Node 4 中。 ![img](assets/f9ea0e201aa954cf46c5762835095efe.jpg) -这时如果在 Node 1 和 Node 2 之间增加一个 Node 5,你可以看到原本命中 Node 2 的 Key 3 现在命中到 Node 5,而其它的 Key 都没有变化;同样的道理,如果我们把 Node 3 从集群中移除,那么只会影响到 Key 5 。所以你看, **在增加和删除节点时,只有少量的 Key 会“漂移”到其它节点上,** 而大部分的 Key 命中的节点还是会保持不变,从而可以保证命中率不会大幅下降。 +这时如果在 Node 1 和 Node 2 之间增加一个 Node 5,你可以看到原本命中 Node 2 的 Key 3 现在命中到 Node 5,而其它的 Key 都没有变化;同样的道理,如果我们把 Node 3 从集群中移除,那么只会影响到 Key 5 。所以你看,**在增加和删除节点时,只有少量的 Key 会“漂移”到其它节点上,** 而大部分的 Key 命中的节点还是会保持不变,从而可以保证命中率不会大幅下降。 ![img](assets/4c13c4fd4278dc97d072afe09a1a1b91.jpg) @@ -56,7 +56,7 @@ Hash 分片的算法就是对缓存的 Key 做哈希计算,然后对总的缓 极端情况下,比如一个有三个节点 A、B、C 承担整体的访问,每个节点的访问量平均,A 故障后,B 将承担双倍的压力(A 和 B 的全部请求),当 B 承担不了流量 Crash 后,C 也将因为要承担原先三倍的流量而 Crash,这就造成了整体缓存系统的雪崩。 -说到这儿,你可能觉得很可怕,但也不要太担心, **我们程序员就是要能够创造性地解决各种问题,所以你可以在一致性 Hash 算法中引入虚拟节点的概念。** +说到这儿,你可能觉得很可怕,但也不要太担心,**我们程序员就是要能够创造性地解决各种问题,所以你可以在一致性 Hash 算法中引入虚拟节点的概念。** 它将一个缓存节点计算多个 Hash 值分散到圆环的不同位置,这样既实现了数据的平均,而且当某一个节点故障或者退出的时候,它原先承担的 Key 将以更加平均的方式分配到其他节点上,从而避免雪崩的发生。 @@ -66,7 +66,7 @@ Hash 分片的算法就是对缓存的 Key 做哈希计算,然后对总的缓 ![img](assets/4c10bb2e9b0f6cb9920d4b1c9418b2f8.jpg) -很显然,数据分片最大的优势就是缓解缓存节点的存储和访问压力,但同时它也让缓存的使用更加复杂。在 MultiGet(批量获取)场景下,单个节点的访问量并没有减少,同时节点数太多会造成缓存访问的 SLA(即“服务等级协议”,SLA 代表了网站服务可用性)得不到很好的保证,因为根据木桶原则,SLA 取决于最慢、最坏的节点的情况,节点数过多也会增加出问题的概率, **因此我推荐 4 到 6 个节点为佳。** **2.Memcached 的主从机制** Redis 本身支持主从的部署方式,但是 Memcached 并不支持,所以我们今天主要来了解一下 Memcached 的主从机制是如何在客户端实现的。 +很显然,数据分片最大的优势就是缓解缓存节点的存储和访问压力,但同时它也让缓存的使用更加复杂。在 MultiGet(批量获取)场景下,单个节点的访问量并没有减少,同时节点数太多会造成缓存访问的 SLA(即“服务等级协议”,SLA 代表了网站服务可用性)得不到很好的保证,因为根据木桶原则,SLA 取决于最慢、最坏的节点的情况,节点数过多也会增加出问题的概率,**因此我推荐 4 到 6 个节点为佳。** **2.Memcached 的主从机制** Redis 本身支持主从的部署方式,但是 Memcached 并不支持,所以我们今天主要来了解一下 Memcached 的主从机制是如何在客户端实现的。 在之前的项目中,我就遇到了单个主节点故障导致数据穿透的问题,这时我为每一组 Master 配置一组 Slave,更新数据时主从同步更新。读取时,优先从 Slave 中读数据,如果读取不到数据就穿透到 Master 读取,并且将数据回种到 Slave 中以保持 Slave 数据的热度。 @@ -86,7 +86,7 @@ Hash 分片的算法就是对缓存的 Key 做哈希计算,然后对总的缓 ## 中间代理层方案 -虽然客户端方案已经能解决大部分的问题,但是只能在单一语言系统之间复用。例如微博使用 Java 语言实现了这么一套逻辑,我使用 PHP 就难以复用,需要重新写一套,很麻烦。 **而中间代理层的方案就可以解决这个问题。** 你可以将客户端解决方案的经验移植到代理层中,通过通用的协议(如 Redis 协议)来实现在其他语言中的复用。 +虽然客户端方案已经能解决大部分的问题,但是只能在单一语言系统之间复用。例如微博使用 Java 语言实现了这么一套逻辑,我使用 PHP 就难以复用,需要重新写一套,很麻烦。**而中间代理层的方案就可以解决这个问题。** 你可以将客户端解决方案的经验移植到代理层中,通过通用的协议(如 Redis 协议)来实现在其他语言中的复用。 如果你来自研缓存代理层,你就可以将客户端方案中的高可用逻辑封装在代理层代码里面,这样用户在使用你的代理层的时候就不需要关心缓存的高可用是如何做的,只需要依赖你的代理层就好了。 @@ -104,7 +104,7 @@ Redis 在 2.4 版本中提出了 Redis Sentinel 模式来解决主从 Redis 部 Redis Sentinel 也是集群部署的,这样可以避免 Sentinel 节点挂掉造成无法自动故障恢复的问题,每一个 Sentinel 节点都是无状态的。在 Sentinel 中会配置 Master 的地址,Sentinel 会时刻监控 Master 的状态,当发现 Master 在配置的时间间隔内无响应,就认为 Master 已经挂了,Sentinel 会从从节点中选取一个提升为主节点,并且把所有其他的从节点作为新主的从节点。Sentinel 集群内部在仲裁的时候,会根据配置的值来决定当有几个 Sentinel 节点认为主挂掉可以做主从切换的操作,也就是集群内部需要对缓存节点的状态达成一致才行。 -Redis Sentinel 不属于代理层模式,因为对于缓存的写入和读取请求不会经过 Sentinel 节点。Sentinel 节点在架构上和主从是平级的,是作为管理者存在的, **所以可以认为是在服务端提供的一种高可用方案。** +Redis Sentinel 不属于代理层模式,因为对于缓存的写入和读取请求不会经过 Sentinel 节点。Sentinel 节点在架构上和主从是平级的,是作为管理者存在的,**所以可以认为是在服务端提供的一种高可用方案。** ## 课程小结 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25415\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25415\350\256\262.md" index 905305a24..a01f08a78 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25415\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25415\350\256\262.md" @@ -41,7 +41,7 @@ **类似的场景还有一些:** 比如由于代码的 bug 导致查询数据库的时候抛出了异常,这样可以认为从数据库查询出来的数据为空,同样不会回种缓存。 -那么,当我们从数据库中查询到空值或者发生异常时,我们可以向缓存中回种一个空值。但是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个比较短的过期时间,让空值在短时间之内能够快速过期淘汰。 **下面是这个流程的伪代码:** +那么,当我们从数据库中查询到空值或者发生异常时,我们可以向缓存中回种一个空值。但是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个比较短的过期时间,让空值在短时间之内能够快速过期淘汰。**下面是这个流程的伪代码:** ```java Object nullValue = new Object(); @@ -63,13 +63,13 @@ try { ### 使用布隆过滤器 -1970 年布隆提出了一种布隆过滤器的算法,用来判断一个元素是否在一个集合中。这种算法由一个二进制数组和一个 Hash 算法组成。 **它的基本思路如下:** 我们把集合中的每一个值按照提供的 Hash 算法算出对应的 Hash 值,然后将 Hash 值对数组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从 0 改成 1。在判断一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如果这个位置的值为 1 就认为这个元素在集合中,否则则认为不在集合中。 +1970 年布隆提出了一种布隆过滤器的算法,用来判断一个元素是否在一个集合中。这种算法由一个二进制数组和一个 Hash 算法组成。**它的基本思路如下:** 我们把集合中的每一个值按照提供的 Hash 算法算出对应的 Hash 值,然后将 Hash 值对数组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从 0 改成 1。在判断一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如果这个位置的值为 1 就认为这个元素在集合中,否则则认为不在集合中。 下图是布隆过滤器示意图,我来带你分析一下图内的信息。 ![img](assets/873fcbbb19b49a92f490ae2cf3a30e88.jpg) -A、B、C 等元素组成了一个集合,元素 D 计算出的 Hash 值所对应的的数组中值是 1,所以可以认为 D 也在集合中。而 F 在数组中的值是 0,所以 F 不在数组中。 **那么我们如何使用布隆过滤器来解决缓存穿透的问题呢?** +A、B、C 等元素组成了一个集合,元素 D 计算出的 Hash 值所对应的的数组中值是 1,所以可以认为 D 也在集合中。而 F 在数组中的值是 0,所以 F 不在数组中。**那么我们如何使用布隆过滤器来解决缓存穿透的问题呢?** 还是以存储用户信息的表为例进行讲解。首先,我们初始化一个很大的数组,比方说长度为 20 亿的数组,接下来我们选择一个 Hash 算法,然后我们将目前现有的所有用户的 ID 计算出 Hash 值并且映射到这个大数组中,映射位置的值设置为 1,其它值设置为 0。 @@ -79,7 +79,7 @@ A、B、C 等元素组成了一个集合,元素 D 计算出的 Hash 值所对 布隆过滤器拥有极高的性能,无论是写入操作还是读取操作,时间复杂度都是 O(1),是常量值。在空间上,相对于其他数据结构它也有很大的优势,比如,20 亿的数组需要 2000000000/8/1024/1024 = 238M 的空间,而如果使用数组来存储,假设每个用户 ID 占用 4 个字节的空间,那么存储 20 亿用户需要 2000000000 * 4 / 1024 / 1024 = 7600M 的空间,是布隆过滤器的 32 倍。 -不过,任何事物都有两面性,布隆过滤器也不例外, **它主要有两个缺陷:** +不过,任何事物都有两面性,布隆过滤器也不例外,**它主要有两个缺陷:** \\1. 它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中; @@ -91,27 +91,27 @@ A、B、C 等元素组成了一个集合,元素 D 计算出的 Hash 值所对 **那么你可能会问为什么不映射成更长的 Hash 值呢?** 因为更长的 Hash 值会带来更高的存储成本和计算成本。即使使用 32 位的 Hash 算法,它的值空间长度是 2 的 32 次幂减一,约等于 42 亿,用来映射 20 亿的用户数据,碰撞几率依然有接近 50%。 -Hash 的碰撞就造成了两个用户 ID ,A 和 B 会计算出相同的 Hash 值,那么如果 A 是注册的用户,它的 Hash 值对应的数组中的值是 1,那么 B 即使不是注册用户,它在数组中的位置和 A 是相同的,对应的值也是 1, **这就产生了误判。** +Hash 的碰撞就造成了两个用户 ID ,A 和 B 会计算出相同的 Hash 值,那么如果 A 是注册的用户,它的 Hash 值对应的数组中的值是 1,那么 B 即使不是注册用户,它在数组中的位置和 A 是相同的,对应的值也是 1,**这就产生了误判。** -布隆过滤器的误判有一个特点,就是它只会出现“false positive”的情况。这是什么意思呢?当布隆过滤器判断元素在集合中时,这个元素可能不在集合中。但是一旦布隆过滤器判断这个元素不在集合中时,它一定不在集合中。 **这一点非常适合解决缓存穿透的问题。** 为什么呢? +布隆过滤器的误判有一个特点,就是它只会出现“false positive”的情况。这是什么意思呢?当布隆过滤器判断元素在集合中时,这个元素可能不在集合中。但是一旦布隆过滤器判断这个元素不在集合中时,它一定不在集合中。**这一点非常适合解决缓存穿透的问题。** 为什么呢? 你想,如果布隆过滤器会将集合中的元素判定为不在集合中,那么我们就不确定,被布隆过滤器判定为不在集合中的元素,是不是在集合中。假设在刚才的场景中,如果有大量查询未注册的用户信息的请求存在,那么这些请求到达布隆过滤器之后,即使布隆过滤器判断为不是注册用户,那么我们也不确定它是不是真的不是注册用户,那么就还是需要去数据库和缓存中查询,这就使布隆过滤器失去了价值。 -所以你看,布隆过滤器虽然存在误判的情况,但是还是会减少缓存穿透的情况发生,只是我们需要尽量减少误判的几率,这样布隆过滤器的判断正确的几率更高,对缓存的穿透也更少。 **一个解决方案是:** +所以你看,布隆过滤器虽然存在误判的情况,但是还是会减少缓存穿透的情况发生,只是我们需要尽量减少误判的几率,这样布隆过滤器的判断正确的几率更高,对缓存的穿透也更少。**一个解决方案是:** 使用多个 Hash 算法为元素计算出多个 Hash 值,只有所有 Hash 值对应的数组中的值都为 1 时,才会认为这个元素在集合中。 **布隆过滤器不支持删除元素的缺陷也和 Hash 碰撞有关。** 给你举一个例子,假如两个元素 A 和 B 都是集合中的元素,它们有相同的 Hash 值,它们就会映射到数组的同一个位置。这时我们删除了 A,数组中对应位置的值也从 1 变成 0,那么在判断 B 的时候发现值是 0,也会判断 B 是不在集合中的元素,就会得到错误的结论。 -**那么我是怎么解决这个问题的呢?** 我会让数组中不再只有 0 和 1 两个值,而是存储一个计数。比如如果 A 和 B 同时命中了一个数组的索引,那么这个位置的值就是 2,如果 A 被删除了就把这个值从 2 改为 1。这个方案中的数组不再存储 bit 位,而是存储数值,也就会增加空间的消耗。 **所以,你要依据业务场景来选择是否能够使用布隆过滤器,** 比如像是注册用户的场景下,因为用户删除的情况基本不存在,所以还是可以使用布隆过滤器来解决缓存穿透的问题的。 +**那么我是怎么解决这个问题的呢?** 我会让数组中不再只有 0 和 1 两个值,而是存储一个计数。比如如果 A 和 B 同时命中了一个数组的索引,那么这个位置的值就是 2,如果 A 被删除了就把这个值从 2 改为 1。这个方案中的数组不再存储 bit 位,而是存储数值,也就会增加空间的消耗。**所以,你要依据业务场景来选择是否能够使用布隆过滤器,** 比如像是注册用户的场景下,因为用户删除的情况基本不存在,所以还是可以使用布隆过滤器来解决缓存穿透的问题的。 **讲了这么多,关于布隆过滤器的使用上,我也给你几个建议:** \\1. 选择多个 Hash 函数计算多个 Hash 值,这样可以减少误判的几率; \\2. 布隆过滤器会消耗一定的内存空间,所以在使用时需要评估你的业务场景下需要多大的内存,存储的成本是否可以接受。 -总的来说, **回种空值和布隆过滤器** 是解决缓存穿透问题的两种最主要的解决方案,但是它们也有各自的适用场景,并不能解决所有问题。比方说当有一个极热点的缓存项,它一旦失效会有大量请求穿透到数据库,这会对数据库造成瞬时极大的压力,我们把这个场景叫做“dog-pile effect”(狗桩效应), +总的来说,**回种空值和布隆过滤器** 是解决缓存穿透问题的两种最主要的解决方案,但是它们也有各自的适用场景,并不能解决所有问题。比方说当有一个极热点的缓存项,它一旦失效会有大量请求穿透到数据库,这会对数据库造成瞬时极大的压力,我们把这个场景叫做“dog-pile effect”(狗桩效应), -这是典型的缓存并发穿透的问题, **那么,我们如何来解决这个问题呢?** 解决狗桩效应的思路是尽量地减少缓存穿透后的并发,方案也比较简单: +这是典型的缓存并发穿透的问题,**那么,我们如何来解决这个问题呢?** 解决狗桩效应的思路是尽量地减少缓存穿透后的并发,方案也比较简单: \\1. 在代码中,控制在某一个热点缓存项失效之后启动一个后台线程,穿透到数据库,将数据加载到缓存中,在缓存未加载之前,所有访问这个缓存的请求都不再穿透而直接返回。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25416\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25416\350\256\262.md" index bc5522727..72f81145d 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25416\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25416\350\256\262.md" @@ -12,7 +12,7 @@ \\2. 对于 Web 网站来说,则包括了 JavaScript 文件,CSS 文件,静态 HTML 文件等等。 -具体到你的电商系统来说,商品的图片,介绍商品使用方法的视频等等静态资源,现在都放在了 Nginx 等 Web 服务器上,它们的读请求量极大,并且对访问速度的要求很高,并且占据了很高的带宽,这时会出现访问速度慢,带宽被占满影响动态请求的问题, **那么你就需要考虑如何针对这些静态资源进行读加速了。** +具体到你的电商系统来说,商品的图片,介绍商品使用方法的视频等等静态资源,现在都放在了 Nginx 等 Web 服务器上,它们的读请求量极大,并且对访问速度的要求很高,并且占据了很高的带宽,这时会出现访问速度慢,带宽被占满影响动态请求的问题,**那么你就需要考虑如何针对这些静态资源进行读加速了。** ## 静态资源加速的考虑点 @@ -24,7 +24,7 @@ 另外,单个视频和图片等静态资源很大,并且访问量又极高,如果使用业务服务器和分布式缓存来承担这些流量,无论是对于内网还是外网的带宽都会是很大的考验。 -所以我们考虑在业务服务器的上层,增加一层特殊的缓存,用来承担绝大部分对于静态资源的访问,这一层特殊缓存的节点需要遍布在全国各地,这样可以让用户选择最近的节点访问。缓存的命中率也需要一定的保证,尽量减少访问资源存储源站的请求数量(回源请求)。 **这一层缓存就是我们这节课的重点:CDN。** +所以我们考虑在业务服务器的上层,增加一层特殊的缓存,用来承担绝大部分对于静态资源的访问,这一层特殊缓存的节点需要遍布在全国各地,这样可以让用户选择最近的节点访问。缓存的命中率也需要一定的保证,尽量减少访问资源存储源站的请求数量(回源请求)。**这一层缓存就是我们这节课的重点:CDN。** ## CDN 的关键技术 @@ -44,13 +44,13 @@ CDN(Content Delivery Network/Content Distribution Network,内容分发网络 ### 1. 如何让用户的请求到达 CDN 节点 -首先,我们考虑一下如何让用户的请求到达 CDN 节点,你可能会觉得,这很简单啊,只需要告诉用户 CDN 节点的 IP 地址,然后请求这个 IP 地址上面部署的 CDN 服务就可以了啊。 **但是这样会有一个问题:** 就是我们使用的是第三方厂商的 CDN 服务,CDN 厂商会给我们一个 CDN 的节点 IP,比如说这个 IP 地址是“111.202.34.130”,那么我们的电商系统中的图片的地址很可能是这样的:“, 这个地址是要存储在数据库中的。 +首先,我们考虑一下如何让用户的请求到达 CDN 节点,你可能会觉得,这很简单啊,只需要告诉用户 CDN 节点的 IP 地址,然后请求这个 IP 地址上面部署的 CDN 服务就可以了啊。**但是这样会有一个问题:** 就是我们使用的是第三方厂商的 CDN 服务,CDN 厂商会给我们一个 CDN 的节点 IP,比如说这个 IP 地址是“111.202.34.130”,那么我们的电商系统中的图片的地址很可能是这样的:“, 这个地址是要存储在数据库中的。 那么如果这个节点 IP 发生了变更怎么办?或者我们如果更改了 CDN 厂商怎么办?是不是要修改所有的商品的 url 域名呢?这就是一个比较大的工作量了。所以,我们要做的事情是将第三方厂商提供的 IP 隐藏起来,给到用户的最好是一个本公司域名的子域名。 **那么如何做到这一点呢?** 这就需要依靠 DNS 来帮我们解决域名映射的问题了。 -DNS(Domain Name System,域名系统)实际上就是一个存储域名和 IP 地址对应关系的分布式数据库。而域名解析的结果一般有两种,一种叫做“A 记录”,返回的是域名对应的 IP 地址;另一种是“CNAME 记录”,返回的是另一个域名,也就是说当前域名的解析要跳转到另一个域名的解析上,实际上 \ 域名的解析结果就是一个 CNAME 记录,域名的解析被跳转到 \ 上了,我们正是利用 CNAME 记录来解决域名映射问题的, **具体是怎么解决的呢?我给你举个例子。** +DNS(Domain Name System,域名系统)实际上就是一个存储域名和 IP 地址对应关系的分布式数据库。而域名解析的结果一般有两种,一种叫做“A 记录”,返回的是域名对应的 IP 地址;另一种是“CNAME 记录”,返回的是另一个域名,也就是说当前域名的解析要跳转到另一个域名的解析上,实际上 \ 域名的解析结果就是一个 CNAME 记录,域名的解析被跳转到 \ 上了,我们正是利用 CNAME 记录来解决域名映射问题的,**具体是怎么解决的呢?我给你举个例子。** 比如你的公司的一级域名叫做 example.com,那么你可以给你的图片服务的域名定义为“img.example.com”,然后将这个域名的解析结果的 CNAME 配置到 CDN 提供的域名上,比如 uclound 可能会提供一个域名是“80f21f91.cdn.ucloud.com.cn”这个域名。这样你的电商系统使用的图片地址可以是“ @@ -70,7 +70,7 @@ DNS(Domain Name System,域名系统)实际上就是一个存储域名和 I 如果没有,就开始 DNS 的迭代查询。先请求根 DNS,根 DNS 返回顶级 DNS(.com)的地址;xn--36qy46dznw.com 顶级 DNS,得到 baidu.com 的域名服务器地址;再从 baidu.com 的域名服务器中查询到 \ 对应的 IP 地址,返回这个 IP 地址的同时,标记这个结果是来自于权威 DNS 的结果,同时写入 Local DNS 的解析结果缓存,这样下一次的解析同一个域名就不需要做 DNS 的迭代查询了。 -经过了向多个 DNS 服务器做查询之后,整个 DNS 的解析的时间有可能会到秒级别, **那么我们如何来解决这个性能问题呢?** **一个解决的思路是:** 在 APP 启动时,对需要解析的域名做预先解析,然后把解析的结果缓存到本地的一个 LRU 缓存里面。这样当我们要使用这个域名的时候,只需要从缓存中直接拿到所需要的 IP 地址就好了,如果缓存中不存在才会走整个 DNS 查询的过程。 **同时,** 为了避免 DNS 解析结果的变更造成缓存内数据失效,我们可以启动一个定时器,定期地更新缓存中的数据。 +经过了向多个 DNS 服务器做查询之后,整个 DNS 的解析的时间有可能会到秒级别,**那么我们如何来解决这个性能问题呢?** **一个解决的思路是:** 在 APP 启动时,对需要解析的域名做预先解析,然后把解析的结果缓存到本地的一个 LRU 缓存里面。这样当我们要使用这个域名的时候,只需要从缓存中直接拿到所需要的 IP 地址就好了,如果缓存中不存在才会走整个 DNS 查询的过程。**同时,** 为了避免 DNS 解析结果的变更造成缓存内数据失效,我们可以启动一个定时器,定期地更新缓存中的数据。 **我曾经测试过这种方式,** 对于 HTTP 请求的响应时间的提升是很明显的,原先 DNS 解析时间经常会超过 1s,使用这种方式后,DNS 解析时间可以控制在 200ms 之内,整个 HTTP 请求的过程也可以减少大概 80ms~100ms。 @@ -84,13 +84,13 @@ DNS(Domain Name System,域名系统)实际上就是一个存储域名和 I ### 2. 如何找到离用户最近的 CDN 节点 -GSLB(Global Server Load Balance,全局负载均衡), 它的含义是对于部署在不同地域的服务器之间做负载均衡,下面可能管理了很多的本地负载均衡组件。 **它有两方面的作用:** +GSLB(Global Server Load Balance,全局负载均衡), 它的含义是对于部署在不同地域的服务器之间做负载均衡,下面可能管理了很多的本地负载均衡组件。**它有两方面的作用:** 一方面,它是一种负载均衡服务器,负载均衡,顾名思义嘛,指的是让流量平均分配使得下面管理的服务器的负载更平均; 另一方面,它还需要保证流量流经的服务器与流量源头在地缘上是比较接近的。 -GSLB 可以通过多种策略,来保证返回的 CDN 节点和用户尽量保证在同一地缘区域,比如说可以将用户的 IP 地址按照地理位置划分为若干的区域,然后将 CDN 节点对应到一个区域上,然后根据用户所在区域来返回合适的节点;也可以通过发送数据包测量 RTT 的方式来决定返回哪一个节点。 **不过,这些原理不是本节课重点内容,** 你了解一下就可以了,我不做详细的介绍。 +GSLB 可以通过多种策略,来保证返回的 CDN 节点和用户尽量保证在同一地缘区域,比如说可以将用户的 IP 地址按照地理位置划分为若干的区域,然后将 CDN 节点对应到一个区域上,然后根据用户所在区域来返回合适的节点;也可以通过发送数据包测量 RTT 的方式来决定返回哪一个节点。**不过,这些原理不是本节课重点内容,** 你了解一下就可以了,我不做详细的介绍。 有了 GSLB 之后,节点的解析过程变成了下图中的样子: @@ -108,6 +108,6 @@ GSLB 可以通过多种策略,来保证返回的 CDN 节点和用户尽量保 3.GSLB 可以给用户返回一个离着他更近的节点,加快静态资源的访问速度。 -作为一个服务端开发人员,你可能会忽略 CDN 的重要性,对于偶尔出现的 CDN 问题嗤之以鼻,觉得这个不是我们应该关心的内容, **这种想法是错的。** +作为一个服务端开发人员,你可能会忽略 CDN 的重要性,对于偶尔出现的 CDN 问题嗤之以鼻,觉得这个不是我们应该关心的内容,**这种想法是错的。** -CDN 是我们系统的门面,其缓存的静态数据,如图片和视频数据的请求量很可能是接口请求数据的几倍甚至更高,一旦发生故障,对于整体系统的影响是巨大的。另外 CDN 的带宽历来是我们研发成本的大头, **尤其是目前处于小视频和直播风口上,** 大量的小视频和直播研发团队都在绞尽脑汁地减少 CDN 的成本。由此看出,CDN 是我们整体系统至关重要的组成部分,而它作为一种特殊的缓存,其命中率和可用性也是我们服务端开发人员需要重点关注的指标。 +CDN 是我们系统的门面,其缓存的静态数据,如图片和视频数据的请求量很可能是接口请求数据的几倍甚至更高,一旦发生故障,对于整体系统的影响是巨大的。另外 CDN 的带宽历来是我们研发成本的大头,**尤其是目前处于小视频和直播风口上,** 大量的小视频和直播研发团队都在绞尽脑汁地减少 CDN 的成本。由此看出,CDN 是我们整体系统至关重要的组成部分,而它作为一种特殊的缓存,其命中率和可用性也是我们服务端开发人员需要重点关注的指标。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25417\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25417\350\256\262.md" index 066f705c9..d79154e0c 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25417\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25417\350\256\262.md" @@ -2,13 +2,13 @@ 你好,我是唐扬。 -在课程一开始,我就带你了解了高并发系统设计的三个目标:性能、可用性和可扩展性,而在提升系统性能方面,我们一直关注的是系统的查询性能。也用了很多的篇幅去讲解数据库的分布式改造,各类缓存的原理和使用技巧。 **究其原因在于,** 我们遇到的大部分场景都是读多写少, **尤其是在一个系统的初级阶段。** +在课程一开始,我就带你了解了高并发系统设计的三个目标:性能、可用性和可扩展性,而在提升系统性能方面,我们一直关注的是系统的查询性能。也用了很多的篇幅去讲解数据库的分布式改造,各类缓存的原理和使用技巧。**究其原因在于,** 我们遇到的大部分场景都是读多写少,**尤其是在一个系统的初级阶段。** 比如说,一个社区的系统初期一定是只有少量的种子用户在生产内容,而大部分的用户都在“围观”别人在说什么。此时,整体的流量比较小,而写流量可能只占整体流量的百分之一,那么即使整体的 QPS 到了 10000 次 / 秒,写请求也只是到了每秒 100 次,如果要对写请求做性能优化,它的性价比确实不太高。 但是,随着业务的发展,你可能会遇到一些存在 **高并发写请求的场景,其中秒杀抢购就是最典型的场景。** 假设你的商城策划了一期秒杀活动,活动在第五天的 00:00 开始,仅限前 200 名,那么秒杀即将开始时,后台会显示用户正在疯狂地刷新 APP 或者浏览器来保证自己能够尽量早的看到商品。 -这时,你面对的依旧是读请求过高, **那么应对的措施有哪些呢?** 因为用户查询的是少量的商品数据,属于查询的热点数据,你可以采用缓存策略,将请求尽量挡在上层的缓存中,能被静态化的数据,比如说商城里的图片和视频数据,尽量做到静态化,这样就可以命中 CDN 节点缓存,减少 Web 服务器的查询量和带宽负担。Web 服务器比如 Nginx 可以直接访问分布式缓存节点,这样可以避免请求到达 Tomcat 等业务服务器。 +这时,你面对的依旧是读请求过高,**那么应对的措施有哪些呢?** 因为用户查询的是少量的商品数据,属于查询的热点数据,你可以采用缓存策略,将请求尽量挡在上层的缓存中,能被静态化的数据,比如说商城里的图片和视频数据,尽量做到静态化,这样就可以命中 CDN 节点缓存,减少 Web 服务器的查询量和带宽负担。Web 服务器比如 Nginx 可以直接访问分布式缓存节点,这样可以避免请求到达 Tomcat 等业务服务器。 当然,你可以加上一些限流的策略,比如,对于短时间之内来自某一个用户、某一个 IP 或者某一台设备的重复请求做丢弃处理。 @@ -18,7 +18,7 @@ ## 我所理解的消息队列 -关于消息队列是什么,你可能有所了解了,所以有关它的概念讲解,就不是本节课的重点,这里只聊聊我自己对消息队列的看法。在我历年的工作经历中,我一直把消息队列看作暂时存储数据的一个容器,认为消息队列是一个平衡低速系统和高速系统处理任务时间差的工具, **我给你举个形象的例子。** 比方说,古代的臣子经常去朝见皇上陈述一些国家大事,等着皇上拍板做决策。但是大臣很多,如果同时去找皇上,你说一句我说一句,皇上肯定会崩溃。后来变成臣子到了午门之后要原地等着皇上将他们一个一个地召见进大殿商议国事,这样就可以缓解皇上处理事情的压力了。你可以把午门看作一个暂时容纳臣子的容器,也就是我们所说的消息队列。 +关于消息队列是什么,你可能有所了解了,所以有关它的概念讲解,就不是本节课的重点,这里只聊聊我自己对消息队列的看法。在我历年的工作经历中,我一直把消息队列看作暂时存储数据的一个容器,认为消息队列是一个平衡低速系统和高速系统处理任务时间差的工具,**我给你举个形象的例子。** 比方说,古代的臣子经常去朝见皇上陈述一些国家大事,等着皇上拍板做决策。但是大臣很多,如果同时去找皇上,你说一句我说一句,皇上肯定会崩溃。后来变成臣子到了午门之后要原地等着皇上将他们一个一个地召见进大殿商议国事,这样就可以缓解皇上处理事情的压力了。你可以把午门看作一个暂时容纳臣子的容器,也就是我们所说的消息队列。 其实,你在一些组件中都会看到消息队列的影子: @@ -38,7 +38,7 @@ 刚才提到,在秒杀场景下,短时间之内数据库的写流量会很高,那么依照我们以前的思路应该对数据做分库分表。如果已经做了分库分表,那么就需要扩展更多的数据库来应对更高的写流量。但是无论是分库分表,还是扩充更多的数据库,都会比较复杂,原因是你需要将数据库中的数据做迁移,这个时间就要按天甚至按周来计算了。 -而在秒杀场景下,高并发的写请求并不是持续的,也不是经常发生的,而只有在秒杀活动开始后的几秒或者十几秒时间内才会存在。为了应对这十几秒的瞬间写高峰,就要花费几天甚至几周的时间来扩容数据库,再在秒杀之后花费几天的时间来做缩容, **这无疑是得不偿失的。** **所以,我们的思路是:** 将秒杀请求暂存在消息队列中,然后业务服务器会响应用户“秒杀结果正在计算中”,释放了系统资源之后再处理其它用户的请求。 +而在秒杀场景下,高并发的写请求并不是持续的,也不是经常发生的,而只有在秒杀活动开始后的几秒或者十几秒时间内才会存在。为了应对这十几秒的瞬间写高峰,就要花费几天甚至几周的时间来扩容数据库,再在秒杀之后花费几天的时间来做缩容,**这无疑是得不偿失的。** **所以,我们的思路是:** 将秒杀请求暂存在消息队列中,然后业务服务器会响应用户“秒杀结果正在计算中”,释放了系统资源之后再处理其它用户的请求。 我们会在后台启动若干个队列处理程序,消费消息队列中的消息,再执行校验库存、下单等逻辑。因为只有有限个队列处理线程在执行,所以落入后端数据库上的并发请求是有限的。而请求是可以在消息队列中被短暂地堆积,当库存被消耗完之后,消息队列中堆积的请求就可以被丢弃了。 @@ -68,7 +68,7 @@ 比如数据团队对你说,在秒杀活动之后想要统计活动的数据,借此来分析活动商品的受欢迎程度、购买者人群的特点以及用户对于秒杀互动的满意程度等等指标。而我们需要将大量的数据发送给数据团队,那么要怎么做呢? -**一个思路是:** 可以使用 HTTP 或者 RPC 的方式来同步地调用,也就是数据团队这边提供一个接口,我们实时将秒杀的数据推送给它, **但是这样调用会有两个问题:** 整体系统的耦合性比较强,当数据团队的接口发生故障时,会影响到秒杀系统的可用性。 +**一个思路是:** 可以使用 HTTP 或者 RPC 的方式来同步地调用,也就是数据团队这边提供一个接口,我们实时将秒杀的数据推送给它,**但是这样调用会有两个问题:** 整体系统的耦合性比较强,当数据团队的接口发生故障时,会影响到秒杀系统的可用性。 当数据系统需要新的字段,就要变更接口的参数,那么秒杀系统也要随着一起变更。 @@ -92,6 +92,6 @@ 解耦合可以提升你的整体系统的鲁棒性。 -当然,你要知道,在使用消息队列之后虽然可以解决现有的问题,但是系统的复杂度也会上升。比如上面提到的业务流程中,同步流程和异步流程的边界在哪里?消息是否会丢失,是否会重复?请求的延迟如何能够减少?消息接收的顺序是否会影响到业务流程的正常执行?如果消息处理流程失败了之后是否需要补发? **这些问题都是我们需要考虑的。** 我会利用接下来的两节课,针对最主要的两个问题来讲讲解决思路:一个是如何处理消息的丢失和重复,另一个是如何减少消息的延迟。 +当然,你要知道,在使用消息队列之后虽然可以解决现有的问题,但是系统的复杂度也会上升。比如上面提到的业务流程中,同步流程和异步流程的边界在哪里?消息是否会丢失,是否会重复?请求的延迟如何能够减少?消息接收的顺序是否会影响到业务流程的正常执行?如果消息处理流程失败了之后是否需要补发?**这些问题都是我们需要考虑的。** 我会利用接下来的两节课,针对最主要的两个问题来讲讲解决思路:一个是如何处理消息的丢失和重复,另一个是如何减少消息的延迟。 引入了消息队列的同时也会引入了新的问题,需要新的方案来解决,这就是系统设计的挑战,也是系统设计独有的魅力,而我们也会在这些挑战中不断提升技术能力和系统设计能力。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25418\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25418\350\256\262.md" index 19e521f7b..2a6c813a5 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25418\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25418\350\256\262.md" @@ -2,13 +2,13 @@ 你好,我是唐扬。 -经过上一节课,我们在电商系统中增加了消息队列,用它来对峰值写流量做削峰填谷,对次要的业务逻辑做异步处理,对不同的系统模块做解耦合。因为业务逻辑从同步代码中移除了,所以,我们也要有相应的队列处理程序来处理消息、执行业务逻辑, **这时,你的系统架构变成了下面的样子:** +经过上一节课,我们在电商系统中增加了消息队列,用它来对峰值写流量做削峰填谷,对次要的业务逻辑做异步处理,对不同的系统模块做解耦合。因为业务逻辑从同步代码中移除了,所以,我们也要有相应的队列处理程序来处理消息、执行业务逻辑,**这时,你的系统架构变成了下面的样子:** ![img](assets/c9f44acbc4025b2ff1f0a4b9fd0941a6.jpg) 这是一个简化版的架构图,实际上,随着业务逻辑越来越复杂,会引入更多的外部系统和服务来解决业务上的问题。比如说,我们会引入 Elasticsearch 来解决商品和店铺搜索的问题,也会引入审核系统,来对售卖的商品、用户的评论做自动的和人工的审核,你会越来越多地使用消息队列与外部系统解耦合,以及提升系统性能。 -比如说,你的电商系统需要上一个新的红包功能:用户在购买一定数量的商品之后,由你的系统给用户发一个现金的红包,鼓励用户消费。由于发放红包的过程不应该在购买商品的主流程之内,所以你考虑使用消息队列来异步处理。 **这时,你发现了一个问题:** 如果消息在投递的过程中发生丢失,那么用户就会因为没有得到红包而投诉。相反,如果消息在投递的过程中出现了重复,那么你的系统就会因为发送两个红包而损失。 +比如说,你的电商系统需要上一个新的红包功能:用户在购买一定数量的商品之后,由你的系统给用户发一个现金的红包,鼓励用户消费。由于发放红包的过程不应该在购买商品的主流程之内,所以你考虑使用消息队列来异步处理。**这时,你发现了一个问题:** 如果消息在投递的过程中发生丢失,那么用户就会因为没有得到红包而投诉。相反,如果消息在投递的过程中出现了重复,那么你的系统就会因为发送两个红包而损失。 那么我们如何保证,产生的消息一定会被消费到,并且只被消费一次呢?这个问题虽然听起来很浅显,很好理解,但是实际上却藏着很多玄机,本节课我就带你深入探讨。 @@ -36,15 +36,15 @@ 不过,这种方案可能会造成消息的重复,从而导致在消费的时候会重复消费同样的消息。比方说,消息生产时由于消息队列处理慢或者网络的抖动,导致虽然最终写入消息队列成功,但在生产端却超时了,生产者重传这条消息就会形成重复的消息,那么针对上面的例子,直观显示在你面前的就会是你收到了两个现金红包。 -那么消息发送到了消息队列之后是否就万无一失了呢?当然不是, **在消息队列中消息仍然有丢失的风险。** #### 2. 在消息队列中丢失消息 +那么消息发送到了消息队列之后是否就万无一失了呢?当然不是,**在消息队列中消息仍然有丢失的风险。** #### 2. 在消息队列中丢失消息 拿 Kafka 举例,消息在 Kafka 中是存储在本地磁盘上的,而为了减少消息存储时对磁盘的随机 I/O,我们一般会将消息先写入到操作系统的 Page Cache 中,然后再找合适的时机刷新到磁盘上。 -比如,Kafka 可以配置当达到某一时间间隔,或者累积一定的消息数量的时候再刷盘, **也就是所说的异步刷盘。** 来看一个形象的比喻:假如你经营一个图书馆,读者每还一本书你都要去把图书归位,不仅工作量大而且效率低下,但是如果你可以选择每隔 3 小时,或者图书达到一定数量的时候再把图书归位,这样可以把同一类型的书一起归位,节省了查找图书位置的时间,这样就可以提高效率了。 +比如,Kafka 可以配置当达到某一时间间隔,或者累积一定的消息数量的时候再刷盘,**也就是所说的异步刷盘。** 来看一个形象的比喻:假如你经营一个图书馆,读者每还一本书你都要去把图书归位,不仅工作量大而且效率低下,但是如果你可以选择每隔 3 小时,或者图书达到一定数量的时候再把图书归位,这样可以把同一类型的书一起归位,节省了查找图书位置的时间,这样就可以提高效率了。 -不过,如果发生机器掉电或者机器异常重启,那么 Page Cache 中还没有来得及刷盘的消息就会丢失了。 **那么怎么解决呢?** 你可能会把刷盘的间隔设置很短,或者设置累积一条消息就就刷盘,但这样频繁刷盘会对性能有比较大的影响,而且从经验来看,出现机器宕机或者掉电的几率也不高, **所以我不建议你这样做。** ![img](assets/6c667c8c21baf27468c314105e522243.jpg) +不过,如果发生机器掉电或者机器异常重启,那么 Page Cache 中还没有来得及刷盘的消息就会丢失了。**那么怎么解决呢?** 你可能会把刷盘的间隔设置很短,或者设置累积一条消息就就刷盘,但这样频繁刷盘会对性能有比较大的影响,而且从经验来看,出现机器宕机或者掉电的几率也不高,**所以我不建议你这样做。**![img](assets/6c667c8c21baf27468c314105e522243.jpg) -如果你的电商系统对消息丢失的容忍度很低, **那么你可以考虑以集群方式部署 Kafka 服务,通过部署多个副本备份数据,保证消息尽量不丢失。** 那么它是怎么实现的呢? +如果你的电商系统对消息丢失的容忍度很低,**那么你可以考虑以集群方式部署 Kafka 服务,通过部署多个副本备份数据,保证消息尽量不丢失。** 那么它是怎么实现的呢? Kafka 集群中有一个 Leader 负责消息的写入和消费,可以有多个 Follower 负责数据的备份。Follower 中有一个特殊的集合叫做 ISR(in-sync replicas),当 Leader 故障时,新选举出来的 Leader 会从 ISR 中选择,默认 Leader 的数据会异步地复制给 Follower,这样在 Leader 发生掉电或者宕机时,Kafka 会从 Follower 中消费消息,减少消息丢失的可能。 @@ -52,7 +52,7 @@ Kafka 集群中有一个 Leader 负责消息的写入和消费,可以有多个 ![img](assets/648951000b3c7e969f8d04e42da6ac3f.jpg) -从上面这张图来看,当设置“acks=all”时,需要同步执行 1,3,4 三个步骤,对于消息生产的性能来说也是有比较大的影响的,所以你在实际应用中需要仔细地权衡考量。 **我给你的建议是:** +从上面这张图来看,当设置“acks=all”时,需要同步执行 1,3,4 三个步骤,对于消息生产的性能来说也是有比较大的影响的,所以你在实际应用中需要仔细地权衡考量。**我给你的建议是:** \\1. 如果你需要确保消息一条都不能丢失,那么建议不要开启消息队列的同步刷盘,而是需要使用集群的方式来解决,可以配置当所有 ISR Follower 都接收到消息才返回成功。 @@ -82,7 +82,7 @@ Kafka 集群中有一个 Leader 负责消息的写入和消费,可以有多个 比如,男生和女生吵架,女生抓住一个点不放,传递“你不在乎我了吗?”(生产消息)的信息。那么当多次埋怨“你不在乎我了吗?”的时候(多次生产相同消息),她不知道的是,男生的耳朵(消息处理)会自动把 N 多次的信息屏蔽,就像只听到一次一样,这就是幂等性。 -如果我们消费一条消息的时候,要给现有的库存数量减 1,那么如果消费两条相同的消息就会给库存数量减 2,这就不是幂等的。而如果消费一条消息后,处理逻辑是将库存的数量设置为 0,或者是如果当前库存数量是 10 时则减 1,这样在消费多条消息时,所得到的结果就是相同的, **这就是幂等的。** **说白了,你可以这么理解“幂等”:** 一件事儿无论做多少次都和做一次产生的结果是一样的,那么这件事儿就具有幂等性。 +如果我们消费一条消息的时候,要给现有的库存数量减 1,那么如果消费两条相同的消息就会给库存数量减 2,这就不是幂等的。而如果消费一条消息后,处理逻辑是将库存的数量设置为 0,或者是如果当前库存数量是 10 时则减 1,这样在消费多条消息时,所得到的结果就是相同的,**这就是幂等的。** **说白了,你可以这么理解“幂等”:** 一件事儿无论做多少次都和做一次产生的结果是一样的,那么这件事儿就具有幂等性。 #### 2. 在生产、消费过程中增加消息幂等性的保证 @@ -98,7 +98,7 @@ Kafka 集群中有一个 Leader 负责消息的写入和消费,可以有多个 在通用层面,你可以在消息被生产的时候,使用发号器给它生成一个全局唯一的消息 ID,消息被处理之后,把这个 ID 存储在数据库中,在处理下一条消息之前,先从数据库里面查询这个全局 ID 是否被消费过,如果被消费过就放弃消费。 -你可以看到,无论是生产端的幂等性保证方式,还是消费端通用的幂等性保证方式,它们的共同特点都是为每一个消息生成一个唯一的 ID,然后在使用这个消息的时候,先比对这个 ID 是否已经存在,如果存在,则认为消息已经被使用过。所以这种方式是一种标准的实现幂等的方式, **你在项目之中可以拿来直接使用,** 它在逻辑上的伪代码就像下面这样: +你可以看到,无论是生产端的幂等性保证方式,还是消费端通用的幂等性保证方式,它们的共同特点都是为每一个消息生成一个唯一的 ID,然后在使用这个消息的时候,先比对这个 ID 是否已经存在,如果存在,则认为消息已经被使用过。所以这种方式是一种标准的实现幂等的方式,**你在项目之中可以拿来直接使用,** 它在逻辑上的伪代码就像下面这样: ```plaintext boolean isIDExisted = selectByID(ID); // 判断 ID 是否存在 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25419\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25419\350\256\262.md" index e8c258b89..b1b72d613 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25419\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25419\350\256\262.md" @@ -2,11 +2,11 @@ 你好,我是唐扬。 -学完前面两节课之后,相信你对在垂直电商项目中,如何使用消息队列应对秒杀时的峰值流量已经有所了解。当然了,你也应该知道要如何做,才能保证消息不会丢失,尽量避免消息重复带来的影响。 **那么我想让你思考一下:** 除了这些内容,你在使用消息队列时还需要关注哪些点呢? +学完前面两节课之后,相信你对在垂直电商项目中,如何使用消息队列应对秒杀时的峰值流量已经有所了解。当然了,你也应该知道要如何做,才能保证消息不会丢失,尽量避免消息重复带来的影响。**那么我想让你思考一下:** 除了这些内容,你在使用消息队列时还需要关注哪些点呢? **先来看一个场景:** 在你的垂直电商项目中,你会在用户下单支付之后,向消息队列里面发送一条消息,队列处理程序消费了消息后,会增加用户的积分,或者给用户发送优惠券。那么用户在下单之后,等待几分钟或者十几分钟拿到积分和优惠券是可以接受的,但是一旦消息队列出现大量堆积,用户消费完成后几小时还拿到优惠券,那就会有用户投诉了。 -这时,你要关注的就是消息队列中,消息的延迟了,这其实是消费性能的问题,那么你要如何提升消费性能,保证更短的消息延迟呢? **在我看来,** 你首先需要掌握如何来监控消息的延迟,因为有了数据之后,你才可以知道目前的延迟数据是否满足要求,也可以评估优化之后的效果。然后,你要掌握使用消息队列的正确姿势,以及关注消息队列本身是如何保证消息尽快被存储和投递的。 +这时,你要关注的就是消息队列中,消息的延迟了,这其实是消费性能的问题,那么你要如何提升消费性能,保证更短的消息延迟呢?**在我看来,** 你首先需要掌握如何来监控消息的延迟,因为有了数据之后,你才可以知道目前的延迟数据是否满足要求,也可以评估优化之后的效果。然后,你要掌握使用消息队列的正确姿势,以及关注消息队列本身是如何保证消息尽快被存储和投递的。 接下来,我们先来看看第一点:如何监控消息延迟。 @@ -46,15 +46,15 @@ 第五列就是消费消息的堆积数(也就是第四列与第三列的差值)。 -通过这个命令你可以很方便地了解消费者的消费情况。 **其次,第二个工具是 JMX。** Kafka 通过 JMX 暴露了消息堆积的数据,我在本地启动了一个 console consumer,然后使用 jconsole 连接这个 consumer,你就可以看到这个 consumer 的堆积数据了(就是下图中红框里的数据)。这些数据你可以写代码来获取,这样也可以方便地输出到监控系统中, **我比较推荐这种方式。** ![img](assets/3384d3fcb52f98815fac667e5b543e2c.jpg) +通过这个命令你可以很方便地了解消费者的消费情况。**其次,第二个工具是 JMX。** Kafka 通过 JMX 暴露了消息堆积的数据,我在本地启动了一个 console consumer,然后使用 jconsole 连接这个 consumer,你就可以看到这个 consumer 的堆积数据了(就是下图中红框里的数据)。这些数据你可以写代码来获取,这样也可以方便地输出到监控系统中,**我比较推荐这种方式。**![img](assets/3384d3fcb52f98815fac667e5b543e2c.jpg) -除了使用消息队列提供的工具以外,你还可以通过生成监控消息的方式,来监控消息的延迟。 **具体怎么做呢?** +除了使用消息队列提供的工具以外,你还可以通过生成监控消息的方式,来监控消息的延迟。**具体怎么做呢?** 你先定义一种特殊的消息,然后启动一个监控程序,将这个消息定时地循环写入到消息队列中,消息的内容可以是生成消息的时间戳,并且也会作为队列的消费者消费数据。业务处理程序消费到这个消息时直接丢弃掉,而监控程序在消费到这个消息时,就可以和这个消息的生成时间做比较,如果时间差达到某一个阈值就可以向我们报警。 ![img](assets/34820c0b27e66af37fda116a1a98347f.jpg) -这两种方式都可以监控消息的消费延迟情况, **而从我的经验出来,我比较推荐两种方式结合来使用。** 比如在我的实际项目中,我会优先在监控程序中获取 JMX 中的队列堆积数据,做到 dashboard 报表中,同时也会启动探测进程,确认消息的延迟情况是怎样的。 +这两种方式都可以监控消息的消费延迟情况,**而从我的经验出来,我比较推荐两种方式结合来使用。** 比如在我的实际项目中,我会优先在监控程序中获取 JMX 中的队列堆积数据,做到 dashboard 报表中,同时也会启动探测进程,确认消息的延迟情况是怎样的。 在我看来,消息的堆积是对于消息队列的基础监控,这是你无论如何都要做的。但是,了解了消息的堆积情况,并不能很直观地了解消息消费的延迟,你也只能利用经验来确定堆积的消息量到了多少才会影响到用户的体验;而第二种方式对于消费延迟的监控则更加直观,而且从时间的维度来做监控也比较容易确定报警阈值。 @@ -90,7 +90,7 @@ 所以,你在写消费客户端的时候要考虑这种场景,拉取不到消息可以等待一段时间再来拉取,等待的时间不宜过长,否则会增加消息的延迟。我一般建议固定的 10ms~100ms,也可以按照一定步长递增,比如第一次拉取不到消息等待 10ms,第二次 20ms,最长可以到 100ms,直到拉取到消息再回到 10ms。 -说完了消费端的做法之后, **再来说说消息队列本身在读取性能优化方面做了哪些事情。** +说完了消费端的做法之后,**再来说说消息队列本身在读取性能优化方面做了哪些事情。** 我曾经也做过一个消息中间件,在最初设计中间件的时候,我主要从两方面考虑读取性能问题: @@ -126,6 +126,6 @@ 选择高性能的数据存储方式,配合零拷贝技术,可以提升消息的消费性能。 -其实,队列是一种常用的组件,只要涉及到队列,任务的堆积就是一个不可忽视的问题, **我遇到过的很多故障都是源于此。** +其实,队列是一种常用的组件,只要涉及到队列,任务的堆积就是一个不可忽视的问题,**我遇到过的很多故障都是源于此。** 比如说,前一段时间处理的一个故障,前期只是因为数据库性能衰减有少量的慢请求,结果这些慢请求占满了 Tomcat 线程池,导致整体服务的不可用。如果我们能对 Tomcat 线程池的任务堆积情况有实时地监控,或者说对线程池有一些保护策略,比方说线程全部使用之后丢弃请求,也许就会避免故障的发生。在此,我希望你在实际的工作中能够引以为戒,只要有队列就要监控它的堆积情况,把问题消灭在萌芽之中。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25421\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25421\350\256\262.md" index d8fb40816..1843a364e 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25421\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25421\350\256\262.md" @@ -78,7 +78,7 @@ 再比如,我们在做社区业务的时候,会有多个模块需要使用地理位置服务,将 IP 信息或者经纬度信息,转换为城市信息。比如,推荐内容的时候,可以结合用户的城市信息,做附近内容的推荐;展示内容信息的时候,也需要展示城市信息等等。 -那么,如果每一个模块都实现这么一套逻辑就会导致代码不够重用。因此,我们可以把将 IP 信息或者经纬度信息,转换为城市信息,包装成单独的服务供其它模块调用,也就是, **我们可以将与业务无关的公用服务抽取出来,下沉成单独的服务。** +那么,如果每一个模块都实现这么一套逻辑就会导致代码不够重用。因此,我们可以把将 IP 信息或者经纬度信息,转换为城市信息,包装成单独的服务供其它模块调用,也就是,**我们可以将与业务无关的公用服务抽取出来,下沉成单独的服务。** 按照以上两种拆分方式将系统拆分之后,每一个服务的功能内聚,维护人员职责明确,增加了新的功能只需要测试自己的服务就可以了,而一旦服务出了问题,也可以通过服务熔断、降级的方式减少对于其他服务的影响(我会在第 34 讲中系统地讲解)。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25422\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25422\350\256\262.md" index 6c030fcfa..071435f51 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25422\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25422\350\256\262.md" @@ -36,11 +36,11 @@ **原则二,** 你需要关注服务拆分的粒度,先粗略拆分,再逐渐细化。在服务拆分的初期,你其实很难确定,服务究竟要拆分成什么样。但是,从“微服务”这几个字来看,服务的粒度貌似应该足够小,甚至有“一方法一服务”的说法。不过,服务多了也会带来问题,像是服务个数的增加会增加运维的成本。再比如,原本一次请求只需要调用进程内的多个方法,现在则需要跨网络调用多个 RPC 服务,在性能上肯定会有所下降。 -**所以我推荐的做法是:** 拆分初期可以把服务粒度拆的粗一些,后面随着团队对于业务和微服务理解的加深,再考虑把服务粒度细化。 **比如说,** 对于一个社区系统来说,你可以先把和用户关系相关的业务逻辑,都拆分到用户关系服务中,之后,再把比如黑名单的逻辑独立成黑名单服务。 +**所以我推荐的做法是:** 拆分初期可以把服务粒度拆的粗一些,后面随着团队对于业务和微服务理解的加深,再考虑把服务粒度细化。**比如说,** 对于一个社区系统来说,你可以先把和用户关系相关的业务逻辑,都拆分到用户关系服务中,之后,再把比如黑名单的逻辑独立成黑名单服务。 **原则三,** 拆分的过程,要尽量避免影响产品的日常功能迭代,也就是说,要一边做产品功能迭代,一边完成服务化拆分。 -**还是拿我之前维护的一个项目举例。** 我曾经在竞品对手快速发展的时期做了服务的拆分,拆分的方式是停掉所有业务开发,全盘推翻重构,结果错失了产品发展的最佳机会,最终败给了竞争对手。因此,我们的拆分只能在现有一体化系统的基础上,不断剥离业务独立部署, **剥离的顺序,你可以参考以下几点:** +**还是拿我之前维护的一个项目举例。** 我曾经在竞品对手快速发展的时期做了服务的拆分,拆分的方式是停掉所有业务开发,全盘推翻重构,结果错失了产品发展的最佳机会,最终败给了竞争对手。因此,我们的拆分只能在现有一体化系统的基础上,不断剥离业务独立部署,**剥离的顺序,你可以参考以下几点:** \\1. 优先剥离比较独立的边界服务(比如短信服务、地理位置服务),从非核心的服务出发,减少拆分对现有业务的影响,也给团队一个练习、试错的机会; @@ -60,15 +60,15 @@ 微服务化只是一种架构手段,有效拆分后,可以帮助实现服务的敏捷开发和部署。但是,由于将原本一体化架构的应用,拆分成了,多个通过网络通信的分布式服务,为了在分布式环境下,协调多个服务正常运行,就必然引入一定的复杂度,这些复杂度主要体现在以下几个方面: -\\1. 服务接口的调用,不再是同一进程内的方法调用,而是跨进程的网络调用,这会增加接口响应时间的增加。此时,我们就要选择高效的服务调用框架,同时,接口调用方需要知道服务部署在哪些机器的哪个端口上,这些信息需要存储在一个分布式一致性的存储中, **于是就需要引入服务注册中心,** 这一点,是我在 24 讲会提到的内容。 **不过在这里我想强调的是,** 注册中心管理的是服务完整的生命周期,包括对于服务存活状态的检测。 +\\1. 服务接口的调用,不再是同一进程内的方法调用,而是跨进程的网络调用,这会增加接口响应时间的增加。此时,我们就要选择高效的服务调用框架,同时,接口调用方需要知道服务部署在哪些机器的哪个端口上,这些信息需要存储在一个分布式一致性的存储中,**于是就需要引入服务注册中心,** 这一点,是我在 24 讲会提到的内容。**不过在这里我想强调的是,** 注册中心管理的是服务完整的生命周期,包括对于服务存活状态的检测。 \\2. 多个服务之间有着错综复杂的依赖关系。一个服务会依赖多个其它服务,也会被多个服务所依赖,那么一旦被依赖的服务的性能出现问题,产生大量的慢请求,就会导致依赖服务的工作线程池中的线程被占满,那么依赖的服务也会出现性能问题。接下来,问题就会沿着依赖网,逐步向上蔓延,直到整个系统出现故障为止。 -为了避免这种情况的发生, **我们需要引入服务治理体系,** 针对出问题的服务,采用熔断、降级、限流、超时控制的方法,使得问题被限制在单一服务中,保护服务网络中的其它服务不受影响。 +为了避免这种情况的发生,**我们需要引入服务治理体系,** 针对出问题的服务,采用熔断、降级、限流、超时控制的方法,使得问题被限制在单一服务中,保护服务网络中的其它服务不受影响。 \\3. 服务拆分到多个进程后,一条请求的调用链路上,涉及多个服务,那么一旦这个请求的响应时间增长,或者是出现错误,我们就很难知道,是哪一个服务出现的问题。另外,整体系统一旦出现故障,很可能外在的表现是所有服务在同一时间都出现了问题,你在问题定位时,很难确认哪一个服务是源头,这就需要 **引入分布式追踪工具,以及更细致的服务端监控报表。** -我在 25 讲和 30 讲会详细的剖析这个内容, **在这里我想强调的是,** 监控报表关注的是,依赖服务和资源的宏观性能表现;分布式追踪关注的是,单一慢请求中的性能瓶颈分析,两者需要结合起来帮助你来排查问题。 +我在 25 讲和 30 讲会详细的剖析这个内容,**在这里我想强调的是,** 监控报表关注的是,依赖服务和资源的宏观性能表现;分布式追踪关注的是,单一慢请求中的性能瓶颈分析,两者需要结合起来帮助你来排查问题。 以上这些微服务化后,在开发方面引入的问题,就是接下来,“分布式服务篇”和“维护篇”的主要讨论内容。 @@ -86,12 +86,12 @@ 1.“康威定律”提到,设计系统的组织,其产生的设计等同于组织间的沟通结构。通俗一点说,就是你的团队组织结构是什么样的,你的架构就会长成什么样。 -如果你的团队分为服务端开发团队,DBA 团队,运维团队,测试团队,那么你的架构就是一体化的,所有的团队共同为一个大系统负责,团队内成员众多,沟通成本就会很高;而如果你想实现微服务化的架构, **那么你的团队也要按照业务边界拆分,** 每一个模块由一个自治的小团队负责,这个小团队里面有开发、测试、运维和 DBA,这样沟通就只发生在这个小团队内部,沟通的成本就会明显降低。 +如果你的团队分为服务端开发团队,DBA 团队,运维团队,测试团队,那么你的架构就是一体化的,所有的团队共同为一个大系统负责,团队内成员众多,沟通成本就会很高;而如果你想实现微服务化的架构,**那么你的团队也要按照业务边界拆分,** 每一个模块由一个自治的小团队负责,这个小团队里面有开发、测试、运维和 DBA,这样沟通就只发生在这个小团队内部,沟通的成本就会明显降低。 \\2. 微服务化的一个目标是减少研发的成本,其中也包括沟通的成本,所以小团队内部成员不宜过多。 按照亚马逊 CEO,贝佐斯的“两个披萨”的理论,如果两个披萨不够你的团队吃,那么你的团队就太大了,需要拆分,所以一个小团队包括开发、运维、测试以 6~8 个人为最佳; -\\3. 如果你的团队人数不多,还没有做好微服务化的准备,而你又感觉到研发和部署的成本确实比较高,那么一个折中的方案是, **你可以优先做工程的拆分。** +\\3. 如果你的团队人数不多,还没有做好微服务化的准备,而你又感觉到研发和部署的成本确实比较高,那么一个折中的方案是,**你可以优先做工程的拆分。** 比如说,如果你使用的是 Java 语言,你可以依据业务的边界,将代码拆分到不同的子工程中,然后子工程之间以 jar 包的方式依赖,这样每个子工程代码量减少,可以减少打包时间;并且子工程代码内部,可以做到高内聚低耦合,一定程度上减少研发的成本,也不失为一个不错的保守策略。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25423\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25423\350\256\262.md" index 559ce5bf1..5c6d6cdd5 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25423\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25423\350\256\262.md" @@ -20,9 +20,9 @@ 说到 RPC(Remote Procedure Call,远程过程调用),你不会陌生,它指的是通过网络,调用另一台计算机上部署服务的技术。 -而 RPC 框架就封装了网络调用的细节,让你像调用本地服务一样,调用远程部署的服务。你也许觉得只有像 Dubbo、Grpc、Thrift 这些新兴的框架才算是 RPC 框架, **其实严格来说,你很早之前就接触到与 RPC 相关的技术了。** 比如,Java 原生就有一套远程调用框架 **叫做 RMI(Remote Method Invocation),** 它可以让 Java 程序通过网络,调用另一台机器上的 Java 对象的方法。它是一种远程调用的方法,也是 J2EE 时代大名鼎鼎的 EJB 的实现基础。 +而 RPC 框架就封装了网络调用的细节,让你像调用本地服务一样,调用远程部署的服务。你也许觉得只有像 Dubbo、Grpc、Thrift 这些新兴的框架才算是 RPC 框架,**其实严格来说,你很早之前就接触到与 RPC 相关的技术了。** 比如,Java 原生就有一套远程调用框架 **叫做 RMI(Remote Method Invocation),** 它可以让 Java 程序通过网络,调用另一台机器上的 Java 对象的方法。它是一种远程调用的方法,也是 J2EE 时代大名鼎鼎的 EJB 的实现基础。 -时至今日,你仍然可以通过 Spring 的“RmiServiceExporter”将 Spring 管理的 bean 暴露成一个 RMI 的服务,从而继续使用 RMI 来实现跨进程的方法调用。之所以 RMI 没有像 Dubbo,Grpc 一样大火, **是因为它存在着一些缺陷:** +时至今日,你仍然可以通过 Spring 的“RmiServiceExporter”将 Spring 管理的 bean 暴露成一个 RMI 的服务,从而继续使用 RMI 来实现跨进程的方法调用。之所以 RMI 没有像 Dubbo,Grpc 一样大火,**是因为它存在着一些缺陷:** RMI 使用专为 Java 远程对象定制的协议 JRMP(Java Remote Messaging Protocol)进行通信,这限制了它的通信双方,只能是 Java 语言的程序,无法实现跨语言通信; @@ -32,13 +32,13 @@ RMI 使用 Java 原生的对象序列化方式,生成的字节数组空间较 **借上面几个例子,我主要是想告诉你,** RPC 并不是互联网时代的产物,也不是服务化之后才衍生出来的技术,而是一种规范,只要是封装了网络调用的细节,能够实现远程调用其他服务,就可以算作是一种 RPC 技术了。 -那么你的垂直电商项目在使用 RPC 框架之后, **会产生什么变化呢?** 在我来看,在性能上的变化是不可忽视的, **我给你举个例子。** 比方说,你的电商系统中,商品详情页面需要商品数据、评论数据还有店铺数据,如果在一体化的架构中,你只需要从商品库,评论库和店铺库获取数据就可以了,不考虑缓存的情况下有三次网络请求。 +那么你的垂直电商项目在使用 RPC 框架之后,**会产生什么变化呢?** 在我来看,在性能上的变化是不可忽视的,**我给你举个例子。** 比方说,你的电商系统中,商品详情页面需要商品数据、评论数据还有店铺数据,如果在一体化的架构中,你只需要从商品库,评论库和店铺库获取数据就可以了,不考虑缓存的情况下有三次网络请求。 但是,如果独立出商品服务、评论服务和店铺服务之后,那么就需要分别调用这三个服务,而这三个服务又会分别调用各自的数据库,这就是六次网络请求。如果你服务拆分的更细粒度,那么多出的网络调用就会越多,请求的延迟就会更长,而这就是你为了提升系统的扩展性,在性能上所付出的代价。 ![img](assets/1dba9b34e2973ec185b353becfc64fce.jpg) -那么,我们要如果优化 RPC 的性能,从而尽量减少网络调用,对于性能的影响呢?在这里,你首先需要了解一次 RPC 的调用都经过了哪些步骤,因为这样,你才可以针对这些步骤中可能存在的性能瓶颈点提出优化方案。 **步骤如下:** 在一次 RPC 调用过程中,客户端首先会将调用的类名、方法名、参数名、参数值等信息,序列化成二进制流; +那么,我们要如果优化 RPC 的性能,从而尽量减少网络调用,对于性能的影响呢?在这里,你首先需要了解一次 RPC 的调用都经过了哪些步骤,因为这样,你才可以针对这些步骤中可能存在的性能瓶颈点提出优化方案。**步骤如下:** 在一次 RPC 调用过程中,客户端首先会将调用的类名、方法名、参数名、参数值等信息,序列化成二进制流; 然后客户端将二进制流,通过网络发送给服务端; @@ -46,7 +46,7 @@ RMI 使用 Java 原生的对象序列化方式,生成的字节数组空间较 服务端将返回值序列化,再通过网络发送给客户端; -客户端对结果反序列化之后,就可以得到调用的结果了。 **过程图如下:** ![img](assets/f98bd80af8a4e7258251db1084e0383e.jpg) +客户端对结果反序列化之后,就可以得到调用的结果了。**过程图如下:**![img](assets/f98bd80af8a4e7258251db1084e0383e.jpg) 从这张图中你可以看到,有网络传输的过程,也有将请求序列化和反序列化的过程, 所以,如果要提升 RPC 框架的性能,需要从 **网络传输和序列化** 两方面来优化。 @@ -94,31 +94,31 @@ RMI 使用 Java 原生的对象序列化方式,生成的字节数组空间较 这五种 I/O 模型中最被广泛使用的是 **多路 I/O 复用,** Linux 系统中的 select、epoll 等系统调用都是支持多路 I/O 复用模型的,Java 中的高性能网络框架 Netty 默认也是使用这种模型。所以,我们可以选择它。 -那么,选择好了一种高性能的 I/O 模型,是不是就能实现,数据在网络上的高效传输呢?其实并没有那么简单,网络性能的调优涉及很多方面, **其中不可忽视的一项就是网络参数的调优,** 接下来,我带你了解其中一个典型例子。当然,你可以结合网络基础知识,以及成熟 RPC 框架(比如 Dubbo)的源码来深入了解,网络参数调优的方方面面。 +那么,选择好了一种高性能的 I/O 模型,是不是就能实现,数据在网络上的高效传输呢?其实并没有那么简单,网络性能的调优涉及很多方面,**其中不可忽视的一项就是网络参数的调优,** 接下来,我带你了解其中一个典型例子。当然,你可以结合网络基础知识,以及成熟 RPC 框架(比如 Dubbo)的源码来深入了解,网络参数调优的方方面面。 -**在之前的项目中,** 我的团队曾经写过一个简单的 RPC 通信框架。在进行测试的时候发现,远程调用一个空业务逻辑的方法时,平均响应时间居然可以到几十毫秒,这明显不符合我们的预期,在我们看来,运行一个空的方法,应该在 1 毫秒之内可以返回。于是,我先在测试的时候使用 tcpdump 抓了包,发现一次请求的 Ack 包竟然要经过 40ms 才返回。在网上 google 了一下原因,发现原因和一个叫做 tcp_nodelay 的参数有关。 **这个参数是什么作用呢?** tcp 协议的包头有 20 字节,ip 协议的包头也有 20 字节,如果仅仅传输 1 字节的数据,在网络上传输的就有 20 + 20 + 1 = 41 字节,其中真正有用的数据只有 1 个字节,这对效率和带宽是极大的浪费。所以在 1984 年的时候,John Nagle 提出了以他的名字命名的 Nagle\`s 算法, **他期望:** +**在之前的项目中,** 我的团队曾经写过一个简单的 RPC 通信框架。在进行测试的时候发现,远程调用一个空业务逻辑的方法时,平均响应时间居然可以到几十毫秒,这明显不符合我们的预期,在我们看来,运行一个空的方法,应该在 1 毫秒之内可以返回。于是,我先在测试的时候使用 tcpdump 抓了包,发现一次请求的 Ack 包竟然要经过 40ms 才返回。在网上 google 了一下原因,发现原因和一个叫做 tcp_nodelay 的参数有关。**这个参数是什么作用呢?** tcp 协议的包头有 20 字节,ip 协议的包头也有 20 字节,如果仅仅传输 1 字节的数据,在网络上传输的就有 20 + 20 + 1 = 41 字节,其中真正有用的数据只有 1 个字节,这对效率和带宽是极大的浪费。所以在 1984 年的时候,John Nagle 提出了以他的名字命名的 Nagle\`s 算法,**他期望:** 如果是连续的小数据包,大小没有一个 MSS(Maximum Segment Size,最大分段大小),并且还没有收到之前发送的数据包的 Ack 信息,那么这些小数据包就会在发送端暂存起来,直到小数据包累积到一个 MSS,或者收到一个 Ack 为止。 -这原本是为了减少不必要的网络传输,但是如果接收端开启了 DelayedACK(延迟 ACK 的发送,这样可以合并多个 ACK,提升网络传输效率), **那就会发生,** 发送端发送第一个数据包后,接收端没有返回 ACK,这时发送端发送了第二个数据包,因为 Nagle\`s 算法的存在,并且第一个发送包的 ACK 还没有返回,所以第二个包会暂存起来。而 DelayedACK 的超时时间,默认是 40ms,所以一旦到了 40ms,接收端回给发送端 ACK,那么发送端才会发送第二个包, **这样就增加了延迟。** **解决的方式非常简单:** 只要在 socket 上开启 tcp_nodelay 就好了,这个参数关闭了 Nagle\`s 算法,这样发送端就不需要等到上一个发送包的 ACK 返回,直接发送新的数据包就好了。这对于强网络交互的场景来说非常的适用,基本上,如果你要自己实现一套网络框架,tcp_nodelay 这个参数最好是要开启的。 +这原本是为了减少不必要的网络传输,但是如果接收端开启了 DelayedACK(延迟 ACK 的发送,这样可以合并多个 ACK,提升网络传输效率),**那就会发生,** 发送端发送第一个数据包后,接收端没有返回 ACK,这时发送端发送了第二个数据包,因为 Nagle\`s 算法的存在,并且第一个发送包的 ACK 还没有返回,所以第二个包会暂存起来。而 DelayedACK 的超时时间,默认是 40ms,所以一旦到了 40ms,接收端回给发送端 ACK,那么发送端才会发送第二个包,**这样就增加了延迟。** **解决的方式非常简单:** 只要在 socket 上开启 tcp_nodelay 就好了,这个参数关闭了 Nagle\`s 算法,这样发送端就不需要等到上一个发送包的 ACK 返回,直接发送新的数据包就好了。这对于强网络交互的场景来说非常的适用,基本上,如果你要自己实现一套网络框架,tcp_nodelay 这个参数最好是要开启的。 ## 选择合适的序列化方式 -在对网络数据传输完成调优之后,另外一个需要关注的点就是, **数据的序列化和反序列化。** 通常所说的序列化,是将传输对象转换成二进制串的过程,而反序列化则是相反的动作,是将二进制串转换成对象的过程。 +在对网络数据传输完成调优之后,另外一个需要关注的点就是,**数据的序列化和反序列化。** 通常所说的序列化,是将传输对象转换成二进制串的过程,而反序列化则是相反的动作,是将二进制串转换成对象的过程。 -从上面的 RPC 调用过程中你可以看到,一次 RPC 调用需要经历两次数据序列化的过程,和两次数据反序列化的过程,可见它们对于 RPC 的性能影响是很大的, **那么我们在选择序列化方式的时候需要考虑哪些因素呢?** 首先需要考虑的肯定是性能嘛,性能包括时间上的开销和空间上的开销,时间上的开销就是序列化和反序列化的速度,这是显而易见需要重点考虑的,而空间上的开销则是序列化后的二进制串的大小,过大的二进制串也会占据传输带宽,影响传输效率。 +从上面的 RPC 调用过程中你可以看到,一次 RPC 调用需要经历两次数据序列化的过程,和两次数据反序列化的过程,可见它们对于 RPC 的性能影响是很大的,**那么我们在选择序列化方式的时候需要考虑哪些因素呢?** 首先需要考虑的肯定是性能嘛,性能包括时间上的开销和空间上的开销,时间上的开销就是序列化和反序列化的速度,这是显而易见需要重点考虑的,而空间上的开销则是序列化后的二进制串的大小,过大的二进制串也会占据传输带宽,影响传输效率。 除去性能之外,我们需要考虑的是它是否可以跨语言,跨平台,这一点也非常重要,因为一般的公司的技术体系都不是单一的,使用的语言也不是单一的,那么如果你的 RPC 框架中传输的数据只能被一种语言解析,那么这无疑限制了框架的使用。 另外,扩展性也是一个需要考虑的重点问题。你想想,如果对象增加了一个字段就会造成传输协议的不兼容,导致服务调用失败,这会是多么可怕的事情。 -综合上面的几个考虑点,在我看来, **我们的序列化备选方案主要有以下几种:** 首先是大家熟知的 JSON,它起源于 JavaScript,是一种最广泛使用的序列化协议,它的优势简单易用,人言可读,同时在性能上相比 XML 有比较大的优势。 +综合上面的几个考虑点,在我看来,**我们的序列化备选方案主要有以下几种:** 首先是大家熟知的 JSON,它起源于 JavaScript,是一种最广泛使用的序列化协议,它的优势简单易用,人言可读,同时在性能上相比 XML 有比较大的优势。 -另外的 Thrift 和 Protobuf 都是需要引入 IDL(Interface description language)的,也就是需要按照约定的语法写一个 IDL 文件,然后通过特定的编译器将它转换成各语言对应的代码,从而实现跨语言的特点。 **Thrift** 是 Facebook 开源的高性能的序列化协议,也是一个轻量级的 RPC 框架; **Protobuf** 是谷歌开源的序列化协议。它们的共同特点是,无论在空间上还是时间上都有着很高的性能,缺点就是由于 IDL 存在带来一些使用上的不方便。 +另外的 Thrift 和 Protobuf 都是需要引入 IDL(Interface description language)的,也就是需要按照约定的语法写一个 IDL 文件,然后通过特定的编译器将它转换成各语言对应的代码,从而实现跨语言的特点。**Thrift** 是 Facebook 开源的高性能的序列化协议,也是一个轻量级的 RPC 框架; **Protobuf** 是谷歌开源的序列化协议。它们的共同特点是,无论在空间上还是时间上都有着很高的性能,缺点就是由于 IDL 存在带来一些使用上的不方便。 -那么,你要如何选择这几种序列化协议呢? **这里我给你几点建议:** +那么,你要如何选择这几种序列化协议呢?**这里我给你几点建议:** 如果对于性能要求不高,在传输数据占用带宽不大的场景下,可以使用 JSON 作为序列化协议; diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25424\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25424\350\256\262.md" index 568a1c073..af5f8d19b 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25424\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25424\350\256\262.md" @@ -8,7 +8,7 @@ ## 你所知道的服务发现 -服务注册和发现不是一个新的概念,你在之前的实际项目中也一定了解过,只是你可能没怎么注意罢了。比如说,你知道 Nginx 是一个反向代理组件,那么 Nginx 需要知道,应用服务器的地址是什么,这样才能够将流量透传到应用服务器上, **这就是服务发现的过程。** **那么 Nginx 是怎么实现的呢?** 它是把应用服务器的地址配置在了文件中。 +服务注册和发现不是一个新的概念,你在之前的实际项目中也一定了解过,只是你可能没怎么注意罢了。比如说,你知道 Nginx 是一个反向代理组件,那么 Nginx 需要知道,应用服务器的地址是什么,这样才能够将流量透传到应用服务器上,**这就是服务发现的过程。** **那么 Nginx 是怎么实现的呢?** 它是把应用服务器的地址配置在了文件中。 这固然是一种解决的思路,实际上,我在早期的项目中也是这么做的。那时,项目刚刚做了服务化拆分,RPC 服务端的地址,就是配置在了客户端的代码中,不过,这样做之后出现了几个问题: @@ -46,11 +46,11 @@ **对于 RPC 服务来说,** 我们可以先将 RPC 服务从注册中心的服务列表中删除掉,然后观察 RPC 服务端没有流量之后,再将服务端停掉。有了优雅关闭之后,RPC 服务端再重启的时候,就会减少对客户端的影响。 -在这个过程中,服务的上线和下线是由服务端主动向注册中心注册、和取消注册来实现的,这在正常的流程中是没有问题的。 **可是,如果某一个服务端意外故障,** 比如说机器掉电,网络不通等情况,服务端就没有办法向注册中心通信,将自己从服务列表中删除,那么客户端也就不会得到通知,它就会继续向一个故障的服务端发起请求,也就会有错误发生了。那这种情况如何来避免呢?其实,这种情况是一个服务状态管理的问题。 +在这个过程中,服务的上线和下线是由服务端主动向注册中心注册、和取消注册来实现的,这在正常的流程中是没有问题的。**可是,如果某一个服务端意外故障,** 比如说机器掉电,网络不通等情况,服务端就没有办法向注册中心通信,将自己从服务列表中删除,那么客户端也就不会得到通知,它就会继续向一个故障的服务端发起请求,也就会有错误发生了。那这种情况如何来避免呢?其实,这种情况是一个服务状态管理的问题。 ## 服务状态管理如何来做 -针对上面我提到的问题, **我们一般会有两种解决思路。** 第一种思路是主动探测, **方法是这样的:** +针对上面我提到的问题,**我们一般会有两种解决思路。** 第一种思路是主动探测,**方法是这样的:** 你的 RPC 服务要打开一个端口,然后由注册中心每隔一段时间(比如 30 秒)探测这些端口是否可用,如果可用就认为服务仍然是正常的,否则就可以认为服务不可用,那么注册中心就可以把服务从列表里面删除了。 @@ -62,7 +62,7 @@ **还有一个问题是:** 如果 RPC 服务端部署的实例比较多,那么每次探测的成本也会比较高,探测的时间也比较长,这样当一个服务不可用时,可能会有一段时间的延迟,才会被注册中心探测到。 -**因此,我们后面把它改造成了心跳模式。** 这也是大部分注册中心提供的,检测连接上来的 RPC 服务端是否存活的方式,比如 Eureka、ZooKeeper, **在我来看,这种心跳机制可以这样实现:** +**因此,我们后面把它改造成了心跳模式。** 这也是大部分注册中心提供的,检测连接上来的 RPC 服务端是否存活的方式,比如 Eureka、ZooKeeper,**在我来看,这种心跳机制可以这样实现:** 注册中心为每一个连接上来的 RPC 服务节点,记录最近续约的时间,RPC 服务节点在启动注册到注册中心后,就按照一定的时间间隔(比如 30 秒),向注册中心发送心跳包。注册中心在接受到心跳包之后,会更新这个节点的最近续约时间。然后,注册中心会启动一个定时器,定期检测当前时间和节点,最近续约时间的差值,如果达到一个阈值(比如说 90 秒),那么认为这个服务节点不可用。 @@ -78,17 +78,17 @@ ![img](assets/b31fa6bc6b383675a80917e7491be209.jpg) -在测试的过程中,系统运行稳定,但是某一天早上五点,我突然发现,所有的服务节点都被摘除了,客户端因为拿不到服务端的节点地址列表全部调用失败,整体服务宕机。经过排查我发现,云服务器上部署的注册中心,竟然将所有的服务节点全部删除了!进一步排查之后, **原来是自研注册中心出现了 Bug。** +在测试的过程中,系统运行稳定,但是某一天早上五点,我突然发现,所有的服务节点都被摘除了,客户端因为拿不到服务端的节点地址列表全部调用失败,整体服务宕机。经过排查我发现,云服务器上部署的注册中心,竟然将所有的服务节点全部删除了!进一步排查之后,**原来是自研注册中心出现了 Bug。** 在正常的情况下,无论是自建机房,还是云服务器上的服务节点,都会向各自机房的注册中心注册地址信息,并且发送心跳。而这些地址信息,以及服务的最近续约时间,都是存储在 Redis 主库中,各自机房的注册中心,会读各自机房的从库来获取最近续约时间,从而判断服务节点是否有效。 Redis 的主从同步数据是通过专线来传输的,出现故障之前,专线带宽被占满,导致主从同步延迟。这样一来,云上部署的 Redis 从库中存储的最近续约时间,就没有得到及时更新,随着主从同步延迟越发严重,最终,云上部署的注册中心发现了,当前时间与最近续约时间的差值,超过了摘除的阈值,所以将所有的节点摘除,从而导致了故障。 -有了这次惨痛的教训, **我们给注册中心增加了保护的策略:** 如果摘除的节点占到了服务集群节点数的 40%,就停止摘除服务节点,并且给服务的开发同学和,运维同学报警处理(这个阈值百分比可以调整,保证了一定的灵活性)。 +有了这次惨痛的教训,**我们给注册中心增加了保护的策略:** 如果摘除的节点占到了服务集群节点数的 40%,就停止摘除服务节点,并且给服务的开发同学和,运维同学报警处理(这个阈值百分比可以调整,保证了一定的灵活性)。 **据我所知,** Eureka 也采用了类似的策略,来避免服务节点被过度摘除,导致服务集群不足以承担流量的问题。如果你使用的是 ZooKeeper 或者 ETCD 这种无保护策略的分布式一致性组件,那你可以考虑在客户端,实现保护策略的逻辑,比如说当摘除的节点超过一定比例时,你在 RPC 客户端就不再处理变更通知,你可以依据自己的实际情况来实现。 -除此之外,在实际项目中,我们还发现注册中心另一个重要的问题就是“通知风暴”。你想一想,变更一个服务的一个节点,会产生多少条推送消息?假如你的服务有 100 个调用者,有 100 个节点,那么变更一个节点会推送 100 * 100 = 10000 个节点的数据。那么如果多个服务集群同时上线或者发生波动时,注册中心推送的消息就会更多,会严重占用机器的带宽资源,这就是我所说的“通知风暴”。 **那么怎么解决这个问题呢?** 你可以从以下几个方面来思考: +除此之外,在实际项目中,我们还发现注册中心另一个重要的问题就是“通知风暴”。你想一想,变更一个服务的一个节点,会产生多少条推送消息?假如你的服务有 100 个调用者,有 100 个节点,那么变更一个节点会推送 100 * 100 = 10000 个节点的数据。那么如果多个服务集群同时上线或者发生波动时,注册中心推送的消息就会更多,会严重占用机器的带宽资源,这就是我所说的“通知风暴”。**那么怎么解决这个问题呢?** 你可以从以下几个方面来思考: 首先,要控制一组注册中心管理的服务集群的规模,具体限制多少没有统一的标准,你需要结合你的业务以及注册中心的选型来考虑,主要考察的指标就是注册中心服务器的峰值带宽; @@ -98,9 +98,9 @@ Redis 的主从同步数据是通过专线来传输的,出现故障之前, 最后,如果是自建的注册中心,你也可以在其中加入一些保护策略,比如说如果通知的消息量达到某一个阈值就停止变更通知。 -其实,服务的注册和发现,归根结底是服务治理中的一环, **服务治理(service governance),** 其实更直白的翻译应该是服务的管理,也就是解决多个服务节点,组成集群的时候,产生的一些复杂的问题。为了帮助你理解, **我来做个简单的比喻。** 你可以把集群看作是一个微型的城市,把道路看做是组成集群的服务,把行走在道路上的车当做是流量,那么服务治理就是对于整个城市道路的管理。 +其实,服务的注册和发现,归根结底是服务治理中的一环,**服务治理(service governance),** 其实更直白的翻译应该是服务的管理,也就是解决多个服务节点,组成集群的时候,产生的一些复杂的问题。为了帮助你理解,**我来做个简单的比喻。** 你可以把集群看作是一个微型的城市,把道路看做是组成集群的服务,把行走在道路上的车当做是流量,那么服务治理就是对于整个城市道路的管理。 -如果你新建了一条街道(相当于启动了一个新的服务节点),那么就要通知所有的车辆(流量)有新的道路可以走了;你关闭了一条街道,你也要通知所有车辆不要从这条路走了, **这就是服务的注册和发现。** 我们在道路上安装监控,监视每条道路的流量情况, **这就是服务的监控。** 道路一旦出现拥堵或者道路需要维修,那么就需要暂时封闭这条道路,由城市来统一调度车辆,走不堵的道路, **这就是熔断以及引流。** 道路之间纵横交错四通八达,一旦在某条道路上出现拥堵,但是又发现这条道路从头堵到尾,说明事故并不是发生在这条道路上,那么就需要从整体链路上来排查事故究竟处在哪个位置, **这就是分布式追踪。** 不同道路上的车辆有多有少,那么就需要有一个警察来疏导,在某一个时间走哪一条路会比较快, **这就是负载均衡。** +如果你新建了一条街道(相当于启动了一个新的服务节点),那么就要通知所有的车辆(流量)有新的道路可以走了;你关闭了一条街道,你也要通知所有车辆不要从这条路走了,**这就是服务的注册和发现。** 我们在道路上安装监控,监视每条道路的流量情况,**这就是服务的监控。** 道路一旦出现拥堵或者道路需要维修,那么就需要暂时封闭这条道路,由城市来统一调度车辆,走不堵的道路,**这就是熔断以及引流。** 道路之间纵横交错四通八达,一旦在某条道路上出现拥堵,但是又发现这条道路从头堵到尾,说明事故并不是发生在这条道路上,那么就需要从整体链路上来排查事故究竟处在哪个位置,**这就是分布式追踪。** 不同道路上的车辆有多有少,那么就需要有一个警察来疏导,在某一个时间走哪一条路会比较快,**这就是负载均衡。** 而这些问题,我会在后面的课程中针对性地讲解。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25425\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25425\350\256\262.md" index 65664ce3d..08917fa68 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25425\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25425\350\256\262.md" @@ -8,13 +8,13 @@ 现在,你的系统运行平稳,老板很高兴,你也安心了很多。而且你认为,在经过了服务化拆分之后,服务的可扩展性增强了很多,可以通过横向扩展服务节点的方式,进行平滑地扩容了,对于应对峰值流量也更有信心了。 -**但是这时出现了问题:** 你通过监控发现,系统的核心下单接口在晚高峰的时候,会有少量的慢请求,用户也投诉在 APP 上下单时,等待的时间比较长。而下单的过程可能会调用多个 RPC 服务,或者使用多个资源,一时之间,你很难快速判断,究竟是哪个服务或者资源出了问题,从而导致整体流程变慢, **于是,你和你的团队开始想办法如何排查这个问题。** +**但是这时出现了问题:** 你通过监控发现,系统的核心下单接口在晚高峰的时候,会有少量的慢请求,用户也投诉在 APP 上下单时,等待的时间比较长。而下单的过程可能会调用多个 RPC 服务,或者使用多个资源,一时之间,你很难快速判断,究竟是哪个服务或者资源出了问题,从而导致整体流程变慢,**于是,你和你的团队开始想办法如何排查这个问题。** ## 一体化架构中的慢请求排查如何做 -因为在分布式环境下,请求要在多个服务之间调用,所以对于慢请求问题的排查会更困难, **我们不妨从简单的入手,** 先看看在一体化架构中,是如何排查这个慢请求的问题的。 +因为在分布式环境下,请求要在多个服务之间调用,所以对于慢请求问题的排查会更困难,**我们不妨从简单的入手,** 先看看在一体化架构中,是如何排查这个慢请求的问题的。 -最简单的思路是:打印下单操作的每一个步骤的耗时情况,然后通过比较这些耗时的数据,找到延迟最高的一步,然后再来看看这个步骤要如何的优化。如果有必要的话,你还需要针对步骤中的子步骤,再增加日志来继续排查, **简单的代码就像下面这样:** +最简单的思路是:打印下单操作的每一个步骤的耗时情况,然后通过比较这些耗时的数据,找到延迟最高的一步,然后再来看看这个步骤要如何的优化。如果有必要的话,你还需要针对步骤中的子步骤,再增加日志来继续排查,**简单的代码就像下面这样:** ```plaintext long start = System.currentTimeMillis(); @@ -55,11 +55,11 @@ Logs.info("rid : " + tl.get() + ", process C cost " + (System.currentTimeMillis( 有了 requestId,你就可以清晰地了解一个调用链路上的耗时分布情况了。 -于是,你给你的代码增加了大量的日志,来排查下单操作缓慢的问题。 **很快,** 你发现是某一个数据库查询慢了才导致了下单缓慢,然后你优化了数据库索引,问题最终得到了解决。 +于是,你给你的代码增加了大量的日志,来排查下单操作缓慢的问题。**很快,** 你发现是某一个数据库查询慢了才导致了下单缓慢,然后你优化了数据库索引,问题最终得到了解决。 -**正当你要松一口气的时候,问题接踵而至:** 又有用户反馈某些商品业务打开缓慢;商城首页打开缓慢。你开始焦头烂额地给代码中增加耗时日志,而这时你意识到,每次排查一个接口就需要增加日志、重启服务, **这并不是一个好的办法,于是你开始思考解决的方案。** **其实,从我的经验出发来说,** 一个接口响应时间慢,一般是出在跨网络的调用上,比如说请求数据库、缓存或者依赖的第三方服务。所以,我们只需要针对这些调用的客户端类,做切面编程,通过插入一些代码打印它们的耗时就好了。 +**正当你要松一口气的时候,问题接踵而至:** 又有用户反馈某些商品业务打开缓慢;商城首页打开缓慢。你开始焦头烂额地给代码中增加耗时日志,而这时你意识到,每次排查一个接口就需要增加日志、重启服务,**这并不是一个好的办法,于是你开始思考解决的方案。** **其实,从我的经验出发来说,** 一个接口响应时间慢,一般是出在跨网络的调用上,比如说请求数据库、缓存或者依赖的第三方服务。所以,我们只需要针对这些调用的客户端类,做切面编程,通过插入一些代码打印它们的耗时就好了。 -说到切面编程(AOP)你应该并不陌生,它是面向对象编程的一种延伸,可以在不修改源代码的前提下,给应用程序添加功能,比如说鉴权,打印日志等等。如果你对切面编程的概念理解的还不透彻,那我给你做个比喻, **帮你理解一下。** 这就像开发人员在向代码仓库提交代码后,他需要对代码编译、构建、执行单元测试用例,以保证提交的代码是没有问题的。但是,如果每个人提交了代码都做这么多事儿,无疑会对开发同学造成比较大的负担,那么你可以配置一个持续集成的流程,在提交代码之后,自动帮你完成这些操作,这个持续集成的流程就可以认为是一个切面。 **一般来说,切面编程的实现分为两类:** +说到切面编程(AOP)你应该并不陌生,它是面向对象编程的一种延伸,可以在不修改源代码的前提下,给应用程序添加功能,比如说鉴权,打印日志等等。如果你对切面编程的概念理解的还不透彻,那我给你做个比喻,**帮你理解一下。** 这就像开发人员在向代码仓库提交代码后,他需要对代码编译、构建、执行单元测试用例,以保证提交的代码是没有问题的。但是,如果每个人提交了代码都做这么多事儿,无疑会对开发同学造成比较大的负担,那么你可以配置一个持续集成的流程,在提交代码之后,自动帮你完成这些操作,这个持续集成的流程就可以认为是一个切面。**一般来说,切面编程的实现分为两类:** 一类是静态代理,典型的代表是 AspectJ,它的特点是在编译期做切面代码注入; @@ -71,9 +71,9 @@ Logs.info("rid : " + tl.get() + ", process C cost " + (System.currentTimeMillis( 而动态代理不会去修改生成的 Class 文件,而是会在运行期生成一个代理对象,这个代理对象对源对象做了字节码增强,来完成切面所要执行的操作。由于在运行期需要生成代理对象,所以动态代理的性能要比静态代理要差。 -我们做切面的原因,是想生成一些调试的日志,所以我们期望尽量减少对于原先接口性能的影响。 **因此,我推荐采用静态代理的方式,实现切面编程。** +我们做切面的原因,是想生成一些调试的日志,所以我们期望尽量减少对于原先接口性能的影响。**因此,我推荐采用静态代理的方式,实现切面编程。** -如果你的系统中需要增加切面,来做一些校验、限流或者日志打印的工作, **我也建议你考虑使用静态代理的方式,** 使用 AspectJ 做切面的简单代码实现就像下面这样: +如果你的系统中需要增加切面,来做一些校验、限流或者日志打印的工作,**我也建议你考虑使用静态代理的方式,** 使用 AspectJ 做切面的简单代码实现就像下面这样: ```java @Aspect @@ -103,9 +103,9 @@ public class Tracer { } ``` -这样,你就在你的系统的每个接口中,打印出了所有访问数据库、缓存、外部接口的耗时情况,一次请求可能要打印十几条日志,如果你的电商系统的 QPS 是 10000 的话,就是每秒钟会产生十几万条日志,对于磁盘 I/O 的负载是巨大的, **那么这时,你就要考虑如何减少日志的数量。** **你可以考虑对请求做采样,** 采样的方式也简单,比如你想采样 10% 的日志,那么你可以只打印“requestId%10==0”的请求。 +这样,你就在你的系统的每个接口中,打印出了所有访问数据库、缓存、外部接口的耗时情况,一次请求可能要打印十几条日志,如果你的电商系统的 QPS 是 10000 的话,就是每秒钟会产生十几万条日志,对于磁盘 I/O 的负载是巨大的,**那么这时,你就要考虑如何减少日志的数量。** **你可以考虑对请求做采样,** 采样的方式也简单,比如你想采样 10% 的日志,那么你可以只打印“requestId%10==0”的请求。 -有了这些日志之后,当给你一个 requestId 的时候,你发现自己并不能确定这个请求到了哪一台服务器上,所以你不得不登陆所有的服务器,去搜索这个 requestId 才能定位请求。 **这样无疑会增加问题排查的时间。** **你可以考虑的解决思路是:** 把日志不打印到本地文件中,而是发送到消息队列里,再由消息处理程序写入到集中存储中,比如 Elasticsearch。这样,你在排查问题的时候,只需要拿着 requestId 到 Elasticsearch 中查找相关的记录就好了。在加入消息队列和 Elasticsearch 之后,我们这个排查程序的架构图也会有所改变: +有了这些日志之后,当给你一个 requestId 的时候,你发现自己并不能确定这个请求到了哪一台服务器上,所以你不得不登陆所有的服务器,去搜索这个 requestId 才能定位请求。**这样无疑会增加问题排查的时间。** **你可以考虑的解决思路是:** 把日志不打印到本地文件中,而是发送到消息队列里,再由消息处理程序写入到集中存储中,比如 Elasticsearch。这样,你在排查问题的时候,只需要拿着 requestId 到 Elasticsearch 中查找相关的记录就好了。在加入消息队列和 Elasticsearch 之后,我们这个排查程序的架构图也会有所改变: ![img](assets/ae25d911a438dc8ca1adb816595a787a.jpg) @@ -121,11 +121,11 @@ public class Tracer { ## 如何来做分布式 Trace -你可能会问:题目既然是“分布式 Trace:横跨几十个分布式组件的慢请求要如何排查?”,那么我为什么要花费大量的篇幅,来说明在一体化架构中如何排查问题呢? **这是因为在分布式环境下,** 你基本上也是依据上面,我提到的这几点来构建分布式追踪的中间件的。 +你可能会问:题目既然是“分布式 Trace:横跨几十个分布式组件的慢请求要如何排查?”,那么我为什么要花费大量的篇幅,来说明在一体化架构中如何排查问题呢?**这是因为在分布式环境下,** 你基本上也是依据上面,我提到的这几点来构建分布式追踪的中间件的。 在一体化架构中,单次请求的所有的耗时日志,都被记录在一台服务器上,而在微服务的场景下,单次请求可能跨越多个 RPC 服务,这就造成了,单次的请求的日志会分布在多个服务器上。 -当然,你也可以通过 requestId 将多个服务器上的日志串起来,但是仅仅依靠 requestId 很难表达清楚服务之间的调用关系,所以从日志中,就无法了解服务之间是谁在调用谁。因此,我们采用 traceId + spanId 这两个数据维度来记录服务之间的调用关系(这里 traceId 就是 requestId),也就是使用 traceId 串起单次请求,用 spanId 记录每一次 RPC 调用。 **说起来可能比较抽象,我给你举一个具体的例子。** +当然,你也可以通过 requestId 将多个服务器上的日志串起来,但是仅仅依靠 requestId 很难表达清楚服务之间的调用关系,所以从日志中,就无法了解服务之间是谁在调用谁。因此,我们采用 traceId + spanId 这两个数据维度来记录服务之间的调用关系(这里 traceId 就是 requestId),也就是使用 traceId 串起单次请求,用 spanId 记录每一次 RPC 调用。**说起来可能比较抽象,我给你举一个具体的例子。** 比如,你的请求从用户端过来,先到达 A 服务,A 服务会分别调用 B 和 C 服务,B 服务又会调用 D 和 E 服务。 @@ -151,7 +151,7 @@ A 调用 C 服务时,traceId 依然不变,spanId 则变为了 1.2,代表 这样,无论是数据库等资源的响应时间,还是 RPC 服务的响应时间就都汇总到了消息队列中,在经过一些处理之后,最终被写入到 Elasticsearch 中以便给开发和运维同学查询使用。 -而在这里,你大概率会遇到的问题还是性能的问题,也就是因为引入了分布式追踪中间件,导致对于磁盘 I/O 和网络 I/O 的影响, **而我给你的“避坑”指南就是:** 如果你是自研的分布式 trace 中间件,那么一定要提供一个开关,方便在线上随时将日志打印关闭;如果使用开源的组件,可以开始设置一个较低的日志采样率,观察系统性能情况再调整到一个合适的数值。 +而在这里,你大概率会遇到的问题还是性能的问题,也就是因为引入了分布式追踪中间件,导致对于磁盘 I/O 和网络 I/O 的影响,**而我给你的“避坑”指南就是:** 如果你是自研的分布式 trace 中间件,那么一定要提供一个开关,方便在线上随时将日志打印关闭;如果使用开源的组件,可以开始设置一个较低的日志采样率,观察系统性能情况再调整到一个合适的数值。 ## 课程小结 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25426\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25426\350\256\262.md" index 88d55019d..4ab1594f1 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25426\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25426\350\256\262.md" @@ -26,13 +26,13 @@ 由于这类服务需要承担全量的请求,所以对于性能的要求极高。代理类的负载均衡服务有很多开源实现,比较著名的有 LVS,Nginx 等等。LVS 在 OSI 网络模型中的第四层,传输层工作,所以 LVS 又可以称为四层负载;而 Nginx 运行在 OSI 网络模型中的第七层,应用层,所以又可以称它为七层负载(你可以回顾一下02 讲的内容)。 -在项目的架构中,我们一般会同时部署 LVS 和 Nginx 来做 HTTP 应用服务的负载均衡。也就是说,在入口处部署 LVS,将流量分发到多个 Nginx 服务器上,再由 Nginx 服务器分发到应用服务器上, **为什么这么做呢?** +在项目的架构中,我们一般会同时部署 LVS 和 Nginx 来做 HTTP 应用服务的负载均衡。也就是说,在入口处部署 LVS,将流量分发到多个 Nginx 服务器上,再由 Nginx 服务器分发到应用服务器上,**为什么这么做呢?** 主要和 LVS 和 Nginx 的特点有关,LVS 是在网络栈的四层做请求包的转发,请求包转发之后,由客户端和后端服务直接建立连接,后续的响应包不会再经过 LVS 服务器,所以相比 Nginx,性能会更高,也能够承担更高的并发。 可 LVS 缺陷是工作在四层,而请求的 URL 是七层的概念,不能针对 URL 做更细致地请求分发,而且 LVS 也没有提供探测后端服务是否存活的机制;而 Nginx 虽然比 LVS 的性能差很多,但也可以承担每秒几万次的请求,并且它在配置上更加灵活,还可以感知后端服务是否出现问题。 -因此,LVS 适合在入口处,承担大流量的请求分发,而 Nginx 要部署在业务服务器之前做更细维度的请求分发。 **我给你的建议是,** 如果你的 QPS 在十万以内,那么可以考虑不引入 LVS 而直接使用 Nginx 作为唯一的负载均衡服务器,这样少维护一个组件,也会减少系统的维护成本。 +因此,LVS 适合在入口处,承担大流量的请求分发,而 Nginx 要部署在业务服务器之前做更细维度的请求分发。**我给你的建议是,** 如果你的 QPS 在十万以内,那么可以考虑不引入 LVS 而直接使用 Nginx 作为唯一的负载均衡服务器,这样少维护一个组件,也会减少系统的维护成本。 不过这两个负载均衡服务适用于普通的 Web 服务,对于微服务架构来说,它们是不合适的。因为微服务架构中的服务节点存储在注册中心里,使用 LVS 就很难和注册中心交互,获取全量的服务节点列表。另外,一般微服务架构中,使用的是 RPC 协议而不是 HTTP 协议,所以 Nginx 也不能满足要求。 @@ -67,7 +67,7 @@ return serverList.get(currentIndex); 它其实是一种通用的策略,基本上,大部分的负载均衡服务器都支持。轮询的策略可以做到将请求尽量平均地分配到所有服务节点上,但是,它没有考虑服务节点的具体配置情况。比如,你有三个服务节点,其中一个服务节点的配置是 8 核 8G,另外两个节点的配置是 4 核 4G,那么如果使用轮询的方式来平均分配请求的话,8 核 8G 的节点分到的请求数量和 4 核 4G 的一样多,就不能发挥性能上的优势了 -所以,我们考虑给节点加上权重值,比如给 8 核 8G 的机器配置权重为 2,那么就会给它分配双倍的流量, **这种策略就是带有权重的轮询策略。** +所以,我们考虑给节点加上权重值,比如给 8 核 8G 的机器配置权重为 2,那么就会给它分配双倍的流量,**这种策略就是带有权重的轮询策略。** 除了这两种策略之外,目前开源的负载均衡服务还提供了很多静态策略: @@ -81,17 +81,17 @@ Dubbo 也提供了随机选取策略,以及一致性 hash 的策略。 而目前开源的负载均衡服务中,也会提供一些动态策略,我强调一下它们的原理。 -在负载均衡服务器上会收集对后端服务的调用信息,比如从负载均衡端到后端服务的活跃连接数,或者是调用的响应时间,然后从中选择连接数最少的服务,或者响应时间最短的后端服务。 **我举几个具体的例子:** Dubbo 提供的 LeastAcive 策略,就是优先选择活跃连接数最少的服务; +在负载均衡服务器上会收集对后端服务的调用信息,比如从负载均衡端到后端服务的活跃连接数,或者是调用的响应时间,然后从中选择连接数最少的服务,或者响应时间最短的后端服务。**我举几个具体的例子:** Dubbo 提供的 LeastAcive 策略,就是优先选择活跃连接数最少的服务; -Spring Cloud 全家桶中的 Ribbon 提供了 WeightedResponseTimeRule 是使用响应时间,给每个服务节点计算一个权重,然后依据这个权重,来给调用方分配服务节点。 **这些策略的思考点** 是从调用方的角度出发,选择负载最小、资源最空闲的服务来调用,以期望能得到更高的服务调用性能,也就能最大化地使用服务器的空闲资源,请求也会响应地更迅速, **所以,我建议你,** 在实际开发中,优先考虑使用动态的策略。 +Spring Cloud 全家桶中的 Ribbon 提供了 WeightedResponseTimeRule 是使用响应时间,给每个服务节点计算一个权重,然后依据这个权重,来给调用方分配服务节点。**这些策略的思考点** 是从调用方的角度出发,选择负载最小、资源最空闲的服务来调用,以期望能得到更高的服务调用性能,也就能最大化地使用服务器的空闲资源,请求也会响应地更迅速,**所以,我建议你,** 在实际开发中,优先考虑使用动态的策略。 -到目前为止,你已经可以根据上面的分析,选择适合自己的负载均衡策略,并选择一个最优的服务节点, **那么问题来了:** 你怎么保证选择出来的这个节点,一定是一个可以正常服务的节点呢?如果你采用的是轮询的策略,选择出来的,是一个故障节点又要怎么办呢?所以,为了降低请求被分配到一个故障节点的几率,有些负载均衡服务器,还提供了对服务节点的故障检测功能。 +到目前为止,你已经可以根据上面的分析,选择适合自己的负载均衡策略,并选择一个最优的服务节点,**那么问题来了:** 你怎么保证选择出来的这个节点,一定是一个可以正常服务的节点呢?如果你采用的是轮询的策略,选择出来的,是一个故障节点又要怎么办呢?所以,为了降低请求被分配到一个故障节点的几率,有些负载均衡服务器,还提供了对服务节点的故障检测功能。 ## 如何检测节点是否故障 24 讲中,我带你了解到,在微服务化架构中,服务节点会定期地向注册中心发送心跳包,这样注册中心就能够知晓服务节点是否故障,也就可以确认传递给负载均衡服务的节点,一定是可用的。 -但对于 Nginx 来说, **我们要如何保证配置的服务节点是可用的呢?** 这就要感谢淘宝开源的 Nginx 模块nginx_upstream_check_module了,这个模块可以让 Nginx 定期地探测后端服务的一个指定的接口,然后根据返回的状态码,来判断服务是否还存活。当探测不存活的次数达到一定阈值时,就自动将这个后端服务从负载均衡服务器中摘除。 **它的配置样例如下:** +但对于 Nginx 来说,**我们要如何保证配置的服务节点是可用的呢?** 这就要感谢淘宝开源的 Nginx 模块nginx_upstream_check_module了,这个模块可以让 Nginx 定期地探测后端服务的一个指定的接口,然后根据返回的状态码,来判断服务是否还存活。当探测不存活的次数达到一定阈值时,就自动将这个后端服务从负载均衡服务器中摘除。**它的配置样例如下:** ```plaintext upstream server { @@ -109,7 +109,7 @@ Nginx 按照上面的方式配置之后,你的业务服务器也要实现一 **在服务刚刚启动时,** 可以初始化默认的 HTTP 状态码是 500,这样 Nginx 就不会很快将这个服务节点标记为可用,也就可以等待服务中,依赖的资源初始化完成,避免服务初始启动时的波动。 -**在完全初始化之后,** 再将 HTTP 状态码变更为 200,Nginx 经过两次探测后,就会标记服务为可用。在服务关闭时,也应该先将 HTTP 状态码变更为 500,等待 Nginx 探测将服务标记为不可用后,前端的流量也就不会继续发往这个服务节点。在等待服务正在处理的请求全部处理完毕之后,再对服务做重启,可以避免直接重启导致正在处理的请求失败的问题。 **这是启动和关闭线上 Web 服务时的标准姿势,你可以在项目中参考使用。** +**在完全初始化之后,** 再将 HTTP 状态码变更为 200,Nginx 经过两次探测后,就会标记服务为可用。在服务关闭时,也应该先将 HTTP 状态码变更为 500,等待 Nginx 探测将服务标记为不可用后,前端的流量也就不会继续发往这个服务节点。在等待服务正在处理的请求全部处理完毕之后,再对服务做重启,可以避免直接重启导致正在处理的请求失败的问题。**这是启动和关闭线上 Web 服务时的标准姿势,你可以在项目中参考使用。** ## 课程小结 @@ -121,4 +121,4 @@ Nginx 按照上面的方式配置之后,你的业务服务器也要实现一 Nginx 可以引入 nginx_upstream_check_module,对后端服务做定期的存活检测,后端的服务节点在重启时,也要秉承着“先切流量后重启”的原则,尽量减少节点重启对于整体系统的影响。 -你可能会认为,像 Nginx、LVS 应该是运维所关心的组件,作为开发人员不用操心维护。 **不过通过今天的学习你应该可以看到:** 负载均衡服务是提升系统扩展性,和性能的重要组件,在高并发系统设计中,它发挥的作用是无法替代的。理解它的原理,掌握使用它的正确姿势,应该是每一个后端开发同学的必修课。 +你可能会认为,像 Nginx、LVS 应该是运维所关心的组件,作为开发人员不用操心维护。**不过通过今天的学习你应该可以看到:** 负载均衡服务是提升系统扩展性,和性能的重要组件,在高并发系统设计中,它发挥的作用是无法替代的。理解它的原理,掌握使用它的正确姿势,应该是每一个后端开发同学的必修课。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25427\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25427\350\256\262.md" index 10cda1dda..f691c22b1 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25427\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25427\350\256\262.md" @@ -10,19 +10,19 @@ **但是这样会有一个问题:** 不同的三个服务上使用同一种策略,在代码上会有冗余,无法做到重用,如果其他服务上也出现类似的问题,还要通过拷贝代码来实现,肯定是不行的。 -不过作为 Java 程序员, **你很容易想到:** 将限流的功能独立成一个单独的 jar 包,给这三个服务来引用。不过你忽略了一种情况,那就是你的电商团队使用的除了 Java,还有 PHP 和 Golang 等多种语言。 +不过作为 Java 程序员,**你很容易想到:** 将限流的功能独立成一个单独的 jar 包,给这三个服务来引用。不过你忽略了一种情况,那就是你的电商团队使用的除了 Java,还有 PHP 和 Golang 等多种语言。 -## 用多种语言开发的服务是没有办法使用 jar 包,来实现限流功能的, **这时你需要引入 API 网关。** API 网关起到的作用(904) +## 用多种语言开发的服务是没有办法使用 jar 包,来实现限流功能的,**这时你需要引入 API 网关。** API 网关起到的作用(904) API 网关(API Gateway)不是一个开源组件,而是一种架构模式,它是将一些服务共有的功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题。你可以把它看作系统的边界,它可以对出入系统的流量做统一的管控。 -在我看来,API 网关可以分为两类: **一类叫做入口网关,一类叫做出口网关。** 入口网关是我们经常使用的网关种类,它部署在负载均衡服务器和应用服务器之间, **主要有几方面的作用。** +在我看来,API 网关可以分为两类: **一类叫做入口网关,一类叫做出口网关。** 入口网关是我们经常使用的网关种类,它部署在负载均衡服务器和应用服务器之间,**主要有几方面的作用。** -它提供客户端一个统一的接入地址,API 网关可以将用户的请求动态路由到不同的业务服务上,并且做一些必要的协议转换工作。 **在你的系统中,你部署的微服务对外暴露的协议可能不同:** 有些提供的是 HTTP 服务;有些已经完成 RPC 改造,对外暴露 RPC 服务;有些遗留系统可能还暴露的是 Web Service 服务。API 网关可以对客户端屏蔽这些服务的部署地址,以及协议的细节,给客户端的调用带来很大的便捷。 +它提供客户端一个统一的接入地址,API 网关可以将用户的请求动态路由到不同的业务服务上,并且做一些必要的协议转换工作。**在你的系统中,你部署的微服务对外暴露的协议可能不同:** 有些提供的是 HTTP 服务;有些已经完成 RPC 改造,对外暴露 RPC 服务;有些遗留系统可能还暴露的是 Web Service 服务。API 网关可以对客户端屏蔽这些服务的部署地址,以及协议的细节,给客户端的调用带来很大的便捷。 另一方面,在 API 网关中,我们可以植入一些服务治理的策略,比如服务的熔断、降级,流量控制和分流等等(关于服务降级和流量控制的细节,我会在后面的课程中具体讲解,在这里,你只要知道它们可以在 API 网关中实现就可以了)。 -再有,客户端的认证和授权的实现,也可以放在 API 网关中。你要知道,不同类型的客户端使用的认证方式是不同的。 **在我之前项目中,** 手机 APP 使用 Oauth 协议认证,HTML5 端和 Web 端使用 Cookie 认证,内部服务使用自研的 Token 认证方式。这些认证方式在 API 网关上,可以得到统一处理,应用服务不需要了解认证的细节。 +再有,客户端的认证和授权的实现,也可以放在 API 网关中。你要知道,不同类型的客户端使用的认证方式是不同的。**在我之前项目中,** 手机 APP 使用 Oauth 协议认证,HTML5 端和 Web 端使用 Cookie 认证,内部服务使用自研的 Token 认证方式。这些认证方式在 API 网关上,可以得到统一处理,应用服务不需要了解认证的细节。 另外,API 网关还可以做一些与黑白名单相关的事情,比如针对设备 ID、用户 IP、用户 ID 等维度的黑白名单。 @@ -46,7 +46,7 @@ Netfix 开源的 API 网关 Zuul,在 1.0 版本的时候使用的是同步阻 而在 Zuul2.0 中,Netfix 团队将 servlet 改造成了一个 netty server(netty 服务),采用 I/O 多路复用的模型处理接入的 I/O 请求,并且将之前同步阻塞调用后端服务的方式,改造成使用 netty client(netty 客户端)非阻塞调用的方式。改造之后,Netfix 团队经过测试发现性能提升了 20% 左右。 -除此之外,API 网关中执行的动作有些是可以预先定义好的,比如黑白名单的设置,接口动态路由;有些则是需要业务方依据自身业务来定义。 **所以,API 网关的设计要注意扩展性,** 也就是你可以随时在网关的执行链路上,增加一些逻辑,也可以随时下掉一些逻辑(也就是所谓的热插拔)。 +除此之外,API 网关中执行的动作有些是可以预先定义好的,比如黑白名单的设置,接口动态路由;有些则是需要业务方依据自身业务来定义。**所以,API 网关的设计要注意扩展性,** 也就是你可以随时在网关的执行链路上,增加一些逻辑,也可以随时下掉一些逻辑(也就是所谓的热插拔)。 所以一般来说,我们可以把每一个操作定义为一个 filter(过滤器),然后使用“责任链模式”将这些 filter 串起来。责任链可以动态地组织这些 filter,解耦 filter 之间的关系,无论是增加还是减少 filter,都不会对其他的 filter 有任何的影响。 @@ -54,7 +54,7 @@ Netfix 开源的 API 网关 Zuul,在 1.0 版本的时候使用的是同步阻 ![img](assets/a1c11d4059e55b0521dd0cf19cf73488.jpg) -**另外还需要注意的一点是,** 为了提升网关对于请求的并行处理能力,我们一般会使用线程池来并行的执行请求。 **不过,这就带来一个问题:** 如果商品服务出现问题,造成响应缓慢,那么调用商品服务的线程就会被阻塞无法释放,久而久之,线程池中的线程就会被商品服务所占据,那么其他服务也会受到级联的影响。因此,我们需要针对不同的服务做线程隔离,或者保护。 **在我看来有两种思路:** +**另外还需要注意的一点是,** 为了提升网关对于请求的并行处理能力,我们一般会使用线程池来并行的执行请求。**不过,这就带来一个问题:** 如果商品服务出现问题,造成响应缓慢,那么调用商品服务的线程就会被阻塞无法释放,久而久之,线程池中的线程就会被商品服务所占据,那么其他服务也会受到级联的影响。因此,我们需要针对不同的服务做线程隔离,或者保护。**在我看来有两种思路:** 如果你后端的服务拆分得不多,可以针对不同的服务,采用不同的线程池,这样商品服务的故障就不会影响到支付服务和用户服务了; @@ -76,17 +76,17 @@ Tyk是一种 Go 语言实现的轻量级 API 网关,有着丰富的插件资 ## 如何在你的系统中引入 API 网关呢? -目前为止,我们的电商系统已经经过了服务化改造,在服务层和客户端之间有一层薄薄的 Web 层, **这个 Web 层做的事情主要有两方面:** 一方面是对服务层接口数据的聚合。比如,商品详情页的接口,可能会聚合服务层中,获取商品信息、用户信息、店铺信息以及用户评论等多个服务接口的数据; +目前为止,我们的电商系统已经经过了服务化改造,在服务层和客户端之间有一层薄薄的 Web 层,**这个 Web 层做的事情主要有两方面:** 一方面是对服务层接口数据的聚合。比如,商品详情页的接口,可能会聚合服务层中,获取商品信息、用户信息、店铺信息以及用户评论等多个服务接口的数据; 另一方面 Web 层还需要将 HTTP 请求转换为 RPC 请求,并且对前端的流量做一些限制,对于某些请求添加设备 ID 的黑名单等等。 因此,我们在做改造的时候,可以先将 API 网关从 Web 层中独立出来,将协议转换、限流、黑白名单等事情,挪到 API 网关中来处理,形成独立的入口网关层; -而针对服务接口数据聚合的操作, **一般有两种解决思路:** 再独立出一组网关专门做服务聚合、超时控制方面的事情,我们一般把前一种网关叫做流量网关,后一种可以叫做业务网关; +而针对服务接口数据聚合的操作,**一般有两种解决思路:** 再独立出一组网关专门做服务聚合、超时控制方面的事情,我们一般把前一种网关叫做流量网关,后一种可以叫做业务网关; 抽取独立的服务层,专门做接口聚合的操作。这样服务层就大概分为原子服务层和聚合服务层。 -我认为,接口数据聚合是业务操作,与其放在通用的网关层来实现,不如放在更贴近业务的服务层来实现, **所以,我更倾向于第二种方案。** ![img](assets/ab701c40ed8229606a4bf90db327c2f2.jpg) +我认为,接口数据聚合是业务操作,与其放在通用的网关层来实现,不如放在更贴近业务的服务层来实现,**所以,我更倾向于第二种方案。**![img](assets/ab701c40ed8229606a4bf90db327c2f2.jpg) 同时,我们可以在系统和第三方支付服务,以及登陆服务之间部署出口网关服务。原先,你会在拆分出来的支付服务中,完成对于第三方支付接口所需要数据的加密、签名等操作,再调用第三方支付接口,完成支付请求。现在,你把对数据的加密、签名的操作放在出口网关中,这样一来,支付服务只需要调用出口网关的统一支付接口就可以了。 @@ -96,7 +96,7 @@ Tyk是一种 Go 语言实现的轻量级 API 网关,有着丰富的插件资 ## 课程小结 -本节课我带你了解了 API 网关在系统中的作用,在实现中的一些关键的点,以及如何将 API 网关引入你的系统, **我想强调的重点如下:** +本节课我带你了解了 API 网关在系统中的作用,在实现中的一些关键的点,以及如何将 API 网关引入你的系统,**我想强调的重点如下:** API 网关分为入口网关和出口网关两类,入口网关作用很多,可以隔离客户端和微服务,从中提供协议转换、安全策略、认证、限流、熔断等功能。出口网关主要是为调用第三方服务提供统一的出口,在其中可以对调用外部的 API 做统一的认证、授权,审计以及访问控制; @@ -106,4 +106,4 @@ API 网关中的线程池,可以针对不同的接口或者服务做隔离和 API 网关可以替代原本系统中的 Web 层,将 Web 层中的协议转换、认证、限流等功能挪入到 API 网关中,将服务聚合的逻辑下沉到服务层。 -API 网关可以为 API 的调用提供便捷,也可以为将一些服务治理的功能独立出来,达到复用的目的,虽然在性能上可能会有一些损耗, **但是一般来说,** 使用成熟的开源 API 网关组件,这些损耗都是可以接受的。所以,当你的微服务系统越来越复杂时,你可以考虑使用 API 网关作为整体系统的门面。 +API 网关可以为 API 的调用提供便捷,也可以为将一些服务治理的功能独立出来,达到复用的目的,虽然在性能上可能会有一些损耗,**但是一般来说,** 使用成熟的开源 API 网关组件,这些损耗都是可以接受的。所以,当你的微服务系统越来越复杂时,你可以考虑使用 API 网关作为整体系统的门面。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25428\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25428\350\256\262.md" index ad4e5bf74..911a43267 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25428\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25428\350\256\262.md" @@ -18,7 +18,7 @@ 这样,当其中某一个机房出现网络故障、火灾,甚至整个城市发生地震、洪水等大的不可抗的灾难时,你可以随时将用户的流量切换到其它地域的机房中,从而保证系统可以不间断地持续运行。这种架构听起来非常美好,但是在实现上却是非常复杂和困难的,那么它复杂在哪儿呢? -假如我们有两个机房 A 和 B 都部署了应用服务,数据库的主库和从库部署在 A 机房,那么机房 B 的应用如何访问到数据呢? **有两种思路。** +假如我们有两个机房 A 和 B 都部署了应用服务,数据库的主库和从库部署在 A 机房,那么机房 B 的应用如何访问到数据呢?**有两种思路。** 一个思路是直接跨机房读取 A 机房的从库: @@ -28,7 +28,7 @@ ![img](assets/4924474ef8379137c6effe923a19e04d.jpg) -无论是哪一种思路, **都涉及到跨机房的数据传输,** 这就对机房之间延迟情况有比较高的要求了。而机房之间的延迟,和机房之间的距离息息相关, **你可以记住几个数字:** +无论是哪一种思路,**都涉及到跨机房的数据传输,** 这就对机房之间延迟情况有比较高的要求了。而机房之间的延迟,和机房之间的距离息息相关,**你可以记住几个数字:** \\1. 北京同地双机房之间的专线延迟一般在 1ms~3ms。 @@ -40,19 +40,19 @@ \\2. 国内异地双机房之间的专线延迟会在 50ms 之内。 -具体的延迟数据依据距离的不同而不同。比如,北京到天津的专线延迟,会在 10ms 之内;而北京到上海的延迟就会提高到接近 30ms;如果想要在北京和广州部署双机房,那么延迟就会到达 50ms 了。 **在这个延迟数据下,** 要想保证接口的响应时间在 200ms 之内,就要尽量减少跨机房的服务调用,更要避免跨机房的数据库和缓存操作了。 +具体的延迟数据依据距离的不同而不同。比如,北京到天津的专线延迟,会在 10ms 之内;而北京到上海的延迟就会提高到接近 30ms;如果想要在北京和广州部署双机房,那么延迟就会到达 50ms 了。**在这个延迟数据下,** 要想保证接口的响应时间在 200ms 之内,就要尽量减少跨机房的服务调用,更要避免跨机房的数据库和缓存操作了。 \\3. 如果你的业务是国际化的服务,需要部署跨国的双机房,那么机房之间的延迟就更高了,依据各大云厂商的数据来看,比如,从国内想要访问部署在美国西海岸的服务,这个延迟会在 100ms~200ms 左右。在这个延迟下,就要避免数据跨机房同步调用,而只做异步的数据同步。 -如果你正在考虑多机房部署的架构,那么这些数字都是至关重要的基础数据, **你需要牢牢记住,** 避免出现跨机房访问数据造成性能衰减问题。 +如果你正在考虑多机房部署的架构,那么这些数字都是至关重要的基础数据,**你需要牢牢记住,** 避免出现跨机房访问数据造成性能衰减问题。 -## 机房之间的数据延迟,在客观上是存在的,你没有办法改变,你可以做的,就是尽量避免数据延迟对于接口响应时间的影响。那么在数据延迟下, **你要如何设计多机房部署的方案呢?** 逐步迭代多机房部署方案 +## 机房之间的数据延迟,在客观上是存在的,你没有办法改变,你可以做的,就是尽量避免数据延迟对于接口响应时间的影响。那么在数据延迟下,**你要如何设计多机房部署的方案呢?** 逐步迭代多机房部署方案 ### 1. 同城双活 制定多机房部署的方案不是一蹴而就的,而是不断迭代发展的。我在上面提到,同城机房之间的延时在 1ms~3ms 左右,对于跨机房调用的容忍度比较高,所以,这种同城双活的方案复杂度会比较低。 -但是,它只能做到机房级别的容灾,无法做到城市级别的容灾。不过,相比于城市发生地震、洪水等自然灾害来说,机房网络故障、掉电出现的概率要大的多。所以,如果你的系统不需要考虑城市级别的容灾,一般做到同城双活就足够了。 **那么,同城双活的方案要如何设计呢?** **假设这样的场景:** 你在北京有 A 和 B 两个机房,A 是联通的机房,B 是电信的机房,机房之间以专线连接,方案设计时,核心思想是,尽量避免跨机房的调用。 **具体方案如下:** 首先,数据库的主库可以部署在一个机房中,比如部署在 A 机房中,那么 A 和 B 机房数据都会被写入到 A 机房中。然后,在 A、B 两个机房中各部署一个从库,通过主从复制的方式,从主库中同步数据,这样双机房的查询请求可以查询本机房的从库。一旦 A 机房发生故障,可以通过主从切换的方式,将 B 机房的从库提升为主库,达到容灾的目的。 +但是,它只能做到机房级别的容灾,无法做到城市级别的容灾。不过,相比于城市发生地震、洪水等自然灾害来说,机房网络故障、掉电出现的概率要大的多。所以,如果你的系统不需要考虑城市级别的容灾,一般做到同城双活就足够了。**那么,同城双活的方案要如何设计呢?** **假设这样的场景:** 你在北京有 A 和 B 两个机房,A 是联通的机房,B 是电信的机房,机房之间以专线连接,方案设计时,核心思想是,尽量避免跨机房的调用。**具体方案如下:** 首先,数据库的主库可以部署在一个机房中,比如部署在 A 机房中,那么 A 和 B 机房数据都会被写入到 A 机房中。然后,在 A、B 两个机房中各部署一个从库,通过主从复制的方式,从主库中同步数据,这样双机房的查询请求可以查询本机房的从库。一旦 A 机房发生故障,可以通过主从切换的方式,将 B 机房的从库提升为主库,达到容灾的目的。 缓存也可以部署在两个机房中,查询请求也读取本机房的缓存,如果缓存中数据不存在,就穿透到本机房的从库中,加载数据。数据的更新可以更新双机房中的数据,保证数据的一致性。 @@ -66,11 +66,11 @@ ### 2. 异地多活 -上面这个方案,足够应对你目前的需要,但是,你的业务是不断发展的,如果有朝一日,你的电商系统的流量达到了京东或者淘宝的级别,那么你就要考虑,即使机房所在的城市发生重大的自然灾害,也要保证系统的可用性。 **而这时,你需要采用异地多活的方案** (据我所知,阿里和饿了么采用的都是异地多活的方案)。 +上面这个方案,足够应对你目前的需要,但是,你的业务是不断发展的,如果有朝一日,你的电商系统的流量达到了京东或者淘宝的级别,那么你就要考虑,即使机房所在的城市发生重大的自然灾害,也要保证系统的可用性。**而这时,你需要采用异地多活的方案** (据我所知,阿里和饿了么采用的都是异地多活的方案)。 在考虑异地多活方案时,你首先要考虑异地机房的部署位置。它部署的不能太近,否则发生自然灾害时,很可能会波及。所以,如果你的主机房在北京,那么异地机房就尽量不要建设在天津,而是可以选择上海、广州这样距离较远的位置。但这就会造成更高的数据传输延迟,同城双活中,使用的跨机房写数据库的方案,就不合适了。 -所以,在数据写入时,你要保证只写本机房的数据存储服务,再采取数据同步的方案,将数据同步到异地机房中。 **一般来说,数据同步的方案有两种:** +所以,在数据写入时,你要保证只写本机房的数据存储服务,再采取数据同步的方案,将数据同步到异地机房中。**一般来说,数据同步的方案有两种:** 一种基于存储系统的主从复制,比如 MySQL 和 Redis。也就是在一个机房部署主库,在异地机房部署从库,两者同步主从复制, 实现数据的同步。 @@ -78,18 +78,18 @@ **我建议你,** 采用两种同步相结合的方式,比如,你可以基于消息的方式,同步缓存的数据、HBase 数据等。然后基于存储,主从复制同步 MySQL、Redis 等数据。 -无论是采取哪种方案,数据从一个机房,传输到另一个机房都会有延迟,所以,你需要尽量保证用户在读取自己的数据时,读取数据主库所在的机房。为了达到这一点,你需要对用户做分片,让一个用户每次的读写都尽量在同一个机房中。同时,在数据读取和服务调用时,也要尽量调用本机房的服务。 **这里有一个场景:** 假如在电商系统中,用户 A 要查看所有订单的信息,而这些订单中,店铺的信息和卖家的信息很可能是存储在异地机房中,那么你应该优先保证服务调用,和数据读取在本机房中进行,即使读取的是跨机房从库的数据,会有一些延迟,也是可以接受的。 +无论是采取哪种方案,数据从一个机房,传输到另一个机房都会有延迟,所以,你需要尽量保证用户在读取自己的数据时,读取数据主库所在的机房。为了达到这一点,你需要对用户做分片,让一个用户每次的读写都尽量在同一个机房中。同时,在数据读取和服务调用时,也要尽量调用本机房的服务。**这里有一个场景:** 假如在电商系统中,用户 A 要查看所有订单的信息,而这些订单中,店铺的信息和卖家的信息很可能是存储在异地机房中,那么你应该优先保证服务调用,和数据读取在本机房中进行,即使读取的是跨机房从库的数据,会有一些延迟,也是可以接受的。 ![img](assets/0138791e6164ea89380f262467820173.jpg) ## 课程小结 -本节课,为了提升系统的可用性和稳定性,我带你探讨了多机房部署的难点,以及同城双机房和异地多活的部署架构, **在这里,我想强调几个重点:** 不同机房的数据传输延迟,是造成多机房部署困难的主要原因,你需要知道,同城多机房的延迟一般在 1ms~3ms,异地机房的延迟在 50ms 以下,而跨国机房的延迟在 200ms 以下。 +本节课,为了提升系统的可用性和稳定性,我带你探讨了多机房部署的难点,以及同城双机房和异地多活的部署架构,**在这里,我想强调几个重点:** 不同机房的数据传输延迟,是造成多机房部署困难的主要原因,你需要知道,同城多机房的延迟一般在 1ms~3ms,异地机房的延迟在 50ms 以下,而跨国机房的延迟在 200ms 以下。 同城多机房方案可以允许有跨机房数据写入的发生,但是数据的读取,和服务的调用应该尽量保证在同一个机房中。 异地多活方案则应该避免跨机房同步的数据写入和读取,而是采取异步的方式,将数据从一个机房同步到另一个机房。 -多机房部署是一个业务发展到一定规模,对于机房容灾有需求时,才会考虑的方案,能不做则尽量不要做。一旦你的团队决定做多机房部署,那么同城双活已经能够满足你的需求了,这个方案相比异地多活要简单很多。而在业界,很少有公司,能够搭建一套真正的异步多活架构,这是因为这套架构在实现时过于复杂, **所以,轻易不要尝试。** +多机房部署是一个业务发展到一定规模,对于机房容灾有需求时,才会考虑的方案,能不做则尽量不要做。一旦你的团队决定做多机房部署,那么同城双活已经能够满足你的需求了,这个方案相比异地多活要简单很多。而在业界,很少有公司,能够搭建一套真正的异步多活架构,这是因为这套架构在实现时过于复杂,**所以,轻易不要尝试。** 总之,架构需要依据系统的量级和对可用性、性能、扩展性的要求,不断演进和调整,盲目地追求架构的“先进性”只能造成方案的复杂,增加运维成本,从而给你的系统维护带来不便。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25429\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25429\350\256\262.md" index 59ef2861f..7acc1b764 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25429\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25429\350\256\262.md" @@ -20,9 +20,9 @@ ## 跨语言体系带来的挑战 -其实,一个公司的不同团队,使用不同的开发语言是比较常见的。比如,微博的主要开发语言是 Java 和 PHP,近几年也有一些使用 Go 开发的系统。而使用不同的语言开发出来的微服务, **在相互调用时会存在两方面的挑战:** 一方面,服务之间的通信协议上,要对多语言友好,要想实现跨语言调用,关键点是选择合适的序列化方式。 **我给你举一个例子。** +其实,一个公司的不同团队,使用不同的开发语言是比较常见的。比如,微博的主要开发语言是 Java 和 PHP,近几年也有一些使用 Go 开发的系统。而使用不同的语言开发出来的微服务,**在相互调用时会存在两方面的挑战:** 一方面,服务之间的通信协议上,要对多语言友好,要想实现跨语言调用,关键点是选择合适的序列化方式。**我给你举一个例子。** -比如,你用 Java 开发一个 RPC 服务,使用的是 Java 原生的序列化方式,这种序列化方式对于其它语言并不友好,那么,你使用其它语言,调用这个 RPC 服务时,就很难解析序列化之后的二进制流。 **所以,我建议你,** 在选择序列化协议时,考虑序列化协议是否对多语言友好,比如,你可以选择 Protobuf、Thrift,这样一来,跨语言服务调用的问题,就可以很容易地解决了。 +比如,你用 Java 开发一个 RPC 服务,使用的是 Java 原生的序列化方式,这种序列化方式对于其它语言并不友好,那么,你使用其它语言,调用这个 RPC 服务时,就很难解析序列化之后的二进制流。**所以,我建议你,** 在选择序列化协议时,考虑序列化协议是否对多语言友好,比如,你可以选择 Protobuf、Thrift,这样一来,跨语言服务调用的问题,就可以很容易地解决了。 另一方面,使用新语言开发的微服务,无法使用之前积累的,服务治理的策略。比如说,RPC 客户端在使用注册中心,订阅服务的时候,为了避免每次 RPC 调用都要与注册中心交互,一般会在 RPC 客户端,缓存节点的数据。如果注册中心中的服务节点发生了变更,那么 RPC 客户端的节点缓存会得到通知,并且变更缓存数据。 @@ -30,7 +30,7 @@ 除此之外,负载均衡、熔断降级、流量控制、打印分布式追踪日志等等,这些服务治理的策略都需要重新实现,而使用其它语言重新实现这些策略无疑会带来巨大的工作量,也是中间件研发中,一个很大的痛点。 -## 那么,你要如何屏蔽服务化架构中,服务治理的细节,或者说, **如何让服务治理的策略在多语言之间复用呢?** 可以考虑将服务治理的细节,从 RPC 客户端中拆分出来,形成一个代理层单独部署。这个代理层可以使用单一的语言实现,所有的流量都经过代理层,来使用其中的服务治理策略。这是一种“关注点分离”的实现方式, **也是 Service Mesh 的核心思想。** Service Mesh 是如何工作的 +## 那么,你要如何屏蔽服务化架构中,服务治理的细节,或者说,**如何让服务治理的策略在多语言之间复用呢?** 可以考虑将服务治理的细节,从 RPC 客户端中拆分出来,形成一个代理层单独部署。这个代理层可以使用单一的语言实现,所有的流量都经过代理层,来使用其中的服务治理策略。这是一种“关注点分离”的实现方式,**也是 Service Mesh 的核心思想。** Service Mesh 是如何工作的 ### 1. 什么是 Service Mesh @@ -40,7 +40,7 @@ Service Mesh 主要处理服务之间的通信,它的主要实现形式就是 在这种形式下,RPC 客户端将数据包先发送给,与自身同主机部署的 Sidecar,在 Sidecar 中经过服务发现、负载均衡、服务路由、流量控制之后,再将数据发往指定服务节点的 Sidecar,在服务节点的 Sidecar 中,经过记录访问日志、记录分布式追踪日志、限流之后,再将数据发送给 RPC 服务端。 -这种方式,可以把业务代码和服务治理的策略隔离开,将服务治理策略下沉,让它成为独立的基础模块。这样一来,不仅可以实现跨语言,服务治理策略的复用,还能对这些 Sidecar 做统一的管理。 **目前,业界提及最多的 Service Mesh 方案当属** **istio** **,** 它的玩法是这样的: +这种方式,可以把业务代码和服务治理的策略隔离开,将服务治理策略下沉,让它成为独立的基础模块。这样一来,不仅可以实现跨语言,服务治理策略的复用,还能对这些 Sidecar 做统一的管理。**目前,业界提及最多的 Service Mesh 方案当属** **istio** **,** 它的玩法是这样的: ![img](assets/604415b5d99ca176baf1c628d0677c64.jpg) @@ -54,7 +54,7 @@ Service Mesh 主要处理服务之间的通信,它的主要实现形式就是 ### 2. 如何将流量转发到 Sidecar 中 -在 Service Mesh 的实现中,一个主要的问题,是如何尽量无感知地引入 Sidecar 作为网络代理,也就是说,无论是数据流入还是数据流出时,都要将数据包重定向到 Sidecar 的端口上。 **实现思路一般有两个:** +在 Service Mesh 的实现中,一个主要的问题,是如何尽量无感知地引入 Sidecar 作为网络代理,也就是说,无论是数据流入还是数据流出时,都要将数据包重定向到 Sidecar 的端口上。**实现思路一般有两个:** 第一种,使用 iptables 的方式来实现流量透明的转发,而 Istio 就默认了,使用 iptables 来实现数据包的转发。为了能更清晰的说明流量转发的原理,我们先简单地回顾一下什么是 iptables。 @@ -105,7 +105,7 @@ iptables -t nat -A ISTIO_INBOUND -p tcp --dport "${port}" -j ISTIO_IN_REDIRECT / ![img](assets/d344cb29d46dc480e67eabf57ddda622.jpg) -当然,除了 iptables 和轻量级客户端两种方式外,目前在探索的方案还有Cilium,这个方案可以从 Socket 层面实现请求的转发,也就可以避免 iptables 方式在性能上的损耗。 **在这几种方案中,我建议你使用轻量级客户端的方式,** 这样虽然会有一些改造成本,但是却在实现上最简单,可以快速的让 Service Mesh 在你的项目中落地。 +当然,除了 iptables 和轻量级客户端两种方式外,目前在探索的方案还有Cilium,这个方案可以从 Socket 层面实现请求的转发,也就可以避免 iptables 方式在性能上的损耗。**在这几种方案中,我建议你使用轻量级客户端的方式,** 这样虽然会有一些改造成本,但是却在实现上最简单,可以快速的让 Service Mesh 在你的项目中落地。 当然,无论采用哪种方式,你都可以实现将 Sidecar 部署到,客户端和服务端的调用链路上,让它代理进出流量,这样,你就可以使用运行在 Sidecar 中的服务治理的策略了。至于这些策略我在前面的课程中都带你了解过(你可以回顾 23 至 26 讲的课程),这里就不再赘述了。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25430\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25430\350\256\262.md" index 329f93fcb..d24a44b18 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25430\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25430\350\256\262.md" @@ -22,15 +22,15 @@ ## 监控指标如何选择 -你在搭建监控系统时,所面临的第一个问题就是,选择什么样的监控指标,也就是监控什么。有些同学在给一个新的系统,设定监控指标的时候,会比较迷茫,不知道从哪方面入手。其实,有一些成熟的理论和套路,你可以直接拿来使用。比如,谷歌针对分布式系统监控的经验总结,四个黄金信号(Four Golden Signals)。它指的是,在服务层面一般需要监控四个指标, **分别是延迟,通信量、错误和饱和度。** 延迟指的是请求的响应时间。比如,接口的响应时间、访问数据库和缓存的响应时间。 +你在搭建监控系统时,所面临的第一个问题就是,选择什么样的监控指标,也就是监控什么。有些同学在给一个新的系统,设定监控指标的时候,会比较迷茫,不知道从哪方面入手。其实,有一些成熟的理论和套路,你可以直接拿来使用。比如,谷歌针对分布式系统监控的经验总结,四个黄金信号(Four Golden Signals)。它指的是,在服务层面一般需要监控四个指标,**分别是延迟,通信量、错误和饱和度。** 延迟指的是请求的响应时间。比如,接口的响应时间、访问数据库和缓存的响应时间。 通信量可以理解为吞吐量,也就是单位时间内,请求量的大小。比如,访问第三方服务的请求量,访问消息队列的请求量。 -错误表示当前系统发生的错误数量。 **这里需要注意的是,** 我们需要监控的错误既有显示的,比如在监控 Web 服务时,出现 4 **和 5** 的响应码;也有隐示的,比如,Web 服务虽然返回的响应码是 200,但是却发生了一些和业务相关的错误(出现了数组越界的异常或者空指针异常等),这些都是错误的范畴。 +错误表示当前系统发生的错误数量。**这里需要注意的是,** 我们需要监控的错误既有显示的,比如在监控 Web 服务时,出现 4 **和 5** 的响应码;也有隐示的,比如,Web 服务虽然返回的响应码是 200,但是却发生了一些和业务相关的错误(出现了数组越界的异常或者空指针异常等),这些都是错误的范畴。 饱和度指的是服务或者资源到达上限的程度(也可以说是服务或者资源的利用率),比如说 CPU 的使用率,内存使用率,磁盘使用率,缓存数据库的连接数等等。 -这四个黄金信号提供了通用的监控指标, **除此之外,你还可以借鉴 RED 指标体系。** 这个体系,是四个黄金信号中衍生出来的,其中,R 代表请求量(Request rate),E 代表错误(Error),D 代表响应时间(Duration),少了饱和度的指标。你可以把它当作一种简化版的通用监控指标体系。 +这四个黄金信号提供了通用的监控指标,**除此之外,你还可以借鉴 RED 指标体系。** 这个体系,是四个黄金信号中衍生出来的,其中,R 代表请求量(Request rate),E 代表错误(Error),D 代表响应时间(Duration),少了饱和度的指标。你可以把它当作一种简化版的通用监控指标体系。 当然,一些组件或者服务还有独特的指标,这些指标也是需要你特殊关注的。比如,课程中提到的数据库主从延迟数据、消息队列的堆积情况、缓存的命中率等等。我把高并发系统中常见组件的监控指标,整理成了一张表格,其中没有包含诸如 CPU、内存、网络、磁盘等基础监控指标,只是业务上监控指标,主要方便你在实际工作中参考使用。 @@ -40,9 +40,9 @@ ## 如何采集数据指标 -说到监控指标的采集,我们一般会依据采集数据源的不同,选用不同的采集方式, **总结起来,大概有以下几种类型:** **首先,** Agent 是一种比较常见的,采集数据指标的方式。 +说到监控指标的采集,我们一般会依据采集数据源的不同,选用不同的采集方式,**总结起来,大概有以下几种类型:** **首先,** Agent 是一种比较常见的,采集数据指标的方式。 -我们通过在数据源的服务器上,部署自研或者开源的 Agent,来收集收据,发送给监控系统,实现数据的采集。在采集数据源上的信息时,Agent 会依据数据源上,提供的一些接口获取数据, **我给你举两个典型的例子。** 比如,你要从 Memcached 服务器上,获取它的性能数据,那么,你就可以在 Agent 中,连接这个 Memcached 服务器,并且发送一个 stats 命令,获取服务器的统计信息。然后,你就可以从返回的信息中,挑选重要的监控指标,发送给监控服务器,形成 Memcached 服务的监控报表。你也可以从这些统计信息中,看出当前 Memcached 服务器,是否存在潜在的问题。下面是我推荐的,一些重要的状态项, **你可以参考使用。** +我们通过在数据源的服务器上,部署自研或者开源的 Agent,来收集收据,发送给监控系统,实现数据的采集。在采集数据源上的信息时,Agent 会依据数据源上,提供的一些接口获取数据,**我给你举两个典型的例子。** 比如,你要从 Memcached 服务器上,获取它的性能数据,那么,你就可以在 Agent 中,连接这个 Memcached 服务器,并且发送一个 stats 命令,获取服务器的统计信息。然后,你就可以从返回的信息中,挑选重要的监控指标,发送给监控服务器,形成 Memcached 服务的监控报表。你也可以从这些统计信息中,看出当前 Memcached 服务器,是否存在潜在的问题。下面是我推荐的,一些重要的状态项,**你可以参考使用。** ```plaintext STAT cmd_get 201809037423 // 计算查询的 QPS @@ -59,16 +59,16 @@ STAT evictions 11008640149 // 当前被 memcached 服务器剔除的 item 数 ``` 另外,如果你是 Java 的开发者,那么一般使用 Java 语言开发的中间件,或者组件,都可以通过 JMX 获取统计或者监控信息。比如,在19 讲中,我提到可以使用 JMX,监控 Kafka 队列的堆积数,再比如,你也可以通过 JMX 监控 JVM 内存信息和 GC 相关的信息。 -另一种很重要的数据获取方式, **是在代码中埋点。** 这个方式与 Agent 的不同之处在于,Agent 主要收集的是组件服务端的信息,而埋点则是从客户端的角度,来描述所使用的组件,和服务的性能和可用性。 **那么埋点的方式怎么选择呢?** 你可以使用25 讲分布式 Trace 组件中,提到的面向切面编程的方式;也可以在资源客户端中,直接计算调用资源或者服务的耗时、调用量、慢请求数,并且发送给监控服务器。 **这里你需要注意一点,** 由于调用缓存、数据库的请求量会比较高,一般会单机也会达到每秒万次,如果不经过任何优化,把每次请求耗时都发送给监控服务器,那么,监控服务器会不堪重负。所以,我们一般会在埋点时,先做一些汇总。比如,每隔 10 秒汇总这 10 秒内,对同一个资源的请求量总和、响应时间分位值、错误数等,然后发送给监控服务器。这样,就可以大大减少发往监控服务器的请求量了。 **最后,** 日志也是你监控数据的重要来源之一。 +另一种很重要的数据获取方式,**是在代码中埋点。** 这个方式与 Agent 的不同之处在于,Agent 主要收集的是组件服务端的信息,而埋点则是从客户端的角度,来描述所使用的组件,和服务的性能和可用性。**那么埋点的方式怎么选择呢?** 你可以使用25 讲分布式 Trace 组件中,提到的面向切面编程的方式;也可以在资源客户端中,直接计算调用资源或者服务的耗时、调用量、慢请求数,并且发送给监控服务器。**这里你需要注意一点,** 由于调用缓存、数据库的请求量会比较高,一般会单机也会达到每秒万次,如果不经过任何优化,把每次请求耗时都发送给监控服务器,那么,监控服务器会不堪重负。所以,我们一般会在埋点时,先做一些汇总。比如,每隔 10 秒汇总这 10 秒内,对同一个资源的请求量总和、响应时间分位值、错误数等,然后发送给监控服务器。这样,就可以大大减少发往监控服务器的请求量了。**最后,** 日志也是你监控数据的重要来源之一。 你所熟知的 Tomcat 和 Nginx 的访问日志,都是重要的监控日志。你可以通过开源的日志采集工具,将这些日志中的数据发送给监控服务器。目前,常用的日志采集工具有很多,比如,Apache Flume、Fluentd和Filebeat,你可以选择一种熟悉的使用。比如在我的项目中,我会倾向于使用 Filebeat 来收集监控日志数据。 监控数据的处理和存储 ---------- 在采集到监控数据之后,你就可以对它们进行处理和存储了,在此之前,我们一般会先用消息队列来承接数据,主要的作用是削峰填谷,防止写入过多的监控数据,让监控服务产生影响。 与此同时,我们一般会部署两个队列处理程序,来消费消息队列中的数据。 一个处理程序接收到数据后,把数据写入到 Elasticsearch,然后通过 Kibana 展示数据,这份数据主要是用来做原始数据的查询; -另一个处理程序是一些流式处理的中间件,比如,Spark、Storm。它们从消息队列里,接收数据后会做一些处理,这些处理包括: **- 解析数据格式,尤其是日志格式。** 从里面提取诸如请求量、响应时间、请求 URL 等数据; **- 对数据做一些聚合运算。** 比如,针对 Tomcat 访问日志,可以计算同一个 URL 一段时间之内的请求量、响应时间分位值、非 200 请求量的大小等等。 **- 将数据存储在时间序列数据库中。** 这类数据库的特点是,可以对带有时间标签的数据,做更有效的存储,而我们的监控数据恰恰带有时间标签,并且按照时间递增,非常适合存储在时间序列数据库中。目前业界比较常用的时序数据库有 InfluxDB、OpenTSDB、Graphite,各大厂的选择均有不同,你可以选择一种熟悉的来使用。 **- 最后,** 你就可以通过 Grafana 来连接时序数据库,将监控数据绘制成报表,呈现给开发和运维的同学了。 +另一个处理程序是一些流式处理的中间件,比如,Spark、Storm。它们从消息队列里,接收数据后会做一些处理,这些处理包括: **- 解析数据格式,尤其是日志格式。** 从里面提取诸如请求量、响应时间、请求 URL 等数据; **- 对数据做一些聚合运算。** 比如,针对 Tomcat 访问日志,可以计算同一个 URL 一段时间之内的请求量、响应时间分位值、非 200 请求量的大小等等。**- 将数据存储在时间序列数据库中。** 这类数据库的特点是,可以对带有时间标签的数据,做更有效的存储,而我们的监控数据恰恰带有时间标签,并且按照时间递增,非常适合存储在时间序列数据库中。目前业界比较常用的时序数据库有 InfluxDB、OpenTSDB、Graphite,各大厂的选择均有不同,你可以选择一种熟悉的来使用。**- 最后,** 你就可以通过 Grafana 来连接时序数据库,将监控数据绘制成报表,呈现给开发和运维的同学了。 ![img](assets/88a8d8c2461297fed4e95214f4325e62.jpg) -至此,你和你的团队,也就完成了垂直电商系统,服务端监控系统搭建的全过程。这里我想再多说一点,我们从不同的数据源中采集了很多的指标,最终在监控系统中一般会形成以下几个报表,你在实际的工作中可以参考借鉴: **1. 访问趋势报表。** 这类报表接入的是 Web 服务器,和应用服务器的访问日志,展示了服务整体的访问量、响应时间情况、错误数量、带宽等信息。它主要反映的是,服务的整体运行情况,帮助你来发现问题。 **2. 性能报表。** 这类报表对接的是资源和依赖服务的埋点数据,展示了被埋点资源的访问量和响应时间情况。它反映了资源的整体运行情况,当你从访问趋势报表发现问题后,可以先从性能报表中,找到究竟是哪一个资源或者服务出现了问题。 **3. 资源报表。** 这类报表主要对接的是,使用 Agent 采集的,资源的运行情况数据。当你从性能报表中,发现某一个资源出现了问题,那么就可以进一步从这个报表中,发现资源究竟出现了什么问题,是连接数异常增高,还是缓存命中率下降。这样可以进一步帮你分析问题的根源,找到解决问题的方案。 +至此,你和你的团队,也就完成了垂直电商系统,服务端监控系统搭建的全过程。这里我想再多说一点,我们从不同的数据源中采集了很多的指标,最终在监控系统中一般会形成以下几个报表,你在实际的工作中可以参考借鉴: **1. 访问趋势报表。** 这类报表接入的是 Web 服务器,和应用服务器的访问日志,展示了服务整体的访问量、响应时间情况、错误数量、带宽等信息。它主要反映的是,服务的整体运行情况,帮助你来发现问题。**2. 性能报表。** 这类报表对接的是资源和依赖服务的埋点数据,展示了被埋点资源的访问量和响应时间情况。它反映了资源的整体运行情况,当你从访问趋势报表发现问题后,可以先从性能报表中,找到究竟是哪一个资源或者服务出现了问题。**3. 资源报表。** 这类报表主要对接的是,使用 Agent 采集的,资源的运行情况数据。当你从性能报表中,发现某一个资源出现了问题,那么就可以进一步从这个报表中,发现资源究竟出现了什么问题,是连接数异常增高,还是缓存命中率下降。这样可以进一步帮你分析问题的根源,找到解决问题的方案。 课程小结 ---- 本节课,我带你了解了,服务端监控搭建的过程,在这里,你需要了解以下几个重点: diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25431\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25431\350\256\262.md" index aaea030a5..700358531 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25431\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25431\350\256\262.md" @@ -6,13 +6,13 @@ 不过,你很快发现,有一些问题,服务端的监控报表无法排查,甚至无法感知。比如,有用户反馈创建订单失败,但是从服务端的报表来看,并没有什么明显的性能波动,从存储在 Elasticsearch 里的原始日志中,甚至没有找到这次创建订单的请求。这有可能是客户端有 Bug,或者网络抖动导致创建订单的请求并没有发送到服务端。 -再比如,有些用户会反馈,使用长城宽带打开商品详情页面特别慢,甚至出现 DNS 解析失败的情况。 **那么,当我们遇到这类问题时,要如何排查和优化呢?** +再比如,有些用户会反馈,使用长城宽带打开商品详情页面特别慢,甚至出现 DNS 解析失败的情况。**那么,当我们遇到这类问题时,要如何排查和优化呢?** -这里面涉及一个概念叫应用性能管理(Application Performance Management,简称 APM), **它的含义是:** 对应用各个层面做全方位的监测,期望及时发现可能存在的问题,并加以解决,从而提升系统的性能和可用性。 +这里面涉及一个概念叫应用性能管理(Application Performance Management,简称 APM),**它的含义是:** 对应用各个层面做全方位的监测,期望及时发现可能存在的问题,并加以解决,从而提升系统的性能和可用性。 你是不是觉得和之前讲到的服务端监控很相似?其实,服务端监控的核心关注点是后端服务的性能和可用性,而应用性能管理的核心关注点是终端用户的使用体验,也就是你需要衡量,从客户端请求发出开始,到响应数据返回到客户端为止,这个端到端的整体链路上的性能情况。 -## 如果你能做到这一点,那么无论是订单创建问题的排查,还是长城宽带用户页面打开缓慢的问题,都可以通过这套监控来发现和排查。 **那么,如何搭建这么一套端到端的监控体系呢?** 如何搭建 APM 系统 +## 如果你能做到这一点,那么无论是订单创建问题的排查,还是长城宽带用户页面打开缓慢的问题,都可以通过这套监控来发现和排查。**那么,如何搭建这么一套端到端的监控体系呢?** 如何搭建 APM 系统 与搭建服务端监控系统类似,在搭建端到端的,应用性能管理系统时,我们也可以从数据的采集、存储和展示几个方面来思考。 @@ -20,7 +20,7 @@ 虽然客户端需要监控的指标很多,比如监控网络情况,监控客户端卡顿情况、垃圾收集数据等等,但我们可以定义一种通用的数据采集格式。 -比如,在我之前的公司里,采集的数据包含下面几个部分,SDK 将这几部分数据转换成 JSON 格式后,就可以发送给 APM 通道服务了。 **这几部分数据格式,你可以在搭建自己的 APM 系统时,直接拿来参考。** +比如,在我之前的公司里,采集的数据包含下面几个部分,SDK 将这几部分数据转换成 JSON 格式后,就可以发送给 APM 通道服务了。**这几部分数据格式,你可以在搭建自己的 APM 系统时,直接拿来参考。** 系统部分:包括数据协议的版本号,以及下面提到的消息头、端消息体、业务消息体的长度; @@ -46,7 +46,7 @@ ## 需要监控用户的哪些信息 -在我看来,搭建端到端的监控体系的首要目标,是解决如何监控客户端网络的问题,这是因为我们遇到的客户端问题, **大部分的原因还是出在客户端网络上。** +在我看来,搭建端到端的监控体系的首要目标,是解决如何监控客户端网络的问题,这是因为我们遇到的客户端问题,**大部分的原因还是出在客户端网络上。** 在中国复杂的网络环境下,大的运营商各行其是,各自为政,在不同的地区的链路质量各有不同,而小的运营商又鱼龙混杂,服务质量得不到保障。我给你说一个典型的问题。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25432\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25432\350\256\262.md" index eac88b1cc..1666642a0 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25432\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25432\350\256\262.md" @@ -14,7 +14,7 @@ 不过,我想让你回想一下,自己是怎么做压力测试的?是不是像很多同学一样:先搭建一套与正式环境功能相同的测试环境,并且导入或者生成一批测试数据,然后在另一台服务器,启动多个线程并发地调用需要压测的接口(接口的参数一般也会设置成相同的,比如,想要压测获取商品信息的接口,那么压测时会使用同一个商品 ID)。最后,通过统计访问日志,或者查看测试环境的监控系统,来记录最终压测 QPS 是多少之后,直接交差? -这么做压力测试其实是不正确的, **错误之处主要有以下几点:** +这么做压力测试其实是不正确的,**错误之处主要有以下几点:** \\1. 首先,做压力测试时,最好使用线上的数据和线上的环境,因为,你无法确定自己搭建的测试环境与正式环境的差异,是否会影响到压力测试的结果; @@ -28,9 +28,9 @@ **那么究竟什么是压力测试呢?** 压力测试指的是,在高并发大流量下,进行的测试,测试人员可以通过观察系统在峰值负载下的表现,从而找到系统中存在的性能隐患。 -与监控一样,压力测试是一种常见的,发现系统中存在问题的方式,也是保障系统可用性和稳定性的重要手段。而在压力测试的过程中,我们不能只针对某一个核心模块来做压测,而需要将接入层、所有后端服务、数据库、缓存、消息队列、中间件以及依赖的第三方服务系统及其资源,都纳入压力测试的目标之中。因为,一旦用户的访问行为增加,包含上述组件服务的整个链路都会受到不确定的大流量的冲击,因此,它们都需要依赖压力测试来发现可能存在的性能瓶颈, **这种针对整个调用链路执行的压力测试也称为“全链路压测”。** 由于在互联网项目中,功能迭代的速度很快,系统的复杂性也变得越来越高,新增加的功能和代码很可能会成为新的性能瓶颈点。也许半年前做压力测试时,单台机器可以承担每秒 1000 次请求,现在很可能就承担每秒 800 次请求了。所以,压力测试应该作为系统稳定性保障的常规手段,周期性地进行。 +与监控一样,压力测试是一种常见的,发现系统中存在问题的方式,也是保障系统可用性和稳定性的重要手段。而在压力测试的过程中,我们不能只针对某一个核心模块来做压测,而需要将接入层、所有后端服务、数据库、缓存、消息队列、中间件以及依赖的第三方服务系统及其资源,都纳入压力测试的目标之中。因为,一旦用户的访问行为增加,包含上述组件服务的整个链路都会受到不确定的大流量的冲击,因此,它们都需要依赖压力测试来发现可能存在的性能瓶颈,**这种针对整个调用链路执行的压力测试也称为“全链路压测”。** 由于在互联网项目中,功能迭代的速度很快,系统的复杂性也变得越来越高,新增加的功能和代码很可能会成为新的性能瓶颈点。也许半年前做压力测试时,单台机器可以承担每秒 1000 次请求,现在很可能就承担每秒 800 次请求了。所以,压力测试应该作为系统稳定性保障的常规手段,周期性地进行。 -## 但是,通常做一次全链路压力测试,需要联合 DBA、运维、依赖服务方、中间件架构等多个团队,一起协调进行,无论是人力成本还是沟通协调的成本都比较高。同时,在压力测试的过程中,如果没有很好的监控机制,那么还会对线上系统造成不利的影响。 **为了解决这些问题,我们需要搭建一套自动化的全链路压测平台,来降低成本和风险。** 如何搭建全链路压测平台 +## 但是,通常做一次全链路压力测试,需要联合 DBA、运维、依赖服务方、中间件架构等多个团队,一起协调进行,无论是人力成本还是沟通协调的成本都比较高。同时,在压力测试的过程中,如果没有很好的监控机制,那么还会对线上系统造成不利的影响。**为了解决这些问题,我们需要搭建一套自动化的全链路压测平台,来降低成本和风险。** 如何搭建全链路压测平台 搭建全链路压测平台,主要有两个关键点。 @@ -54,7 +54,7 @@ 一般来说,我们系统的入口流量是来自于客户端的 HTTP 请求,所以,我们会考虑在系统高峰期时,将这些入口流量拷贝一份,在经过一些流量清洗的工作之后(比如过滤一些无效的请求),将数据存储在像是 HBase、MongoDB 这些 NoSQL 存储组件,或者亚马逊 S3 这些云存储服务中,我们称之为流量数据工厂。 -这样,当我们要压测的时候,就可以从这个工厂中获取数据,将数据切分多份后下发到多个压测节点上了, **在这里,我想强调几个,你需要特殊注意的点。** +这样,当我们要压测的时候,就可以从这个工厂中获取数据,将数据切分多份后下发到多个压测节点上了,**在这里,我想强调几个,你需要特殊注意的点。** 首先,我们可以使用多种方式来实现流量的拷贝。最简单的一种方式:直接拷贝负载均衡服务器的访问日志,数据就以文本的方式写入到流量数据工厂中,但是这样产生的数据在发起压测时,需要自己写解析的脚本来解析访问日志,会增加压测时候的成本,不太建议使用。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25433\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25433\350\256\262.md" index c7ab40f82..aa8d0025b 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25433\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25433\350\256\262.md" @@ -49,7 +49,7 @@ 配置中心可以算是微服务架构中的一个标配组件了。业界也提供了多种开源方案供你选择,比较出名的有携程开源的 Apollo,百度开源的 Disconf,360 开源的 QConf,Spring Cloud 的组件 Spring Cloud Config 等等。 -在我看来,Apollo 支持不同环境,不同集群的配置,有完善的管理功能,支持灰度发布、更改热发布等功能, **在所有配置中心中功能比较齐全,推荐你使用。** +在我看来,Apollo 支持不同环境,不同集群的配置,有完善的管理功能,支持灰度发布、更改热发布等功能,**在所有配置中心中功能比较齐全,推荐你使用。** 那么,配置中心的组件在实现上,有哪些关键的点呢?如果你想对配置中心组件有更强地把控力,想要自行研发一套符合自己业务场景的组件,又要如何入手呢? @@ -83,13 +83,13 @@ 另一种长连的方式,则是在配置中心服务端保存每个连接关注的配置项列表。这样,当配置中心感知到配置变化后,就可以通过这个连接,把变更的配置推送给客户端。这种方式需要保持长连,也需要保存连接和配置的对应关系,实现上要比轮询的方式复杂一些,但是相比轮询方式来说,能够更加实时地获取配置变更的消息。 -而在我看来,配置服务中存储的配置变更频率不高,所以对于实时性要求不高,但是期望实现上能够足够简单, **所以如果选择自研配置中心的话,可以考虑使用轮询的方式。** +而在我看来,配置服务中存储的配置变更频率不高,所以对于实时性要求不高,但是期望实现上能够足够简单,**所以如果选择自研配置中心的话,可以考虑使用轮询的方式。** #### 如何保证配置中心高可用 除了变更通知以外,在配置中心实现中,另外一个比较关键的点在于如何保证它的可用性,因为对于配置中心来说,它的可用性的重要程度要远远大于性能。这是因为我们一般会在服务器启动时,从配置中心中获取配置,如果配置获取的性能不高,那么外在的表现也只是应用启动时间慢了,对于业务的影响不大;但是,如果获取不到配置,很可能会导致启动失败。 -比如,我们把数据库的地址存储在了配置中心里,如果配置中心宕机导致我们无法获取数据库的地址,那么自然应用程序就会启动失败。 **因此,我们的诉求是让配置中心“旁路化”。** 也就是说,即使配置中心宕机,或者配置中心依赖的存储宕机,我们仍然能够保证应用程序是可以启动的。那么这是如何实现的呢? +比如,我们把数据库的地址存储在了配置中心里,如果配置中心宕机导致我们无法获取数据库的地址,那么自然应用程序就会启动失败。**因此,我们的诉求是让配置中心“旁路化”。** 也就是说,即使配置中心宕机,或者配置中心依赖的存储宕机,我们仍然能够保证应用程序是可以启动的。那么这是如何实现的呢? 我们一般会在配置中心的客户端上,增加两级缓存:第一级缓存是内存的缓存;另外一级缓存是文件的缓存。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25434\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25434\350\256\262.md" index 7bca450f7..315349be4 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25434\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25434\350\256\262.md" @@ -24,13 +24,13 @@ 所以,一旦作为入口的 A 流量增加,你可能会考虑把 A、B 和 D 服务扩容,忽略 C。那么 C 就有可能因为无法承担这么大的流量,导致请求处理缓慢,进一步会让 B 在调用 C 的时候,B 中的请求被阻塞,等待 C 返回响应结果。这样一来,B 服务中被占用的线程资源就不能释放。 -久而久之,B 就会因为线程资源被占满,无法处理后续的请求。那么从 A 发往 B 的请求,就会被放入 B 服务线程池的队列中,然后 A 调用 B 响应时间变长,进而拖垮 A 服务。你看,仅仅因为非核心服务 C 的响应时间变长,就可以导致整体服务宕机, **这就是我们经常遇到的一种服务雪崩情况。** ![img](assets/42ccaedc09f890924caae689f0323443.jpg) +久而久之,B 就会因为线程资源被占满,无法处理后续的请求。那么从 A 发往 B 的请求,就会被放入 B 服务线程池的队列中,然后 A 调用 B 响应时间变长,进而拖垮 A 服务。你看,仅仅因为非核心服务 C 的响应时间变长,就可以导致整体服务宕机,**这就是我们经常遇到的一种服务雪崩情况。**![img](assets/42ccaedc09f890924caae689f0323443.jpg) 那么我们要如何避免出现上面这种情况呢?从我刚刚的介绍中你可以看到,因为服务调用方等待服务提供方的响应时间过长,它的资源被耗尽,才引发了级联反应,发生雪崩。 所以在分布式环境下,系统最怕的反而不是某一个服务或者组件宕机,而是最怕它响应缓慢,因为,某一个服务或者组件宕机也许只会影响系统的部分功能,但它响应一慢,就会出现雪崩拖垮整个系统。 -而我们在部门内部讨论方案的时候,会格外注意这个问题,解决的思路就是在检测到某一个服务的响应时间出现异常时,切断调用它的服务与它之间的联系,让服务的调用快速返回失败,从而释放这次请求持有的资源。 **这个思路也就是我们经常提到的降级和熔断机制。** +而我们在部门内部讨论方案的时候,会格外注意这个问题,解决的思路就是在检测到某一个服务的响应时间出现异常时,切断调用它的服务与它之间的联系,让服务的调用快速返回失败,从而释放这次请求持有的资源。**这个思路也就是我们经常提到的降级和熔断机制。** 那么降级和熔断分别是怎么做的呢?它们之间有什么相同点和不同点呢?你在自己的项目中要如何实现熔断降级呢? diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25435\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25435\350\256\262.md" index b38ff37ab..34333d2d4 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25435\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25435\350\256\262.md" @@ -75,13 +75,13 @@ public boolena isRateLimit() { 滑动窗口的算法解决了临界时间点上突发流量无法控制的问题,但是却因为要存储每个小的时间窗口内的计数,所以空间复杂度有所增加。 -虽然滑动窗口算法解决了窗口边界的大流量的问题,但是它和固定窗口算法一样,还是无法限制短时间之内的集中流量,也就是说无法控制流量让它们更加平滑。 **因此,在实际的项目中,我很少使用基于时间窗口的限流算法,而是使用其他限流的算法:一种算法叫做漏桶算法,一种叫做令牌筒算法。** #### 漏桶算法与令牌筒算法 +虽然滑动窗口算法解决了窗口边界的大流量的问题,但是它和固定窗口算法一样,还是无法限制短时间之内的集中流量,也就是说无法控制流量让它们更加平滑。**因此,在实际的项目中,我很少使用基于时间窗口的限流算法,而是使用其他限流的算法:一种算法叫做漏桶算法,一种叫做令牌筒算法。** #### 漏桶算法与令牌筒算法 漏桶算法的原理很简单,它就像在流量产生端和接收端之间增加一个漏桶,流量会进入和暂存到漏桶里面,而漏桶的出口处会按照一个固定的速率将流量漏出到接收端(也就是服务接口)。 如果流入的流量在某一段时间内大增,超过了漏桶的承受极限,那么多余的流量就会触发限流策略,被拒绝服务。 -经过了漏桶算法之后,随机产生的流量就会被整形成为比较平滑的流量到达服务端,从而避免了突发的大流量对于服务接口的影响。 **这很像倚天屠龙记里,九阳真经的口诀:他强由他强,清风拂山岗,他横由他横,明月照大江 。** 也就是说,无论流入的流量有多么强横,多么不规则,经过漏桶处理之后,流出的流量都会变得比较平滑。 +经过了漏桶算法之后,随机产生的流量就会被整形成为比较平滑的流量到达服务端,从而避免了突发的大流量对于服务接口的影响。**这很像倚天屠龙记里,九阳真经的口诀:他强由他强,清风拂山岗,他横由他横,明月照大江 。** 也就是说,无论流入的流量有多么强横,多么不规则,经过漏桶处理之后,流出的流量都会变得比较平滑。 而在实现时,我们一般会使用消息队列作为漏桶的实现,流量首先被放入到消息队列中排队,由固定的几个队列处理程序来消费流量,如果消息队列中的流量溢出,那么后续的流量就会被拒绝。这个算法的思想是不是与消息队列削峰填谷的作用相似呢? @@ -97,9 +97,9 @@ public boolena isRateLimit() { ![img](assets/4054d20a39fb41e7f9aa924205ba839b.jpg) -如果要从这两种算法中做选择,我更倾向于使用令牌桶算法, **原因是漏桶算法在面对突发流量的时候,采用的解决方式是缓存在漏桶中,** 这样流量的响应时间就会增长,这就与互联网业务低延迟的要求不符;而令牌桶算法可以在令牌中暂存一定量的令牌,能够应对一定的突发流量,所以一般我会使用令牌桶算法来实现限流方案,而 Guava 中的限流方案就是使用令牌桶算法来实现的。 +如果要从这两种算法中做选择,我更倾向于使用令牌桶算法,**原因是漏桶算法在面对突发流量的时候,采用的解决方式是缓存在漏桶中,** 这样流量的响应时间就会增长,这就与互联网业务低延迟的要求不符;而令牌桶算法可以在令牌中暂存一定量的令牌,能够应对一定的突发流量,所以一般我会使用令牌桶算法来实现限流方案,而 Guava 中的限流方案就是使用令牌桶算法来实现的。 -你可以看到,使用令牌桶算法就需要存储令牌的数量,如果是单机上实现限流的话,可以在进程中使用一个变量来存储;但是如果在分布式环境下,不同的机器之间无法共享进程中的变量,我们就一般会使用 Redis 来存储这个令牌的数量。这样的话,每次请求的时候都需要请求一次 Redis 来获取一个令牌,会增加几毫秒的延迟,性能上会有一些损耗。 **因此,一个折中的思路是:** 我们可以在每次取令牌的时候,不再只获取一个令牌,而是获取一批令牌,这样可以尽量减少请求 Redis 的次数。 +你可以看到,使用令牌桶算法就需要存储令牌的数量,如果是单机上实现限流的话,可以在进程中使用一个变量来存储;但是如果在分布式环境下,不同的机器之间无法共享进程中的变量,我们就一般会使用 Redis 来存储这个令牌的数量。这样的话,每次请求的时候都需要请求一次 Redis 来获取一个令牌,会增加几毫秒的延迟,性能上会有一些损耗。**因此,一个折中的思路是:** 我们可以在每次取令牌的时候,不再只获取一个令牌,而是获取一批令牌,这样可以尽量减少请求 Redis 的次数。 ## 课程小结 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25437\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25437\350\256\262.md" index 63aa9925d..5fde31616 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25437\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25437\350\256\262.md" @@ -60,7 +60,7 @@ select repost_count, comment_count, praise_count, view_count from t_weibo_count 除了考虑计数的读取性能之外,由于热门微博的计数变化频率相当快,也需要考虑如何提升计数的写入性能。比如,每次在转发一条微博的时候,都需要增加这条微博的转发数,那么如果明星发布结婚、离婚的微博,瞬时就可能会产生几万甚至几十万的转发。如果是你的话,要如何降低写压力呢? -你可能已经想到用消息队列来削峰填谷了,也就是说,我们在转发微博的时候向消息队列写入一条消息,然后在消息处理程序中给这条微博的转发计数加 1。 **这里需要注意的一点,** 我们可以通过批量处理消息的方式进一步减小 Redis 的写压力,比如像下面这样连续更改三次转发数(我用 SQL 来表示来方便你理解): +你可能已经想到用消息队列来削峰填谷了,也就是说,我们在转发微博的时候向消息队列写入一条消息,然后在消息处理程序中给这条微博的转发计数加 1。**这里需要注意的一点,** 我们可以通过批量处理消息的方式进一步减小 Redis 的写压力,比如像下面这样连续更改三次转发数(我用 SQL 来表示来方便你理解): ```sql UPDATE t_weibo_count SET repost_count = repost_count + 1 WHERE weibo_id = 1; @@ -76,7 +76,7 @@ UPDATE t_weibo_count SET repost_count = repost_count + 3 WHERE weibo_id = 1; ## 如何降低计数系统的存储成本 -讲到这里,我其实已经告诉你一个支撑高并发查询请求的计数系统是如何实现的了。但是在微博的场景下,计数的量级是万亿的级别,这也给我们提了更高的要求, **就是如何在有限的存储成本下实现对于全量计数数据的存取。** 你知道,Redis 是使用内存来存储信息,相比于使用磁盘存储数据的 MySQL 来说,存储的成本不可同日而语,比如一台服务器磁盘可以挂载到 2 个 T,但是内存可能只有 128G,这样磁盘的存储空间就是内存的 16 倍。而 Redis 基于通用性的考虑,对于内存的使用比较粗放,存在大量的指针以及额外数据结构的开销,如果要存储一个 KV 类型的计数信息,Key 是 8 字节 Long 类型的 weibo_id,Value 是 4 字节 int 类型的转发数,存储在 Redis 中之后会占用超过 70 个字节的空间,空间的浪费是巨大的。 **如果你面临这个问题,要如何优化呢?** +讲到这里,我其实已经告诉你一个支撑高并发查询请求的计数系统是如何实现的了。但是在微博的场景下,计数的量级是万亿的级别,这也给我们提了更高的要求,**就是如何在有限的存储成本下实现对于全量计数数据的存取。** 你知道,Redis 是使用内存来存储信息,相比于使用磁盘存储数据的 MySQL 来说,存储的成本不可同日而语,比如一台服务器磁盘可以挂载到 2 个 T,但是内存可能只有 128G,这样磁盘的存储空间就是内存的 16 倍。而 Redis 基于通用性的考虑,对于内存的使用比较粗放,存在大量的指针以及额外数据结构的开销,如果要存储一个 KV 类型的计数信息,Key 是 8 字节 Long 类型的 weibo_id,Value 是 4 字节 int 类型的转发数,存储在 Redis 中之后会占用超过 70 个字节的空间,空间的浪费是巨大的。**如果你面临这个问题,要如何优化呢?** 我建议你先对原生 Redis 做一些改造,采用新的数据结构和数据类型来存储计数数据。我在改造时,主要涉及了两点: diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25438\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25438\350\256\262.md" index 543a86229..0fc2f7adc 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25438\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25438\350\256\262.md" @@ -31,7 +31,7 @@ for(Long id : userIds) { 这样看来,似乎简单可行,但随着系统中的用户越来越多,这个方案存在两个致命的问题。 -首先,获取全量用户就是一个比较耗时的操作,相当于对用户库做一次全表的扫描,这不仅会对数据库造成很大的压力,而且查询全量用户数据的响应时间是很长的,对于在线业务来说是难以接受的。如果你的用户库已经做了分库分表,那么就要扫描所有的库表,响应时间就更长了。 **不过有一个折中的方法,** 那就是在发送系统通知之前,先从线下的数据仓库中获取全量的用户 ID,并且存储在一个本地的文件中,然后再轮询所有的用户 ID,给这些用户增加未读计数。 +首先,获取全量用户就是一个比较耗时的操作,相当于对用户库做一次全表的扫描,这不仅会对数据库造成很大的压力,而且查询全量用户数据的响应时间是很长的,对于在线业务来说是难以接受的。如果你的用户库已经做了分库分表,那么就要扫描所有的库表,响应时间就更长了。**不过有一个折中的方法,** 那就是在发送系统通知之前,先从线下的数据仓库中获取全量的用户 ID,并且存储在一个本地的文件中,然后再轮询所有的用户 ID,给这些用户增加未读计数。 这似乎是一个可行的技术方案,然而它给所有人增加未读计数,会消耗非常长的时间。你计算一下,假如你的系统中有一个亿的用户,给一个用户增加未读数需要消耗 1ms,那么给所有人都增加未读计数就需要 100000000 * 1 /1000 = 100000 秒,也就是超过一天的时间;即使你启动 100 个线程并发的设置,也需要十几分钟的时间才能完成,而用户很难接受这么长的延迟时间。 @@ -55,7 +55,7 @@ for(Long id : userIds) { ![img](assets/ae6a5e9e04be08d18c493729458d543f.jpg) -这个红点和系统通知类似,也是一种通知全量用户的手段,如果逐个通知用户,延迟也是无法接受的。 **因此你可以采用和系统通知类似的方案。** +这个红点和系统通知类似,也是一种通知全量用户的手段,如果逐个通知用户,延迟也是无法接受的。**因此你可以采用和系统通知类似的方案。** 首先,我们为每一个用户存储一个时间戳,代表最近点过这个红点的时间,用户点了红点,就把这个时间戳设置为当前时间;然后,我们也记录一个全局的时间戳,这个时间戳标识最新的一次打点时间,如果你在后台操作给全体用户打点,就更新这个时间戳为当前时间。而我们在判断是否需要展示红点时,只需要判断用户的时间戳和全局时间戳的大小,如果用户时间戳小于全局时间戳,代表在用户最后一次点击红点之后又有新的红点推送,那么就要展示红点,反之,就不展示红点了。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25439\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25439\350\256\262.md" index a122f33c4..5bc5fe784 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25439\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25439\350\256\262.md" @@ -4,7 +4,7 @@ 前两节课中,我带你探究了如何设计和实现互联网系统中一个常见模块——计数系统。它的业务逻辑其实非常简单,基本上最多只有三个接口,获取计数、增加计数和重置计数。所以我们在考虑方案的时候考察点也相对较少,基本上使用缓存就可以实现一个兼顾性能、可用性和鲁棒性的方案了。然而大型业务系统的逻辑会非常复杂,在方案设计时通常需要灵活运用多种技术,才能共同承担高并发大流量的冲击。那么接下来,我将带你了解如何设计社区系统中最为复杂、并发量也最高的信息流系统。这样,你可以从中体会怎么应用之前学习的组件了。 -最早的信息流系统起源于微博,我们知道,微博是基于关注关系来实现内容分发的,也就是说,如果用户 A 关注了用户 B,那么用户 A 就需要在自己的信息流中,实时地看到用户 B 发布的最新内容, **这是微博系统的基本逻辑,也是它能够让信息快速流通、快速传播的关键。** 由于微博的信息流一般是按照时间倒序排列的,所以我们通常把信息流系统称为 TimeLine(时间线)。那么当我们设计一套信息流系统时需要考虑哪些点呢? +最早的信息流系统起源于微博,我们知道,微博是基于关注关系来实现内容分发的,也就是说,如果用户 A 关注了用户 B,那么用户 A 就需要在自己的信息流中,实时地看到用户 B 发布的最新内容,**这是微博系统的基本逻辑,也是它能够让信息快速流通、快速传播的关键。** 由于微博的信息流一般是按照时间倒序排列的,所以我们通常把信息流系统称为 TimeLine(时间线)。那么当我们设计一套信息流系统时需要考虑哪些点呢? ## 设计信息流系统的关注点有哪些 @@ -55,7 +55,7 @@ select feedId from inbox where userId = "B"; 比如,我在网易微博的时候就是采用推模式来实现微博信息流的。当时为了提升数据库的插入性能,我们采用了 TokuDB 作为 MySQL 的存储引擎,这个引擎架构的核心是一个名为分形树的索引结构(Fractal Tree Indexes)。我们知道数据库在写入的时候会产生对磁盘的随机写入,造成磁盘寻道,影响数据写入的性能;而分形树结构和我们在11 讲中提到的 LSM 一样,可以将数据的随机写入转换成顺序写入,提升写入的性能。另外,TokuDB 相比于 InnoDB 来说,数据压缩的性能更高,经过官方的测试,TokuDB 可以将存储在 InnoDB 中的 4TB 的数据压缩到 200G,这对于写入数据量很大的业务来说也是一大福音。然而,相比于 InnoDB 来说,TokuDB 的删除和查询性能都要差一些,不过可以使用缓存加速查询性能,而微博的删除频率不高,因此这对于推模式下的消息流来说影响有限。 -其次,存储成本很高。 **在这个方案中我们一般会这么来设计表结构:** 先设计一张 Feed 表,这个表主要存储微博的基本信息,包括微博 ID、创建人的 ID、创建时间、微博内容、微博状态(删除还是正常)等等,它使用微博 ID 做哈希分库分表; +其次,存储成本很高。**在这个方案中我们一般会这么来设计表结构:** 先设计一张 Feed 表,这个表主要存储微博的基本信息,包括微博 ID、创建人的 ID、创建时间、微博内容、微博状态(删除还是正常)等等,它使用微博 ID 做哈希分库分表; 另外一张表是用户的发件箱和收件箱表,也叫做 TimeLine 表(时间线表),主要有三个字段,用户 ID、微博 ID 和创建时间。它使用用户的 ID 做哈希分库分表。 @@ -63,13 +63,13 @@ select feedId from inbox where userId = "B"; 由于推模式需要给每一个用户都维护一份收件箱的数据,所以数据的存储量极大,你可以想一想,谢娜的粉丝目前已经超过 1.2 亿,那么如果采用推模式的话,谢娜每发送一条微博就会产生超过 1.2 亿条的数据,多么可怕! **我们的解决思路是:** 除了选择压缩率更高的存储引擎之外,还可以定期地清理数据,因为微博的数据有比较明显的实效性,用户更加关注最近几天发布的数据,通常不会翻阅很久之前的微博,所以你可以定期地清理用户的收件箱,比如只保留最近 1 个月的数据就可以了。 -除此之外,推模式下我们还通常会遇到扩展性的问题。在微博中有一个分组的功能,它的作用是你可以将关注的人分门别类,比如你可以把关注的人分为“明星”“技术”“旅游”等类别,然后把杨幂放入“明星”分类里,将 InfoQ 放在“技术”类别里。 **那么引入了分组之后,会对推模式有什么样的影响呢?** 首先是一个用户不止有一个收件箱,比如我有一个全局收件箱,还会针对每一个分组再分别创建一个收件箱,而一条微博在发布之后也需要被复制到更多的收件箱中了。 +除此之外,推模式下我们还通常会遇到扩展性的问题。在微博中有一个分组的功能,它的作用是你可以将关注的人分门别类,比如你可以把关注的人分为“明星”“技术”“旅游”等类别,然后把杨幂放入“明星”分类里,将 InfoQ 放在“技术”类别里。**那么引入了分组之后,会对推模式有什么样的影响呢?** 首先是一个用户不止有一个收件箱,比如我有一个全局收件箱,还会针对每一个分组再分别创建一个收件箱,而一条微博在发布之后也需要被复制到更多的收件箱中了。 如果杨幂发了一条微博,那么不仅需要插入到我的收件箱中,还需要插入到我的“明星”收件箱中,这样不仅增加了消息分发的压力,同时由于每一个收件箱都需要单独存储,所以存储成本也就更高。 最后,在处理取消关注和删除微博的逻辑时会更加复杂。比如当杨幂删除了一条微博,那么如果要删除她所有粉丝收件箱中的这条微博,会带来额外的分发压力,我们还是尽量不要这么做。 -而如果你将一个人取消关注,那么需要从你的收件箱中删除这个人的所有微博,假设他发了非常多的微博,那么即使你之后很久不登录,也需要从你的收件箱中做大量的删除操作,有些得不偿失。 **所以你可以采用的策略是:** 在读取自己信息流的时候,判断每一条微博是否被删除以及你是否还关注这条微博的作者,如果没有的话,就不展示这条微博的内容了。使用了这个策略之后,就可以尽量减少对于数据库多余的写操作了。 **那么说了这么多,推模式究竟适合什么样的业务的场景呢?** 在我看来,它比较适合于一个用户的粉丝数比较有限的场景,比如说微信朋友圈,你可以理解为我在微信中增加一个好友是关注了他也被他关注,所以好友的上限也就是粉丝的上限(朋友圈应该是 5000)。有限的粉丝数可以保证消息能够尽量快地被推送给所有的粉丝,增加的存储成本也比较有限。如果你的业务中粉丝数是有限制的,那么在实现以关注关系为基础的信息流时,也可以采用推模式来实现。 +而如果你将一个人取消关注,那么需要从你的收件箱中删除这个人的所有微博,假设他发了非常多的微博,那么即使你之后很久不登录,也需要从你的收件箱中做大量的删除操作,有些得不偿失。**所以你可以采用的策略是:** 在读取自己信息流的时候,判断每一条微博是否被删除以及你是否还关注这条微博的作者,如果没有的话,就不展示这条微博的内容了。使用了这个策略之后,就可以尽量减少对于数据库多余的写操作了。**那么说了这么多,推模式究竟适合什么样的业务的场景呢?** 在我看来,它比较适合于一个用户的粉丝数比较有限的场景,比如说微信朋友圈,你可以理解为我在微信中增加一个好友是关注了他也被他关注,所以好友的上限也就是粉丝的上限(朋友圈应该是 5000)。有限的粉丝数可以保证消息能够尽量快地被推送给所有的粉丝,增加的存储成本也比较有限。如果你的业务中粉丝数是有限制的,那么在实现以关注关系为基础的信息流时,也可以采用推模式来实现。 ## 课程小结 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25440\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25440\350\256\262.md" index d96387d8f..247aeccbe 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25440\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25440\350\256\262.md" @@ -39,7 +39,7 @@ for(Long id : uids) { return merge(ids); // 合并排序所有的 id ``` -拉模式之所以可以解决推模式下的所有问题,是因为在业务上关注数始终是有上限的,那么它是不是一个无懈可击的方案呢? **当然不是,拉模式也会有一些问题,在我看来主要有这样两点。** +拉模式之所以可以解决推模式下的所有问题,是因为在业务上关注数始终是有上限的,那么它是不是一个无懈可击的方案呢?**当然不是,拉模式也会有一些问题,在我看来主要有这样两点。** 第一点,不同于推模式下获取信息流的时候,只是简单地查询收件箱中的数据,在拉模式下,我们需要对多个发件箱的数据做聚合,这个查询和聚合的成本比较高。微博的关注上限是 2000,假如你关注了 2000 人,就要查询这 2000 人发布的微博信息,然后再对查询出来的信息做聚合。 @@ -47,7 +47,7 @@ return merge(ids); // 合并排序所有的 id 在实际执行中,我们对用户的浏览行为做了抽量分析,发现 97% 的用户都是在浏览最近 5 天之内的微博,也就是说,用户很少翻看五天之前的微博内容,所以我们只缓存了每个用户最近 5 天发布的微博 ID。假设我们部署 6 个缓存节点来存储这些微博 ID,在每次聚合时并行从这几个缓存节点中批量查询多个用户的微博 ID,获取到之后再在应用服务内存中排序后就好了,这就是对缓存的 6 次请求,可以保证在 5 毫秒之内返回结果。 -第二,缓存节点的带宽成本比较高。你想一下,假设微博信息流的访问量是每秒 10 万次请求,也就是说,每个缓存节点每秒要被查询 10 万次。假设一共部署 6 个缓存节点,用户人均关注是 90,平均来说每个缓存节点要存储 15 个用户的数据。如果每个人平均每天发布 2 条微博,5 天就是发布 10 条微博,15 个用户就要存储 150 个微博 ID。每个微博 ID 要是 8 个字节,150 个微博 ID 大概就是 1kB 的数据,单个缓存节点的带宽就是 1kB * 10 万 = 100MB,基本上跑满了机器网卡带宽了。 **那么我们要如何对缓存的带宽做优化呢?** +第二,缓存节点的带宽成本比较高。你想一下,假设微博信息流的访问量是每秒 10 万次请求,也就是说,每个缓存节点每秒要被查询 10 万次。假设一共部署 6 个缓存节点,用户人均关注是 90,平均来说每个缓存节点要存储 15 个用户的数据。如果每个人平均每天发布 2 条微博,5 天就是发布 10 条微博,15 个用户就要存储 150 个微博 ID。每个微博 ID 要是 8 个字节,150 个微博 ID 大概就是 1kB 的数据,单个缓存节点的带宽就是 1kB * 10 万 = 100MB,基本上跑满了机器网卡带宽了。**那么我们要如何对缓存的带宽做优化呢?** 在14 讲中我提到,部署多个缓存副本提升缓存可用性,其实,缓存副本也可以分摊带宽的压力。我们知道在部署缓存副本之后,请求会先查询副本中的数据,只有不命中的请求才会查询主缓存的数据。假如原本缓存带宽是 100M,我们部署 4 组缓存副本,缓存副本的命中率是 60%,那么主缓存带宽就降到 100M * 40% = 40M,而每组缓存副本的带宽为 100M / 4 = 25M,这样每一组缓存的带宽都降为可接受的范围之内了。 @@ -59,7 +59,7 @@ return merge(ids); // 合并排序所有的 id 但是,有的同学可能会说:我在系统搭建初期已经基于推模式实现了一套信息流系统,如果把它推倒重新使用拉模式实现的话,系统的改造成本未免太高了。有没有一种基于推模式的折中的方案呢? -其实我在网易微博的时候,网易微博的信息流就是基于推模式来实现的,当用户的粉丝量大量上涨之后, **我们通过对原有系统的改造实现了一套推拉结合的方案,也能够基本解决推模式存在的问题,具体怎么做呢?** +其实我在网易微博的时候,网易微博的信息流就是基于推模式来实现的,当用户的粉丝量大量上涨之后,**我们通过对原有系统的改造实现了一套推拉结合的方案,也能够基本解决推模式存在的问题,具体怎么做呢?** 方案的核心在于大 V 用户在发布微博的时候,不再推送到全量用户,而是只推送给活跃的用户。这个方案在实现的时候有几个关键的点。 diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25441\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25441\350\256\262.md" index 54b2f88a0..eb7398139 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25441\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25441\350\256\262.md" @@ -4,13 +4,13 @@ 在“数据库优化方案(二):写入数据量增加时,如何实现分库分表?”中我曾经提到,由于 MySQL 不像 MongoDB 那样支持数据的 Auto Sharding(自动分片),所以无论是将 MySQL 单库拆分成多个数据库,还是由于数据存储的瓶颈,不得不将多个数据库拆分成更多的数据库时,你都要考虑如何做数据的迁移。 -其实,在实际工作中,不只是对数据库拆分时会做数据迁移, **很多场景都需要你给出数据迁移的方案,** 比如说某一天,你的老板想要将应用从自建机房迁移到云上,那么你就要考虑将所有自建机房中的数据,包括 MySQL,Redis,消息队列等组件中的数据,全部迁移到云上,这无论对哪种规模的公司来说都是一项浩瀚的工程,所以你需要在迁移之前,准备完善的迁移方案。 +其实,在实际工作中,不只是对数据库拆分时会做数据迁移,**很多场景都需要你给出数据迁移的方案,** 比如说某一天,你的老板想要将应用从自建机房迁移到云上,那么你就要考虑将所有自建机房中的数据,包括 MySQL,Redis,消息队列等组件中的数据,全部迁移到云上,这无论对哪种规模的公司来说都是一项浩瀚的工程,所以你需要在迁移之前,准备完善的迁移方案。 “数据的迁移”的问题比较重要,也比较繁琐,也是开发和运维同学关注的重点。在课程更新的过程中,我看到有很多同学,比如 @每天晒白牙,@枫叶 11,@撒旦的堕落等等,在留言区询问如何做数据迁移,所以我策划了一期加餐,准备从数据库迁移和缓存迁移两个方面,带你掌握数据迁移的方法,也带你了解数据迁移过程中,需要注意的关键点,尽量让你避免踩坑。 ## 如何平滑地迁移数据库中的数据 -你可能会认为:数据迁移无非是将数据从一个数据库拷贝到另一个数据库,可以通过 MySQL 主从同步的方式做到准实时的数据拷贝;也可以通过 mysqldump 工具将源库的数据导出,再导入到新库, **这有什么复杂的呢?** +你可能会认为:数据迁移无非是将数据从一个数据库拷贝到另一个数据库,可以通过 MySQL 主从同步的方式做到准实时的数据拷贝;也可以通过 mysqldump 工具将源库的数据导出,再导入到新库,**这有什么复杂的呢?** 其实,这两种方式只能支持单库到单库的迁移,无法支持单库到多库多表的场景。而且即便是单库到单库的迁移,迁移过程也需要满足以下几个目标: @@ -30,7 +30,7 @@ \\1. 将新的库配置为源库的从库,用来同步数据;如果需要将数据同步到多库多表,那么可以使用一些第三方工具获取 Binlog 的增量日志(比如开源工具 Canal),在获取增量日志之后就可以按照分库分表的逻辑写入到新的库表中了。 -\\2. 同时,我们需要改造业务代码,在数据写入的时候,不仅要写入旧库,也要写入新库。当然,基于性能的考虑,我们可以异步地写入新库,只要保证旧库写入成功即可。 **但是,我们需要注意的是,** 需要将写入新库失败的数据记录在单独的日志中,这样方便后续对这些数据补写,保证新库和旧库的数据一致性。 +\\2. 同时,我们需要改造业务代码,在数据写入的时候,不仅要写入旧库,也要写入新库。当然,基于性能的考虑,我们可以异步地写入新库,只要保证旧库写入成功即可。**但是,我们需要注意的是,** 需要将写入新库失败的数据记录在单独的日志中,这样方便后续对这些数据补写,保证新库和旧库的数据一致性。 \\3. 然后,我们就可以开始校验数据了。由于数据库中数据量很大,做全量的数据校验不太现实。你可以抽取部分数据,具体数据量依据总体数据量而定,只要保证这些数据是一致的就可以。 @@ -42,13 +42,13 @@ **其中,最容易出问题的步骤就是数据校验的工作,** 所以,我建议你在未开始迁移数据之前先写好数据校验的工具或者脚本,在测试环境上测试充分之后,再开始正式的数据迁移。 -如果是将数据从自建机房迁移到云上,你也可以使用这个方案, **只是你需要考虑的一个重要的因素是:** 自建机房到云上的专线的带宽和延迟,你需要尽量减少跨专线的读操作,所以在切换读流量的时候,你需要保证自建机房的应用服务器读取本机房的数据库,云上的应用服务器读取云上的数据库。这样在完成迁移之前,只要将自建机房的应用服务器停掉,并且将写入流量都切到新库就可以了。 +如果是将数据从自建机房迁移到云上,你也可以使用这个方案,**只是你需要考虑的一个重要的因素是:** 自建机房到云上的专线的带宽和延迟,你需要尽量减少跨专线的读操作,所以在切换读流量的时候,你需要保证自建机房的应用服务器读取本机房的数据库,云上的应用服务器读取云上的数据库。这样在完成迁移之前,只要将自建机房的应用服务器停掉,并且将写入流量都切到新库就可以了。 ![img](assets/b88aefdb07049f2019c922cdb9cb3154.jpg) -这种方案是一种比较通用的方案,无论是迁移 MySQL 中的数据,还是迁移 Redis 中的数据,甚至迁移消息队列都可以使用这种方式, **你在实际的工作中可以直接拿来使用。** +这种方案是一种比较通用的方案,无论是迁移 MySQL 中的数据,还是迁移 Redis 中的数据,甚至迁移消息队列都可以使用这种方式,**你在实际的工作中可以直接拿来使用。** -这种方式的 **好处是:** 迁移的过程可以随时回滚,将迁移的风险降到了最低。 **劣势是:** 时间周期比较长,应用有改造的成本。 +这种方式的 **好处是:** 迁移的过程可以随时回滚,将迁移的风险降到了最低。**劣势是:** 时间周期比较长,应用有改造的成本。 #### 级联同步方案 @@ -68,7 +68,7 @@ **这种方案的回滚方案也比较简单,** 可以先将读流量切换到备库,再暂停应用的写入,将写流量切换到备库,这样所有的流量都切换到了备库,也就是又回到了自建机房的环境,就可以认为已经回滚了。 -上面的级联迁移方案可以应用在,将 MySQL 从自建机房迁移到云上的场景,也可以应用在将 Redis 从自建机房迁移到云上的场景, **如果你有类似的需求可以直接拿来应用。** 这种方案 **优势是** 简单易实施,在业务上基本没有改造的成本; **缺点是** 在切写的时候需要短暂的停止写入,对于业务来说是有损的,不过如果在业务低峰期来执行切写,可以将对业务的影响降至最低。 +上面的级联迁移方案可以应用在,将 MySQL 从自建机房迁移到云上的场景,也可以应用在将 Redis 从自建机房迁移到云上的场景,**如果你有类似的需求可以直接拿来应用。** 这种方案 **优势是** 简单易实施,在业务上基本没有改造的成本; **缺点是** 在切写的时候需要短暂的停止写入,对于业务来说是有损的,不过如果在业务低峰期来执行切写,可以将对业务的影响降至最低。 ## 数据迁移时如何预热缓存 @@ -76,7 +76,7 @@ 你说的没错,但是你还需要考虑的是缓存的命中率。 -如果你部署一个空的缓存,那么所有的请求就都穿透到数据库,数据库可能因为承受不了这么大的压力而宕机,这样你的服务就会不可用了。 **所以,缓存迁移的重点是保持缓存的热度。** +如果你部署一个空的缓存,那么所有的请求就都穿透到数据库,数据库可能因为承受不了这么大的压力而宕机,这样你的服务就会不可用了。**所以,缓存迁移的重点是保持缓存的热度。** 刚刚我提到,Redis 的数据迁移可以使用双写的方案或者级联同步的方案,所以在这里我就不考虑 Redis 缓存的同步了,而是以 Memcached 为例来说明。 @@ -84,7 +84,7 @@ 在“缓存的使用姿势(二):缓存如何做到高可用?”中,我曾经提到,为了保证缓存的可用性,我们可以部署多个副本组来尽量将请求阻挡在数据库层之上。 -数据的写入流程是写入 Master、Slave 和所有的副本组,而在读取数据的时候,会先读副本组的数据,如果读取不到再到 Master 和 Slave 里面加载数据,再写入到副本组中。 **那么,我们就可以在云上部署一个副本组,** 这样,云上的应用服务器读取云上的副本组,如果副本组没有查询到数据,就可以从自建机房部署的主从缓存上加载数据,回种到云上的副本组上。 +数据的写入流程是写入 Master、Slave 和所有的副本组,而在读取数据的时候,会先读副本组的数据,如果读取不到再到 Master 和 Slave 里面加载数据,再写入到副本组中。**那么,我们就可以在云上部署一个副本组,** 这样,云上的应用服务器读取云上的副本组,如果副本组没有查询到数据,就可以从自建机房部署的主从缓存上加载数据,回种到云上的副本组上。 ![img](assets/abc0b5e4c80097d8e02000b30e7ea9c6.jpg) @@ -108,13 +108,13 @@ ![img](assets/7f41a529a322e396232ac7963ec082f4.jpg) -使用了这种方式,我们可以实现缓存数据的迁移,又可以尽量控制专线的带宽和请求的延迟情况, **你也可以直接在项目中使用。** +使用了这种方式,我们可以实现缓存数据的迁移,又可以尽量控制专线的带宽和请求的延迟情况,**你也可以直接在项目中使用。** ## 课程小结 以上我提到的数据迁移的方案,都是我在实际项目中,经常用到的、经受过实战考验的方案,希望你能通过这节课的学习,将这些方案运用到你的项目中,解决实际的问题。与此同时,我想再次跟你强调一下本节课的重点内容: -双写的方案是数据库、Redis 迁移的通用方案, **你可以在实际工作中直接加以使用。** 双写方案中最重要的,是通过数据校验来保证数据的一致性,这样就可以在迁移过程中随时回滚; +双写的方案是数据库、Redis 迁移的通用方案,**你可以在实际工作中直接加以使用。** 双写方案中最重要的,是通过数据校验来保证数据的一致性,这样就可以在迁移过程中随时回滚; 如果你需要将自建机房的数据迁移到云上,那么也可以考虑 **使用级联复制的方案,** 这种方案会造成数据的短暂停写,需要在业务低峰期执行; diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25442\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25442\350\256\262.md" index 98c4dc28f..b5eadf0dc 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25442\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25442\350\256\262.md" @@ -10,11 +10,11 @@ 回顾这些年的工作,我想和你分享几点我个人的看法。我刚开始工作时,经常听别人说程序员是有年纪限制的,35 岁是程序员的终结年龄,那时说实话我心里是有一些忐忑的,可随着年龄不断增长,我看到越来越多的人在 35 岁之后还在行业中如鱼得水,我想,35 这个数字并非强调个人的年纪,而是泛指一个阶段,强调在那个阶段,我们可能会因为个人的种种原因安于现状,不再更新自己的知识库,这是非常错误的。 -**化用《礼记》中的话,首先,我们要博学之。** 你要不断革新知识,所谓的天花板其实更多的是知识性的天花板,活到老学到老才是你在这个行业的必胜法宝,所以,我们应该利用各种优质平台以及零散的时间学习,但是同时你要注意,现在的知识偏向碎片化,如何有条理、系统地学习,将知识梳理成体系,化作自己的内功,是比较关键和困难的。 **在这里我给你几点建议:** 基础知识要体系化,读书是一种很好的获取体系化知识的途径,比如研读《算法导论》提升对数据结构和算法的理解,研读《TCP/IP 协议详解》深入理解我们最熟悉的 TCP/IP 协议栈等等; +**化用《礼记》中的话,首先,我们要博学之。** 你要不断革新知识,所谓的天花板其实更多的是知识性的天花板,活到老学到老才是你在这个行业的必胜法宝,所以,我们应该利用各种优质平台以及零散的时间学习,但是同时你要注意,现在的知识偏向碎片化,如何有条理、系统地学习,将知识梳理成体系,化作自己的内功,是比较关键和困难的。**在这里我给你几点建议:** 基础知识要体系化,读书是一种很好的获取体系化知识的途径,比如研读《算法导论》提升对数据结构和算法的理解,研读《TCP/IP 协议详解》深入理解我们最熟悉的 TCP/IP 协议栈等等; 多读一些经典项目的源代码,比如 Dubbo,Spring 等等,从中领会设计思想,你的编码能力会得到极大的提高; -多利用碎片化的时间读一些公众号的文章,补充书里没有实践案例的不足,借此提升技术视野。 **其次要慎思之。** 诚然,看书拓展知识的过程中我们需要思考,在实际工作中我们也需要深入思考。没有一个理论可以适应所有的突发状况,高并发系统更是如此。它状况百出,我们最好的应对方法就是在理论的指导下,对每一次的突发状况都进行深入的总结和思考。 **然后是审问之。** 这种问既是“扪心自问”: +多利用碎片化的时间读一些公众号的文章,补充书里没有实践案例的不足,借此提升技术视野。**其次要慎思之。** 诚然,看书拓展知识的过程中我们需要思考,在实际工作中我们也需要深入思考。没有一个理论可以适应所有的突发状况,高并发系统更是如此。它状况百出,我们最好的应对方法就是在理论的指导下,对每一次的突发状况都进行深入的总结和思考。**然后是审问之。** 这种问既是“扪心自问”: 这次的突发问题的根本原因是什么? @@ -22,9 +22,9 @@ 解决这个问题最优的思路是什么? -同时,也应该是一种他问,是与团队合作,头脑风暴之后的一种补充,我们说你有一个苹果,我有一个苹果我们相互交换,每个人依然只有一个苹果,但是你有一种思想,我也有一种思想,我们相互交换,每个人就有两种思想,所以不断进行团队交流也是一种好的提升自我的方式。 **接着是明辨之。** 进行了广泛的阅读,积累了大量的工作案例,还要将这些内化于心的知识形成清晰的判断力。某个明星微博的突然沦陷,社区系统的突然挂掉,只是分分钟的事情,要想成为一个优秀的架构师,你必须运用自身的本领进行清晰地判断,快速找到解决方案,只有这样才能把损失控制在最小的范围内。而这种清晰的判断力绝对是因人而异的,你有怎样的知识储备,有怎样的深入思考,就会有怎样清晰的判断力。 **最后要笃行之。** 学了再多的理论,做了再多的思考,也不能确保能够解决所有问题,对于高并发问题,我们还需要在实践中不断提升自己的能力。 +同时,也应该是一种他问,是与团队合作,头脑风暴之后的一种补充,我们说你有一个苹果,我有一个苹果我们相互交换,每个人依然只有一个苹果,但是你有一种思想,我也有一种思想,我们相互交换,每个人就有两种思想,所以不断进行团队交流也是一种好的提升自我的方式。**接着是明辨之。** 进行了广泛的阅读,积累了大量的工作案例,还要将这些内化于心的知识形成清晰的判断力。某个明星微博的突然沦陷,社区系统的突然挂掉,只是分分钟的事情,要想成为一个优秀的架构师,你必须运用自身的本领进行清晰地判断,快速找到解决方案,只有这样才能把损失控制在最小的范围内。而这种清晰的判断力绝对是因人而异的,你有怎样的知识储备,有怎样的深入思考,就会有怎样清晰的判断力。**最后要笃行之。** 学了再多的理论,做了再多的思考,也不能确保能够解决所有问题,对于高并发问题,我们还需要在实践中不断提升自己的能力。 -相信你经常会看到这样的段子,比如很多人会觉得我们的固定形象就是“带着眼镜,穿着格子衬衫,背着双肩包,去优衣库就是一筐筐买衣服”。调侃归调侃,我们不必认真,也不必对外在过于追求,因为最终影响你职业生涯的,是思考、是内涵、是知识储备。 **那么如何让自己更精锐呢?** 我想首先要有梯度。我们总希望任何工作都能有个进度条,我们的职业生涯也应该有一个有梯度的进度条,比如,从职场菜鸟到大神再到财务自由,每一步要用多久的时间,如何才能一步一步上升,当然,未必人人能够如鱼得水,但有梦想总是好的,这样你才有目标,自己的生活才会有奔头。 +相信你经常会看到这样的段子,比如很多人会觉得我们的固定形象就是“带着眼镜,穿着格子衬衫,背着双肩包,去优衣库就是一筐筐买衣服”。调侃归调侃,我们不必认真,也不必对外在过于追求,因为最终影响你职业生涯的,是思考、是内涵、是知识储备。**那么如何让自己更精锐呢?** 我想首先要有梯度。我们总希望任何工作都能有个进度条,我们的职业生涯也应该有一个有梯度的进度条,比如,从职场菜鸟到大神再到财务自由,每一步要用多久的时间,如何才能一步一步上升,当然,未必人人能够如鱼得水,但有梦想总是好的,这样你才有目标,自己的生活才会有奔头。 有了梯度的目标之后,接下来要有速度,就像产品逼迫你一样,你也要逼迫自己,让自己不断地加油,不断地更新、提升、完善,尽快实现自己的职业目标。 @@ -34,4 +34,4 @@ 在文章结尾,我为你准备了一份调查问卷,题目不多,希望你能抽出两三分钟填写一下。我非常希望听听你对这个专栏的意见和建议,期待你的反馈!专栏的结束,也是另一种开始,我会将内容进行迭代,比如 11 月中旬到 12 月末,我有为期一个月的封闭期,在这期间没有来得及回复的留言,我会花时间处理完;再比如,会针对一些同学的共性问题策划一期答疑或者加餐。 -最后,我想再次强调一下为什么要努力提升自己,提升业务能力, **直白一点儿说,那是希望我们都有自主选择的权利,而不是被迫谋生;我有话语权,而不是被迫执行,随着年纪的增加,我越发觉得成就感和尊严,能够带给我们快乐。** \ No newline at end of file +最后,我想再次强调一下为什么要努力提升自己,提升业务能力,**直白一点儿说,那是希望我们都有自主选择的权利,而不是被迫谋生;我有话语权,而不是被迫执行,随着年纪的增加,我越发觉得成就感和尊严,能够带给我们快乐。** \ No newline at end of file diff --git "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25444\350\256\262.md" "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25444\350\256\262.md" index 537e19dfe..5bf8deab4 100644 --- "a/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25444\350\256\262.md" +++ "b/docs/Design/\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\241 40 \351\227\256/\347\254\25444\350\256\262.md" @@ -12,7 +12,7 @@ 这些信息着实戳中了我。 -回想起来,自己所处的行业是非常传统的 IT 行业,几乎与“互联网”不着边,所以我平时特别难接触一线的技术栈。然而,虽然行业传统,但并不妨碍日常工作中高并发的出现, **比如,偶尔出现的线上促销活动。** +回想起来,自己所处的行业是非常传统的 IT 行业,几乎与“互联网”不着边,所以我平时特别难接触一线的技术栈。然而,虽然行业传统,但并不妨碍日常工作中高并发的出现,**比如,偶尔出现的线上促销活动。** 单纯从我自己的角度出发,除了因为开篇词戳中之外,选择这个课程,还在于自己想拓宽视野、激发潜能,另一方面,当真的遇到“高并发”时,不至于望洋兴叹,脑海一片空白。 @@ -30,7 +30,7 @@ **其实,还有好多节课都给我留下了深刻的印象,** 比如,第 2 讲、第 10 讲、第 13 讲等等。 -单看《02 | 架构分层:我们为什么一定要这么做?》这个题目,我一开始会觉得“老生常谈”,软件分层在实际项目中运用的太多太多了,老师为什么单独拿出来一讲介绍呢?然而当我看到“如果业务逻辑很简单的话,可不可以从表示层直接到数据访问层,甚至直接读数据库呢?”这句话时, **联系到了自己的实际业务:** +单看《02 | 架构分层:我们为什么一定要这么做?》这个题目,我一开始会觉得“老生常谈”,软件分层在实际项目中运用的太多太多了,老师为什么单独拿出来一讲介绍呢?然而当我看到“如果业务逻辑很简单的话,可不可以从表示层直接到数据访问层,甚至直接读数据库呢?”这句话时,**联系到了自己的实际业务:** 我所参与的一个工程,确实因为业务逻辑基本等同数据库逻辑,所以从表示层直接与数据访问层交互了。但是如果数据库或者数据访问层发生改动,那将要修改表示层的多个地方,万一漏掉了需要调整的地方,连问题都不好查了,并且如果以后再无意地引入逻辑层,修改的层次也将变多。 @@ -44,7 +44,7 @@ 除此之外,在学习《13 | 缓存的使用姿势(一):如何选择缓存的读写策略?》之前,我的项目中没有过多地考虑,数据库与缓存的一致性。比如,我在写入数据时,选择了先写数据库,再写缓存,考虑到写数据库失败后事务回滚,缓存也不会被写入;如果缓存写入失败,再设计重试机制。 -看起来好像蛮 OK 的样子,但是因为没有考虑到在多线程更新的情况下,确实会造成双方的不一致, **所造成的后果是:有时候从前端查询到的结果与真实数据不符。** 后来,根据唐扬老师提到的 Cache Aside(旁路缓存)策略,我顿然醒悟,然后将这一策略用于该工程中,效果不错。这节课,我从唐扬老师的亲身经历中,学到了不少的经验,直接用到了自己的项目中。 +看起来好像蛮 OK 的样子,但是因为没有考虑到在多线程更新的情况下,确实会造成双方的不一致,**所造成的后果是:有时候从前端查询到的结果与真实数据不符。** 后来,根据唐扬老师提到的 Cache Aside(旁路缓存)策略,我顿然醒悟,然后将这一策略用于该工程中,效果不错。这节课,我从唐扬老师的亲身经历中,学到了不少的经验,直接用到了自己的项目中。 真的很感谢唐扬老师,也很开心能够遇到这门课程,在这里,想由衷地表达自己的感谢之情。 @@ -68,11 +68,11 @@ **在最后,我也想分享一下自己为什么用专栏这种形式来学习。** 善用搜索引擎的同学们都有体会,搜索出来的知识分布在各处,雷同的不少,有经验的介绍甚少,我没办法在有限的时间内,将搜索到的知识形成体系。 -当然了,要想系统地学习可以借助书籍。 **但是对我来说,** 书籍类学习周期长,章节之间的关联性也不大,容易学了这里忘了那里。书籍多是讲一个专业点,对于跨专业的知识经常一笔带过,而专栏,是有作者自己的理解在里边,前后之间有贯通,学习起来轻松愉悦。 +当然了,要想系统地学习可以借助书籍。**但是对我来说,** 书籍类学习周期长,章节之间的关联性也不大,容易学了这里忘了那里。书籍多是讲一个专业点,对于跨专业的知识经常一笔带过,而专栏,是有作者自己的理解在里边,前后之间有贯通,学习起来轻松愉悦。 **就拿一致性 Hash 这个知识点来说,** 我从网上看了不少关于一致性 Hash 的文章,但没有看到应用,更别谈应用中的缺陷,有的描述甚至让我误认为节点变化后,数据也会跟着迁移。唐扬老师的《14 | 缓存的使用姿势(二):缓存如何做到高可用?》,倒是给了我网络上看不到的盲区,通过在留言区与老师交流后,颇有一种豁然开朗的收获感。 -当然了,这些只是我个人的感受,见仁见智, **你或许有自己的学习方法,也或许大家的起点不同,** 在这里,我只想把自己的真实感受分享出来,也十分感谢大家倾听我的故事。 +当然了,这些只是我个人的感受,见仁见智,**你或许有自己的学习方法,也或许大家的起点不同,** 在这里,我只想把自己的真实感受分享出来,也十分感谢大家倾听我的故事。 总的来说,想要提升自己,并没有捷径,只有一步一步地踏实前行,从踩过的坑中,努力地爬出来。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25402\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25402\350\256\262.md" index c89ddad84..70dfb8627 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25402\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25402\350\256\262.md" @@ -8,7 +8,7 @@ JVM 难不难? 自然是 “难者不会,会者不难”。万丈高楼平地 ### 1.1 JDK、JRE、JVM 的关系 -**JDK** JDK(Java Development Kit) 是用于开发 Java 应用程序的软件开发工具集合,包括了 Java 运行时的环境(JRE)、解释器(Java)、编译器(javac)、Java 归档(jar)、文档生成器(Javadoc)等工具。简单的说我们要开发 Java 程序,就需要安装某个版本的 JDK 工具包。 **JRE** JRE(Java Runtime Enviroment )提供 Java 应用程序执行时所需的环境,由 Java 虚拟机(JVM)、核心类、支持文件等组成。简单的说,我们要是想在某个机器上运行 Java 程序,可以安装 JDK,也可以只安装 JRE,后者体积比较小。 **JVM** +**JDK** JDK(Java Development Kit) 是用于开发 Java 应用程序的软件开发工具集合,包括了 Java 运行时的环境(JRE)、解释器(Java)、编译器(javac)、Java 归档(jar)、文档生成器(Javadoc)等工具。简单的说我们要开发 Java 程序,就需要安装某个版本的 JDK 工具包。**JRE** JRE(Java Runtime Enviroment )提供 Java 应用程序执行时所需的环境,由 Java 虚拟机(JVM)、核心类、支持文件等组成。简单的说,我们要是想在某个机器上运行 Java 程序,可以安装 JDK,也可以只安装 JRE,后者体积比较小。**JVM** Java Virtual Machine(Java 虚拟机)有三层含义,分别是: @@ -37,7 +37,7 @@ Java 程序的开发运行过程为: ### 1.2 JDK 的发展过程与版本变迁 -说了这么多 JDK 相关的概念,我们再来看一下 JDK 的发展过程。 **JDK 版本列表** JDK版本 +说了这么多 JDK 相关的概念,我们再来看一下 JDK 的发展过程。**JDK 版本列表** JDK版本 发布时间 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25405\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25405\350\256\262.md" index 4717533e4..d9bb168fe 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25405\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25405\350\256\262.md" @@ -2,7 +2,7 @@ Java 中的字节码,英文名为 `bytecode`, 是 Java 代码编译后的中间代码格式。JVM 需要读取并解析字节码才能执行相应的任务。 -**从技术人员的角度看** ,Java 字节码是 JVM 的指令集。JVM 加载字节码格式的 class 文件,校验之后通过 JIT 编译器转换为本地机器代码执行。 简单说字节码就是我们编写的 Java 应用程序大厦的每一块砖,如果没有字节码的支撑,大家编写的代码也就没有了用武之地,无法运行。也可以说,Java 字节码就是 JVM 执行的指令格式。 +**从技术人员的角度看**,Java 字节码是 JVM 的指令集。JVM 加载字节码格式的 class 文件,校验之后通过 JIT 编译器转换为本地机器代码执行。 简单说字节码就是我们编写的 Java 应用程序大厦的每一块砖,如果没有字节码的支撑,大家编写的代码也就没有了用武之地,无法运行。也可以说,Java 字节码就是 JVM 执行的指令格式。 那么我们为什么需要掌握它呢? diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25407\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25407\350\256\262.md" index bda2578f3..3ab9ce788 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25407\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25407\350\256\262.md" @@ -69,7 +69,7 @@ JVM 中,每个正在运行的线程,都有自己的线程栈。 线程栈包 ![dd71b714-e026-4679-b589-52c8b9226b6f.jpg](assets/6yhj7.jpg) -每启动一个线程,JVM 就会在栈空间栈分配对应的 **线程栈** , 比如 1MB 的空间(`-Xss1m`)。 +每启动一个线程,JVM 就会在栈空间栈分配对应的 **线程栈**, 比如 1MB 的空间(`-Xss1m`)。 线程栈也叫做 Java 方法栈。 如果使用了 JNI 方法,则会分配一个单独的本地方法栈(Native Stack)。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25411\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25411\350\256\262.md" index f7cb5d0ba..4afac65d3 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25411\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25411\350\256\262.md" @@ -227,11 +227,11 @@ JDWP 是异步的,在收到某个应答之前,可以发送多个命令包。 下面的 Header 字段是命令包与应答包通用的。 -**length** length 字段表示整个数据包(包括 header)的字节数。因为数据包 header 的大小为 11 个字节,因此没有 data 的数据包会将此字段值设置为 11。 **id** id 字段用于唯一标识每一对数据包(command/reply)。应答包 id 值必须与对应的命令包 ID 相同。这样异步方式的命令和应答就能匹配起来。同一个来源发送的所有未完成命令包的 id 字段必须唯一。(调试器发出的命令包,与 JVM 发出的命令包如果 ID 相同也没关系。) 除此之外,对 ID 的分配没有任何要求。对于大多数实现而言,使用自增计数器就足够了。id 的取值允许 232 个数据包,足以应对各种调试场景。 **flags** flags 标志用于修改命令的排队和处理方式,也用来标记源自 JVM 的数据包。当前只定义了一个标志位 0x80,表示此数据包是应答包。协议的未来版本可能会定义其他标志。 +**length** length 字段表示整个数据包(包括 header)的字节数。因为数据包 header 的大小为 11 个字节,因此没有 data 的数据包会将此字段值设置为 11。**id** id 字段用于唯一标识每一对数据包(command/reply)。应答包 id 值必须与对应的命令包 ID 相同。这样异步方式的命令和应答就能匹配起来。同一个来源发送的所有未完成命令包的 id 字段必须唯一。(调试器发出的命令包,与 JVM 发出的命令包如果 ID 相同也没关系。) 除此之外,对 ID 的分配没有任何要求。对于大多数实现而言,使用自增计数器就足够了。id 的取值允许 232 个数据包,足以应对各种调试场景。**flags** flags 标志用于修改命令的排队和处理方式,也用来标记源自 JVM 的数据包。当前只定义了一个标志位 0x80,表示此数据包是应答包。协议的未来版本可能会定义其他标志。 ### 命令包的 Header -除了前面的通用 Header 字段,命令包还有以下请求头。 **command set** +除了前面的通用 Header 字段,命令包还有以下请求头。**command set** 该字段主要用于通过一种有意义的方式对命令进行分组。Sun 定义的命令集,通过在 JDI 中支持的接口进行分组。例如,所有支持 VirtualMachine 接口的命令都在 VirtualMachine 命令集里面。命令集空间大致分为以下几类: @@ -243,7 +243,7 @@ JDWP 是异步的,在收到某个应答之前,可以发送多个命令包。 ### 应答包的 Header -除了前面的通用 Header 字段,应答包还有以下请求头。 **error code** +除了前面的通用 Header 字段,应答包还有以下请求头。**error code** 此字段用于标识是否成功处理了对应的命令包。0 值表示成功,非零值表示错误。返回的错误代码由具体的命令集/命令规定,但是通常会映射为 JVM TI 标准错误码。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25413\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25413\350\256\262.md" index a036e3d70..682a43319 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25413\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25413\350\256\262.md" @@ -86,7 +86,7 @@ JVM 中包含了多种 GC 算法,如 Parallel Scavenge(并行清除),Par 标记清除算法最重要的优势,就是不再因为循环引用而导致内存泄露: **标记—清除** (Mark and Sweep)是最经典的垃圾收集算法。将理论用于生产实践时,会有很多需要优化调整的地方,以适应具体环境。后面我们会通过一个简单的例子,看看如何才能保证 JVM 能安全持续地分配对象。 -而这种处理方式不好的地方在于:垃圾收集过程中,需要暂停应用程序的所有线程。假如不暂停,则对象间的引用关系会一直不停地发生变化,那样就没法进行统计了。这种情况叫做 **STW 停顿** ( **Stop The World pause** ,全线暂停),让应用程序暂时停止,让 JVM 进行内存清理工作。如果把 JVM 里的环境看做一个世界,就好像我们经常在电影里看到的全世界时间静止了一样。有很多原因会触发 STW 停顿,其中垃圾收集是最主要的原因。 +而这种处理方式不好的地方在于:垃圾收集过程中,需要暂停应用程序的所有线程。假如不暂停,则对象间的引用关系会一直不停地发生变化,那样就没法进行统计了。这种情况叫做 **STW 停顿** ( **Stop The World pause**,全线暂停),让应用程序暂时停止,让 JVM 进行内存清理工作。如果把 JVM 里的环境看做一个世界,就好像我们经常在电影里看到的全世界时间静止了一样。有很多原因会触发 STW 停顿,其中垃圾收集是最主要的原因。 ### 碎片整理 @@ -214,17 +214,17 @@ JVM 中的引用是一个抽象的概念,如果 GC 移动某个对象,就会 在标记阶段有几个需要注意的地方:在标记阶段,需要暂停所有应用线程,以遍历所有对象的引用关系。因为不暂停就没法跟踪一直在变化的引用关系图。这种情景叫做 **Stop The World pause** ( **全线停顿** ),而可以安全地暂停线程的点叫做安全点(safe point),然后,JVM 就可以专心执行清理工作。安全点可能有多种因素触发,当前,GC 是触发安全点最常见的原因。 -此阶段暂停的时间,与堆内存大小,对象的总数没有直接关系,而是由 **存活对象** (alive objects)的数量来决定。所以增加堆内存的大小并不会直接影响标记阶段占用的时间。 **标记** 阶段完成后,GC 进行下一步操作,删除不可达对象。 +此阶段暂停的时间,与堆内存大小,对象的总数没有直接关系,而是由 **存活对象** (alive objects)的数量来决定。所以增加堆内存的大小并不会直接影响标记阶段占用的时间。**标记** 阶段完成后,GC 进行下一步操作,删除不可达对象。 #### **删除不可达对象(Removing Unused Objects)** 各种 GC 算法在删除不可达对象时略有不同,但总体可分为三类:清除(sweeping)、整理(compacting)和复制(copying)。\[下一小节\] 将详细讲解这些算法 #### **清除(Sweeping)** **Mark and Sweep(标记—清除)** 算法的概念非常简单:直接忽略所有的垃圾。也就是说在标记阶段完成后,所有不可达对象占用的内存空间,都被认为是空闲的,因此可以用来分配新对象。 -这种算法需要使用 **空闲表(free-list)** ,来记录所有的空闲区域,以及每个区域的大小。维护空闲表增加了对象分配时的开销。此外还存在另一个弱点 —— 明明还有很多空闲内存,却可能没有一个区域的大小能够存放需要分配的对象,从而导致分配失败(在 Java 中就是 [OutOfMemoryError](https://plumbr.eu/outofmemoryerror))。 +这种算法需要使用 **空闲表(free-list)**,来记录所有的空闲区域,以及每个区域的大小。维护空闲表增加了对象分配时的开销。此外还存在另一个弱点 —— 明明还有很多空闲内存,却可能没有一个区域的大小能够存放需要分配的对象,从而导致分配失败(在 Java 中就是 [OutOfMemoryError](https://plumbr.eu/outofmemoryerror))。 ![6898662.png](assets/f84e7570-322d-11ea-b6b0-159b6a0308ab) -#### **整理(Compacting)** **标记—清除—整理算法(Mark-Sweep-Compact)** ,将所有被标记的对象(存活对象),迁移到内存空间的起始处,消除了“标记—清除算法”的缺点 +#### **整理(Compacting)** **标记—清除—整理算法(Mark-Sweep-Compact)**,将所有被标记的对象(存活对象),迁移到内存空间的起始处,消除了“标记—清除算法”的缺点 相应的缺点就是 GC 暂停时间会增加,因为需要将所有对象复制到另一个地方,然后修改指向这些对象的引用。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25414\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25414\350\256\262.md" index efc38b386..907c94655 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25414\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25414\350\256\262.md" @@ -177,23 +177,23 @@ G1 并发标记的过程与 CMS 基本上是一样的。G1 的并发标记通过 - 如果在标记阶段确定某个小堆块中没有存活对象,只包含垃圾; - 在 STW 转移暂停期间,同时包含垃圾和存活对象的老年代小堆块。 -当堆内存的总体使用比例达到一定数值,就会触发并发标记。这个默认比例是 45%,但也可以通过 JVM 参数 **InitiatingHeapOccupancyPercent** 来设置。和 CMS 一样,G1 的并发标记也是由多个阶段组成,其中一些阶段是完全并发的,还有一些阶段则会暂停应用线程。 **阶段 1:Initial Mark(初始标记)** 此阶段标记所有从 GC 根对象直接可达的对象。在 CMS 中需要一次 STW 暂停,但 G1 里面通常是在转移暂停的同时处理这些事情,所以它的开销是很小的。 **阶段 2:Root Region Scan(Root 区扫描)** 此阶段标记所有从“根区域”可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。 +当堆内存的总体使用比例达到一定数值,就会触发并发标记。这个默认比例是 45%,但也可以通过 JVM 参数 **InitiatingHeapOccupancyPercent** 来设置。和 CMS 一样,G1 的并发标记也是由多个阶段组成,其中一些阶段是完全并发的,还有一些阶段则会暂停应用线程。**阶段 1:Initial Mark(初始标记)** 此阶段标记所有从 GC 根对象直接可达的对象。在 CMS 中需要一次 STW 暂停,但 G1 里面通常是在转移暂停的同时处理这些事情,所以它的开销是很小的。**阶段 2:Root Region Scan(Root 区扫描)** 此阶段标记所有从“根区域”可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。 -因为在并发标记的过程中迁移对象会造成很多麻烦,所以此阶段必须在下一次转移暂停之前完成。如果必须启动转移暂停,则会先要求根区域扫描中止,等它完成才能继续扫描。在当前版本的实现中,根区域是存活的小堆块:包括下一次转移暂停中肯定会被清理的那部分年轻代小堆块。 **阶段 3:Concurrent Mark(并发标记)** 此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。 +因为在并发标记的过程中迁移对象会造成很多麻烦,所以此阶段必须在下一次转移暂停之前完成。如果必须启动转移暂停,则会先要求根区域扫描中止,等它完成才能继续扫描。在当前版本的实现中,根区域是存活的小堆块:包括下一次转移暂停中肯定会被清理的那部分年轻代小堆块。**阶段 3:Concurrent Mark(并发标记)** 此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。 -为了确保标记开始时的快照准确性,所有应用线程并发对对象图执行引用更新,G1 要求放弃前面阶段为了标记目的而引用的过时引用。 **阶段 4:Remark(再次标记)** 和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 +为了确保标记开始时的快照准确性,所有应用线程并发对对象图执行引用更新,G1 要求放弃前面阶段为了标记目的而引用的过时引用。**阶段 4:Remark(再次标记)** 和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 -G1 收集器会短暂地停止应用线程,停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。 这一阶段也执行某些额外的清理,如引用处理或者类卸载(class unloading)。 **阶段 5:Cleanup(清理)** 最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升 GC 的效率。此阶段也为下一次标记执行必需的所有整理工作(house-keeping activities):维护并发标记的内部状态。 +G1 收集器会短暂地停止应用线程,停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。 这一阶段也执行某些额外的清理,如引用处理或者类卸载(class unloading)。**阶段 5:Cleanup(清理)** 最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升 GC 的效率。此阶段也为下一次标记执行必需的所有整理工作(house-keeping activities):维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的:例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的 STW 暂停,才能不受应用线程的影响并完成作业。 #### **转移暂停:混合模式(Evacuation Pause(mixed))** 并发标记完成之后,G1 将执行一次混合收集(mixed collection),就是不只清理年轻代,还将一部分老年代区域也加入到 回收集 中。混合模式的转移暂停不一定紧跟并发标记阶段。有很多规则和历史数据会影响混合模式的启动时机。比如,假若在老年代中可以并发地腾出很多的小堆块,就没有必要启动混合模式。因此,在并发标记与混合转移暂停之间,很可能会存在多次 young 模式的转移暂停 -具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时性能指标,存活性,以及在并发标记期间收集的 GC 效率等数据,外加一些可配置的 JVM 选项。混合收集的过程,很大程度上和前面的 fully-young gc 是一样的。 **Remembered Sets(历史记忆集)简介** Remembered Sets(历史记忆集)用来支持不同的小堆块进行独立回收。 +具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时性能指标,存活性,以及在并发标记期间收集的 GC 效率等数据,外加一些可配置的 JVM 选项。混合收集的过程,很大程度上和前面的 fully-young gc 是一样的。**Remembered Sets(历史记忆集)简介** Remembered Sets(历史记忆集)用来支持不同的小堆块进行独立回收。 例如,在回收小堆块 A、B、C 时,我们必须要知道是否有从 D 区或者 E 区指向其中的引用,以确定它们的存活性. 但是遍历整个堆需要相当长的时间,这就违背了增量收集的初衷,因此必须采取某种优化手段。类似于其他 GC 算法中的“卡片”方式来支持年轻代的垃圾收集,G1 中使用的则是 Remembered Sets。 -如下图所示,每个小堆块都有一个 **Remembered Set** ,列出了从外部指向本块的所有引用。这些引用将被视为附加的 GC 根。注意,在并发标记过程中,老年代中被确定为垃圾的对象会被忽略,即使有外部引用指向它们:因为在这种情况下引用者也是垃圾(如垃圾对象之间的引用或者循环引用)。 +如下图所示,每个小堆块都有一个 **Remembered Set**,列出了从外部指向本块的所有引用。这些引用将被视为附加的 GC 根。注意,在并发标记过程中,老年代中被确定为垃圾的对象会被忽略,即使有外部引用指向它们:因为在这种情况下引用者也是垃圾(如垃圾对象之间的引用或者循环引用)。 ![79450295.png](assets/a8fc6560-32ee-11ea-a438-7fd5b76593d7) @@ -215,7 +215,7 @@ G1 收集器会短暂地停止应用线程,停止并发更新信息的写入 可以看到,G1 作为 CMS 的代替者出现,解决了 CMS 中的各种疑难问题,包括暂停时间的可预测性,并终结了堆内存的碎片化。对单业务延迟非常敏感的系统来说,如果 CPU 资源不受限制,那么 G1 可以说是 HotSpot 中最好的选择,特别是在最新版本的 JVM 中。当然这种降低延迟的优化也不是没有代价的:由于额外的写屏障和守护线程,G1 的开销会更大。如果系统属于吞吐量优先型的,又或者 CPU 持续占用 100%,而又不在乎单次 GC 的暂停时间,那么 CMS 是更好的选择。 -> 总之, **G1 适合大内存,需要较低延迟的场景** 。 +> 总之,**G1 适合大内存,需要较低延迟的场景** 。 选择正确的 GC 算法,唯一可行的方式就是去尝试,并找出不合理的地方,一般性的指导原则: diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25415\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25415\350\256\262.md" index 724aaac04..7bc1e691a 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25415\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25415\350\256\262.md" @@ -251,7 +251,7 @@ Java 12 正式发布于 2019 年 3 月 19 日,这个版本引入了一款新 > [https://wiki.openjdk.java.net/display/shenandoah/Main](https://wiki.openjdk.java.net/display/shenandoah/Main) -作为 ZGC 的另一个选择,Shenandoah 是一款 **超低延迟垃圾收集器(Ultra-Low-Pause-Time Garbage Collector)** ,其设计目标是管理大型的多核服务器上,超大型的堆内存。GC 线程与应用线程并发执行、使得虚拟机的停顿时间非常短暂。 +作为 ZGC 的另一个选择,Shenandoah 是一款 **超低延迟垃圾收集器(Ultra-Low-Pause-Time Garbage Collector)**,其设计目标是管理大型的多核服务器上,超大型的堆内存。GC 线程与应用线程并发执行、使得虚拟机的停顿时间非常短暂。 #### **Shenandoah 的特点** Shenandoah GC 立项比 ZGC 更早,Red Hat 早在 2014 年就宣布启动开展此项目,实现 JVM 上 GC 低延迟的需求 @@ -309,7 +309,7 @@ GC(3) Concurrent cleanup 76244M->56620M(102400M) 12.242ms - 让 `-Xmx` 等于 `-Xms`:设置初始堆大小与最大值一致,可以减轻堆内存扩容带来的压力,与 AlwaysPreTouch 参数配合使用,在启动时申请所有内存,避免在使用中出现系统停顿。 - `-XX:+UseTransparentHugePages`:能够大大提高大堆的性能。 -#### **启发式参数** 启发式参数告知 Shenandoah GC何时开始GC处理,以及确定要归集的堆块。可以使用 `-XX:ShenandoahGCHeuristics=` 来选择不同的启发模式,有些启发模式可以配置一些参数,帮助我们更好地使用 GC。可用的启发模式如下。 **1. 自适应模式(adaptive)** +#### **启发式参数** 启发式参数告知 Shenandoah GC何时开始GC处理,以及确定要归集的堆块。可以使用 `-XX:ShenandoahGCHeuristics=` 来选择不同的启发模式,有些启发模式可以配置一些参数,帮助我们更好地使用 GC。可用的启发模式如下。**1. 自适应模式(adaptive)** 此为默认参数,通过观察之前的一些 GC 周期,以便在堆耗尽之前尝试启动下一个 GC 周期。 @@ -336,7 +336,7 @@ GC(3) Concurrent cleanup 76244M->56620M(102400M) 12.242ms - `-XX:ConcGCThreads=#`:设置并发 GC 线程数,可以减少并发 GC 线程的数量,以便为应用程序运行留出更多空间 - `-XX:ShenandoahAllocationThreshold=#`:设置从上一个 GC 周期到新的 GC 周期开始之前的内存分配百分比 -**4. 被动模式(passive)** 内存一旦用完,则发生 STW,用于系统诊断和功能测试。 **5. 积极模式(aggressive)** 它将尽快在上一个 GC 周期完成时启动新的 GC 周期(类似于“紧凑型”),并且将全部的存活对象归集到一块,这会严重影响性能,但是可以被用来测试 GC 本身。 +**4. 被动模式(passive)** 内存一旦用完,则发生 STW,用于系统诊断和功能测试。**5. 积极模式(aggressive)** 它将尽快在上一个 GC 周期完成时启动新的 GC 周期(类似于“紧凑型”),并且将全部的存活对象归集到一块,这会严重影响性能,但是可以被用来测试 GC 本身。 有时候启发式模式会在判断后把更新引用阶段和并发标记阶段合并。可以通过 `-XX:ShenandoahUpdateRefsEarly=[on|off]` 强制启用和禁用这个特性。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25416\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25416\350\256\262.md" index 398dcb50c..3f5f9446d 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25416\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25416\350\256\262.md" @@ -71,7 +71,7 @@ GraalVM 的混合式多语言编程可以解决开发中常见的这些问题: #### **跨语言的工作原理** GraalVM 提供了一种在不同语言之间无缝传值的方法,而不需要像其它虚拟机一样进行序列化和反序列化。这样就保证了跨语言的零开销互操作性,也就是说性能无损失,所以才号称高性能虚拟机 -GraalVM 开发了“跨语言互操作协议”,它是一种特殊的接口协议,每种运行在 GraalVM 之上的语言都要实现这种协议,这样就能保证跨语言的互操作性。语言和语言之间无须了解对方就可以高效传值。该协议还在不断改进中,未来会支持更多特性。 **弱化主语言** GraalVM 开发了一个实验性的启动器 Polyglot。在 Polyglot 里面不存在主语言的概念,每种语言都是平等的,可以使用 Polyglot 运行任意语言编写的程序,而不需要前面的每种语言单独一个启动器。Polyglot 会通过文件的扩展名来自动分类语言。 **Shell** GraalVM 还开发了一个动态语言的 Shell,该 Shell 默认使用 JS 语言,可以使用命令切换到任意其它语言进行解释操作。 +GraalVM 开发了“跨语言互操作协议”,它是一种特殊的接口协议,每种运行在 GraalVM 之上的语言都要实现这种协议,这样就能保证跨语言的互操作性。语言和语言之间无须了解对方就可以高效传值。该协议还在不断改进中,未来会支持更多特性。**弱化主语言** GraalVM 开发了一个实验性的启动器 Polyglot。在 Polyglot 里面不存在主语言的概念,每种语言都是平等的,可以使用 Polyglot 运行任意语言编写的程序,而不需要前面的每种语言单独一个启动器。Polyglot 会通过文件的扩展名来自动分类语言。**Shell** GraalVM 还开发了一个动态语言的 Shell,该 Shell 默认使用 JS 语言,可以使用命令切换到任意其它语言进行解释操作。 #### **将 Java 程序编译为可执行文件** 我们知道,Hotspot 推出之后,号称达到了 C++ 80% 的性能,其关键诀窍就在于 JIT 即时编译 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25417\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25417\350\256\262.md" index d99f1c13c..f95ce5847 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25417\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25417\350\256\262.md" @@ -149,7 +149,7 @@ Heap 在程序执行完成后、JVM 关闭前,还会输出各个内存池的使用情况,从最后面的输出中可以看到。 -下面我们来简单解读上面输出的堆内存信息。 **Heap 堆内存使用情况** +下面我们来简单解读上面输出的堆内存信息。**Heap 堆内存使用情况** ```java PSYoungGen total 872960K, used 32300K [0x......) diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25418\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25418\350\256\262.md" index c317d6f21..1620843f4 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25418\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25418\350\256\262.md" @@ -17,7 +17,7 @@ demo.jvm0204.GCLogAnalysis 从 JDK 9 开始,可以使用命令 `java -Xlog:help` 来查看当前 JVM 支持的日志参数,本文不进行详细的介绍,有兴趣的同学可以查看 [JEP 158: Unified JVM Logging](https://openjdk.java.net/jeps/158) 和 [JEP 271: Unified GC Logging](https://openjdk.java.net/jeps/271)。 -**另外** ,JMX 技术提供了 GC 事件的通知机制,监听 GC 事件的示例程序我们会在《应对容器时代面临的挑战》这一章节中给出。 +**另外**,JMX 技术提供了 GC 事件的通知机制,监听 GC 事件的示例程序我们会在《应对容器时代面临的挑战》这一章节中给出。 但很多情况下 JMX 通知事件中报告的 GC 数据并不完全,只是一个粗略的统计汇总。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25419\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25419\350\256\262.md" index 11d32d4bd..c43dd5e1b 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25419\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25419\350\256\262.md" @@ -178,7 +178,7 @@ CMS 的日志是一种完全不同的格式,并且很长,因为 CMS 对老 [Times: user=0.00 sys=0.00,real=0.00 secs] ``` -在实际运行中,CMS 在进行老年代的并发垃圾回收时,可能会伴随着多次年轻代的 Minor GC(想想是为什么)。在这种情况下,Full GC 的日志中可能会掺杂着多次 Minor GC 事件。 **阶段 1:Initial Mark(初始标记)** 前面章节提到过,这个阶段伴随着 STW 暂停。初始标记的目标是标记所有的根对象,包括 GC ROOT 直接引用的对象,以及被年轻代中所有存活对象所引用的对象。后面这部分也非常重要,因为老年代是独立进行回收的。 +在实际运行中,CMS 在进行老年代的并发垃圾回收时,可能会伴随着多次年轻代的 Minor GC(想想是为什么)。在这种情况下,Full GC 的日志中可能会掺杂着多次 Minor GC 事件。**阶段 1:Initial Mark(初始标记)** 前面章节提到过,这个阶段伴随着 STW 暂停。初始标记的目标是标记所有的根对象,包括 GC ROOT 直接引用的对象,以及被年轻代中所有存活对象所引用的对象。后面这部分也非常重要,因为老年代是独立进行回收的。 先看这个阶段的日志: @@ -196,7 +196,7 @@ CMS 的日志是一种完全不同的格式,并且很长,因为 CMS 对老 1. `CMS Initial Mark`:这个阶段的名称为“Initial Mark”,会标记所有的 GC Root。 1. `[1 CMS-initial-mark: 342870K(349568K)]`:这部分数字表示老年代的使用量,以及老年代的空间大小。 1. `363883K(506816K),0.0002262 secs`:当前堆内存的使用量,以及可用堆的大小、消耗的时间。可以看出这个时间非常短,只有 0.2 毫秒左右,因为要标记的这些 Roo 数量很少。 -1. `[Times: user=0.00 sys=0.00,real=0.00 secs]`:初始标记事件暂停的时间,可以看到可以忽略不计。 **阶段 2:Concurrent Mark(并发标记)** 在并发标记阶段,CMS 从前一阶段“Initial Mark”找到的 ROOT 开始算起,遍历老年代并标记所有的存活对象。 +1. `[Times: user=0.00 sys=0.00,real=0.00 secs]`:初始标记事件暂停的时间,可以看到可以忽略不计。**阶段 2:Concurrent Mark(并发标记)** 在并发标记阶段,CMS 从前一阶段“Initial Mark”找到的 ROOT 开始算起,遍历老年代并标记所有的存活对象。 看看这个阶段的 GC 日志: @@ -212,7 +212,7 @@ CMS 的日志是一种完全不同的格式,并且很长,因为 CMS 对老 1. `CMS-concurrent-mark`:指明了是 CMS 垃圾收集器所处的阶段为并发标记(“Concurrent Mark”)。 1. `0.001/0.001 secs`:此阶段的持续时间,分别是 GC 线程消耗的时间和实际消耗的时间。 -1. `[Times: user=0.00 sys=0.00,real=0.01 secs]`:`Times` 对并发阶段来说这些时间并没多少意义,因为是从并发标记开始时刻计算的,而这段时间应用线程也在执行,所以这个时间只是一个大概的值。 **阶段 3:Concurrent Preclean(并发预清理)** 此阶段同样是与应用线程并发执行的,不需要停止应用线程。 +1. `[Times: user=0.00 sys=0.00,real=0.01 secs]`:`Times` 对并发阶段来说这些时间并没多少意义,因为是从并发标记开始时刻计算的,而这段时间应用线程也在执行,所以这个时间只是一个大概的值。**阶段 3:Concurrent Preclean(并发预清理)** 此阶段同样是与应用线程并发执行的,不需要停止应用线程。 看看并发预清理阶段的 GC 日志: @@ -228,7 +228,7 @@ CMS 的日志是一种完全不同的格式,并且很长,因为 CMS 对老 1. `CMS-concurrent-preclean`:表明这是并发预清理阶段的日志,这个阶段会统计前面的并发标记阶段执行过程中发生了改变的对象。 1. `0.001/0.001 secs`:此阶段的持续时间,分别是 GC 线程运行时间和实际占用的时间。 -1. `[Times: user=0.00 sys=0.00,real=0.00 secs]`:Times 这部分对并发阶段来说没多少意义,因为是从开始时间计算的,而这段时间内不仅 GC 线程在执行并发预清理,应用线程也在运行。 **阶段 4:Concurrent Abortable Preclean(可取消的并发预清理)** 此阶段也不停止应用线程,尝试在会触发 STW 的 Final Remark 阶段开始之前,尽可能地多干一些活。 +1. `[Times: user=0.00 sys=0.00,real=0.00 secs]`:Times 这部分对并发阶段来说没多少意义,因为是从开始时间计算的,而这段时间内不仅 GC 线程在执行并发预清理,应用线程也在运行。**阶段 4:Concurrent Abortable Preclean(可取消的并发预清理)** 此阶段也不停止应用线程,尝试在会触发 STW 的 Final Remark 阶段开始之前,尽可能地多干一些活。 本阶段的具体时间取决于多种因素,因为它循环做同样的事情,直到满足某一个退出条件(如迭代次数、有用工作量、消耗的系统时间等等)。 @@ -248,7 +248,7 @@ CMS 的日志是一种完全不同的格式,并且很长,因为 CMS 对老 1. `0.000/0.000 secs`:此阶段 GC 线程的运行时间和实际占用的时间。从本质上讲,GC 线程试图在执行 STW 暂停之前等待尽可能长的时间。默认条件下,此阶段可以持续最长 5 秒钟的时间。 1. `[Times: user=0.00 sys=0.00,real=0.00 secs]`:“Times”这部分对并发阶段来说没多少意义,因为程序在并发阶段中持续运行。 -此阶段完成的工作可能对 STW 停顿的时间有较大影响,并且有许多重要的[配置选项](https://blogs.oracle.com/jonthecollector/entry/did_you_know)和失败模式。 **阶段 5:Final Remark(最终标记)** 最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW 停顿。 +此阶段完成的工作可能对 STW 停顿的时间有较大影响,并且有许多重要的[配置选项](https://blogs.oracle.com/jonthecollector/entry/did_you_know)和失败模式。**阶段 5:Final Remark(最终标记)** 最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW 停顿。 本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次 STW 暂停来处理各种复杂的情况。 @@ -288,7 +288,7 @@ CMS 的日志是一种完全不同的格式,并且很长,因为 CMS 对老 1. `368965K(506816K),0.0015928 secs`:此阶段完成后,整个堆内存的使用量和总容量。 1. `[Times: user=0.01 sys=0.00,real=0.00 secs]`:GC 事件的持续时间。 -在这 5 个标记阶段完成后,老年代中的所有存活对象都被标记上了,接下来 JVM 会将所有不使用的对象清除,以回收老年代空间。 **阶段 6:Concurrent Sweep(并发清除)** 此阶段与应用程序并发执行,不需要 STW 停顿。目的是删除不再使用的对象,并回收他们占用的内存空间。 +在这 5 个标记阶段完成后,老年代中的所有存活对象都被标记上了,接下来 JVM 会将所有不使用的对象清除,以回收老年代空间。**阶段 6:Concurrent Sweep(并发清除)** 此阶段与应用程序并发执行,不需要 STW 停顿。目的是删除不再使用的对象,并回收他们占用的内存空间。 看看这部分的 GC 日志: @@ -304,7 +304,7 @@ CMS 的日志是一种完全不同的格式,并且很长,因为 CMS 对老 1. `CMS-concurrent-sweep`:此阶段的名称,“Concurrent Sweep”,并发清除老年代中所有未被标记的对象、也就是不再使用的对象,以释放内存空间。 1. `0.000/0.000 secs`:此阶段的持续时间和实际占用的时间,这是一个四舍五入值,只精确到小数点后 3 位。 -1. `[Times: user=0.00 sys=0.00,real=0.00 secs]`:“Times”部分对并发阶段来说没有多少意义,因为是从并发标记开始时计算的,而这段时间内不仅是并发标记线程在执行,程序线程也在运行。 **阶段 7:Concurrent Reset(并发重置)** +1. `[Times: user=0.00 sys=0.00,real=0.00 secs]`:“Times”部分对并发阶段来说没有多少意义,因为是从并发标记开始时计算的,而这段时间内不仅是并发标记线程在执行,程序线程也在运行。**阶段 7:Concurrent Reset(并发重置)** 此阶段与应用程序线程并发执行,重置 CMS 算法相关的内部数据结构,下一次触发 GC 时就可以直接使用。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25421\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25421\350\256\262.md" index 81dc380fb..c792f847b 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25421\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25421\350\256\262.md" @@ -54,11 +54,11 @@ GCEasy 工具由 [Tier1app 公司](https://tier1app.com/) 开发和支持,这 比如使用我们前面生成的 gc.demo.log 文件,然后点击页面上的分析按钮,就可以生成分析报告。 -如果日志内容很大,我们也可以粘贴或者上传一部分 GC 日志进行分析。 **1. 总体报告** ![img](assets/96481700-68ce-11ea-bc7d-05803d82869a) +如果日志内容很大,我们也可以粘贴或者上传一部分 GC 日志进行分析。**1. 总体报告**![img](assets/96481700-68ce-11ea-bc7d-05803d82869a) -可以看到检测到了内存问题。 **2. JVM 内存大小分析** ![img](assets/a577e070-68ce-11ea-ae6a-117f7e51795f) +可以看到检测到了内存问题。**2. JVM 内存大小分析**![img](assets/a577e070-68ce-11ea-ae6a-117f7e51795f) -这里有对内存的分配情况的细节图表。 **3. GC 暂停时间的分布情况** 关键的性能指标:平均 GC 暂停时间 45.7ms,最大暂停时间 70.0ms。绝大部分 GC 暂停时间分布在 30~60ms,占比 89%。 +这里有对内存的分配情况的细节图表。**3. GC 暂停时间的分布情况** 关键的性能指标:平均 GC 暂停时间 45.7ms,最大暂停时间 70.0ms。绝大部分 GC 暂停时间分布在 30~60ms,占比 89%。 ![img](assets/0da8eed0-68d1-11ea-bb52-d9f36e195645) **4. GC 之后的内存情况统计** GC 执行以后堆内存的使用情况。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25422\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25422\350\256\262.md" index ad3c931a5..19382da62 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25422\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25422\350\256\262.md" @@ -308,7 +308,7 @@ HotSpot 为 Java 提供了管程锁(Monitor),线程执行程序代码时 竞争情景下的同步操作,使用高级自适应自旋技术来优化和提高吞吐量,这种优化对于高并发高竞争的锁争用场景也是有效的。 -HotSpot JVM 这么一优化之后,Java 自带的同步操作对于大多数系统来说,就不再有之前版本的性能问题。 **线程切换的代价:** Linux 时间片默认 0.75~6ms;Win XP 大约 10~15ms 左右;各个系统可能略有差别,但都在毫秒级别。假设 CPU 是 2G HZ,则每个时间片大约对应 2 百万个时钟周期,如果切换一次就有这么大的开销,系统的性能就会很糟糕。 +HotSpot JVM 这么一优化之后,Java 自带的同步操作对于大多数系统来说,就不再有之前版本的性能问题。**线程切换的代价:** Linux 时间片默认 0.75~6ms;Win XP 大约 10~15ms 左右;各个系统可能略有差别,但都在毫秒级别。假设 CPU 是 2G HZ,则每个时间片大约对应 2 百万个时钟周期,如果切换一次就有这么大的开销,系统的性能就会很糟糕。 所以 JDK 的信号量实现经过了自旋优化,先进行一定量时间的自旋操作,充分利用了操作系统已经分配给当前线程的时间片,否则这个时间片就被浪费了。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25423\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25423\350\256\262.md" index 07e8939e7..66527e011 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25423\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25423\350\256\262.md" @@ -61,13 +61,13 @@ - **Integer** :占用 16 字节(8+4=12+补齐),因为 int 部分占 4 个字节。所以使用 Integer 比原生类型 int 要多消耗 300% 的内存。 - **Long** :一般占用 16 个字节(8+8=16),当然,对象的实际大小由底层平台的内存对齐确定,具体由特定 CPU 平台的 JVM 实现决定。看起来一个 long 类型的对象,比起原生类型 long 多占用了 8 个字节(也多消耗了 100%)。相比之下,Integer 有 4 字节的补齐,很可能是因为 JVM 强制进行了 8 字节的边界对齐。 -其他容器类型占用的空间也不小。 **多维数组** :这是另一个惊喜。 +其他容器类型占用的空间也不小。**多维数组** :这是另一个惊喜。 在进行数值或科学计算时,开发人员经常会使用 `int[dim1][dim2]` 这种构造方式。 在二维数组 `int[dim1][dim2]` 中,每个嵌套的数组 `int[dim2]` 都是一个单独的 Object,会额外占用 16 字节的空间。某些情况下,这种开销是一种浪费。当数组维度更大时,这种开销特别明显。 -例如,`int[128][2]` 实例占用 3600 字节。而 `int[256]` 实例则只占用 1040 字节。里面的有效存储空间是一样的,3600 比起 1040 多了 246% 的额外开销。在极端情况下,`byte[256][1]`,额外开销的比例是 19 倍!而在 C/C++ 中,同样的语法却不增加额外的存储开销。 **String** :String 对象的空间随着内部字符数组的增长而增长。当然,String 类的对象有 24 个字节的额外开销。 +例如,`int[128][2]` 实例占用 3600 字节。而 `int[256]` 实例则只占用 1040 字节。里面的有效存储空间是一样的,3600 比起 1040 多了 246% 的额外开销。在极端情况下,`byte[256][1]`,额外开销的比例是 19 倍!而在 C/C++ 中,同样的语法却不增加额外的存储开销。**String** :String 对象的空间随着内部字符数组的增长而增长。当然,String 类的对象有 24 个字节的额外开销。 对于 10 字符以内的非空 String,增加的开销比起有效载荷(每个字符 2 字节 + 4 个字节的 length),多占用了 100% 到 400% 的内存。 @@ -83,7 +83,7 @@ class X { // 8 字节-指向 class 定义的引用 我们可能会认为,一个 X 类的实例占用 17 字节的空间。但是由于需要对齐(padding),JVM 分配的内存是 8 字节的整数倍,所以占用的空间不是 17 字节,而是 24 字节。 -当然,运行 JOL 的示例之后,会发现 JVM 会依次先排列 parent-class 的 fields,然后到本 class 的字段时,也是先排列 8 字节的, **排完了 8 字节的再排 4 字节的 field** ,以此类推。当然,还会 “加塞子”,尽量不浪费空间。 +当然,运行 JOL 的示例之后,会发现 JVM 会依次先排列 parent-class 的 fields,然后到本 class 的字段时,也是先排列 8 字节的,**排完了 8 字节的再排 4 字节的 field**,以此类推。当然,还会 “加塞子”,尽量不浪费空间。 Java 内置的序列化,也会基于这个布局,带来的坑就是加字段后就不兼容了。只加方法不固定 serialVersionUID 也出问题。所以有点经验的都不喜欢用内置序列化,例如自定义类型存到 Redis 时。 @@ -123,7 +123,7 @@ JOL 还支持代码方式调用,示例: 在 Java 中,创建一个新对象时,例如 `Integer num = new Integer(5)`,并不需要手动分配内存。因为 JVM 自动封装并处理了内存分配。在程序执行过程中,JVM 会在必要时检查内存中还有哪些对象仍在使用,而不再使用的那些对象则会被丢弃,并将其占用的内存回收和重用。这个过程称为“[垃圾收集](http://blog.csdn.net/renfufei/article/details/53432995)”。JVM 中负责垃圾回收的模块叫做“[垃圾收集器(GC)](http://blog.csdn.net/renfufei/article/details/54407417)”。 -Java 的自动内存管理依赖 [GC](http://blog.csdn.net/column/details/14851.html),GC 会一遍又一遍地扫描内存区域,将不使用的对象删除。简单来说, **Java 中的内存泄漏,就是那些逻辑上不再使用的对象,却没有被 垃圾收集程序 给干掉** 。从而导致垃圾对象继续占用堆内存中,逐渐堆积,最后产生 `java.lang.OutOfMemoryError: Java heap space` 错误。 +Java 的自动内存管理依赖 [GC](http://blog.csdn.net/column/details/14851.html),GC 会一遍又一遍地扫描内存区域,将不使用的对象删除。简单来说,**Java 中的内存泄漏,就是那些逻辑上不再使用的对象,却没有被 垃圾收集程序 给干掉** 。从而导致垃圾对象继续占用堆内存中,逐渐堆积,最后产生 `java.lang.OutOfMemoryError: Java heap space` 错误。 很容易写个 Bug 程序,来模拟内存泄漏: @@ -328,7 +328,7 @@ public class ClearRequestCacheFilter implements Filter{ #### **好用的分析工具:MAT** **1. MAT 介绍** MAT 全称是 Eclipse Memory Analyzer Tools -其优势在于,可以从 GC root 进行对象引用分析,计算各个 root 所引用的对象有多少,比较容易定位内存泄露。MAT 是一款独立的产品,100MB 不到,可以从官方下载:[下载地址](https://www.eclipse.org/mat/)。 **2. MAT 示例** 现象描述:系统进行慢 SQL 优化调整之后上线,在测试环境没有发现什么问题,但运行一段时间之后发现 CPU 跑满,下面我们就来分析案例。 +其优势在于,可以从 GC root 进行对象引用分析,计算各个 root 所引用的对象有多少,比较容易定位内存泄露。MAT 是一款独立的产品,100MB 不到,可以从官方下载:[下载地址](https://www.eclipse.org/mat/)。**2. MAT 示例** 现象描述:系统进行慢 SQL 优化调整之后上线,在测试环境没有发现什么问题,但运行一段时间之后发现 CPU 跑满,下面我们就来分析案例。 先查看本机的 Java 进程: @@ -417,9 +417,9 @@ http-nio-8086-exec-8 ... at org.mybatis.spring.SqlSessionTemplate.selectOne at com.sun.proxy.Proxy195.countVOBy(Lcom/ **** /domain/vo/home/residents/ResidentsInfomationVO;)I (Unknown Source) - at com. **** .bi.home.service.residents.impl.ResidentsInfomationServiceImpl.countVOBy(....)Ljava/lang/Integer; (ResidentsInfomationServiceImpl.java:164) - at com. **** .bi.home.service.residents.impl.ResidentsInfomationServiceImpl.selectAllVOByPage(....)Ljava/util/Map; (ResidentsInfomationServiceImpl.java:267) - at com. **** .web.controller.personFocusGroups.DocPersonFocusGroupsController.loadPersonFocusGroups(....)Lcom/ **** /domain/vo/JSONMessage; (DocPersonFocusGroupsController.java:183) + at com. ****.bi.home.service.residents.impl.ResidentsInfomationServiceImpl.countVOBy(....)Ljava/lang/Integer; (ResidentsInfomationServiceImpl.java:164) + at com. ****.bi.home.service.residents.impl.ResidentsInfomationServiceImpl.selectAllVOByPage(....)Ljava/util/Map; (ResidentsInfomationServiceImpl.java:267) + at com. ****.web.controller.personFocusGroups.DocPersonFocusGroupsController.loadPersonFocusGroups(....)Lcom/ **** /domain/vo/JSONMessage; (DocPersonFocusGroupsController.java:183) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run()V (TaskThread.java:61) at java.lang.Thread.run()V (Thread.java:745) ``` @@ -427,7 +427,7 @@ http-nio-8086-exec-8 其中比较关键的信息,就是找到我们自己的 package,如: ```plaintext -com. **** .....ResidentsInfomationServiceImpl.selectAllVOByPage +com. ****.....ResidentsInfomationServiceImpl.selectAllVOByPage ``` 并且其中给出了 Java 源文件所对应的行号。 @@ -458,7 +458,7 @@ com. **** .....ResidentsInfomationServiceImpl.selectAllVOByPage MAT 还提供了其他信息,都可以点开看看,也可以为我们诊断问题提供一些依据。 -#### **JDK 内置故障排查工具:jhat** jhat 是 Java 堆分析工具(Java heap Analyzes Tool)。在 JDK6u7 之后成为 JDK 标配。使用该命令需要有一定的 Java 开发经验,官方不对此工具提供技术支持和客户服务。 **1. jhat 用法** +#### **JDK 内置故障排查工具:jhat** jhat 是 Java 堆分析工具(Java heap Analyzes Tool)。在 JDK6u7 之后成为 JDK 标配。使用该命令需要有一定的 Java 开发经验,官方不对此工具提供技术支持和客户服务。**1. jhat 用法** ```plaintext jhat [options] heap-dump-file @@ -467,7 +467,7 @@ jhat [options] heap-dump-file 参数: - **_options_** 可选命令行参数,请参考下面的 \[Options\]。 -- **_heap-dump-file_** 要查看的二进制 Java 堆转储文件(Java binary heap dump file)。如果某个转储文件中包含了多份 heap dumps,可在文件名之后加上 `#` 的方式指定解析哪一个 dump,如:`myfile.hprof#3`。 **2. jhat 示例** 使用 jmap 工具转储堆内存、可以使用如下方式: +- **_heap-dump-file_** 要查看的二进制 Java 堆转储文件(Java binary heap dump file)。如果某个转储文件中包含了多份 heap dumps,可在文件名之后加上 `#` 的方式指定解析哪一个 dump,如:`myfile.hprof#3`。**2. jhat 示例** 使用 jmap 工具转储堆内存、可以使用如下方式: ```plaintext jmap -dump:file=DumpFileName.txt,format=b @@ -495,7 +495,7 @@ Started HTTP server on port 7000 Server is ready. ``` -使用参数 `-J-Xmx1024m` 是因为默认 JVM 的堆内存可能不足以加载整个 dump 文件,可根据需要进行调整。然后我们可以根据提示知道端口号是 7000,接着使用浏览器访问 即可看到相关分析结果。 **3. 详细说明** jhat 命令支持预先设计的查询,比如显示某个类的所有实例。 +使用参数 `-J-Xmx1024m` 是因为默认 JVM 的堆内存可能不足以加载整个 dump 文件,可根据需要进行调整。然后我们可以根据提示知道端口号是 7000,接着使用浏览器访问 即可看到相关分析结果。**3. 详细说明** jhat 命令支持预先设计的查询,比如显示某个类的所有实例。 还支持 **对象查询语言** (OQL,Object Query Language),OQL 有点类似 SQL,专门用来查询堆转储。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25424\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25424\350\256\262.md" index a34cd5da9..b9cd95e34 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25424\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25424\350\256\262.md" @@ -199,7 +199,7 @@ public class MicroGenerator { 但实际情况可能并不乐观,很多第三方库,以及某些受限的共享资源,如 thread、JDBC 驱动,以及文件系统句柄(handles),都会导致不能彻底卸载之前的 classloader。 -那么在 redeploy 时,之前的 class 仍然驻留在 PermGen 中, **每次重新部署都会产生几十 MB,甚至上百 MB 的垃圾** 。就像牛皮癣一样待在内存里。 +那么在 redeploy 时,之前的 class 仍然驻留在 PermGen 中,**每次重新部署都会产生几十 MB,甚至上百 MB 的垃圾** 。就像牛皮癣一样待在内存里。 假设某个应用在启动时,通过初始化代码加载 JDBC 驱动连接数据库。根据 JDBC 规范,驱动会将自身注册到 java.sql.DriverManager,也就是将自身的一个实例(instance)添加到 DriverManager 中的一个 static 域。 @@ -207,13 +207,13 @@ public class MicroGenerator { 而 java.lang.ClassLoader 实例持有着其加载的所有 class,通常是几十/上百 MB 的内存。可以看到,redeploy 时会占用另一块差不多大小的 PermGen 空间,多次 redeploy 之后,就会造成“java.lang.OutOfMemoryError: PermGen space”错误,在日志文件中,你应该会看到相关的错误信息。 -#### **解决方案** 既然我们了解到了问题的所在,那么就可以考虑对应的解决办法。 **1. 解决程序启动时产生的 OutOfMemoryError** 在程序启动时,如果 PermGen 耗尽而产生 OutOfMemoryError 错误,那很容易解决。增加 PermGen 的大小,让程序拥有更多的内存来加载 class 即可。修改 `-XX:MaxPermSize` 启动参数,例如 +#### **解决方案** 既然我们了解到了问题的所在,那么就可以考虑对应的解决办法。**1. 解决程序启动时产生的 OutOfMemoryError** 在程序启动时,如果 PermGen 耗尽而产生 OutOfMemoryError 错误,那很容易解决。增加 PermGen 的大小,让程序拥有更多的内存来加载 class 即可。修改 `-XX:MaxPermSize` 启动参数,例如 ```java java -XX:MaxPermSize=512m com.yourcompany.YourClass ``` -以上配置允许 JVM 使用的最大 PermGen 空间为 512MB,如果还不够,就会抛出 OutOfMemoryError。 **2. 解决 redeploy 时产生的 OutOfMemoryError** 我们可以进行堆转储分析(heap dump analysis)——在 redeploy 之后,执行堆转储,类似下面这样: +以上配置允许 JVM 使用的最大 PermGen 空间为 512MB,如果还不够,就会抛出 OutOfMemoryError。**2. 解决 redeploy 时产生的 OutOfMemoryError** 我们可以进行堆转储分析(heap dump analysis)——在 redeploy 之后,执行堆转储,类似下面这样: ```plaintext jmap -dump:format=b,file=dump.hprof @@ -221,7 +221,7 @@ jmap -dump:format=b,file=dump.hprof 然后通过堆转储分析器(如强悍的 Eclipse MAT)加载 dump 得到的文件。找出重复的类,特别是类加载器(classloader)对应的 class。你可能需要比对所有的 classloader,来找出当前正在使用的那个。 -对于不使用的类加载器(inactive classloader),需要先确定最短路径的 [GC root](http://blog.csdn.net/renfufei/article/details/54407417#t0) ,看看是哪一个阻止其被 [垃圾收集器](http://blog.csdn.net/renfufei/article/details/54144385) 所回收。这样才能找到问题的根源。如果是第三方库的原因,那么可以搜索 Google/StackOverflow 来查找解决方案。如果是自己的代码问题,则需要修改代码,在恰当的时机来解除相关引用。 **3. 解决运行时产生的 OutOfMemoryError** 如果在运行的过程中发生 OutOfMemoryError,首先需要确认 [GC 是否能从 PermGen 中卸载 class](http://blog.csdn.net/renfufei/article/details/54144385#t6)。 +对于不使用的类加载器(inactive classloader),需要先确定最短路径的 [GC root](http://blog.csdn.net/renfufei/article/details/54407417#t0) ,看看是哪一个阻止其被 [垃圾收集器](http://blog.csdn.net/renfufei/article/details/54144385) 所回收。这样才能找到问题的根源。如果是第三方库的原因,那么可以搜索 Google/StackOverflow 来查找解决方案。如果是自己的代码问题,则需要修改代码,在恰当的时机来解除相关引用。**3. 解决运行时产生的 OutOfMemoryError** 如果在运行的过程中发生 OutOfMemoryError,首先需要确认 [GC 是否能从 PermGen 中卸载 class](http://blog.csdn.net/renfufei/article/details/54144385#t6)。 官方的 JVM 在这方面是相当的保守(在加载 class 之后,就一直让其驻留在内存中,即使这个类不再被使用)。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25425\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25425\350\256\262.md" index 853053c2b..3c91bbd8b 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25425\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25425\350\256\262.md" @@ -209,11 +209,11 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.162-b12 mixed mode): 打开官网首页:[http://fastthread.io/](http://fastthread.io/)。 -#### **文件上传方式** ![img](assets/98bd8e60-7504-11ea-92e8-fb0928480567) +#### **文件上传方式**![img](assets/98bd8e60-7504-11ea-92e8-fb0928480567) 选择文件并上传,然后鼠标点击“分析”(Analyze)按钮即可。 -#### **上传文本方式** ![img](assets/a5e18240-7504-11ea-94a5-05a63ed48ac3) +#### **上传文本方式**![img](assets/a5e18240-7504-11ea-94a5-05a63ed48ac3) 两种方式步骤都差不多,选择 RAW 方式上传文本字符串,然后点击分析按钮。 @@ -273,17 +273,17 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.162-b12 mixed mode): 但如果有很多方法阻塞或等待,则线程快照中展示的热点方法位置可以快速确定问题出现的代码行。 -#### **CPU 消耗信息** ![img](assets/cc8891d0-7505-11ea-b77f-634b57f46967) +#### **CPU 消耗信息**![img](assets/cc8891d0-7505-11ea-b77f-634b57f46967) 这里的提示信息不太明显,但给出了一些学习资源,这些资源请参考本文末尾给出的博客链接地址。 -#### **GC 线程信息** ![img](assets/c466a910-7505-11ea-94a5-05a63ed48ac3) +#### **GC 线程信息**![img](assets/c466a910-7505-11ea-94a5-05a63ed48ac3) 这里看到 GC 线程数是 8 个,这个值跟具体的 CPU 内核数量相差不大就算是正常的。 GC 线程数如果太多或者太少,会造成很多问题,我们在后面的章节中通过案例进行讲解。 -#### **线程栈深度** ![7277060.png](assets/b3d1aa50-7505-11ea-a8c0-4fdc777140d0) +#### **线程栈深度**![7277060.png](assets/b3d1aa50-7505-11ea-a8c0-4fdc777140d0) 这里都小于10,说明堆栈都不深。 @@ -293,7 +293,7 @@ GC 线程数如果太多或者太少,会造成很多问题,我们在后面 简单死锁是指两个线程之间互相死等资源锁。那么什么复杂死锁呢? 这个问题留给同学们自己搜索。 -#### **火焰图** ![7336167.png](assets/a0e32b30-7505-11ea-8fb8-ffe43c2e987a) +#### **火焰图**![7336167.png](assets/a0e32b30-7505-11ea-8fb8-ffe43c2e987a) 火焰图挺有趣,将所有线程调用栈汇总到一张图片中。 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25426\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25426\350\256\262.md" index 7dd1fba9e..f911a0a18 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25426\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25426\350\256\262.md" @@ -50,7 +50,7 @@ Java 的堆内存溢出(OOM),是指堆内存用满了,GC 没法回收导 这样配置之后,就允许某个占用了最多资源的进程,在操作系统内存不足时,也不会杀掉他,而是先去杀别的进程。 -#### **案例** 我们通过以下这个案例来展示 OOM Killer。 **1. 问题描述** 某个 Java 应用经常挂掉,原因疑似 Java 进程被杀死。 **2. 配置信息** +#### **案例** 我们通过以下这个案例来展示 OOM Killer。**1. 问题描述** 某个 Java 应用经常挂掉,原因疑似 Java 进程被杀死。**2. 配置信息** 配置如下: @@ -59,7 +59,7 @@ Java 的堆内存溢出(OOM),是指堆内存用满了,GC 没法回收导 - CPU:4 核,虚拟 CPU Intel Xeon E5-2650 2.60GHz - 物理内存:8GB -**3. 可用内存** 内存不足:4 个 Java 进程,2.1+1.7+1.7+1.3=6.8G,已占用绝大部分内存。 **4. 查看日志** Linux 系统的 OOM Killer 日志: +**3. 可用内存** 内存不足:4 个 Java 进程,2.1+1.7+1.7+1.3=6.8G,已占用绝大部分内存。**4. 查看日志** Linux 系统的 OOM Killer 日志: ```plaintext sudo cat /var/log/messages | grep killer -A 2 -B 2 @@ -163,7 +163,7 @@ BTrace 提供了命令行工具,但使用起不如在 JVisualVM 中方便, #### **JVisualVM 环境中使用 BTrace** 安装 JVisualVM 插件的操作,我们在前面的章节《\[JDK 内置图形界面工具\]》中介绍过 -细心的同学可能已经发现,在安装 JVisualVM 的插件时,有一款插件叫做“BTrace Workbench”。安装这款插件之后,在对应的 JVM 实例上点右键,就可以进入 BTrace 的操作界面。 **1. BTrace 插件安装** 打开 VisualVM,选择菜单“工具–插件(G)”: +细心的同学可能已经发现,在安装 JVisualVM 的插件时,有一款插件叫做“BTrace Workbench”。安装这款插件之后,在对应的 JVM 实例上点右键,就可以进入 BTrace 的操作界面。**1. BTrace 插件安装** 打开 VisualVM,选择菜单“工具–插件(G)”: ![82699966.png](assets/d59e3c10-7808-11ea-8c74-b966eb0a8d67) @@ -189,7 +189,7 @@ BTrace 提供了命令行工具,但使用起不如在 JVisualVM 中方便, ![83257210.png](assets/5c553bf0-7809-11ea-8e11-89f2c26dd0be) -点击“完成”按钮即可。 **BTrace 插件使用** ![85267702.png](assets/689845b0-7809-11ea-8856-57a8f16560a7) +点击“完成”按钮即可。**BTrace 插件使用**![85267702.png](assets/689845b0-7809-11ea-8856-57a8f16560a7) 打开后默认的界面如下: @@ -534,7 +534,7 @@ java -javaagent:/path-to/aprof.jar com.yourcompany.YourApplication ... cut for brevity ... ``` -上面的输出是按照 size 进行排序的。可以看出,80.44% 的 bytes 和 68.81% 的 objects 是在 ManyTargetsGarbageProducer.newRandomClassObject() 方法中分配的。其中, **int[]** 数组占用了 40.19% 的内存,是最大的一个。 +上面的输出是按照 size 进行排序的。可以看出,80.44% 的 bytes 和 68.81% 的 objects 是在 ManyTargetsGarbageProducer.newRandomClassObject() 方法中分配的。其中,**int[]** 数组占用了 40.19% 的内存,是最大的一个。 继续往下看,会发现 allocation traces(分配痕迹)相关的内容,也是以 allocation size 排序的: @@ -553,7 +553,7 @@ java -javaagent:/path-to/aprof.jar com.yourcompany.YourApplication 和其他工具一样,AProf 揭露了 分配的大小以及位置信息(allocation size and locations),从而能够快速找到最耗内存的部分。 -在我们看来, **AProf** 是非常有用的分配分析器,因为它只专注于内存分配,所以做得最好。同时这款工具是开源免费的,资源开销也最小。 +在我们看来,**AProf** 是非常有用的分配分析器,因为它只专注于内存分配,所以做得最好。同时这款工具是开源免费的,资源开销也最小。 ### 参考链接 diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25427\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25427\350\256\262.md" index a13405047..859e7a4a6 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25427\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25427\350\256\262.md" @@ -25,7 +25,7 @@ ### 为什么问题排查这么困难? -#### **生产环境中进行故障排查的困难** 在生产环境中针对特定问题进行故障排除时,往往会有诸多限制,从而导致排查的过程变得痛苦。 **1. 影响到客户的时间越短越好** 面对客户的抱怨,解决问题最快的办法可能是:“ **只要重启机器就能让系统恢复正常** ” +#### **生产环境中进行故障排查的困难** 在生产环境中针对特定问题进行故障排除时,往往会有诸多限制,从而导致排查的过程变得痛苦。**1. 影响到客户的时间越短越好** 面对客户的抱怨,解决问题最快的办法可能是:“ **只要重启机器就能让系统恢复正常** ” 用最快的方法来避免对用户产生影响是很自然的需求。 @@ -33,7 +33,7 @@ 如果重新启动实例,则无法再采集实际发生的情况,导致我们并没有从这次故障中学习,从而获得收益。 -即使重启解决了目前的问题,但问题原因本身仍然存在,一直是一个定时炸弹,还可能会接二连三地发生。 **2. 安全方面的限制** +即使重启解决了目前的问题,但问题原因本身仍然存在,一直是一个定时炸弹,还可能会接二连三地发生。**2. 安全方面的限制** 接下来是安全性相关的限制,这些限制导致生产环境是独立和隔离的,一般来说,开发人员可能没有权限访问生产环境。如果没有权限访问生产环境,那就只能进行远程故障排除,并涉及到所有与之相关的问题: @@ -41,9 +41,9 @@ 特别是将临时补丁程序发布到生产环境时,“希望它能生效”,但这种试错的情况却可能导致越来越糟糕。 -**因为测试和发布流程可能又要消耗几小时甚至几天** ,进一步增加了解决问题实际消耗的时间。 +**因为测试和发布流程可能又要消耗几小时甚至几天**,进一步增加了解决问题实际消耗的时间。 -如果还需要分多次上线这种“不一定生效的补丁程序”,则很可能会消耗几个星期才能解决问题。 **3. 工具引发的问题** 还有很重要的一点是需要使用的工具: **安装使用的某些工具在特点场景下可能会使情况变得更糟** 。 +如果还需要分多次上线这种“不一定生效的补丁程序”,则很可能会消耗几个星期才能解决问题。**3. 工具引发的问题** 还有很重要的一点是需要使用的工具: **安装使用的某些工具在特点场景下可能会使情况变得更糟** 。 例如: diff --git "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25429\350\256\262.md" "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25429\350\256\262.md" index 045d77fd2..4fc1621a1 100644 --- "a/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25429\350\256\262.md" +++ "b/docs/Java/JVM \346\240\270\345\277\203\346\212\200\346\234\257 32 \350\256\262/\347\254\25429\350\256\262.md" @@ -287,7 +287,7 @@ Total 根据这些信息,就可以计算出观测周期内的提升速率:平均提升速率为 92MB/秒,峰值为 140.95MB/秒。 -请注意, **只能根据 Minor GC 计算提升速率** 。Full GC 的日志不能用于计算提升速率,因为 Major GC 会清理掉老年代中的一部分对象。 +请注意,**只能根据 Minor GC 计算提升速率** 。Full GC 的日志不能用于计算提升速率,因为 Major GC 会清理掉老年代中的一部分对象。 #### **提升速率的意义** 和分配速率一样,提升速率也会影响 GC 暂停的频率。但分配速率主要影响 [minor GC](http://blog.csdn.net/renfufei/article/details/54144385#t8),而提升速率则影响 [major GC](http://blog.csdn.net/renfufei/article/details/54144385#t8) 的频率。有大量的对象提升,自然很快将老年代填满。老年代填充的越快,则 Major GC 事件的频率就会越高 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25400\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25400\350\256\262.md" index 71a6f5b5c..740d31693 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25400\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25400\350\256\262.md" @@ -5,13 +5,13 @@ Java 是一门历史悠久的编程语言,可以毫无争议地说,Java 是 我所知道的诸如阿里巴巴、京东、百度、腾讯、美团、去哪儿等互联网公司,基本都是以 Java 为首要编程语言的。即使在最新的云计算领域,Java 仍然是 AWS、Google App Engine 等平台上,使用最多的编程语言;甚至是微软 Azure 云上,Java 也以微弱劣势排在前三位。所以,在这些大公司的面试中,基本都会以 Java 为切入点,考评一个面试者的技术能力。 -应聘初级、中级 Java 工程师,通常只要求扎实的 Java 和计算机科学基础,掌握主流开源框架的使用;Java 高级工程师或者技术专家,则往往全面考察 Java IO/NIO、并发、虚拟机等,不仅仅是了解, **更要求对底层源代码层面的掌握,并对分布式、安全、性能等领域能力有进一步的要求** 。 +应聘初级、中级 Java 工程师,通常只要求扎实的 Java 和计算机科学基础,掌握主流开源框架的使用;Java 高级工程师或者技术专家,则往往全面考察 Java IO/NIO、并发、虚拟机等,不仅仅是了解,**更要求对底层源代码层面的掌握,并对分布式、安全、性能等领域能力有进一步的要求** 。 我在 Oracle 已经工作了近 7 年,负责过北京 Java 核心类库、国际化、分发服务等技术团队的组建,面试过从初级到非常资深的 Java 开发工程师。由于 Java 组工作任务的特点,我非常注重面试者的计算机科学基础和编程语言的理解深度,我甚至不要求面试者非要精通Java,如果对 C/C++ 等其他语言能够掌握得非常系统和深入,也是符合需求的。 -工作多年以及在面试中,我经常能体会到,有些面试者确实是认真努力工作, **但坦白说表现出的能力水平却不足以通过面试** ,通常是两方面原因: +工作多年以及在面试中,我经常能体会到,有些面试者确实是认真努力工作,**但坦白说表现出的能力水平却不足以通过面试**,通常是两方面原因: -- “知其然不知其所以然”。做了多年技术,开发了很多业务应用,但似乎并未思考过种种 **技术选择背后的逻辑** 。坦白说,我并不放心把具有一定深度的任务交给他。更重要的是,我并不确定他未来技术能力的成长潜力有多大。团队所从事的是公司核心产品,工作于基础技术领域, **我们不需要那些“差不多”或“还行”的代码,而是需要达到一定水准的高质量设计与实现** 。我相信很多其他技术团队的要求会更多、更高。 +- “知其然不知其所以然”。做了多年技术,开发了很多业务应用,但似乎并未思考过种种 **技术选择背后的逻辑** 。坦白说,我并不放心把具有一定深度的任务交给他。更重要的是,我并不确定他未来技术能力的成长潜力有多大。团队所从事的是公司核心产品,工作于基础技术领域,**我们不需要那些“差不多”或“还行”的代码,而是需要达到一定水准的高质量设计与实现** 。我相信很多其他技术团队的要求会更多、更高。 知识碎片化,不成系统。在面试中,面试者似乎无法完整、清晰地描述自己所开发的系统,或者使用的相关技术。平时可能埋头苦干,或者过于死磕某个实现细节,并没有抬头审视这些技术。比如,有的面试者,有一些并发编程经验,但对基本的并发类库掌握却并不扎实,似乎觉得在用的时候进行“面向搜索引擎的编程”就足够了。这种情况下,我没有信心这个面试者有高效解决复杂问题、设计复杂系统的能力。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25401\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25401\350\256\262.md" index 192a5c08d..857eb07f3 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25401\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25401\350\256\262.md" @@ -10,13 +10,13 @@ Java 本身是一种面向对象的语言,最显著的特性有两个方面, 我们日常会接触到 JRE(Java Runtime Environment)或者 JDK(Java Development Kit)。 JRE,也就是 Java 运行环境,包含了 JVM 和 Java 类库,以及一些模块等。而 JDK 可以看作是 JRE 的一个超集,提供了更多工具,比如编译器、各种诊断工具等。 -对于“Java 是解释执行”这句话,这个说法不太准确。我们开发的 Java 的源代码,首先通过 Javac 编译成为字节码(bytecode),然后,在运行时,通过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码。但是常见的 JVM,比如我们大多数情况使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)编译器,也就是通常所说的动态编译器,JIT 能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于 **编译执行** ,而不是解释执行了。 +对于“Java 是解释执行”这句话,这个说法不太准确。我们开发的 Java 的源代码,首先通过 Javac 编译成为字节码(bytecode),然后,在运行时,通过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码。但是常见的 JVM,比如我们大多数情况使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)编译器,也就是通常所说的动态编译器,JIT 能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于 **编译执行**,而不是解释执行了。 ## 考点分析 其实这个问题,问得有点笼统。题目本身是非常开放的,往往考察的是多个方面,比如,基础知识理解是否很清楚;是否掌握 Java 平台主要模块和运行原理等。很多面试者会在这种问题上吃亏,稍微紧张了一下,不知道从何说起,就给出个很简略的回答。 -对于这类笼统的问题,你需要尽量 **表现出自己的思维深入并系统化,Java 知识理解得也比较全面** ,一定要避免让面试官觉得你是个“知其然不知其所以然”的人。毕竟明白基本组成和机制,是日常工作中进行问题诊断或者性能调优等很多事情的基础,相信没有招聘方会不喜欢“热爱学习和思考”的面试者。 +对于这类笼统的问题,你需要尽量 **表现出自己的思维深入并系统化,Java 知识理解得也比较全面**,一定要避免让面试官觉得你是个“知其然不知其所以然”的人。毕竟明白基本组成和机制,是日常工作中进行问题诊断或者性能调优等很多事情的基础,相信没有招聘方会不喜欢“热爱学习和思考”的面试者。 即使感觉自己的回答不是非常完善,也不用担心。我个人觉得这种笼统的问题,有时候回答得稍微片面也很正常,大多数有经验的面试官,不会因为一道题就对面试者轻易地下结论。通常会尽量引导面试者,把他的真实水平展现出来,这种问题就是做个开场热身,面试官经常会根据你的回答扩展相关问题。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25402\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25402\350\256\262.md" index 48ce19939..655364c2d 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25402\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25402\350\256\262.md" @@ -25,7 +25,7 @@ Error,是 Throwable 不是 Exception。 我们在日常编程中,如何处理好异常是比较考验功底的,我觉得需要掌握两个方面。 -第一, **理解 Throwable、Exception、Error 的设计和分类** 。比如,掌握那些应用最为广泛的子类,以及如何自定义异常等。 +第一,**理解 Throwable、Exception、Error 的设计和分类** 。比如,掌握那些应用最为广泛的子类,以及如何自定义异常等。 很多面试官会进一步追问一些细节,比如,你了解哪些 Error、Exception 或者 RuntimeException?我画了一个简单的类图,并列出来典型例子,可以给你作为参考,至少做到基本心里有数。 @@ -33,7 +33,7 @@ Error,是 Throwable 不是 Exception。 其中有些子类型,最好重点理解一下,比如 NoClassDefFoundError 和 ClassNotFoundException 有什么区别,这也是个经典的入门题目。 -第二, **理解 Java 语言中操作 Throwable 的元素和实践** 。掌握最基本的语法是必须的,如 try-catch-finally 块,throw、throws 关键字等。与此同时,也要懂得如何处理典型场景。 +第二,**理解 Java 语言中操作 Throwable 的元素和实践** 。掌握最基本的语法是必须的,如 try-catch-finally 块,throw、throws 关键字等。与此同时,也要懂得如何处理典型场景。 异常处理代码比较繁琐,比如我们需要写很多千篇一律的捕获代码,或者在 finally 里面做一些资源回收工作。随着 Java 语言的发展,引入了一些更加便利的特性,比如 try-with-resources 和 multiple catch,具体可以参考下面的代码段。在编译时期,会自动生成相应的处理逻辑,比如,自动按照约定俗成 close 那些扩展了 AutoCloseable 或者 Closeable 的对象。 @@ -64,13 +64,13 @@ try { 这段代码虽然很短,但是已经违反了异常处理的两个基本原则。 -第一, **尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常** ,在这里是 Thread.sleep() 抛出的 InterruptedException。 +第一,**尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常**,在这里是 Thread.sleep() 抛出的 InterruptedException。 这是因为在日常的开发和合作中,我们读代码的机会往往超过写代码,软件工程是门协作的艺术,所以我们有义务让自己的代码能够直观地体现出尽量多的信息,而泛泛的 Exception 之类,恰恰隐藏了我们的目的。另外,我们也要保证程序不会捕获到我们不希望捕获的异常。比如,你可能更希望 RuntimeException 被扩散出来,而不是被捕获。 进一步讲,除非深思熟虑了,否则不要捕获 Throwable 或者 Error,这样很难保证我们能够正确程序处理 OutOfMemoryError。 -第二, **不要生吞(swallow)异常** 。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。 +第二,**不要生吞(swallow)异常** 。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。 生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,但是千万不要在产品代码做这种假设! @@ -133,7 +133,7 @@ public void readPreferences(String filename) { [http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/](http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/)。 -当然,很多人也觉得没有必要矫枉过正,因为确实有一些异常,比如和环境相关的 IO、网络等,其实是存在可恢复性的,而且 Java 已经通过业界的海量实践,证明了其构建高质量软件的能力。我就不再进一步解读了,感兴趣的同学可以点击 **链接** ,观看 Bruce Eckel 在 2018 年全球软件开发大会 QCon 的分享 Failing at Failing: How and Why We’ve Been Nonchalantly Moving Away From Exception Handling。 +当然,很多人也觉得没有必要矫枉过正,因为确实有一些异常,比如和环境相关的 IO、网络等,其实是存在可恢复性的,而且 Java 已经通过业界的海量实践,证明了其构建高质量软件的能力。我就不再进一步解读了,感兴趣的同学可以点击 **链接**,观看 Bruce Eckel 在 2018 年全球软件开发大会 QCon 的分享 Failing at Failing: How and Why We’ve Been Nonchalantly Moving Away From Exception Handling。 我们从性能角度来审视一下 Java 的异常处理机制,这里有两个可能会相对昂贵的地方: diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25403\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25403\350\256\262.md" index 32981dc6d..074831181 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25403\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25403\350\256\262.md" @@ -55,7 +55,7 @@ try { ### 1. 注意,final 不是 immutable -我在前面介绍了 final 在实践中的益处,需要注意的是, **final 并不等同于 immutable** ,比如下面这段代码: +我在前面介绍了 final 在实践中的益处,需要注意的是,**final 并不等同于 immutable**,比如下面这段代码: ```java final List strList = new ArrayList<>(); @@ -89,7 +89,7 @@ finalize 的执行是和垃圾收集关联在一起的,一旦实现了非空 有人也许会问,我用 System.runFinalization() 告诉 JVM 积极一点,是不是就可以了?也许有点用,但是问题在于,这还是不可预测、不能保证的,所以本质上还是不能指望。实践中,因为 finalize 拖慢垃圾收集,导致大量对象堆积,也是一种典型的导致 OOM 的原因。 -从另一个角度,我们要确保回收资源就是因为资源都是有限的,垃圾收集时间的不可预测,可能会极大加剧资源占用。这意味着对于消耗非常高频的资源,千万不要指望 finalize 去承担资源释放的主要职责,最多让 finalize 作为最后的“守门员”,况且它已经暴露了如此多的问题。这也是为什么我推荐, **资源用完即显式释放,或者利用资源池来尽量重用** 。 +从另一个角度,我们要确保回收资源就是因为资源都是有限的,垃圾收集时间的不可预测,可能会极大加剧资源占用。这意味着对于消耗非常高频的资源,千万不要指望 finalize 去承担资源释放的主要职责,最多让 finalize 作为最后的“守门员”,况且它已经暴露了如此多的问题。这也是为什么我推荐,**资源用完即显式释放,或者利用资源池来尽量重用** 。 finalize 还会掩盖资源回收时的出错信息,我们看下面一段 JDK 的源代码,截取自 java.lang.ref.Finalizer diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25404\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25404\350\256\262.md" index a547ccc04..bd5c6fa9b 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25404\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25404\350\256\262.md" @@ -101,7 +101,7 @@ try { 0.403: [GC (Allocation Failure) 0.871: [SoftReference, 0 refs, 0.0000393 secs]0.871: [WeakReference, 8 refs, 0.0000138 secs]0.871: [FinalReference, 4 refs, 0.0000094 secs]0.871: [PhantomReference, 0 refs, 0 refs, 0.0000085 secs]0.871: [JNI Weak Reference, 0.0000071 secs][PSYoungGen: 76272K->10720K(141824K)] 128286K->128422K(316928K), 0.4683919 secs] [Times: user=1.17 sys=0.03, real=0.47 secs] ``` -**注意:JDK 9 对 JVM 和垃圾收集日志进行了广泛的重构** ,类似 PrintGCTimeStamps 和 PrintReferenceGC 已经不再存在,我在专栏后面的垃圾收集主题里会更加系统的阐述。 +**注意:JDK 9 对 JVM 和垃圾收集日志进行了广泛的重构**,类似 PrintGCTimeStamps 和 PrintReferenceGC 已经不再存在,我在专栏后面的垃圾收集主题里会更加系统的阐述。 ### 5. Reachability Fence diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25405\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25405\350\256\262.md" index 3e9fa2ed9..93dfb3fe3 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25405\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25405\350\256\262.md" @@ -115,7 +115,7 @@ String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相 -XX:StringTableSize=N ``` -Intern 是一种 **显式地排重机制** ,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。 +Intern 是一种 **显式地排重机制**,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。 幸好在 Oracle JDK 8u20 之后,推出了一个新的特性,也就是 G1 GC 下的字符串排重。它是通过将相同数据的字符串指向同一份数据来做到的,是 JVM 底层的改变,并不需要 Java 类库做什么修改。 @@ -152,7 +152,7 @@ Intern 是一种 **显式地排重机制** ,但是它也有一定的副作用 当然,在极端情况下,字符串也出现了一些能力退化,比如最大字符串的大小。你可以思考下,原来 char 数组的实现,字符串的最大长度就是数组本身的长度限制,但是替换成 byte 数组,同样数组长度下,存储能力是退化了一倍的!还好这是存在于理论中的极限,还没有发现现实应用受此影响。 -在通用的性能测试和产品实验中,我们能非常明显地看到紧凑字符串带来的优势, **即更小的内存占用、更快的操作速度** 。 +在通用的性能测试和产品实验中,我们能非常明显地看到紧凑字符串带来的优势,**即更小的内存占用、更快的操作速度** 。 今天我从 String、StringBuffer 和 StringBuilder 的主要设计和实现特点开始,分析了字符串缓存的 intern 机制、非代码侵入性的虚拟机层面排重、Java9 中紧凑字符的改进,并且初步接触了 JVM 的底层优化机制 intrinsic。从实践的角度,不管是 Compact Strings 还是底层 intrinsic 优化,都说明了使用 Java 基础类库的优势,它们往往能够得到最大程度、最高质量的优化,而且只要升级 JDK 版本,就能零成本地享受这些益处。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25407\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25407\350\256\262.md" index 5c815cc3e..f20b323b5 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25407\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25407\350\256\262.md" @@ -6,9 +6,9 @@ Java 虽然号称是面向对象的语言,但是原始数据类型仍然是重 ## 典型回答 -int 是我们常说的整形数字,是 Java 的 8 个原始数据类型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。 **Java 语言虽然号称一切都是对象,但原始数据类型是例外。** Integer 是 int 对应的包装类,它有一个 int 类型的字段存储数据,并且提供了基本操作,比如数学运算、int 和字符串之间转换等。在 Java 5 中,引入了自动装箱和自动拆箱功能(boxing/unboxing),Java 可以根据上下文,自动进行转换,极大地简化了相关编程。 +int 是我们常说的整形数字,是 Java 的 8 个原始数据类型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。**Java 语言虽然号称一切都是对象,但原始数据类型是例外。** Integer 是 int 对应的包装类,它有一个 int 类型的字段存储数据,并且提供了基本操作,比如数学运算、int 和字符串之间转换等。在 Java 5 中,引入了自动装箱和自动拆箱功能(boxing/unboxing),Java 可以根据上下文,自动进行转换,极大地简化了相关编程。 -关于 Integer 的值缓存,这涉及 Java 5 中另一个改进。构建 Integer 对象的传统方式是直接调用构造器,直接 new 一个对象。但是根据实践,我们发现大部分数据操作都是集中在有限的、较小的数值范围,因而,在 Java 5 中新增了静态工厂方法 valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照 Javadoc, **这个值默认缓存是 -128 到 127 之间。** +关于 Integer 的值缓存,这涉及 Java 5 中另一个改进。构建 Integer 对象的传统方式是直接调用构造器,直接 new 一个对象。但是根据实践,我们发现大部分数据操作都是集中在有限的、较小的数值范围,因而,在 Java 5 中新增了静态工厂方法 valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照 Javadoc,**这个值默认缓存是 -128 到 127 之间。** ## 考点分析 @@ -58,7 +58,7 @@ java/lang/Integer.intValue:()I 自动装箱 / 自动拆箱似乎很酷,在编程实践中,有什么需要注意的吗? -原则上, **建议避免无意中的装箱、拆箱行为** ,尤其是在性能敏感的场合,创建 10 万个 Java 对象和 10 万个整数的开销可不是一个数量级的,不管是内存使用还是处理速度,光是对象头的空间占用就已经是数量级的差距了。 +原则上,**建议避免无意中的装箱、拆箱行为**,尤其是在性能敏感的场合,创建 10 万个 Java 对象和 10 万个整数的开销可不是一个数量级的,不管是内存使用还是处理速度,光是对象头的空间占用就已经是数量级的差距了。 我们其实可以把这个观点扩展开,使用原始数据类型、数组甚至本地代码实现等,在性能极度敏感的场景往往具有比较大的优势,用其替换掉包装类、动态数组(如 ArrayList)等可以作为性能优化的备选项。一些追求极致性能的产品或者类库,会极力避免创建过多对象。当然,在大多数产品代码里,并没有必要这么做,还是以开发效率优先。以我们经常会使用到的计数器实现为例,下面是一个常见的线程安全计数器实现。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25408\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25408\350\256\262.md" index 4efa8a6bf..f1a27cff2 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25408\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25408\350\256\262.md" @@ -8,11 +8,11 @@ 这三者都是实现集合框架中的 List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同。 -Vector 是 Java 早期提供的 **线程安全的动态数组** ,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。 +Vector 是 Java 早期提供的 **线程安全的动态数组**,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。 ArrayList 是应用更加广泛的 **动态数组** 实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。 -LinkedList 顾名思义是 Java 提供的 **双向链表** ,所以它不需要像上面两种那样调整容量,它也不是线程安全的。 +LinkedList 顾名思义是 Java 提供的 **双向链表**,所以它不需要像上面两种那样调整容量,它也不是线程安全的。 ## 考点分析 @@ -28,7 +28,7 @@ LinkedList 顾名思义是 Java 提供的 **双向链表** ,所以它不需要 考察 Java 集合框架,我觉得有很多方面需要掌握: - Java 集合框架的设计结构,至少要有一个整体印象。 -- Java 提供的主要容器(集合和 Map)类型,了解或掌握对应的 **数据结构、算法** ,思考具体技术选择。 +- Java 提供的主要容器(集合和 Map)类型,了解或掌握对应的 **数据结构、算法**,思考具体技术选择。 - 将问题扩展到性能、并发等领域。 - 集合框架的演进与发展。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25409\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25409\350\256\262.md" index 32eec81ab..acfde4e27 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25409\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25409\350\256\262.md" @@ -10,7 +10,7 @@ Hashtable、HashMap、TreeMap 都是最常见的一些 Map 实现,是以 **键 Hashtable 是早期 Java 类库提供的一个[哈希表](https://zh.wikipedia.org/wiki/%E5%93%88%E5%B8%8C%E8%A1%A8)实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用。 -HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以 **它是绝大部分利用键值对存取场景的首选** ,比如,实现一个用户 ID 和用户信息对应的运行时存储结构。 +HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以 **它是绝大部分利用键值对存取场景的首选**,比如,实现一个用户 ID 和用户信息对应的运行时存储结构。 TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。 @@ -42,7 +42,7 @@ Hashtable 比较特别,作为类似 Vector、Stack 的早期集合相关类型 HashMap 等其他 Map 实现则是都扩展了 AbstractMap,里面包含了通用方法抽象。不同 Map 的用途,从类图结构就能体现出来,设计目的已经体现在不同接口上。 -大部分使用 Map 的场景,通常就是放入、访问或者删除,而对顺序没有特别要求,HashMap 在这种情况下基本是最好的选择。 **HashMap的性能表现非常依赖于哈希码的有效性,请务必掌握 hashCode 和 equals 的一些基本约定** ,比如: +大部分使用 Map 的场景,通常就是放入、访问或者删除,而对顺序没有特别要求,HashMap 在这种情况下基本是最好的选择。**HashMap的性能表现非常依赖于哈希码的有效性,请务必掌握 hashCode 和 equals 的一些基本约定**,比如: - equals 相等,hashCode 一定要相等。 - 重写了 hashCode 也要重写 equals。 @@ -181,7 +181,7 @@ if (++size > threshold) i = (n - 1) & hash ``` -仔细观察哈希值的源头,我们会发现,它并不是 key 本身的 hashCode,而是来自于 HashMap 内部的另外一个 hash 方法。注意,为什么这里需要将高位数据移位到低位进行异或运算呢? **这是因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。** +仔细观察哈希值的源头,我们会发现,它并不是 key 本身的 hashCode,而是来自于 HashMap 内部的另外一个 hash 方法。注意,为什么这里需要将高位数据移位到低位进行异或运算呢?**这是因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。** ```java static final int hash(Object kye) { @@ -269,7 +269,7 @@ final void treeifyBin(Node[] tab, int hash) { - 如果容量小于 MIN_TREEIFY_CAPACITY,只会进行简单的扩容。 - 如果容量大于 MIN_TREEIFY_CAPACITY ,则会进行树化改造。 -那么,为什么 HashMap 要树化呢? **本质上这是个安全问题。** 因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,我们知道链表查询是线性的,会严重影响存取的性能。 +那么,为什么 HashMap 要树化呢?**本质上这是个安全问题。** 因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,我们知道链表查询是线性的,会严重影响存取的性能。 而在现实世界,构造哈希冲突的数据并不是非常复杂的事情,恶意代码就可以利用这些数据大量与服务器端交互,导致服务器端 CPU 大量占用,这就构成了哈希碰撞拒绝服务攻击,国内一线互联网公司就发生过类似攻击事件。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25410\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25410\350\256\262.md" index 942f00e56..9ad5dd80d 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25410\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25410\350\256\262.md" @@ -58,7 +58,7 @@ private static class SynchronizedMap 我们再来看看 ConcurrentHashMap 是如何设计实现的,为什么它能大大提高并发效率。 -首先,我这里强调, **ConcurrentHashMap 的设计实现其实一直在演化** ,比如在 Java 8 中就发生了非常大的变化(Java 7 其实也有不少更新),所以,我这里将比较分析结构、实现机制等方面,对比不同版本的主要区别。 +首先,我这里强调,**ConcurrentHashMap 的设计实现其实一直在演化**,比如在 Java 8 中就发生了非常大的变化(Java 7 其实也有不少更新),所以,我这里将比较分析结构、实现机制等方面,对比不同版本的主要区别。 早期 ConcurrentHashMap,其实现是基于: @@ -153,7 +153,7 @@ final V put(K key, int hash, V value, boolean onlyIfAbsent) { 所以,ConcurrentHashMap 的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重试次数 2),来试图获得可靠值。如果没有监控到发生变化(通过对比 Segment.modCount),就直接返回,否则获取锁进行操作。 -下面我来对比一下, **在 Java 8 和之后的版本中,ConcurrentHashMap 发生了哪些变化呢?** +下面我来对比一下,**在 Java 8 和之后的版本中,ConcurrentHashMap 发生了哪些变化呢?** - 总体结构上,它的内部存储变得和我在专栏上一讲介绍的 HashMap 结构非常相似,同样是大的桶(bucket)数组,然后内部也是一个个所谓的链表结构(bin),同步的粒度要更细致一些。 - 其内部仍然有 Segment 定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构上的用处。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25411\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25411\350\256\262.md" index c2e691eaa..8476c5c78 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25411\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25411\350\256\262.md" @@ -214,7 +214,7 @@ public class NIOServer extends Thread { 这个非常精简的样例掀开了 NIO 多路复用的面纱,我们可以分析下主要步骤和元素: - 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。 -- 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。 **注意** ,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。 +- 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。**注意**,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。 - Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。 - 在 sayHelloWorld 方法中,通过 SocketChannel 和 Buffer 进行数据操作,在本例中是发送了一段字符串。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25412\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25412\350\256\262.md" index 68a6f6ae7..e083b9fbc 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25412\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25412\350\256\262.md" @@ -44,7 +44,7 @@ public static void copyFileByChannel(File source, File dest) throws 当然,Java 标准类库本身已经提供了几种 Files.copy 的实现。 -对于 Copy 的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From 的方式 **可能更快** ,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。 +对于 Copy 的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From 的方式 **可能更快**,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。 ## 考点分析 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25413\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25413\350\256\262.md" index c7f520059..9b1e2b97a 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25413\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25413\350\256\262.md" @@ -47,7 +47,7 @@ Java 产品代码中。 Java 8 增加了函数式编程的支持,所以又增加了一类定义,即所谓 functional interface,简单说就是只有一个抽象方法的接口,通常建议使用 @FunctionalInterface Annotation 来标记。Lambda 表达式本身可以看作是一类 functional interface,某种程度上这和面向对象可以算是两码事。我们熟知的 Runnable、Callable 之类,都是 functional interface,这里不再多介绍了,有兴趣你可以参考:[https://www.oreilly.com/learning/java-8-functional-interfaces](https://www.oreilly.com/learning/java-8-functional-interfaces) 。 -还有一点可能让人感到意外,严格说, **Java 8 以后,接口也是可以有方法实现的!** +还有一点可能让人感到意外,严格说,**Java 8 以后,接口也是可以有方法实现的!** 从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,甚至可以定义 private default method。Default method 提供了一种二进制兼容的扩展已有接口的办法。比如,我们熟知的 java.util.Collection,它是 collection 体系的 root interface,在 Java 8 中添加了一系列 default method,主要是增加 Lambda、Stream 相关的功能。我在专栏前面提到的类似 Collections 之类的工具类,很多方法都适合作为 default method 实现在基础接口里面。 @@ -69,7 +69,7 @@ public interface Collection extends Iterable { 谈到面向对象,很多人就会想起设计模式,那些是非常经典的问题和设计方法的总结。我今天来夯实一下基础,先来聊聊面向对象设计的基本方面。 -我们一定要清楚面向对象的基本要素:封装、继承、多态。 **封装** 的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。我们在日常开发中,因为无意间暴露了细节导致的难缠 bug 太多了,比如在多线程环境暴露内部状态,导致的并发修改问题。从另外一个角度看,封装这种隐藏,也提供了简化的界面,避免太多无意义的细节浪费调用者的精力。 **继承** 是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果。 **多态** ,你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的,为了更好说明,请参考下面的样例代码: +我们一定要清楚面向对象的基本要素:封装、继承、多态。**封装** 的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。我们在日常开发中,因为无意间暴露了细节导致的难缠 bug 太多了,比如在多线程环境暴露内部状态,导致的并发修改问题。从另外一个角度看,封装这种隐藏,也提供了简化的界面,避免太多无意义的细节浪费调用者的精力。**继承** 是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果。**多态**,你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的,为了更好说明,请参考下面的样例代码: ```java public int doSomething() { diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25415\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25415\350\256\262.md" index bc714c7c8..e1f256132 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25415\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25415\350\256\262.md" @@ -45,9 +45,9 @@ synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchr 线程安全需要保证几个基本特性: -- **原子性** ,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。 -- **可见性** ,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。 -- **有序性** ,是保证线程内串行语义,避免指令重排等。 +- **原子性**,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。 +- **可见性**,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。 +- **有序性**,是保证线程内串行语义,避免指令重排等。 可能有点晦涩,那么我们看看下面的代码段,分析一下原子性需求体现在哪里。这个例子通过取两次数值然后进行对比,来模拟两次对共享状态的操作。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25417\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25417\350\256\262.md" index 53685bf0f..664ef230a 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25417\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25417\350\256\262.md" @@ -88,7 +88,7 @@ Thread 和 Object 的方法,听起来简单,但是实际应用中被证明 前面谈了不少理论,下面谈谈线程 API 使用,我会侧重于平时工作学习中,容易被忽略的一些方面。 -先来看看守护线程(Daemon Thread),有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置为守护线程,如果 JVM 发现只有守护线程存在时,将结束进程,具体可以参考下面代码段。 **注意,必须在线程启动之前设置。** +先来看看守护线程(Daemon Thread),有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置为守护线程,如果 JVM 发现只有守护线程存在时,将结束进程,具体可以参考下面代码段。**注意,必须在线程启动之前设置。** ```java Thread daemonThread = new Thread(); @@ -158,7 +158,7 @@ private void set(ThreadLocal key, Object value) { 结合[专栏第 4 讲](http://time.geekbang.org/column/article/6970)介绍的引用类型,我们会发现一个特别的地方,通常弱引用都会和引用队列配合清理机制使用,但是 ThreadLocal 是个例外,它并没有这么做。 -这意味着,废弃项目的回收 **依赖于显式地触发,否则就要等待线程结束** ,进而回收相应 ThreadLocalMap!这就是很多 OOM 的来源,所以通常都会建议,应用一定要自己负责 remove,并且不要和线程池配合,因为 worker 线程往往是不会退出的。 +这意味着,废弃项目的回收 **依赖于显式地触发,否则就要等待线程结束**,进而回收相应 ThreadLocalMap!这就是很多 OOM 的来源,所以通常都会建议,应用一定要自己负责 remove,并且不要和线程池配合,因为 worker 线程往往是不会退出的。 今天,我介绍了线程基础,分析了生命周期中的状态和各种方法之间的对应关系,这也有助于我们更好地理解 synchronized 和锁的影响,并介绍了一些需要注意的操作,希望对你有所帮助。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25418\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25418\350\256\262.md" index e4a785ef1..b62fb8768 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25418\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25418\350\256\262.md" @@ -146,7 +146,7 @@ implCloseSelectableChannel (); // 想获得 readLock 在 close 发生时, HttpClient-6-SelectorManager 线程持有 readLock/writeLock,试图获得 closeLock;与此同时,另一个 HttpClient-6-Worker-2 线程,持有 closeLock,试图获得 readLock,这就不可避免地进入了死锁。 -这里比较难懂的地方在于,closeLock 的持有状态(就是我标记为绿色的部分) **并没有在线程栈中显示出来** ,请参考我在下图中标记的部分。 +这里比较难懂的地方在于,closeLock 的持有状态(就是我标记为绿色的部分) **并没有在线程栈中显示出来**,请参考我在下图中标记的部分。 ![img](assets/b7961a84838b5429a8f59826b91ed724.png)更加具体来说,请查看[SocketChannelImpl](http://hg.openjdk.java.net/jdk/jdk/file/ce06058197a4/src/java.base/share/classes/sun/nio/ch/SocketChannelImpl.java)的 663 行,对比 implCloseSelectableChannel() 方法实现和[AbstractInterruptibleChannel.close()](http://hg.openjdk.java.net/jdk/jdk/file/ce06058197a4/src/java.base/share/classes/java/nio/channels/spi/AbstractInterruptibleChannel.java)在 109 行的代码,这里就不展示代码了。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25419\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25419\350\256\262.md" index 7057b5d61..f1f333bdc 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25419\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25419\350\256\262.md" @@ -128,7 +128,7 @@ class MyWorker implements Runnable { 注意,上面的代码,更侧重的是演示 Semaphore 的功能以及局限性,其实有很多线程编程中的反实践,比如使用了 sleep 来协调任务执行,而且使用轮询调用 availalePermits 来检测信号量获取情况,这都是很低效并且脆弱的,通常只是用在测试或者诊断场景。 -总的来说,我们可以看出 Semaphore 就是个 **计数器** , **其基本逻辑基于 acquire/release** ,并没有太复杂的同步逻辑。 +总的来说,我们可以看出 Semaphore 就是个 **计数器**,**其基本逻辑基于 acquire/release**,并没有太复杂的同步逻辑。 如果 Semaphore 的数值被初始化为 1,那么一个线程就可以通过 acquire 进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比如互斥锁是有持有者的,而对于 Semaphore 这种计数器结构,虽然有类似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。 @@ -136,7 +136,7 @@ class MyWorker implements Runnable { - CountDownLatch 是不可以重置的,所以无法重用;而 CyclicBarrier 则没有这种限制,可以重用。 - CountDownLatch 的基本操作组合是 countDown/await。调用 await 的线程阻塞等待 countDown 足够的次数,不管你是在一个线程还是多个线程里 countDown,只要次数足够即可。所以就像 Brain Goetz 说过的,CountDownLatch 操作的是事件。 -- CyclicBarrier 的基本操作组合,则就是 await,当所有的伙伴(parties)都调用了 await,才会继续进行任务,并自动进行重置。 **注意** ,正常情况下,CyclicBarrier 的重置都是自动发生的,如果我们调用 reset 方法,但还有线程在等待,就会导致等待线程被打扰,抛出 BrokenBarrierException 异常。CyclicBarrier 侧重点是线程,而不是调用事件,它的典型应用场景是用来等待并发线程结束。 +- CyclicBarrier 的基本操作组合,则就是 await,当所有的伙伴(parties)都调用了 await,才会继续进行任务,并自动进行重置。**注意**,正常情况下,CyclicBarrier 的重置都是自动发生的,如果我们调用 reset 方法,但还有线程在等待,就会导致等待线程被打扰,抛出 BrokenBarrierException 异常。CyclicBarrier 侧重点是线程,而不是调用事件,它的典型应用场景是用来等待并发线程结束。 如果用 CountDownLatch 去实现上面的排队场景,该怎么做呢?假设有 10 个人排队,我们将其分成 5 个人一批,通过 CountDownLatch 来协调批次,你可以试试下面的示例代码。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25421\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25421\350\256\262.md" index 36907d29a..089cef820 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25421\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25421\350\256\262.md" @@ -120,7 +120,7 @@ private static int workerCountOf(int c) { return c & COUNT_MASK; } private static int ctlOf(int rs, int wc) { return rs | wc; } ``` -为了让你能对线程生命周期有个更加清晰的印象,我这里画了一个简单的状态流转图,对线程池的可能状态和其内部方法之间进行了对应,如果有不理解的方法,请参考 Javadoc。 **注意** ,实际 Java 代码中并不存在所谓 Idle 状态,我添加它仅仅是便于理解。 +为了让你能对线程生命周期有个更加清晰的印象,我这里画了一个简单的状态流转图,对线程池的可能状态和其内部方法之间进行了对应,如果有不理解的方法,请参考 Javadoc。**注意**,实际 Java 代码中并不存在所谓 Idle 状态,我添加它仅仅是便于理解。 ![img](assets/c50ce5f2ff4ae723c6267185699ccda1.png) diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25422\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25422\350\256\262.md" index d6c05d7ef..2467bf0b7 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25422\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25422\350\256\262.md" @@ -121,7 +121,7 @@ CAS 也并不是没有副作用,试想,其常用的失败重试机制,隐 学习 AQS,如果上来就去看它的一系列方法(下图所示),很有可能把自己看晕,这种似懂非懂的状态也没有太大的实践意义。 -我建议的思路是,尽量简化一下,理解为什么需要 AQS,如何使用 AQS, **至少** 要做什么,再进一步结合 JDK 源代码中的实践,理解 AQS 的原理与应用。 +我建议的思路是,尽量简化一下,理解为什么需要 AQS,如何使用 AQS,**至少** 要做什么,再进一步结合 JDK 源代码中的实践,理解 AQS 的原理与应用。 [Doug Lea](https://en.wikipedia.org/wiki/Doug_Lea)曾经介绍过 AQS 的设计初衷。从原理上,一种同步结构往往是可以利用其他的结构实现的,例如我在专栏第 19 讲中提到过可以使用 Semaphore 实现互斥锁。但是,对某种同步结构的倾向,会导致复杂、晦涩的实现逻辑,所以,他选择了将基础的同步相关操作抽象在 AbstractQueuedSynchronizer 中,利用 AQS 为我们构建同步结构提供了范本。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25423\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25423\350\256\262.md" index 09d0eb426..bd344e8ea 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25423\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25423\350\256\262.md" @@ -133,7 +133,7 @@ java -Djava.system.class.loader=com.yourcorp.YourClassLoader HelloWorld java --patch-module java.base=your_patch yourApp ``` -- 扩展类加载器被重命名为平台类加载器(Platform Class-Loader),而且 extension 机制则被移除。也就意味着,如果我们指定 java.ext.dirs 环境变量,或者 lib/ext 目录存在,JVM 将直接返回 **错误** !建议解决办法就是将其放入 classpath 里。 +- 扩展类加载器被重命名为平台类加载器(Platform Class-Loader),而且 extension 机制则被移除。也就意味着,如果我们指定 java.ext.dirs 环境变量,或者 lib/ext 目录存在,JVM 将直接返回 **错误**!建议解决办法就是将其放入 classpath 里。 - 部分不需要 AllPermission 的 Java 基础模块,被降级到平台类加载器中,相应的权限也被更精细粒度地限制起来。 - rt.jar 和 tools.jar 同样是被移除了!JDK 的核心类库以及相关资源,被存储在 jimage 文件中,并通过新的 JRT 文件系统访问,而不是原有的 JAR 文件系统。虽然看起来很惊人,但幸好对于大部分软件的兼容性影响,其实是有限的,更直接地影响是 IDE 等软件,通常只要升级到新版本就可以了。 - 增加了 Layer 的抽象, JVM 启动默认创建 BootLayer,开发者也可以自己去定义和实例化 Layer,可以更加方便的实现类似容器一般的逻辑抽象。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25425\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25425\350\256\262.md" index 614439dcf..267064c3a 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25425\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25425\350\256\262.md" @@ -8,25 +8,25 @@ 通常可以把 JVM 内存区域分为下面几个方面,其中,有的区域是以线程为单位,而有的区域则是整个 JVM 进程唯一的。 -首先, **程序计数器** (PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。 +首先,**程序计数器** (PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。 -第二, **Java 虚拟机栈** (Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。 +第二,**Java 虚拟机栈** (Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。 前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈。 栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。 -第三, **堆** (Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。 +第三,**堆** (Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。 理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。 -第四, **方法区** (Method Area)。这也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。 +第四,**方法区** (Method Area)。这也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。 由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增加了元数据区(Metaspace)。 -第五, **运行时常量池** (Run-Time Constant Pool),这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。 +第五,**运行时常量池** (Run-Time Constant Pool),这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。 -第六, **本地方法栈** (Native Method Stack)。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。 +第六,**本地方法栈** (Native Method Stack)。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。 ## 考点分析 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25426\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25426\350\256\262.md" index 00353216d..6083b278e 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25426\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25426\350\256\262.md" @@ -167,7 +167,7 @@ 可见,不仅总线程数大大降低(25 → 13),而且 GC 设施本身的内存开销就少了非常多。据我所知,AWS Lambda 中 Java 运行时就是使用的 Serial GC,可以大大降低单个 function 的启动和运行开销。 - Compiler 部分,就是 JIT 的开销,显然关闭 TieredCompilation 会降低内存使用。 -- 其他一些部分占比都非常低,通常也不会出现内存使用问题,请参考[官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr022.html#BABCBGFA)。唯一的例外就是 Internal(JDK 11 以后在 Other 部分)部分,其统计信息 **包含着 Direct Buffer 的直接内存** ,这其实是堆外内存中比较敏感的部分,很多堆外内存 OOM 就发生在这里,请参考专栏第 12 讲的处理步骤。原则上 Direct Buffer 是不推荐频繁创建或销毁的,如果你怀疑直接内存区域有问题,通常可以通过类似 instrument 构造函数等手段,排查可能的问题。 +- 其他一些部分占比都非常低,通常也不会出现内存使用问题,请参考[官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr022.html#BABCBGFA)。唯一的例外就是 Internal(JDK 11 以后在 Other 部分)部分,其统计信息 **包含着 Direct Buffer 的直接内存**,这其实是堆外内存中比较敏感的部分,很多堆外内存 OOM 就发生在这里,请参考专栏第 12 讲的处理步骤。原则上 Direct Buffer 是不推荐频繁创建或销毁的,如果你怀疑直接内存区域有问题,通常可以通过类似 instrument 构造函数等手段,排查可能的问题。 JVM 内部结构就介绍到这里,主要目的是为了加深理解,很多方面只有在定制或调优 JVM 运行时才能真正涉及,随着微服务和 Serverless 等技术的兴起,JDK 确实存在着为新特征的工作负载进行定制的需求。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25428\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25428\350\256\262.md" index 94764d6d0..e8984c8cb 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25428\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25428\350\256\262.md" @@ -114,7 +114,7 @@ G1 的类型卸载有什么改进吗?很多资料中都谈到,G1 只有在 前面介绍了 G1 的内部机制,并且穿插了部分调优建议,下面从整体上给出一些调优的建议。 -首先, **建议尽量升级到较新的 JDK 版本** ,从上面介绍的改进就可以看到,很多人们常常讨论的问题,其实升级 JDK 就可以解决了。 +首先,**建议尽量升级到较新的 JDK 版本**,从上面介绍的改进就可以看到,很多人们常常讨论的问题,其实升级 JDK 就可以解决了。 第二,掌握 GC 调优信息收集途径。掌握尽量全面、详细、准确的信息,是各种调优的基础,不仅仅是 GC 调优。我们来看看打开 GC 日志,这似乎是很简单的事情,可是你确定真的掌握了吗? @@ -145,7 +145,7 @@ G1 的类型卸载有什么改进吗?很多资料中都谈到,G1 只有在 -XX:+ParallelRefProcEnabled ``` -需要注意的一点是,JDK 9 中 JVM 和 GC 日志机构进行了重构,其实我前面提到的 **PrintGCDetails 已经被标记为废弃** ,而 **PrintGCDateStamps 已经被移除** ,指定它会导致 JVM 无法启动。可以使用下面的命令查询新的配置参数。 +需要注意的一点是,JDK 9 中 JVM 和 GC 日志机构进行了重构,其实我前面提到的 **PrintGCDetails 已经被标记为废弃**,而 **PrintGCDateStamps 已经被移除**,指定它会导致 JVM 无法启动。可以使用下面的命令查询新的配置参数。 ```properties java -Xlog:help diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25429\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25429\350\256\262.md" index 007bdab34..cd4142739 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25429\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25429\350\256\262.md" @@ -74,12 +74,12 @@ JMM 内部的实现通常是依赖于所谓的内存屏障,通过禁止某些 对于一个 volatile 变量: -- 对该变量的写操作 **之后** ,编译器会插入一个 **写屏障** 。 -- 对该变量的读操作 **之前** ,编译器会插入一个 **读屏障** 。 +- 对该变量的写操作 **之后**,编译器会插入一个 **写屏障** 。 +- 对该变量的读操作 **之前**,编译器会插入一个 **读屏障** 。 内存屏障能够在类似变量读、写操作之后,保证其他线程对 volatile 变量的修改对当前线程可见,或者本地修改对其他线程提供可见性。换句话说,线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值。 -如果你对更多内存屏障的细节感兴趣,或者想了解不同体系结构的处理器模型,建议参考 JSR-133[相关文档](http://gee.cs.oswego.edu/dl/jmm/cookbook.html),我个人认为这些都是和特定硬件相关的,内存屏障之类只是实现 JMM 规范的技术手段,并不是规范的要求。 **从应用开发者的角度,JMM 提供的可见性,体现在类似 volatile 上,具体行为是什么样呢?** +如果你对更多内存屏障的细节感兴趣,或者想了解不同体系结构的处理器模型,建议参考 JSR-133[相关文档](http://gee.cs.oswego.edu/dl/jmm/cookbook.html),我个人认为这些都是和特定硬件相关的,内存屏障之类只是实现 JMM 规范的技术手段,并不是规范的要求。**从应用开发者的角度,JMM 提供的可见性,体现在类似 volatile 上,具体行为是什么样呢?** 我这里循序渐进的举两个例子。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25430\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25430\350\256\262.md" index cdd994c24..e99d93a2a 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25430\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25430\350\256\262.md" @@ -30,7 +30,7 @@ ## 知识扩展 -首先,我们先来搞清楚 Java 在容器环境的局限性来源, **Docker 到底有什么特别** ? +首先,我们先来搞清楚 Java 在容器环境的局限性来源,**Docker 到底有什么特别**? 虽然看起来 Docker 之类容器和虚拟机非常相似,例如,它也有自己的 shell,能独立安装软件包,运行时与其他容器互不干扰。但是,如果深入分析你会发现,Docker 并不是一种完全的 **虚拟化** 技术,而更是一种轻量级的 **隔离** 技术。![img](assets/a069a294d32d7778f3410192221358fb.png) @@ -44,7 +44,7 @@ Docker 仅在类似 Linux 内核之上实现了有限的隔离和虚拟化,并 第一,容器环境对于计算资源的管理方式是全新的,CGroup 作为相对比较新的技术,历史版本的 Java 显然并不能自然地理解相应的资源限制。 -第二,namespace 对于容器内的应用细节增加了一些微妙的差异,比如 jcmd、jstack 等工具会依赖于“/proc//”下面提供的部分信息,但是 Docker 的设计改变了这部分信息的原有结构,我们需要对原有工具进行[修改](https://bugs.openjdk.java.net/browse/JDK-8179498)以适应这种变化。 **从 JVM 运行机制的角度,为什么这些“沟通障碍”会导致 OOM 等问题呢?** +第二,namespace 对于容器内的应用细节增加了一些微妙的差异,比如 jcmd、jstack 等工具会依赖于“/proc//”下面提供的部分信息,但是 Docker 的设计改变了这部分信息的原有结构,我们需要对原有工具进行[修改](https://bugs.openjdk.java.net/browse/JDK-8179498)以适应这种变化。**从 JVM 运行机制的角度,为什么这些“沟通障碍”会导致 OOM 等问题呢?** 你可以思考一下,这个问题实际是反映了 JVM 如何根据系统资源(内存、CPU 等)情况,在启动时设置默认参数。 @@ -57,7 +57,7 @@ Docker 仅在类似 Linux 内核之上实现了有限的隔离和虚拟化,并 更加严重的是,JVM 的一些原有诊断或备用机制也会受到影响。为保证服务的可用性,一种常见的选择是依赖“-XX:OnOutOfMemoryError”功能,通过调用处理脚本的形式来做一些补救措施,比如自动重启服务等。但是,这种机制是基于 fork 实现的,当 Java 进程已经过度提交内存时,fork 新的进程往往已经不可能正常运行了。 -根据前面的总结,似乎问题非常棘手,那我们在实践中, **如何解决这些问题呢?** 首先,如果你能够 **升级到最新的 JDK 版本** ,这个问题就迎刃而解了。 +根据前面的总结,似乎问题非常棘手,那我们在实践中,**如何解决这些问题呢?** 首先,如果你能够 **升级到最新的 JDK 版本**,这个问题就迎刃而解了。 - 针对这种情况,JDK 9 中引入了一些实验性的参数,以方便 Docker 和 Java“沟通”,例如针对内存限制,可以使用下面的参数设置: diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25433\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25433\350\256\262.md" index caf963f02..be9b3cd06 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25433\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25433\350\256\262.md" @@ -90,7 +90,7 @@ vmstat -1 -10 讲到这里,如果你对系统性能非常感兴趣,我建议参考[Brendan Gregg](http://www.brendangregg.com/linuxperf.html)提供的完整图谱,我所介绍的只能算是九牛一毛。但我还是建议尽量结合实际需求,免得迷失在其中。![img](assets/93aa8c4516fd2266472ca4eab1b0cc40.png) -对于 **JVM 层面的性能分析** ,我们已经介绍过非常多了: +对于 **JVM 层面的性能分析**,我们已经介绍过非常多了: - 利用 JMC、JConsole 等工具进行运行时监控。 - 利用各种工具,在运行时进行堆转储分析,或者获取各种角度的统计数据(如[jstat](https://docs.oracle.com/javase/7/docs/technotes/tools/share/jstat.html) -gcutil 分析 GC、内存分带等)。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25434\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25434\350\256\262.md" index 93689b42e..3742169ad 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25434\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25434\350\256\262.md" @@ -53,7 +53,7 @@ public int lambdaMaxInteger() { 性能往往是特定情景下的评价,泛泛地说性能“好”或者“快”,往往是具有误导性的。通过引入基准测试,我们可以定义性能对比的明确条件、具体的指标,进而保证得到 **定量的、可重复的** 对比数据,这是工程中的实际需要。 -不同的基准测试其具体内容和范围也存在很大的不同。如果是专业的性能工程师,更加熟悉的可能是类似[SPEC](https://www.spec.org/)提供的工业标准的系统级测试;而对于大多数 Java 开发者,更熟悉的则是范围相对较小、关注点更加细节的微基准测试(Micro-Benchmark)。我在文章开头提的问题,就是典型的微基准测试,也是我今天的侧重点。 **什么时候需要开发微基准测试呢?** +不同的基准测试其具体内容和范围也存在很大的不同。如果是专业的性能工程师,更加熟悉的可能是类似[SPEC](https://www.spec.org/)提供的工业标准的系统级测试;而对于大多数 Java 开发者,更熟悉的则是范围相对较小、关注点更加细节的微基准测试(Micro-Benchmark)。我在文章开头提的问题,就是典型的微基准测试,也是我今天的侧重点。**什么时候需要开发微基准测试呢?** 我认为,当需要对一个大型软件的某小部分的性能进行评估时,就可以考虑微基准测试。换句话说,微基准测试大多是 API 级别的验证,或者与其他简单用例场景的对比,例如: @@ -102,7 +102,7 @@ mvn clean install java -jar target/benchmarks.jar ``` -更加具体的上手步骤,请参考相关[指南](http://www.baeldung.com/java-microbenchmark-harness)。JMH 处处透着浓浓的工程师味道,并没有纠结于完善的文档,而是提供了非常棒的[样例代码](http://hg.openjdk.java.net/code-tools/jmh/file/3769055ad883/jmh-samples/src/main/java/org/openjdk/jmh/samples),所以你需要习惯于直接从代码中学习。 **如何保证微基准测试的正确性,有哪些坑需要规避?** +更加具体的上手步骤,请参考相关[指南](http://www.baeldung.com/java-microbenchmark-harness)。JMH 处处透着浓浓的工程师味道,并没有纠结于完善的文档,而是提供了非常棒的[样例代码](http://hg.openjdk.java.net/code-tools/jmh/file/3769055ad883/jmh-samples/src/main/java/org/openjdk/jmh/samples),所以你需要习惯于直接从代码中学习。**如何保证微基准测试的正确性,有哪些坑需要规避?** 首先,构建微基准测试,需要从白盒层面理解代码,尤其是具体的性能开销,不管是 CPU 还是内存分配。这有两个方面的考虑,第一,需要保证我们写出的基准测试符合测试目的,确实验证的是我们要覆盖的功能点,这一讲的问题就是个典型例子;第二,通常对于微基准测试,我们通常希望代码片段确实是有限的,例如,执行时间如果需要很多毫秒(ms),甚至是秒级,那么这个有效性就要存疑了,也不便于诊断问题所在。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25435\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25435\350\256\262.md" index c8df9fa3c..b74630611 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25435\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25435\350\256\262.md" @@ -35,7 +35,7 @@ JVM 的即时编译器优化是指将热点代码以方法为单位转换成机 javac 优化与 JVM 内部优化也存在关联,毕竟它负责了字节码的生成。例如,Java 9 中的字符串拼接,会被 javac 替换成对 StringConcatFactory 的调用,进而为 JVM 进行字符串拼接优化提供了统一的入口。在实际场景中,还可以通过不同的[策略](https://openjdk.java.net/jeps/280)选项来干预这个过程。 -今天我要讲的重点是 **JVM 运行时的优化** ,在通常情况下,编译器和解释器是共同起作用的,具体流程可以参考下面的示意图。![img](assets/5c095075dcda0f39f0e7395ab9636378.png) +今天我要讲的重点是 **JVM 运行时的优化**,在通常情况下,编译器和解释器是共同起作用的,具体流程可以参考下面的示意图。![img](assets/5c095075dcda0f39f0e7395ab9636378.png) JVM 会根据统计信息,动态决定什么方法被编译,什么方法解释执行,即使是已经编译过的代码,也可能在不同的运行阶段不再是热点,JVM 有必要将这种代码从 Code Cache 中移除出去,毕竟其大小是有限的。 @@ -46,7 +46,7 @@ JVM 会根据统计信息,动态决定什么方法被编译,什么方法解 这么做的理由有很多,例如,不同体系结构的 CPU 在指令等层面存在着差异,定制才能充分发挥出硬件的能力。我们日常使用的典型字符串操作、数组拷贝等基础方法,Hotspot 都提供了内建实现。 -而 **即时编译器(JIT)** ,则是更多优化工作的承担者。JIT 对 Java 编译的基本单元是整个方法,通过对方法调用的计数统计,甄别出热点方法,编译为本地代码。另外一个优化场景,则是最针对所谓热点循环代码,利用通常说的栈上替换技术(OSR,On-Stack Replacement,更加细节请参考[R 大的文章]()),如果方法本身的调用频度还不够编译标准,但是内部有大的循环之类,则还是会有进一步优化的价值。 +而 **即时编译器(JIT)**,则是更多优化工作的承担者。JIT 对 Java 编译的基本单元是整个方法,通过对方法调用的计数统计,甄别出热点方法,编译为本地代码。另外一个优化场景,则是最针对所谓热点循环代码,利用通常说的栈上替换技术(OSR,On-Stack Replacement,更加细节请参考[R 大的文章]()),如果方法本身的调用频度还不够编译标准,但是内部有大的循环之类,则还是会有进一步优化的价值。 从理论上来看,JIT 可以看作就是基于两个计数器实现,方法计数器和回边计数器提供给 JVM 统计数据,以定位到热点代码。实际中的 JIT 机制要复杂得多,郑博士提到了[逃逸分析](https://en.wikipedia.org/wiki/Escape_analysis)、[循环展开](https://en.wikipedia.org/wiki/Loop_unrolling)、方法内联等,包括前面提到的 Intrinsic 等通用机制同样会在 JIT 阶段发生。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25438\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25438\350\256\262.md" index a9bd080fe..4a68b2e56 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25438\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25438\350\256\262.md" @@ -38,7 +38,7 @@ Netty 的设计强调了 “ **Separation Of Concerns** ”,通过精巧设计的事件机制,将业务逻辑和无关技术逻辑进行隔离,并通过各种方便的抽象,一定程度上填补了了基础平台和业务开发之间的鸿沟,更有利于在应用开发中普及业界的最佳实践。 -另外, **Netty > java.nio + java. net!** +另外,**Netty > java.nio + java. net!** 从 API 能力范围来看,Netty 完全是 Java NIO 框架的一个大大的超集,你可以参考 Netty 官方的模块划分。 @@ -66,7 +66,7 @@ Netty 的设计强调了 “ **Separation Of Concerns** ”,通过精巧设计 - [Channel](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/channel/Channel.java),作为一个基于 NIO 的扩展框架,Channel 和 Selector 等概念仍然是 Netty 的基础组件,但是针对应用开发具体需求,提供了相对易用的抽象。 - [EventLoop](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/channel/EventLoop.java),这是 Netty 处理事件的核心机制。例子中使用了 EventLoopGroup。我们在 NIO 中通常要做的几件事情,如注册感兴趣的事件、调度相应的 Handler 等,都是 EventLoop 负责。 - [ChannelFuture](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/channel/ChannelFuture.java),这是 Netty 实现异步 IO 的基础之一,保证了同一个 Channel 操作的调用顺序。Netty 扩展了 Java 标准的 Future,提供了针对自己场景的特有[Future](https://github.com/netty/netty/blob/eb7f751ba519cbcab47d640cd18757f09d077b55/common/src/main/java/io/netty/util/concurrent/Future.java)定义。 -- ChannelHandler,这是应用开发者 **放置业务逻辑的主要地方** ,也是我上面提到的“Separation Of Concerns”原则的体现。 +- ChannelHandler,这是应用开发者 **放置业务逻辑的主要地方**,也是我上面提到的“Separation Of Concerns”原则的体现。 - [ChannelPipeline](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/channel/ChannelPipeline.java),它是 ChannelHandler 链条的容器,每个 Channel 在创建后,自动被分配一个 ChannelPipeline。在上面的示例中,我们通过 ServerBootstrap 注册了 ChannelInitializer,并且实现了 initChannel 方法,而在该方法中则承担了向 ChannelPipleline 安装其他 Handler 的任务。 你可以参考下面的简化示意图,忽略 Inbound/OutBound Handler 的细节,理解这几个基本单元之间的操作流程和对应关系。 diff --git "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25439\350\256\262.md" "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25439\350\256\262.md" index c82332ca8..ba6c22bb2 100644 --- "a/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25439\350\256\262.md" +++ "b/docs/Java/Java \345\237\272\347\241\200 36 \350\256\262/\347\254\25439\350\256\262.md" @@ -33,7 +33,7 @@ Snowflake 的[官方版本](https://github.com/twitter/snowflake)是基于 Scala - Redis、Zookeeper、MongoDB 等中间件,也都有各种唯一 ID 解决方案。其中一些设计也可以算作是 Snowflake 方案的变种。例如,MongoDB 的[ObjectId](https://mongodb.github.io/node-mongodb-native/2.0/tutorials/objectid/)提供了一个 12 byte(96 位)的 ID 定义,其中 32 位用于记录以秒为单位的时间,机器 ID 则为 24 位,16 位用作进程 ID,24 位随机起始的计数序列。 - 国内的一些大厂开源了其自身的部分分布式 ID 实现,InfoQ 就曾经介绍过微信的[seqsvr](http://www.infoq.com/cn/articles/wechat-serial-number-generator-architecture),它采取了相对复杂的两层架构,并根据社交应用的数据特点进行了针对性设计,具体请参考相关[代码实现](https://github.com/nebula-im/seqsvr)。另外,[百度](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md.html)、美团等也都有开源或者分享了不同的分布式 ID 实现,都可以进行参考。 -关于第二个问题, **Snowflake 是否受冬令时切换影响?** +关于第二个问题,**Snowflake 是否受冬令时切换影响?** 我认为没有影响,你可以从 Snowflake 的具体算法实现寻找答案。我们知道 Snowflake 算法的 Java 实现,大都是依赖于 System.currentTimeMillis(),这个数值代表什么呢?从 Javadoc 可以看出,它是返回当前时间和 1970 年 1 月 1 号 UTC 时间相差的毫秒数,这个数值与夏 / 冬令时并没有关系,所以并不受其影响。 @@ -58,7 +58,7 @@ Snowflake 的[官方版本](https://github.com/twitter/snowflake)是基于 Scala 在具体的生产环境中,还有可能提出对 QPS 等方面的具体要求,尤其是在国内一线互联网公司的业务规模下,更是需要考虑峰值业务场景的数量级层次需求。 -第二, **主流方案的优缺点分析** 。 +第二,**主流方案的优缺点分析** 。 对于数据库自增方案,除了实现简单,它生成的 ID 还能够保证固定步长的递增,使用很方便。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25400\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25400\350\256\262.md" index cf1141cfc..2290ebeba 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25400\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25400\350\256\262.md" @@ -2,7 +2,7 @@ 你好,欢迎学习《Java 并发编程核心 78 讲》,我是讲师徐隆曦,硕士毕业于德国慕尼黑工业大学,现就职于滴滴出行,负责小桔车服驾驶安全平台开发。 -###### **扎实的理论基础,宝贵的并发实践经验** ![img](assets/CgoB5l3DgLOAN9TxAADOl2eK1YA757.png) +###### **扎实的理论基础,宝贵的并发实践经验**![img](assets/CgoB5l3DgLOAN9TxAADOl2eK1YA757.png) 工作期间,因为业务需要,我所开发和负责的场景大多数都是大流量和高并发的,其中有很多是对 Java 并发知识的实际应用。学习如逆旅,从小白成长为并发大神,困难重重,既然不能逃避,那么唯有改变对它的态度。 @@ -20,7 +20,7 @@ ![img](assets/CgoB5l3DgLOAEMv7AABnabGYURQ993.png) -- **并发已经逐渐成为基本技能** 流量稍大的系统,随着数据和用户量的不断增加,并发量轻松过万,如果不使用并发编程,那么性能很快就会成为瓶颈。而随着近年来服务器 CPU 性能和核心数的不断提高,又给并发编程带来了广阔的施展拳脚的空间。可谓是 **有需求,同时又有资源** **保障** ,兼具 **天时地利** 。 +- **并发已经逐渐成为基本技能** 流量稍大的系统,随着数据和用户量的不断增加,并发量轻松过万,如果不使用并发编程,那么性能很快就会成为瓶颈。而随着近年来服务器 CPU 性能和核心数的不断提高,又给并发编程带来了广阔的施展拳脚的空间。可谓是 **有需求,同时又有资源** **保障**,兼具 **天时地利** 。 - **并发几乎是** **Java** **面试必考的内容** @@ -48,7 +48,7 @@ - **Java** **编程是众多框架的原理和基础** 无论是 Spring、tomcat 中对线程池的应用、数据库中的乐观锁思想,还是 Log4j2 对阻塞队列的应用等,无不体现着并发编程的思想,并发编程应用广泛,各大框架都和并发编程有着千丝万缕的联系。 -并发编程就像是 **地基** ,掌握好以后,可以做到 **一通百通** 。 **不过,要想学好并发编程,却不是一件容易的事,你有没有以下的感受?** +并发编程就像是 **地基**,掌握好以后,可以做到 **一通百通** 。**不过,要想学好并发编程,却不是一件容易的事,你有没有以下的感受?** - **并发的知识太多、太杂了** diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25401\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25401\350\256\262.md" index bba993e7b..12e889f78 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25401\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25401\350\256\262.md" @@ -136,11 +136,11 @@ public void run() { } ``` -首先,启动线程需要调用 start() 方法,而 start() 方法最终还会调用 run() 方法,我们先来看看第一种方式中 run() 方法究竟是怎么实现的,可以看出 run() 方法的代码非常短小精悍,第 1 行代码 **if (target != null)** ,判断 target 是否等于 null,如果不等于 null,就执行第 2 行代码 target.run(),而 target 实际上就是一个 Runnable,即使用 Runnable 接口实现线程时传给Thread类的对象。 +首先,启动线程需要调用 start() 方法,而 start() 方法最终还会调用 run() 方法,我们先来看看第一种方式中 run() 方法究竟是怎么实现的,可以看出 run() 方法的代码非常短小精悍,第 1 行代码 **if (target != null)**,判断 target 是否等于 null,如果不等于 null,就执行第 2 行代码 target.run(),而 target 实际上就是一个 Runnable,即使用 Runnable 接口实现线程时传给Thread类的对象。 然后,我们来看第二种方式,也就是继承 Thread 方式,实际上,继承 Thread 类之后,会把上述的 run() 方法重写,重写后 run() 方法里直接就是所需要执行的任务,但它最终还是需要调用 thread.start() 方法来启动线程,而 start() 方法最终也会调用这个已经被重写的 run() 方法来执行它的任务,这时我们就可以彻底明白了,事实上创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。 -我们上面已经了解了两种创建线程方式本质上是一样的,它们的不同点仅仅在于 **实现线程运行内容的不同** ,那么运行内容来自于哪里呢? +我们上面已经了解了两种创建线程方式本质上是一样的,它们的不同点仅仅在于 **实现线程运行内容的不同**,那么运行内容来自于哪里呢? 运行内容主要来自于两个地方,要么来自于 target,要么来自于重写的 run() 方法,在此基础上我们进行拓展,可以这样描述:本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25418\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25418\350\256\262.md" index 8a35f645c..bc077ed95 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25418\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25418\350\256\262.md" @@ -74,7 +74,7 @@ if (isRunning(c) && workQueue.offer(command)) { } ``` -如果代码执行到这里,说明当前线程数大于或等于核心线程数或者 addWorker 失败了,那么就需要通过 if (isRunning(c) && workQueue.offer(command)) 检查线程池状态是否为 Running,如果线程池状态是 Running 就把任务放入任务队列中,也就是 **workQueue** .offer( **command** )。如果线程池已经不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略,代码如下所示: +如果代码执行到这里,说明当前线程数大于或等于核心线程数或者 addWorker 失败了,那么就需要通过 if (isRunning(c) && workQueue.offer(command)) 检查线程池状态是否为 Running,如果线程池状态是 Running 就把任务放入任务队列中,也就是 **workQueue**.offer( **command** )。如果线程池已经不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略,代码如下所示: ```java if (! isRunning(recheck) && remove(command)) diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25433\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25433\350\256\262.md" index ab90c49f0..2169fdcfe 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25433\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25433\350\256\262.md" @@ -37,11 +37,11 @@ public synchronized E get(int index) { 读写锁的思想是:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥),原因是由于读操作不会修改原有的数据,因此并发读并不会有安全问题;而写操作是危险的,所以当写操作发生时,不允许有读操作加入,也不允许第二个写线程加入。 -- **对读写锁规则的升级** CopyOnWriteArrayList 的思想比读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,更厉害的是, **写入也不会阻塞读取操作,也就是说你可以在写入的同时进行读取** ,只有写入和写入之间需要进行同步,也就是不允许多个写入同时发生,但是在写入发生时允许读取同时发生。这样一来,读操作的性能就会大幅度提升。 +- **对读写锁规则的升级** CopyOnWriteArrayList 的思想比读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,更厉害的是,**写入也不会阻塞读取操作,也就是说你可以在写入的同时进行读取**,只有写入和写入之间需要进行同步,也就是不允许多个写入同时发生,但是在写入发生时允许读取同时发生。这样一来,读操作的性能就会大幅度提升。 ### 特点 -- **CopyOnWrite的含义** 从 CopyOnWriteArrayList 的名字就能看出它是满足 CopyOnWrite 的 ArrayList,CopyOnWrite 的意思是说,当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器, **完成修改之后,再将原容器的引用指向新的容器** 。这样就完成了整个修改过程。 +- **CopyOnWrite的含义** 从 CopyOnWriteArrayList 的名字就能看出它是满足 CopyOnWrite 的 ArrayList,CopyOnWrite 的意思是说,当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器,**完成修改之后,再将原容器的引用指向新的容器** 。这样就完成了整个修改过程。 这样做的好处是,CopyOnWriteArrayList 利用了“不变性”原理,因为容器每次修改都是创建新副本,所以对于旧容器来说,其实是不可变的,也是线程安全的,无需进一步的同步操作。我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,也不会有修改。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25436\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25436\350\256\262.md" index c783fbb06..b5d02dc3f 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25436\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25436\350\256\262.md" @@ -6,7 +6,7 @@ BlockingQueue 接口的实现类都被放在了 J.U.C 包中,本课时将对 ### ArrayBlockingQueue -让我们先从最基础的 ArrayBlockingQueue 说起。ArrayBlockingQueue 是最典型的 **有界队列** ,其内部是用数组存储元素的,利用 ReentrantLock 实现线程安全。 +让我们先从最基础的 ArrayBlockingQueue 说起。ArrayBlockingQueue 是最典型的 **有界队列**,其内部是用数组存储元素的,利用 ReentrantLock 实现线程安全。 我们在创建它的时候就需要指定它的容量,之后也不可以再扩容了,在构造函数中我们同样可以指定是否是公平的,代码如下: diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25441\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25441\350\256\262.md" index 9b4b6d4e5..19c877f05 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25441\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25441\350\256\262.md" @@ -52,7 +52,7 @@ 比如用一个 AtomicInteger,然后每个线程都调用它的 incrementAndGet 方法。 -## 在利用了原子变量之后就无需加锁,我们可以使用它的 incrementAndGet 方法,这个操作底层由 CPU 指令保证原子性,所以即便是多个线程同时运行,也不会发生线程安全问题。 **原子类和** **volatile** **的使用场景** +## 在利用了原子变量之后就无需加锁,我们可以使用它的 incrementAndGet 方法,这个操作底层由 CPU 指令保证原子性,所以即便是多个线程同时运行,也不会发生线程安全问题。**原子类和** **volatile** **的使用场景** 那下面我们就来说一下原子类和 volatile 各自的使用场景。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25442\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25442\350\256\262.md" index a3db5f28d..6fd5f4bba 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25442\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25442\350\256\262.md" @@ -32,7 +32,7 @@ public class Lesson42 implements Runnable { 因为 value++ 不是一个原子操作,所以上面这段代码是线程不安全的(具体分析详见第 6 讲),所以代码的运行结果会小于 20000,例如会输出 14611 等各种数字。 -我们首先给出 **方法一** ,也就是用原子类来解决这个问题,代码如下所示: +我们首先给出 **方法一**,也就是用原子类来解决这个问题,代码如下所示: ```java public class Lesson42Atomic implements Runnable { @@ -58,7 +58,7 @@ public class Lesson42Atomic implements Runnable { 用原子类之后,我们的计数变量就不再是一个普通的 int 变量了,而是 AtomicInteger 类型的对象,并且自加操作也变成了 incrementAndGet 法。由于原子类可以确保每一次的自加操作都是具备原子性的,所以这段程序是线程安全的,所以以上程序的运行结果会始终等于 20000。 -下面我们给出 **方法二** ,我们用 synchronized 来解决这个问题,代码如下所示: +下面我们给出 **方法二**,我们用 synchronized 来解决这个问题,代码如下所示: ```java public class Lesson42Syn implements Runnable { diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25443\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25443\350\256\262.md" index aba708e63..11ac16679 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25443\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25443\350\256\262.md" @@ -4,13 +4,13 @@ ### Adder 的介绍 -我们要知道 Adder 和 Accumulator 都是 Java 8 引入的,是相对比较新的类。对于 Adder 而言,比如最典型的 LongAdder,我们在第 40 讲的时候已经讲解过了, **在高并发下 LongAdder 比 AtomicLong 效率更高** ,因为对于 AtomicLong 而言,它只适合用于低并发场景,否则在高并发的场景下,由于 CAS 的冲突概率大,会导致经常自旋,影响整体效率。 +我们要知道 Adder 和 Accumulator 都是 Java 8 引入的,是相对比较新的类。对于 Adder 而言,比如最典型的 LongAdder,我们在第 40 讲的时候已经讲解过了,**在高并发下 LongAdder 比 AtomicLong 效率更高**,因为对于 AtomicLong 而言,它只适合用于低并发场景,否则在高并发的场景下,由于 CAS 的冲突概率大,会导致经常自旋,影响整体效率。 而 LongAdder 引入了分段锁的概念,当竞争不激烈的时候,所有线程都是通过 CAS 对同一个 Base 变量进行修改,但是当竞争激烈的时候,LongAdder 会把不同线程对应到不同的 Cell 上进行修改,降低了冲突的概率,从而提高了并发性。 ### Accumulator 的介绍 -那么 Accumulator 又是做什么的呢?Accumulator 和 Adder 非常相似, **实际上 Accumulator 就是一个更通用版本的 Adder** ,比如 LongAccumulator 是 LongAdder 的功能增强版,因为 LongAdder 的 API 只有对数值的加减,而 LongAccumulator 提供了自定义的函数操作。 +那么 Accumulator 又是做什么的呢?Accumulator 和 Adder 非常相似,**实际上 Accumulator 就是一个更通用版本的 Adder**,比如 LongAccumulator 是 LongAdder 的功能增强版,因为 LongAdder 的 API 只有对数值的加减,而 LongAccumulator 提供了自定义的函数操作。 我这样讲解可能有些同学还是不太理解,那就让我们用一个非常直观的代码来举例说明一下,代码如下: @@ -85,7 +85,7 @@ LongAccumulator max = new LongAccumulator((x, y) -> Math.max(x, y), 0); 这时你可能会有一个疑问:在这里为什么不用 for 循环呢?比如说我们之前的例子,从 0 加到 9,我们直接写一个 for 循环不就可以了吗? -确实,用 for 循环也能满足需求,但是用 for 循环的话,它执行的时候是串行,它一定是按照 0+1+2+3+...+8+9 这样的顺序相加的,但是 LongAccumulator 的一大优势就是可以利用线程池来为它工作。 **一旦使用了线程池,那么多个线程之间是可以并行计算的,效率要比之前的串行高得多** 。这也是为什么刚才说它加的顺序是不固定的,因为我们并不能保证各个线程之间的执行顺序,所能保证的就是最终的结果是确定的。 +确实,用 for 循环也能满足需求,但是用 for 循环的话,它执行的时候是串行,它一定是按照 0+1+2+3+...+8+9 这样的顺序相加的,但是 LongAccumulator 的一大优势就是可以利用线程池来为它工作。**一旦使用了线程池,那么多个线程之间是可以并行计算的,效率要比之前的串行高得多** 。这也是为什么刚才说它加的顺序是不固定的,因为我们并不能保证各个线程之间的执行顺序,所能保证的就是最终的结果是确定的。 ### 适用场景 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25444\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25444\350\256\262.md" index e9dc3b523..f2d22df35 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25444\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25444\350\256\262.md" @@ -6,9 +6,9 @@ 在通常的业务开发中,ThreadLocal 有 **两种典型的** 使用场景。 -场景1,ThreadLocal 用作 **保存每个线程独享的对象** ,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。 +场景1,ThreadLocal 用作 **保存每个线程独享的对象**,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。 -场景2,ThreadLocal 用作 **每个线程内需要独立保存信息** ,以便 **供其他方法更方便地获取** 该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。 +场景2,ThreadLocal 用作 **每个线程内需要独立保存信息**,以便 **供其他方法更方便地获取** 该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。 ### 典型场景1 @@ -56,7 +56,7 @@ public class ThreadLocalDemo01 { 00:02 ``` -**2. 10 个线程都要用到 SimpleDateFormat** 假设我们的需求有了升级,不仅仅需要 2 个线程,而是需要 **10 个** ,也就是说,有 10 个线程同时对应 10 个 SimpleDateFormat 对象。我们就来看下面这种写法: +**2. 10 个线程都要用到 SimpleDateFormat** 假设我们的需求有了升级,不仅仅需要 2 个线程,而是需要 **10 个**,也就是说,有 10 个线程同时对应 10 个 SimpleDateFormat 对象。我们就来看下面这种写法: ```java public class ThreadLocalDemo02 { @@ -197,7 +197,7 @@ public class ThreadLocalDemo04 { 16:13 ``` -执行上面的代码就会发现,控制台所打印出来的和我们所期待的是不一致的。我们所期待的是打印出来的时间是不重复的,但是可以看出在这里出现了重复,比如第一行和第二行都是 04 秒,这就代表它内部已经出错了。 **6. 加锁** 出错的原因就在于, **simpleDateFormat 这个对象本身不是一个线程安全的对象** ,不应该被多个线程同时访问。所以我们就想到了一个解决方案,用 synchronized 来加锁。于是代码就修改成下面的样子: +执行上面的代码就会发现,控制台所打印出来的和我们所期待的是不一致的。我们所期待的是打印出来的时间是不重复的,但是可以看出在这里出现了重复,比如第一行和第二行都是 04 秒,这就代表它内部已经出错了。**6. 加锁** 出错的原因就在于,**simpleDateFormat 这个对象本身不是一个线程安全的对象**,不应该被多个线程同时访问。所以我们就想到了一个解决方案,用 synchronized 来加锁。于是代码就修改成下面的样子: ```java public class ThreadLocalDemo05 { @@ -243,7 +243,7 @@ public class ThreadLocalDemo05 { 这样的结果是正常的,没有出现重复的时间。但是由于我们使用了 synchronized 关键字,就会陷入一种排队的状态,多个线程不能同时工作,这样一来,整体的效率就被大大降低了。有没有更好的解决方案呢? -我们希望达到的效果是, **既不浪费过多的内存,同时又想保证线程安全** 。经过思考得出,可以 **让每个线程都拥有一个自己的 simpleDateFormat 对象来达到这个目的** ,这样就能两全其美了。 **7. 使用 ThreadLocal** +我们希望达到的效果是,**既不浪费过多的内存,同时又想保证线程安全** 。经过思考得出,可以 **让每个线程都拥有一个自己的 simpleDateFormat 对象来达到这个目的**,这样就能两全其美了。**7. 使用 ThreadLocal** 那么,要想达到这个目的,我们就可以使用 ThreadLocal。示例代码如下所示: @@ -390,6 +390,6 @@ Service3拿到用户名:拉勾教育 本讲主要介绍了 ThreadLocal 的两个典型的使用场景。 -场景1,ThreadLocal 用作 **保存每个线程独享的对象** ,为每个线程都创建一个副本,每个线程都只能修改自己所拥有的副本, 而不会影响其他线程的副本,这样就让原本在并发情况下,线程不安全的情况变成了线程安全的情况。 +场景1,ThreadLocal 用作 **保存每个线程独享的对象**,为每个线程都创建一个副本,每个线程都只能修改自己所拥有的副本, 而不会影响其他线程的副本,这样就让原本在并发情况下,线程不安全的情况变成了线程安全的情况。 -场景2,ThreadLocal 用作 **每个线程内需要独立保存信息** 的场景, **供其他方法更方便得获取** 该信息,每个线程获取到的信息都可能是不一样的,前面执行的方法设置了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参。 +场景2,ThreadLocal 用作 **每个线程内需要独立保存信息** 的场景,**供其他方法更方便得获取** 该信息,每个线程获取到的信息都可能是不一样的,前面执行的方法设置了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25448\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25448\350\256\262.md" index a86f36c5d..3b51c84e5 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25448\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25448\350\256\262.md" @@ -84,9 +84,9 @@ public interface Callable { 最后总结一下 Callable 和 Runnable 的不同之处: -- **方法名** ,Callable 规定的执行方法是 call(),而 Runnable 规定的执行方法是 run(); -- **返回值** ,Callable 的任务执行后有返回值,而 Runnable 的任务执行后是没有返回值的; -- **抛出异常** ,call() 方法可抛出异常,而 run() 方法是不能抛出受检查异常的; +- **方法名**,Callable 规定的执行方法是 call(),而 Runnable 规定的执行方法是 run(); +- **返回值**,Callable 的任务执行后有返回值,而 Runnable 的任务执行后是没有返回值的; +- **抛出异常**,call() 方法可抛出异常,而 run() 方法是不能抛出受检查异常的; - 和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。 以上就是本课时的内容了。首先介绍了 Runnable 的两个缺陷,第一个是没有返回值,第二个是不能抛出受检查异常;然后分析了为什么会有这样的缺陷,以及为什么设计成这样;接下来分析了 Callable 接口,并且把 Callable 接口和 Runnable 接口的区别进行了对比和总结。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25449\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25449\350\256\262.md" index e235da6cb..f016036e2 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25449\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25449\350\256\262.md" @@ -33,15 +33,15 @@ public interface Future { get 方法最主要的作用就是获取任务执行的结果,该方法在执行时的行为取决于 Callable 任务的状态,可能会发生以下 5 种情况。 -(1)最常见的就是 **当执行 get 的时候,任务已经执行完毕了** ,可以立刻返回,获取到任务执行的结果。 +(1)最常见的就是 **当执行 get 的时候,任务已经执行完毕了**,可以立刻返回,获取到任务执行的结果。 -(2) **任务还没有结果** ,这是有可能的,比如我们往线程池中放一个任务,线程池中可能积压了很多任务,还没轮到我去执行的时候,就去 get 了,在这种情况下,相当于任务还没开始;还有一种情况是 **任务正在执行中** ,但是执行过程比较长,所以我去 get 的时候,它依然在执行的过程中。无论是任务还没开始或在进行中,我们去调用 get 的时候,都会把当前的线程阻塞,直到任务完成再把结果返回回来。 +(2) **任务还没有结果**,这是有可能的,比如我们往线程池中放一个任务,线程池中可能积压了很多任务,还没轮到我去执行的时候,就去 get 了,在这种情况下,相当于任务还没开始;还有一种情况是 **任务正在执行中**,但是执行过程比较长,所以我去 get 的时候,它依然在执行的过程中。无论是任务还没开始或在进行中,我们去调用 get 的时候,都会把当前的线程阻塞,直到任务完成再把结果返回回来。 -(3) **任务执行过程中抛出异常** ,一旦这样,我们再去调用 get 的时候,就会抛出 ExecutionException 异常,不管我们执行 call 方法时里面抛出的异常类型是什么,在执行 get 方法时所获得的异常都是 ExecutionException。 +(3) **任务执行过程中抛出异常**,一旦这样,我们再去调用 get 的时候,就会抛出 ExecutionException 异常,不管我们执行 call 方法时里面抛出的异常类型是什么,在执行 get 方法时所获得的异常都是 ExecutionException。 -(4) **任务被取消了** ,如果任务被取消,我们用 get 方法去获取结果时则会抛出 CancellationException。 +(4) **任务被取消了**,如果任务被取消,我们用 get 方法去获取结果时则会抛出 CancellationException。 -(5) **任务超时** ,我们知道 get 方法有一个重载方法,那就是带延迟参数的,调用了这个带延迟参数的 get 方法之后,如果 call 方法在规定时间内正常顺利完成了任务,那么 get 会正常返回;但是如果到达了指定时间依然没有完成任务,get 方法则会抛出 TimeoutException,代表超时了。 +(5) **任务超时**,我们知道 get 方法有一个重载方法,那就是带延迟参数的,调用了这个带延迟参数的 get 方法之后,如果 call 方法在规定时间内正常顺利完成了任务,那么 get 会正常返回;但是如果到达了指定时间依然没有完成任务,get 方法则会抛出 TimeoutException,代表超时了。 下面用图的形式让过程更清晰: @@ -86,7 +86,7 @@ public class OneFuture { 下面我们再接着看看 Future 的一些其他方法,比如说 isDone() 方法,该方法是用来判断当前这个任务是否执行完毕了。 -**需要注意的是** ,这个方法如果返回 true 则代表执行完成了;如果返回 false 则代表还没完成。但这里如果返回 true,并不代表这个任务是成功执行的,比如说任务执行到一半抛出了异常。那么在这种情况下,对于这个 isDone 方法而言,它其实也是会返回 true 的,因为对它来说,虽然有异常发生了,但是这个任务在未来也不会再被执行,它确实已经执行完毕了。所以 isDone 方法在返回 true 的时候,不代表这个任务是成功执行的,只代表它执行完毕了。 +**需要注意的是**,这个方法如果返回 true 则代表执行完成了;如果返回 false 则代表还没完成。但这里如果返回 true,并不代表这个任务是成功执行的,比如说任务执行到一半抛出了异常。那么在这种情况下,对于这个 isDone 方法而言,它其实也是会返回 true 的,因为对它来说,虽然有异常发生了,但是这个任务在未来也不会再被执行,它确实已经执行完毕了。所以 isDone 方法在返回 true 的时候,不代表这个任务是成功执行的,只代表它执行完毕了。 我们用一个代码示例来看一看,代码如下所示: @@ -132,7 +132,7 @@ java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: Cal ... ``` -**这里要注意** ,我们知道这个异常实际上是在任务刚被执行的时候就抛出了,因为我们的计算任务中是没有其他逻辑的,只有抛出异常。我们再来看,控制台是什么时候打印出异常的呢?它是在 true 打印完毕后才打印出异常信息的,也就是说,在调用 get 方法时打印出的异常。 **这段代码证明了三件事情** :第一件事情,即便任务抛出异常,isDone 方法依然会返回 true;第二件事情,虽然抛出的异常是 IllegalArgumentException,但是对于 get 而言,它抛出的异常依然是 ExecutionException;第三个事情,虽然在任务执行一开始时就抛出了异常,但是真正要等到我们执行 get 的时候,才看到了异常。 +**这里要注意**,我们知道这个异常实际上是在任务刚被执行的时候就抛出了,因为我们的计算任务中是没有其他逻辑的,只有抛出异常。我们再来看,控制台是什么时候打印出异常的呢?它是在 true 打印完毕后才打印出异常信息的,也就是说,在调用 get 方法时打印出的异常。**这段代码证明了三件事情** :第一件事情,即便任务抛出异常,isDone 方法依然会返回 true;第二件事情,虽然抛出的异常是 IllegalArgumentException,但是对于 get 而言,它抛出的异常依然是 ExecutionException;第三个事情,虽然在任务执行一开始时就抛出了异常,但是真正要等到我们执行 get 的时候,才看到了异常。 #### cancel 方法:取消任务的执行 @@ -142,7 +142,7 @@ java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: Cal 第二种情况也比较简单。如果任务已经完成,或者之前已经被取消过了,那么执行 cancel 方法则代表取消失败,返回 false。因为任务无论是已完成还是已经被取消过了,都不能再被取消了。 -第三种情况比较特殊,就是这个任务正在执行,这个时候执行 cancel 方法是不会直接取消这个任务的,而是会根据我们传入的参数做判断。cancel 方法是必须传入一个参数,该参数叫作 **mayInterruptIfRunning** ,它是什么含义呢?如果传入的参数是 true,执行任务的线程就会收到一个中断的信号,正在执行的任务可能会有一些处理中断的逻辑,进而停止,这个比较好理解。如果传入的是 false 则就代表不中断正在运行的任务,也就是说,本次 cancel 不会有任何效果,同时 cancel 方法会返回 false。 +第三种情况比较特殊,就是这个任务正在执行,这个时候执行 cancel 方法是不会直接取消这个任务的,而是会根据我们传入的参数做判断。cancel 方法是必须传入一个参数,该参数叫作 **mayInterruptIfRunning**,它是什么含义呢?如果传入的参数是 true,执行任务的线程就会收到一个中断的信号,正在执行的任务可能会有一些处理中断的逻辑,进而停止,这个比较好理解。如果传入的是 false 则就代表不中断正在运行的任务,也就是说,本次 cancel 不会有任何效果,同时 cancel 方法会返回 false。 那么如何选择传入 true 还是 false 呢? diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25450\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25450\350\256\262.md" index 63fc9af8d..868ef16e2 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25450\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25450\350\256\262.md" @@ -74,7 +74,7 @@ public class FutureDemo { 假设由于网络原因,第一个任务可能长达 1 分钟都没办法返回结果,那么这个时候,我们的主线程会一直卡着,影响了程序的运行效率。 -此时我们就可以用 Future 的带超时参数的 get(long timeout, TimeUnit unit) 方法来解决这个问题。这个方法的作用是,如果在限定的时间内没能返回结果的话,那么便会抛出一个 TimeoutException 异常,随后就可以把这个异常捕获住,或者是再往上抛出去,这样就不会一直卡着了。 **2. Future 的生命周期不能后退** +此时我们就可以用 Future 的带超时参数的 get(long timeout, TimeUnit unit) 方法来解决这个问题。这个方法的作用是,如果在限定的时间内没能返回结果的话,那么便会抛出一个 TimeoutException 异常,随后就可以把这个异常捕获住,或者是再往上抛出去,这样就不会一直卡着了。**2. Future 的生命周期不能后退** Future 的生命周期不能后退,一旦完成了任务,它就永久停在了“已完成”的状态,不能从头再来,也不能让一个已经完成计算的 Future 再次重新执行任务。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25452\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25452\350\256\262.md" index 62e15143b..53956904f 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25452\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25452\350\256\262.md" @@ -61,7 +61,7 @@ pool-1-thread-6调用了慢服务 它会从线程 1 一直到线程 50 都去调用这个慢服务,当然实际调用顺序每次都会不一样,但是这 50 个线程都会去几乎同时调用这个慢服务,在这种情况下,就会导致我们的慢服务崩溃。 -所以,必须严格限制能够同时到达该服务的请求数。比如,我们想限制同时不超过 3 个请求来访问该服务,该怎么实现呢?并且这里有一点值得注意,我们的前提条件是,线程池中确实有 50 个线程,线程数肯定超过了 3 个,那么怎么进一步控制这么多的线程不同时访问慢服务呢?我们可以通过信号量来解决这个问题。 **正常情况下获取许可证** ![img](assets/CgpOIF5fiXKAWCrGAABHA-Ygk4E065.png) +所以,必须严格限制能够同时到达该服务的请求数。比如,我们想限制同时不超过 3 个请求来访问该服务,该怎么实现呢?并且这里有一点值得注意,我们的前提条件是,线程池中确实有 50 个线程,线程数肯定超过了 3 个,那么怎么进一步控制这么多的线程不同时访问慢服务呢?我们可以通过信号量来解决这个问题。**正常情况下获取许可证**![img](assets/CgpOIF5fiXKAWCrGAABHA-Ygk4E065.png) 这张图的方框代表一个许可证为 3 的信号量,每一个绿色的长条代表一个许可证(permit)。现在我们拥有 3 个许可证,并且信号量的特点是非常“慷慨”,只要它持有许可证,别人想请求的话它都会分发的。假设此时 Thread 1 来请求了,在这种情况下,信号量就会把一个许可证给到这边的第一个线程 Thread 1。于是 Thread 1 获得了许可证,变成了下图这个样子: @@ -79,7 +79,7 @@ Thread 1 拿到许可证之后就拥有了访问慢服务的资格,它紧接 ![img](assets/CgpOIF5fibeAEb5lAABlAe_v4qc506.png) -线程 4 在找我们用 acquire 方法请求许可证的时候,它会被阻塞,意味着线程 4 没有拿到许可证,也就没有被允许访问“慢服务”,也就是说此时“慢服务”依然只能被前面的 3 个线程访问,这样就达到我们最开始的目的了:限制同时最多有 3 个线程调用我们的慢服务。 **有线程释放信号量后** 假设此时线程 1 因为最早去的,它执行完了这个任务,于是返回了。返回的时候它会调用 release 方法,表示“我处理完了我的任务,我想把许可证还回去”,所以,此时线程 1 就释放了之前持有的许可证,把它还给了我们的信号量,于是信号量所持有的许可证数量从 0 又变回了 1,如图所示: ![img](assets/CgpOIF5ficqAeHGkAABrDCfhZdc317.png) +线程 4 在找我们用 acquire 方法请求许可证的时候,它会被阻塞,意味着线程 4 没有拿到许可证,也就没有被允许访问“慢服务”,也就是说此时“慢服务”依然只能被前面的 3 个线程访问,这样就达到我们最开始的目的了:限制同时最多有 3 个线程调用我们的慢服务。**有线程释放信号量后** 假设此时线程 1 因为最早去的,它执行完了这个任务,于是返回了。返回的时候它会调用 release 方法,表示“我处理完了我的任务,我想把许可证还回去”,所以,此时线程 1 就释放了之前持有的许可证,把它还给了我们的信号量,于是信号量所持有的许可证数量从 0 又变回了 1,如图所示: ![img](assets/CgpOIF5ficqAeHGkAABrDCfhZdc317.png) 此时由于许可证已经归还给了信号量,那么刚才找我们要许可证的线程 4 就可以顺利地拿到刚刚释放的这个许可证了。于是线程 4 也就拥有了访问慢服务的访问权,接下来它也会去访问这个慢服务。 @@ -113,7 +113,7 @@ acquire() 和 acquireUninterruptibly() 的区别是:是否能响应中断。ac ### 其他主要方法介绍 -除了这几个主要方法以外,还有一些其他的方法,我再来介绍一下。 **(1)public boolean tryAcquire()** tryAcquire 和之前介绍锁的 trylock 思维是一致的,是尝试获取许可证,相当于看看现在有没有空闲的许可证,如果有就获取,如果现在获取不到也没关系,不必陷入阻塞,可以去做别的事。 **(2)public boolean tryAcquire(long timeout, TimeUnit unit)** 同样有一个重载的方法,它里面传入了超时时间。比如传入了 3 秒钟,则意味着最多等待 3 秒钟,如果等待期间获取到了许可证,则往下继续执行;如果超时时间到,依然获取不到许可证,它就认为获取失败,且返回 false。 **(3)availablePermits()** +除了这几个主要方法以外,还有一些其他的方法,我再来介绍一下。**(1)public boolean tryAcquire()** tryAcquire 和之前介绍锁的 trylock 思维是一致的,是尝试获取许可证,相当于看看现在有没有空闲的许可证,如果有就获取,如果现在获取不到也没关系,不必陷入阻塞,可以去做别的事。**(2)public boolean tryAcquire(long timeout, TimeUnit unit)** 同样有一个重载的方法,它里面传入了超时时间。比如传入了 3 秒钟,则意味着最多等待 3 秒钟,如果等待期间获取到了许可证,则往下继续执行;如果超时时间到,依然获取不到许可证,它就认为获取失败,且返回 false。**(3)availablePermits()** 这个方法用来查询可用许可证的数量,返回一个整型的结果。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25453\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25453\350\256\262.md" index 32dff5d5a..5888e8e36 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25453\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25453\350\256\262.md" @@ -16,9 +16,9 @@ ### 主要方法介绍 -下面介绍一下 CountDownLatch 的主要方法。 **(1)构造函数** :public CountDownLatch(int count) { }; +下面介绍一下 CountDownLatch 的主要方法。**(1)构造函数** :public CountDownLatch(int count) { }; -它的构造函数是传入一个参数,该参数 count 是需要倒数的数值。 **(2)await()** :调用 await() 方法的线程开始等待,直到倒数结束,也就是 count 值为 0 的时候才会继续执行。 **(3)await(long timeout, TimeUnit unit)** :await() 有一个重载的方法,里面会传入超时参数,这个方法的作用和 await() 类似,但是这里可以设置超时时间,如果超时就不再等待了。 **(4)countDown()** :把数值倒数 1,也就是将 count 值减 1,直到减为 0 时,之前等待的线程会被唤起。 +它的构造函数是传入一个参数,该参数 count 是需要倒数的数值。**(2)await()** :调用 await() 方法的线程开始等待,直到倒数结束,也就是 count 值为 0 的时候才会继续执行。**(3)await(long timeout, TimeUnit unit)** :await() 有一个重载的方法,里面会传入超时参数,这个方法的作用和 await() 类似,但是这里可以设置超时时间,如果超时就不再等待了。**(4)countDown()** :把数值倒数 1,也就是将 count 值减 1,直到减为 0 时,之前等待的线程会被唤起。 ### 用法 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25455\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25455\350\256\262.md" index 56e56e835..3f340891a 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25455\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25455\350\256\262.md" @@ -60,9 +60,9 @@ public class ConditionDemo { 在这个代码中,有以下三个方法。 -- **method1** ,它代表主线程将要执行的内容,首先获取到锁,打印出“条件不满足,开始 await”,然后调用 condition.await() 方法,直到条件满足之后,则代表这个语句可以继续向下执行了,于是打印出“条件满足了,开始执行后续的任务”,最后会在 finally 中解锁。 -- **method2** ,它同样也需要先获得锁,然后打印出“需要 5 秒钟的准备时间”,接着用 sleep 来模拟准备时间;在时间到了之后,则打印出“准备工作完成”,最后调用 condition.signal() 方法,把之前已经等待的线程唤醒。 -- **main 方法** ,它的主要作用是执行上面这两个方法,它先去实例化我们这个类,然后再用子线程去调用这个类的 method2 方法,接着用主线程去调用 method1 方法。 +- **method1**,它代表主线程将要执行的内容,首先获取到锁,打印出“条件不满足,开始 await”,然后调用 condition.await() 方法,直到条件满足之后,则代表这个语句可以继续向下执行了,于是打印出“条件满足了,开始执行后续的任务”,最后会在 finally 中解锁。 +- **method2**,它同样也需要先获得锁,然后打印出“需要 5 秒钟的准备时间”,接着用 sleep 来模拟准备时间;在时间到了之后,则打印出“准备工作完成”,最后调用 condition.signal() 方法,把之前已经等待的线程唤醒。 +- **main 方法**,它的主要作用是执行上面这两个方法,它先去实例化我们这个类,然后再用子线程去调用这个类的 method2 方法,接着用主线程去调用 method1 方法。 最终这个代码程序运行结果如下所示: diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25456\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25456\350\256\262.md" index d45b62326..9bfbfa939 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25456\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25456\350\256\262.md" @@ -43,7 +43,7 @@ Java 作为一种面向对象的语言,有很多概念,从名称上看起来 - 在编译(包含词法分析、语义分析等步骤)后,在刚才的 _.java 文件之外,会多出一个新的 Java 字节码文件(_.class); - JVM 会分析刚才生成的字节码文件(\*.class),并根据平台等因素,把字节码文件转化为具体平台上的 **机器指令;** - 机器指令则可以直接在 CPU 上运行,也就是最终的程序执行。 -## **为什么需要 JMM** (Java Memory Model, **Java 内存模型)** +## **为什么需要 JMM** (Java Memory Model,**Java 内存模型)** 在更早期的语言中,其实是不存在内存模型的概念的。 @@ -57,7 +57,7 @@ Java 作为一种面向对象的语言,有很多概念,从名称上看起来 有了上面的铺垫,下面我们就介绍一下究竟什么是 JMM。 -### **JMM 是规范** JMM 是和多线程相关的 **一组规范** ,需要各个 JVM 的实现来遵守 JMM 规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。这样一来,即便同一个程序在不同的虚拟机上运行,得到的程序结果也是一致的 +### **JMM 是规范** JMM 是和多线程相关的 **一组规范**,需要各个 JVM 的实现来遵守 JMM 规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。这样一来,即便同一个程序在不同的虚拟机上运行,得到的程序结果也是一致的 如果没有 JMM 内存模型来规范,那么很可能在经过了不同 JVM 的“翻译”之后,导致在不同的虚拟机上运行的结果不一样,那是很大的问题。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25459\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25459\350\256\262.md" index 921a89d28..9b8a5b5be 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25459\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25459\350\256\262.md" @@ -102,7 +102,7 @@ public class VisibilityProblem { - 如果 b = a 要想执行,那么前面 a = 30 也需要执行,此时 b 才能等于 a 的值,也就是 30; - 这也就意味着 change 方法已经执行完毕了。 -可是在这种情况下再打印 a,结果应该是 a = 30,而不应该打印出 a = 10。因为在刚才 change 执行的过程中,a 的值已经被改成 30 了,不再是初始值的 10。所以,如果出现了打印结果为 b = 30;a = 10 这种情况,就意味着发生了 **可见性问题:a 的值已经被第 1 个线程修改了,但是其他线程却看不到** ,由于 a 的最新值却没能及时同步过来,所以才会打印出 a 的旧值。发生上述情况的几率不高。我把发生时的截屏用图片的形式展示给你看看,如下所示: +可是在这种情况下再打印 a,结果应该是 a = 30,而不应该打印出 a = 10。因为在刚才 change 执行的过程中,a 的值已经被改成 30 了,不再是初始值的 10。所以,如果出现了打印结果为 b = 30;a = 10 这种情况,就意味着发生了 **可见性问题:a 的值已经被第 1 个线程修改了,但是其他线程却看不到**,由于 a 的最新值却没能及时同步过来,所以才会打印出 a 的旧值。发生上述情况的几率不高。我把发生时的截屏用图片的形式展示给你看看,如下所示: ![img](assets/Cgq2xl5zjgGAF-mdAABl3iL7a-k359.png) diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25461\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25461\350\256\262.md" index 291115cc2..870557236 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25461\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25461\350\256\262.md" @@ -40,19 +40,19 @@ public class Visibility { 这里有一个注意点,我们之前讲过重排序,那是不是意味着 happens-before 关系的规则和重排序冲突,为了满足 happens-before 关系,就不能重排序了? -答案是否定的。其实只要重排序后的结果依然符合 happens-before 关系,也就是能保证可见性的话,那么就不会因此限制重排序的发生。比如,单线程内,语句 1 在语句 2 的前面,所以根据“单线程规则”,语句 1 happens-before 语句 2,但是并不是说语句 1 一定要在语句 2 之前被执行,例如语句 1 修改的是变量 a 的值,而语句 2 的内容和变量 a 无关,那么语句 1 和语句 2 依然有可能被重排序。当然,如果语句 1 修改的是变量 a,而语句 2 正好是去读取变量 a 的值,那么语句 1 就一定会在语句 2 之前执行了。 **(2)锁操作规则(synchronized 和 Lock 接口等)** : 如果操作 A 是解锁,而操作 B 是对同一个锁的加锁,那么 hb(A, B) 。正如下图所示: ![img](assets/Ciqah157Dw6Aeo7EAAA0bxPJeKw538.png) +答案是否定的。其实只要重排序后的结果依然符合 happens-before 关系,也就是能保证可见性的话,那么就不会因此限制重排序的发生。比如,单线程内,语句 1 在语句 2 的前面,所以根据“单线程规则”,语句 1 happens-before 语句 2,但是并不是说语句 1 一定要在语句 2 之前被执行,例如语句 1 修改的是变量 a 的值,而语句 2 的内容和变量 a 无关,那么语句 1 和语句 2 依然有可能被重排序。当然,如果语句 1 修改的是变量 a,而语句 2 正好是去读取变量 a 的值,那么语句 1 就一定会在语句 2 之前执行了。**(2)锁操作规则(synchronized 和 Lock 接口等)** : 如果操作 A 是解锁,而操作 B 是对同一个锁的加锁,那么 hb(A, B) 。正如下图所示: ![img](assets/Ciqah157Dw6Aeo7EAAA0bxPJeKw538.png) -从上图中可以看到,有线程 A 和线程 B 这两个线程。线程 A 在解锁之前的所有操作,对于线程 B 的对同一个锁的加锁之后的所有操作而言,都是可见的。这就是锁操作的 happens-before 关系的规则。 **(3)volatile 变量规则** : 对一个 volatile 变量的写操作 happen-before 后面对该变量的读操作。 +从上图中可以看到,有线程 A 和线程 B 这两个线程。线程 A 在解锁之前的所有操作,对于线程 B 的对同一个锁的加锁之后的所有操作而言,都是可见的。这就是锁操作的 happens-before 关系的规则。**(3)volatile 变量规则** : 对一个 volatile 变量的写操作 happen-before 后面对该变量的读操作。 -这就代表了如果变量被 volatile 修饰,那么每次修改之后,其他线程在读取这个变量的时候一定能读取到该变量最新的值。我们之前介绍过 volatile 关键字,知道它能保证可见性,而这正是由本条规则所规定的。 **(4)线程启动规则** : Thread 对象的 start 方法 happen-before 此线程 run 方法中的每一个操作。如下图所示: +这就代表了如果变量被 volatile 修饰,那么每次修改之后,其他线程在读取这个变量的时候一定能读取到该变量最新的值。我们之前介绍过 volatile 关键字,知道它能保证可见性,而这正是由本条规则所规定的。**(4)线程启动规则** : Thread 对象的 start 方法 happen-before 此线程 run 方法中的每一个操作。如下图所示: ![img](assets/Cgq2xl57Dw6AdKyOAADBt-00qXo349.png) -在图中的例子中,左侧区域是线程 A 启动了一个子线程 B,而右侧区域是子线程 B,那么子线程 B 在执行 run 方法里面的语句的时候,它一定能看到父线程在执行 threadB.start() 前的所有操作的结果。 **(5)线程 join 规则** : +在图中的例子中,左侧区域是线程 A 启动了一个子线程 B,而右侧区域是子线程 B,那么子线程 B 在执行 run 方法里面的语句的时候,它一定能看到父线程在执行 threadB.start() 前的所有操作的结果。**(5)线程 join 规则** : 我们知道 join 可以让线程之间等待,假设线程 A 通过调用 threadB.start() 启动了一个新线程 B,然后调用 threadB.join() ,那么线程 A 将一直等待到线程 B 的 run 方法结束(不考虑中断等特殊情况),然后 join 方法才返回。在 join 方法返回后,线程 A 中的所有后续操作都可以看到线程 B 的 run 方法中执行的所有操作的结果,也就是线程 B 的 run 方法里面的操作 happens-before 线程 A 的 join 之后的语句。如下图所示: ![img](assets/Cgq2xl57Dw6ADE7rAADRJKFrbWE816.png) **(6)中断规则** : 对线程 interrupt 方法的调用 happens-before 检测该线程的中断事件。 -也就是说,如果一个线程被其他线程 interrupt,那么在检测中断时(比如调用 Thread.interrupted 或者 Thread.isInterrupted 方法)一定能看到此次中断的发生,不会发生检测结果不准的情况。 **(7)并发工具类的规则** : +也就是说,如果一个线程被其他线程 interrupt,那么在检测中断时(比如调用 Thread.interrupted 或者 Thread.isInterrupted 方法)一定能看到此次中断的发生,不会发生检测结果不准的情况。**(7)并发工具类的规则** : - 线程安全的并发容器(如 HashTable)在 get 某个值时一定能看到在此之前发生的 put 等存入操作的结果。也就是说,线程安全的并发容器的存入操作 happens-before 读取操作。 - 信号量(Semaphore)它会释放许可证,也会获取许可证。这里的释放许可证的操作 happens-before 获取许可证的操作,也就是说,如果在获取许可证之前有释放许可证的操作,那么在获取时一定可以看到。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25462\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25462\350\256\262.md" index e2320b9a3..fcadf0c68 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25462\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25462\350\256\262.md" @@ -138,7 +138,7 @@ while (!initialized) **第一层的作用是保证可见性** 。Happens-before 关系中对于 volatile 是这样描述的:对一个 volatile 变量的写操作 happen-before 后面对该变量的读操作。 -这就代表了如果变量被 volatile 修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值。 **第二层的作用就是禁止重排序** 。先介绍一下 as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。用了 volatile 关键字就可以在一定程度上禁止这种重排序。 +这就代表了如果变量被 volatile 修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值。**第二层的作用就是禁止重排序** 。先介绍一下 as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。用了 volatile 关键字就可以在一定程度上禁止这种重排序。 ### volatile 和 synchronized 的关系 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25464\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25464\350\256\262.md" index 8256c341d..a5506a511 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25464\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25464\350\256\262.md" @@ -6,9 +6,9 @@ CAS 其实是我们面试中的常客,因为它是原子类的底层原理,同时也是乐观锁的原理,所以当你去面试的时候,经常会遇到这样的问题“你知道哪些类型的锁”?你可能会回答“悲观锁和乐观锁”,那么下一个问题很有可能是问乐观锁的原理,也就是和 CAS 相关的问题,当然也有可能会继续深入问你 **CAS 的应用场景或者是缺点** 等问题。在本课时和接下来的这两个课时里,我将带领你学习如何回答这些问题。 -首先我们来看一下 CAS 是什么,它的英文全称是 **Compare-And-Swap** ,中文叫做“比较并交换”,它是一种思想、一种算法。 +首先我们来看一下 CAS 是什么,它的英文全称是 **Compare-And-Swap**,中文叫做“比较并交换”,它是一种思想、一种算法。 -在多线程的情况下,各个代码的执行顺序是不能确定的,所以为了保证并发安全,我们可以使用互斥锁。而 CAS 的特点是避免使用互斥锁,当多个线程同时使用 CAS 更新同一个变量时,只有其中一个线程能够操作成功,而其他线程都会更新失败。不过和同步互斥锁不同的是,更新失败的线程并 **不会被阻塞** ,而是被告知这次由于竞争而导致的操作失败,但还可以再次尝试。 +在多线程的情况下,各个代码的执行顺序是不能确定的,所以为了保证并发安全,我们可以使用互斥锁。而 CAS 的特点是避免使用互斥锁,当多个线程同时使用 CAS 更新同一个变量时,只有其中一个线程能够操作成功,而其他线程都会更新失败。不过和同步互斥锁不同的是,更新失败的线程并 **不会被阻塞**,而是被告知这次由于竞争而导致的操作失败,但还可以再次尝试。 CAS 被广泛应用在并发编程领域中,以实现那些不会被打断的数据交换操作,从而就实现了无锁的线程安全。 @@ -16,7 +16,7 @@ CAS 被广泛应用在并发编程领域中,以实现那些不会被打断的 在大多数处理器的指令中,都会实现 CAS 相关的指令,这一条指令就可以完成“ **比较并交换** ”的操作,也正是由于这是一条(而不是多条)CPU 指令,所以 CAS 相关的指令是具备原子性的,这个组合操作在执行期间不会被打断,这样就能保证并发安全。由于这个原子性是由 CPU 保证的,所以无需我们程序员来操心。 -CAS 有三个操作数:内存值 V、预期值 A、要修改的值 B。CAS 最核心的思路就是, **仅当预期值 A 和当前的内存值 V 相同时,才将内存值修改为 B** 。 +CAS 有三个操作数:内存值 V、预期值 A、要修改的值 B。CAS 最核心的思路就是,**仅当预期值 A 和当前的内存值 V 相同时,才将内存值修改为 B** 。 我们对此展开描述一下:CAS 会提前假定当前内存值 V 应该等于值 A,而值 A 往往是之前读取到当时的内存值 V。在执行 CAS 时,如果发现当前的内存值 V 恰好是值 A 的话,那 CAS 就会把内存值 V 改成值 B,而值 B 往往是在拿到值 A 后,在值 A 的基础上经过计算而得到的。如果执行 CAS 时发现此时内存值 V 不等于值 A,则说明在刚才计算 B 的期间内,内存值已经被其他线程修改过了,那么本次 CAS 就不应该再修改了,可以避免多人同时修改导致出错。这就是 CAS 的主要思路和流程。 @@ -38,7 +38,7 @@ JDK 正是利用了这些 CAS 指令,可以实现并发的数据结构,比 ### CAS 的语义 -我们来看一看 CAS 的 **语义** ,有了下面的等价代码之后,理解起来会比前面的图示和文字更加容易,因为代码实际上是一目了然的。接下来我们把 CAS 拆开,看看它内部究竟做了哪些事情。CAS 的等价语义的代码,如下所示: +我们来看一看 CAS 的 **语义**,有了下面的等价代码之后,理解起来会比前面的图示和文字更加容易,因为代码实际上是一目了然的。接下来我们把 CAS 拆开,看看它内部究竟做了哪些事情。CAS 的等价语义的代码,如下所示: ```java /** @@ -56,7 +56,7 @@ public class SimulatedCAS { } ``` -在这段代码中有一个 compareAndSwap 方法,在这个方法里有两个入参, **第 1 个入参期望值 expectedValue,第 2 个入参是 newValue** ,它就是我们计算好的新的值,我们希望把这个新的值去更新到变量上去。 +在这段代码中有一个 compareAndSwap 方法,在这个方法里有两个入参,**第 1 个入参期望值 expectedValue,第 2 个入参是 newValue**,它就是我们计算好的新的值,我们希望把这个新的值去更新到变量上去。 你一定注意到了, compareAndSwap 方法是被 **synchronized** 修饰的,我们用同步方法为 CAS 的等价代码保证了原子性。 @@ -136,7 +136,7 @@ public class DebugCAS implements Runnable { ![img](assets/Ciqah16EXqCAGkU-AAXHUfh2Ojg469.png) -可以看到, **oldValue 拿到的值是 150,因为 value 的值已经被 Thread 1 修改过了** ,所以,150 与 Thread 2 所期望的 expectedValue 的值 100 是不相等的,从而会跳过整个 if 语句,也就不能打印出“Thread 2 执行成功”这句话,最后会返回 oldValue,其实对这个值没有做任何的修改。 +可以看到,**oldValue 拿到的值是 150,因为 value 的值已经被 Thread 1 修改过了**,所以,150 与 Thread 2 所期望的 expectedValue 的值 100 是不相等的,从而会跳过整个 if 语句,也就不能打印出“Thread 2 执行成功”这句话,最后会返回 oldValue,其实对这个值没有做任何的修改。 到这里,两个线程就执行完毕了。在控制台,只打印出 Thread 1 执行成功,而没有打印出 Thread 2 执行成功。其中的原因,我们通过 Debug 的方式已经知晓了。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25465\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25465\350\256\262.md" index 5e29a5945..74e010fc7 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25465\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25465\350\256\262.md" @@ -79,7 +79,7 @@ public boolean offer(E e) { ### 数据库 -在我们的数据库中,也存在对乐观锁和 CAS 思想的应用。在更新数据时,我们可以利用 version 字段在数据库中实现乐观锁和 CAS 操作,而在获取和修改数据时都不需要加悲观锁。 **具体思路** 如下:当我们获取完数据,并计算完毕,准备更新数据时,会检查现在的版本号与之前获取数据时的版本号是否一致,如果一致就说明在计算期间数据没有被更新过,可以直接更新本次数据;如果版本号不一致,则说明计算期间已经有其他线程修改过这个数据了,那就可以选择重新获取数据,重新计算,然后再次尝试更新数据。 +在我们的数据库中,也存在对乐观锁和 CAS 思想的应用。在更新数据时,我们可以利用 version 字段在数据库中实现乐观锁和 CAS 操作,而在获取和修改数据时都不需要加悲观锁。**具体思路** 如下:当我们获取完数据,并计算完毕,准备更新数据时,会检查现在的版本号与之前获取数据时的版本号是否一致,如果一致就说明在计算期间数据没有被更新过,可以直接更新本次数据;如果版本号不一致,则说明计算期间已经有其他线程修改过这个数据了,那就可以选择重新获取数据,重新计算,然后再次尝试更新数据。 假设取出数据的时候 version 版本为 1,相应的 SQL 语句示例如下所示: diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25466\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25466\350\256\262.md" index 1dd721a65..1265d3f9f 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25466\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25466\350\256\262.md" @@ -12,15 +12,15 @@ 决定 CAS 是否进行 swap 的判断标准是“当前的值和预期的值是否一致”,如果一致,就认为在此期间这个数值没有发生过变动,这在大多数情况下是没有问题的。 -但是在有的业务场景下,我们想确切知道 **从上一次看到这个值以来到现在,这个值是否发生过变化** 。例如,这个值假设 **从 A 变成了 B,再由 B 变回了 A** ,此时,我们不仅认为它发生了变化,并且会认为它变化了两次。 +但是在有的业务场景下,我们想确切知道 **从上一次看到这个值以来到现在,这个值是否发生过变化** 。例如,这个值假设 **从 A 变成了 B,再由 B 变回了 A**,此时,我们不仅认为它发生了变化,并且会认为它变化了两次。 -在这种场景下,我们使用 CAS,就看不到这两次的变化,因为仅判断“当前的值和预期的值是否一致”就是不够的了。CAS 检查的并不是值有没有发生过变化,而是去比较这当前的值和预期值是不是相等,如果变量的值从旧值 A 变成了新值 B 再变回旧值 A,由于最开始的值 A 和现在的值 A 是相等的,所以 CAS 会认为变量的值在此期间 **没有发生过变化** 。所以, **CAS 并不能检测出在此期间值是不是被修改过,它只能检查出现在的值和最初的值是不是一样。** 我们举一个例子:假设第一个线程拿到的初始值是 100,然后进行计算,在计算的过程中,有第二个线程把初始值改为了 200,然后紧接着又有第三个线程把 200 改回了 100。等到第一个线程计算完毕去执行 CAS 的时候,它会比较当前的值是不是等于最开始拿到的初始值 100,此时会发现确实是等于 100,所以线程一就认为在此期间值没有被修改过,就理所当然的把这个 100 改成刚刚计算出来的新值,但实际上,在此过程中已经有其他线程把这个值修改过了,这样就会发生 **ABA 问题** 。 +在这种场景下,我们使用 CAS,就看不到这两次的变化,因为仅判断“当前的值和预期的值是否一致”就是不够的了。CAS 检查的并不是值有没有发生过变化,而是去比较这当前的值和预期值是不是相等,如果变量的值从旧值 A 变成了新值 B 再变回旧值 A,由于最开始的值 A 和现在的值 A 是相等的,所以 CAS 会认为变量的值在此期间 **没有发生过变化** 。所以,**CAS 并不能检测出在此期间值是不是被修改过,它只能检查出现在的值和最初的值是不是一样。** 我们举一个例子:假设第一个线程拿到的初始值是 100,然后进行计算,在计算的过程中,有第二个线程把初始值改为了 200,然后紧接着又有第三个线程把 200 改回了 100。等到第一个线程计算完毕去执行 CAS 的时候,它会比较当前的值是不是等于最开始拿到的初始值 100,此时会发现确实是等于 100,所以线程一就认为在此期间值没有被修改过,就理所当然的把这个 100 改成刚刚计算出来的新值,但实际上,在此过程中已经有其他线程把这个值修改过了,这样就会发生 **ABA 问题** 。 -如果发生了 ABA 问题,那么线程一就根本无法知晓在计算过程中是否有其他线程把这个值修改过,由于第一个线程发现当前值和预期值是相等的,所以就会认为在此期间没有线程修改过变量的值,所以它 **接下来的一些操作逻辑,是按照在此期间这个值没被修改过”的逻辑去处理的** ,比如它可能会打印日志:“本次修改十分顺利”,但是它 **本应触发其他的逻辑** ,比如当它发现了在此期间有其他线程修改过这个值,其实本应该打印的是“本次修改过程受到了干扰”。 +如果发生了 ABA 问题,那么线程一就根本无法知晓在计算过程中是否有其他线程把这个值修改过,由于第一个线程发现当前值和预期值是相等的,所以就会认为在此期间没有线程修改过变量的值,所以它 **接下来的一些操作逻辑,是按照在此期间这个值没被修改过”的逻辑去处理的**,比如它可能会打印日志:“本次修改十分顺利”,但是它 **本应触发其他的逻辑**,比如当它发现了在此期间有其他线程修改过这个值,其实本应该打印的是“本次修改过程受到了干扰”。 那么如何解决这个问题呢?添加一个 **版本号** 就可以解决。 -我们在变量值自身之外,再添加一个版本号,那么这个值的变化路径就从 **A→B→A 变成了 1A→2B→3A** ,这样一来,就可以 **通过对比版本号来判断值是否变化过** ,这比我们直接去对比两个值是否一致要更靠谱,所以通过这样的思路就可以解决 ABA 的问题了。 +我们在变量值自身之外,再添加一个版本号,那么这个值的变化路径就从 **A→B→A 变成了 1A→2B→3A**,这样一来,就可以 **通过对比版本号来判断值是否变化过**,这比我们直接去对比两个值是否一致要更靠谱,所以通过这样的思路就可以解决 ABA 的问题了。 在 atomic 包中提供了 **AtomicStampedReference** 这个类,它是专门用来解决 ABA 问题的,解决思路正是利用版本号,AtomicStampedReference 会维护一种类似 \ 的数据结构,其中的 int 就是用于计数的,也就是版本号,它可以对这个对象和 int 版本号同时进行原子更新,从而也就解决了 ABA 问题。因为我们去判断它是否被修改过,不再是以值是否发生变化为标准,而是以版本号是否变化为标准,即使值一样,它们的版本号也是不同的。 @@ -30,9 +30,9 @@ CAS 的第二个缺点就是自旋时间过长。 -由于单次 CAS 不一定能执行成功,所以 **CAS 往往是配合着循环来实现的** ,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。 +由于单次 CAS 不一定能执行成功,所以 **CAS 往往是配合着循环来实现的**,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。 -可是如果我们的应用场景本身就是高并发的场景,就有可能导致 CAS 一直都操作不成功,这样的话, **循环时间就会越来越长** 。而且在此期间,CPU 资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用 CAS,在高并发的场景下,通常 CAS 的效率是不高的。 +可是如果我们的应用场景本身就是高并发的场景,就有可能导致 CAS 一直都操作不成功,这样的话,**循环时间就会越来越长** 。而且在此期间,CPU 资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用 CAS,在高并发的场景下,通常 CAS 的效率是不高的。 #### 范围不能灵活控制 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25467\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25467\350\256\262.md" index 5929dca41..82d8531d4 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25467\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25467\350\256\262.md" @@ -6,15 +6,15 @@ #### 什么是死锁 -**发生在并发中** 首先你要知道,死锁一定发生在并发场景中。我们为了保证线程安全,有时会给程序使用各种能保证并发安全的工具,尤其是锁,但是如果在使用过程中处理不得当,就有可能会导致发生死锁的情况。 **互不相让** 死锁是一种状态,当两个(或多个)线程(或进程)相互持有对方所需要的资源,却又都不主动释放自己手中所持有的资源,导致大家都获取不到自己想要的资源,所有相关的线程(或进程)都无法继续往下执行,在未改变这种状态之前都不能向前推进,我们就把这种状态称为 **死锁状态** ,认为它们发生了死锁。通俗的讲,死锁就是两个或多个线程(或进程) **被无限期地阻塞,相互等待对方手中资源** 的一种状态。 **生活中的例子** 下面我们用图示的方法来展示一种生活中发生死锁的情况,如下图所示: +**发生在并发中** 首先你要知道,死锁一定发生在并发场景中。我们为了保证线程安全,有时会给程序使用各种能保证并发安全的工具,尤其是锁,但是如果在使用过程中处理不得当,就有可能会导致发生死锁的情况。**互不相让** 死锁是一种状态,当两个(或多个)线程(或进程)相互持有对方所需要的资源,却又都不主动释放自己手中所持有的资源,导致大家都获取不到自己想要的资源,所有相关的线程(或进程)都无法继续往下执行,在未改变这种状态之前都不能向前推进,我们就把这种状态称为 **死锁状态**,认为它们发生了死锁。通俗的讲,死锁就是两个或多个线程(或进程) **被无限期地阻塞,相互等待对方手中资源** 的一种状态。**生活中的例子** 下面我们用图示的方法来展示一种生活中发生死锁的情况,如下图所示: ![img](assets/Cgq2xl6NrzCAEFQ0AB-HOvxO39A990.png) -可以看到这张漫画展示了两个绅士分别向对方鞠躬的场景,为了表示礼貌,他们弯下腰之后谁也不愿意先起身,都希望对方起身之后我再起身。可是这样一来,就 **没有任何人可以先起身** ,起身这个动作就一直无法继续执行,两人形成了相互等待的状态,所以这就是一种典型的死锁! **两个线程的例子** 下面我们用动画的形式来看一下 **两个线程** 发生死锁的情况,如下图所示: +可以看到这张漫画展示了两个绅士分别向对方鞠躬的场景,为了表示礼貌,他们弯下腰之后谁也不愿意先起身,都希望对方起身之后我再起身。可是这样一来,就 **没有任何人可以先起身**,起身这个动作就一直无法继续执行,两人形成了相互等待的状态,所以这就是一种典型的死锁! **两个线程的例子** 下面我们用动画的形式来看一下 **两个线程** 发生死锁的情况,如下图所示: ![img](assets/Cgq2xl6NrzGAMfz3AABHRjw_QSE080.png) -此时我们有两个线程,分别是线程 A 和线程 B,假设线程 A 现在持有了锁 A,线程 B 持有了锁 B,然后线程 A 尝试去获取锁 B,当然它获取不到,因为线程 B 还没有释放锁 B。然后线程 B 又来尝试获取锁 A,同样线程 B 也获取不到锁 A,因为锁 A 已经被线程 A 持有了。这样一来,线程 A 和线程 B 就发生了死锁,因为它们都相互持有对方想要的资源,却又不释放自己手中的资源,形成相互等待,而且会一直等待下去。 **多个线程造成死锁的情况** 死锁不仅仅存在于两个线程的场景,在多个线程中也同样存在。如果多个线程之间的依赖关系是环形,存在环路的依赖关系,那么也可能会发生死锁,如下图所示: +此时我们有两个线程,分别是线程 A 和线程 B,假设线程 A 现在持有了锁 A,线程 B 持有了锁 B,然后线程 A 尝试去获取锁 B,当然它获取不到,因为线程 B 还没有释放锁 B。然后线程 B 又来尝试获取锁 A,同样线程 B 也获取不到锁 A,因为锁 A 已经被线程 A 持有了。这样一来,线程 A 和线程 B 就发生了死锁,因为它们都相互持有对方想要的资源,却又不释放自己手中的资源,形成相互等待,而且会一直等待下去。**多个线程造成死锁的情况** 死锁不仅仅存在于两个线程的场景,在多个线程中也同样存在。如果多个线程之间的依赖关系是环形,存在环路的依赖关系,那么也可能会发生死锁,如下图所示: ![img](assets/Cgq2xl6NrzGAeQrqAAA0YIeU1Qg392.png) @@ -22,11 +22,11 @@ #### 死锁的影响 -死锁的影响在不同系统中是不一样的,影响的大小一部分取决于当前这个系统或者环境对死锁的处理能力。 **数据库中** 例如,在数据库系统软件的设计中,考虑了监测死锁以及从死锁中恢复的情况。在执行一个事务的时候可能需要获取多把锁,并一直持有这些锁直到事务完成。在某个事务中持有的锁可能在其他事务中也需要,因此在两个事务之间有可能发生死锁的情况,一旦发生了死锁,如果没有外部干涉,那么两个事务就会永远的等待下去。但数据库系统不会放任这种情况发生,当数据库检测到这一组事务发生了死锁时,根据策略的不同,可能会选择 **放弃某一个事务** ,被放弃的事务就会释放掉它所持有的锁,从而使其他的事务继续顺利进行。此时程序可以重新执行被强行终止的事务,而这个事务现在就可以顺利执行了,因为所有跟它竞争资源的事务都已经在刚才执行完毕,并且释放资源了。 **JVM 中** 在 JVM 中,对于死锁的处理能力就不如数据库那么强大了。如果在 JVM 中发生了死锁,JVM 并 **不会自动进行处理** ,所以一旦死锁发生,就会陷入无穷的等待。 +死锁的影响在不同系统中是不一样的,影响的大小一部分取决于当前这个系统或者环境对死锁的处理能力。**数据库中** 例如,在数据库系统软件的设计中,考虑了监测死锁以及从死锁中恢复的情况。在执行一个事务的时候可能需要获取多把锁,并一直持有这些锁直到事务完成。在某个事务中持有的锁可能在其他事务中也需要,因此在两个事务之间有可能发生死锁的情况,一旦发生了死锁,如果没有外部干涉,那么两个事务就会永远的等待下去。但数据库系统不会放任这种情况发生,当数据库检测到这一组事务发生了死锁时,根据策略的不同,可能会选择 **放弃某一个事务**,被放弃的事务就会释放掉它所持有的锁,从而使其他的事务继续顺利进行。此时程序可以重新执行被强行终止的事务,而这个事务现在就可以顺利执行了,因为所有跟它竞争资源的事务都已经在刚才执行完毕,并且释放资源了。**JVM 中** 在 JVM 中,对于死锁的处理能力就不如数据库那么强大了。如果在 JVM 中发生了死锁,JVM 并 **不会自动进行处理**,所以一旦死锁发生,就会陷入无穷的等待。 #### 几率不高但危害大 -死锁的问题和其他的并发安全问题一样,是概率性的,也就是说,即使存在发生死锁的可能性,也并不是 100% 会发生的。如果每个锁的持有时间很短,那么发生冲突的概率就很低,所以死锁发生的概率也很低。但是在线上系统里,可能每天有几千万次的“获取锁”、“释放锁”操作, **在巨量的次数面前,整个系统发生问题的几率就会被放大** ,只要有某几次操作是有风险的,就可能会导致死锁的发生。 +死锁的问题和其他的并发安全问题一样,是概率性的,也就是说,即使存在发生死锁的可能性,也并不是 100% 会发生的。如果每个锁的持有时间很短,那么发生冲突的概率就很低,所以死锁发生的概率也很低。但是在线上系统里,可能每天有几千万次的“获取锁”、“释放锁”操作,**在巨量的次数面前,整个系统发生问题的几率就会被放大**,只要有某几次操作是有风险的,就可能会导致死锁的发生。 也正是因为死锁“不一定会发生”的特点,导致提前找出死锁成为了一个难题。压力测试虽然可以检测出一部分可能发生死锁的情况,但是并不足以完全模拟真实、长期运行的场景,因此 **没有办法把所有潜在可能发生死锁的代码都找出来** 。 @@ -112,7 +112,7 @@ public class MustDeadLock implements Runnable { - 当线程 1 的 500 毫秒休眠时间结束后,它将尝试去获取 o2 这把锁,此时 o2 这个锁正被线程 2 持有,所以线程 1 无法获取到的 o2。 ![img](assets/Ciqah16NrzKAcColAAA2HwmEHwg667.png) - 紧接着线程 2 也会苏醒过来,它将尝试获取 o1 这把锁,此时 o1 已被线程 1 持有。 ![img](assets/Cgq2xl6NrzKAWAc5AAA1lPZZeKo398.png) -所以现在的状态是, **线程 1 卡在获取 o2 这把锁的位置,而线程 2 卡在获取 o1 这把锁的位置** ,这样一来线程 1 和线程 2 就形成了相互等待,需要对方持有的资源才能继续执行,从而形成了死锁。在这个例子里,如果线程 2 比线程 1 先启动,情况也是类似的,最终也会形成死锁。这就是一个“必然发生死锁的例子”。 +所以现在的状态是,**线程 1 卡在获取 o2 这把锁的位置,而线程 2 卡在获取 o1 这把锁的位置**,这样一来线程 1 和线程 2 就形成了相互等待,需要对方持有的资源才能继续执行,从而形成了死锁。在这个例子里,如果线程 2 比线程 1 先启动,情况也是类似的,最终也会形成死锁。这就是一个“必然发生死锁的例子”。 ![img](assets/Ciqah16NrzKAQ0EzAABXlJN0J2Q517.png) diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25468\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25468\350\256\262.md" index 4696b2c9e..cb8f48060 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25468\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25468\350\256\262.md" @@ -6,10 +6,10 @@ 要想发生死锁有 4 个缺一不可的必要条件,我们一个个来看: -- 第 1 个叫 **互斥条件** ,它的意思是每个资源每次只能被一个线程(或进程,下同)使用,为什么资源不能同时被多个线程或进程使用呢?这是因为如果每个人都可以拿到想要的资源,那就不需要等待,所以是不可能发生死锁的。 -- 第 2 个是 **请求与保持条件** ,它是指当一个线程因请求资源而阻塞时,则需对已获得的资源保持不放。如果在请求资源时阻塞了,并且会自动释放手中资源(例如锁)的话,那别人自然就能拿到我刚才释放的资源,也就不会形成死锁。 -- 第 3 个是 **不剥夺条件** ,它是指线程已获得的资源,在未使用完之前,不会被强行剥夺。比如我们在上一课时中介绍的数据库的例子,它就有可能去强行剥夺某一个事务所持有的资源,这样就不会发生死锁了。所以要想发生死锁,必须满足不剥夺条件,也就是说当现在的线程获得了某一个资源后,别人就不能来剥夺这个资源,这才有可能形成死锁。 -- 第 4 个是 **循环等待条件** ,只有若干线程之间形成一种头尾相接的循环等待资源关系时,才有可能形成死锁,比如在两个线程之间,这种“循环等待”就意味着它们互相持有对方所需的资源、互相等待;而在三个或更多线程中,则需要形成环路,例如依次请求下一个线程已持有的资源等。 +- 第 1 个叫 **互斥条件**,它的意思是每个资源每次只能被一个线程(或进程,下同)使用,为什么资源不能同时被多个线程或进程使用呢?这是因为如果每个人都可以拿到想要的资源,那就不需要等待,所以是不可能发生死锁的。 +- 第 2 个是 **请求与保持条件**,它是指当一个线程因请求资源而阻塞时,则需对已获得的资源保持不放。如果在请求资源时阻塞了,并且会自动释放手中资源(例如锁)的话,那别人自然就能拿到我刚才释放的资源,也就不会形成死锁。 +- 第 3 个是 **不剥夺条件**,它是指线程已获得的资源,在未使用完之前,不会被强行剥夺。比如我们在上一课时中介绍的数据库的例子,它就有可能去强行剥夺某一个事务所持有的资源,这样就不会发生死锁了。所以要想发生死锁,必须满足不剥夺条件,也就是说当现在的线程获得了某一个资源后,别人就不能来剥夺这个资源,这才有可能形成死锁。 +- 第 4 个是 **循环等待条件**,只有若干线程之间形成一种头尾相接的循环等待资源关系时,才有可能形成死锁,比如在两个线程之间,这种“循环等待”就意味着它们互相持有对方所需的资源、互相等待;而在三个或更多线程中,则需要形成环路,例如依次请求下一个线程已持有的资源等。 ### 案例解析 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25469\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25469\350\256\262.md" index cbfad80df..bd621ee22 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25469\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25469\350\256\262.md" @@ -2,13 +2,13 @@ 本课时我们主要介绍“如何用命令和代码来定位死锁”。 -在此之前,我们介绍了什么是死锁,以及死锁发生的必要条件。当然,即便我们很小心地编写代码,也必不可免地依然有可能会发生死锁,一旦死锁发生, **第一步要做的就是把它给找到** ,因为在找到并定位到死锁之后,才能有接下来的补救措施,比如解除死锁、解除死锁之后恢复、对代码进行优化等;若找不到死锁的话,后面的步骤就无从谈起了。 +在此之前,我们介绍了什么是死锁,以及死锁发生的必要条件。当然,即便我们很小心地编写代码,也必不可免地依然有可能会发生死锁,一旦死锁发生,**第一步要做的就是把它给找到**,因为在找到并定位到死锁之后,才能有接下来的补救措施,比如解除死锁、解除死锁之后恢复、对代码进行优化等;若找不到死锁的话,后面的步骤就无从谈起了。 下面就来看一下是如何用命令行的方式找到死锁的。 ### 命令:jstack -这个命令叫作 jstack,它能看到我们 Java 线程的一些相关信息。如果是比较明显的死锁关系,那么这个工具就可以直接检测出来;如果死锁不明显,那么它无法直接检测出来,不过我们也可以 **借此来分析线程状态,进而就可以发现锁的相互依赖关系** ,所以这也是很有利于我们找到死锁的方式。 +这个命令叫作 jstack,它能看到我们 Java 线程的一些相关信息。如果是比较明显的死锁关系,那么这个工具就可以直接检测出来;如果死锁不明显,那么它无法直接检测出来,不过我们也可以 **借此来分析线程状态,进而就可以发现锁的相互依赖关系**,所以这也是很有利于我们找到死锁的方式。 我们就来试一试,执行这个命令。 @@ -163,7 +163,7 @@ public class DetectDeadLock implements Runnable { 这个类是在前面 MustDeadLock 类的基础上做了升级,MustDeadLock 类的主要作用就是让线程 1 和线程 2 分别以不同的顺序来获取到 o1 和 o2 这两把锁,并且形成死锁。在 main 函数中,在启动 t1 和 t2 之后的代码,是我们本次新加入的代码,我们用 Thread.sleep(1000) 来确保已经形成死锁,然后利用 ThreadMXBean 来检查死锁。 -通过 ThreadMXBean 的 findDeadlockedThreads 方法,可以获取到一个 deadlockedThreads 的数组,然后进行判断,当这个数组不为空且长度大于 0 的时候,我们逐个打印出对应的线程信息。比如我们打印出了 **线程 id,也打印出了线程名,同时打印出了它所需要的那把锁正被哪个线程所持有** ,那么这一部分代码的运行结果如下。 +通过 ThreadMXBean 的 findDeadlockedThreads 方法,可以获取到一个 deadlockedThreads 的数组,然后进行判断,当这个数组不为空且长度大于 0 的时候,我们逐个打印出对应的线程信息。比如我们打印出了 **线程 id,也打印出了线程名,同时打印出了它所需要的那把锁正被哪个线程所持有**,那么这一部分代码的运行结果如下。 ```java t1 flag = 1 @@ -174,7 +174,7 @@ t2 flag = 2 一共有四行语句,前两行是“t1 flag = 1“、“t2 flag = 2”,这是发生死锁之前所打印出来的内容;然后的两行语句就是我们检测到的死锁的结果,可以看到,它打印出来的是“线程 id 为 12,线程名为 t2 的线程已经发生了死锁,需要的锁正被线程 t1 持有。”同样的,它也会打印出“线程 id 为 11,线程名为 t1 的线程已经发生死锁,需要的锁正被线程 t2 持有。” -可以看出,ThreadMXBean 也可以帮我们找到并定位死锁,如果我们在业务代码中加入这样的检测,那我们就可以在发生死锁的时候及时地定位, **同时进行报警等其他处理** ,也就增强了我们程序的健壮性。 +可以看出,ThreadMXBean 也可以帮我们找到并定位死锁,如果我们在业务代码中加入这样的检测,那我们就可以在发生死锁的时候及时地定位,**同时进行报警等其他处理**,也就增强了我们程序的健壮性。 ### 总结 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25470\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25470\350\256\262.md" index 0750a3ae5..934edcbad 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25470\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25470\350\256\262.md" @@ -4,7 +4,7 @@ ### 线上发生死锁应该怎么办 -如果线上环境发生了死锁,那么其实不良后果就已经造成了,修复死锁的 **最好时机在于“防患于未然”** ,而不是事后补救。就好比发生火灾时,一旦着了大火,想要不造成损失去扑灭几乎已经不可能了。死锁也是一样的,如果线上发生死锁问题,为了尽快减小损失,最好的办法是保存 JVM 信息、日志等“案发现场”的数据,然后 **立刻重启服务** ,来尝试修复死锁。为什么说重启服务能解决这个问题呢?因为发生死锁往往要有很多前提条件的,并且当并发度足够高的时候才有可能会发生死锁,所以 **重启后再次立刻发生死锁的几率并不是很大** ,当我们重启服务器之后,就可以暂时保证线上服务的可用,然后利用刚才保存过的案发现场的信息, **排查死锁、修改代码,最终重新发布** 。 +如果线上环境发生了死锁,那么其实不良后果就已经造成了,修复死锁的 **最好时机在于“防患于未然”**,而不是事后补救。就好比发生火灾时,一旦着了大火,想要不造成损失去扑灭几乎已经不可能了。死锁也是一样的,如果线上发生死锁问题,为了尽快减小损失,最好的办法是保存 JVM 信息、日志等“案发现场”的数据,然后 **立刻重启服务**,来尝试修复死锁。为什么说重启服务能解决这个问题呢?因为发生死锁往往要有很多前提条件的,并且当并发度足够高的时候才有可能会发生死锁,所以 **重启后再次立刻发生死锁的几率并不是很大**,当我们重启服务器之后,就可以暂时保证线上服务的可用,然后利用刚才保存过的案发现场的信息,**排查死锁、修改代码,最终重新发布** 。 ### 常见修复策略 @@ -18,7 +18,7 @@ #### 避免策略 -**如何避免** 避免策略最主要的思路就是, **优化代码逻辑,从根本上消除发生死锁的可能性** 。通常而言,发生死锁的一个主要原因是顺序相反的去获取不同的锁。因此我们就演示如何通过 **调整锁的获取顺序** 来避免死锁。 **转账时避免死锁** 我们先来看一下转账时发生死锁的情况。这个例子是一个示意性的,是为了学习死锁所而写的例子,所以和真实的银行系统的设计有很大不同,不过没关系,因为我们主要看的是如何避免死锁,而不是转账的业务逻辑。 **(1)发生了死锁** 我们的转账系统为了保证线程安全, **在转账前需要首先获取到两把锁** (两个锁对象),分别是被转出的账户和被转入的账户。如果不做这一层限制,那么在某一个线程修改余额的期间,可能会有其他线程同时修改该变量,可能导致线程安全问题。所以在没有获取到这两把锁之前,是不能对余额进行操作的;只有获取到这两把锁之后,才能进行接下来真正的转账操作。当然,如果要转出的余额大于账户的余额,也不能转账,因为不允许余额变成负数。 +**如何避免** 避免策略最主要的思路就是,**优化代码逻辑,从根本上消除发生死锁的可能性** 。通常而言,发生死锁的一个主要原因是顺序相反的去获取不同的锁。因此我们就演示如何通过 **调整锁的获取顺序** 来避免死锁。**转账时避免死锁** 我们先来看一下转账时发生死锁的情况。这个例子是一个示意性的,是为了学习死锁所而写的例子,所以和真实的银行系统的设计有很大不同,不过没关系,因为我们主要看的是如何避免死锁,而不是转账的业务逻辑。**(1)发生了死锁** 我们的转账系统为了保证线程安全,**在转账前需要首先获取到两把锁** (两个锁对象),分别是被转出的账户和被转入的账户。如果不做这一层限制,那么在某一个线程修改余额的期间,可能会有其他线程同时修改该变量,可能导致线程安全问题。所以在没有获取到这两把锁之前,是不能对余额进行操作的;只有获取到这两把锁之后,才能进行接下来真正的转账操作。当然,如果要转出的余额大于账户的余额,也不能转账,因为不允许余额变成负数。 而这期间就隐藏着发生死锁的可能,我们来看下代码: @@ -90,7 +90,7 @@ a的余额500 b的余额500 ``` -代码是可以正常执行的,打印结果也是符合逻辑的。此时并没有发生死锁,因为 **每个锁的持有时间很短,同时释放也很快** ,所以在低并发的情况下,不容易发生死锁的现象。那我们对代码做一些小调整,让它发生死锁。 +代码是可以正常执行的,打印结果也是符合逻辑的。此时并没有发生死锁,因为 **每个锁的持有时间很短,同时释放也很快**,所以在低并发的情况下,不容易发生死锁的现象。那我们对代码做一些小调整,让它发生死锁。 如果我们在两个 synchronized 之间加上一个 Thread.sleep(500),来模拟银行 **网络迟延** 等情况,那么 transferMoney 方法就变为: @@ -118,7 +118,7 @@ public static void transferMoney(Account from, Account to, int amount) { 可以看到 transferMoney 的变化就在于,在两个 synchronized 之间,也就是获取到第一把锁后、获取到第二把锁前,我们加了睡眠 500 毫秒的语句。此时再运行程序,会有很大的概率发生死锁,从而导致 **控制台中不打印任何语句,而且程序也不会停止** 。 -我们分析一下它为什么会发生死锁,最主要原因就是,两个不同的线程 **获取两个锁的顺序是相反的** (第一个线程获取的这两个账户和第二个线程获取的这两个账户顺序恰好相反, **第一个线程的“转出账户”正是第二个线程的“转入账户”** ),所以我们就可以从这个“相反顺序”的角度出发,来解决死锁问题。 **(2)实际上不在乎获取锁的顺序** 经过思考,我们可以发现,其实转账时,并不在乎两把锁的相对获取顺序。转账的时候,我们无论先获取到转出账户锁对象,还是先获取到转入账户锁对象,只要最终能拿到两把锁,就能进行安全的操作。所以我们来调整一下获取锁的顺序,使得先获取的账户和该账户是“转入”或“转出”无关,而是 **使用 HashCode 的值来决定顺序** ,从而保证线程安全。 +我们分析一下它为什么会发生死锁,最主要原因就是,两个不同的线程 **获取两个锁的顺序是相反的** (第一个线程获取的这两个账户和第二个线程获取的这两个账户顺序恰好相反,**第一个线程的“转出账户”正是第二个线程的“转入账户”** ),所以我们就可以从这个“相反顺序”的角度出发,来解决死锁问题。**(2)实际上不在乎获取锁的顺序** 经过思考,我们可以发现,其实转账时,并不在乎两把锁的相对获取顺序。转账的时候,我们无论先获取到转出账户锁对象,还是先获取到转入账户锁对象,只要最终能拿到两把锁,就能进行安全的操作。所以我们来调整一下获取锁的顺序,使得先获取的账户和该账户是“转入”或“转出”无关,而是 **使用 HashCode 的值来决定顺序**,从而保证线程安全。 修复之后的 transferMoney 方法如下: @@ -154,19 +154,19 @@ public static void transferMoney(Account from, Account to, int amount) { } ``` -可以看到,我们会分别计算出这两个 Account 的 HashCode,然后根据 HashCode 的大小来决定获取锁的顺序。这样一来,不论是哪个线程先执行,不论是转出还是被转入,它获取锁的顺序都会严格根据 HashCode 的值来决定,那么 **大家获取锁的顺序就一样了,就不会出现获取锁顺序相反的情况** ,也就避免了死锁。 **(3)有主键就更安全、方便** 下面我们看一下用主键决定锁获取顺序的方式,它会更加的安全方便。刚才我们使用了 HashCode 作为排序的标准,因为 HashCode 比较通用,每个对象都有,不过这依然有极小的概率会发生 HashCode 相同的情况。在实际生产中,需要排序的往往是一个实体类,而一个实体类一般都会有一个主键 ID, **主键 ID 具有唯一、不重复的特点** ,所以如果我们这个类包含主键属性的话就方便多了,我们也没必要去计算 HashCode,直接使用它的主键 ID 来进行排序,由主键 ID 大小来决定获取锁的顺序,就可以确保避免死锁。 +可以看到,我们会分别计算出这两个 Account 的 HashCode,然后根据 HashCode 的大小来决定获取锁的顺序。这样一来,不论是哪个线程先执行,不论是转出还是被转入,它获取锁的顺序都会严格根据 HashCode 的值来决定,那么 **大家获取锁的顺序就一样了,就不会出现获取锁顺序相反的情况**,也就避免了死锁。**(3)有主键就更安全、方便** 下面我们看一下用主键决定锁获取顺序的方式,它会更加的安全方便。刚才我们使用了 HashCode 作为排序的标准,因为 HashCode 比较通用,每个对象都有,不过这依然有极小的概率会发生 HashCode 相同的情况。在实际生产中,需要排序的往往是一个实体类,而一个实体类一般都会有一个主键 ID,**主键 ID 具有唯一、不重复的特点**,所以如果我们这个类包含主键属性的话就方便多了,我们也没必要去计算 HashCode,直接使用它的主键 ID 来进行排序,由主键 ID 大小来决定获取锁的顺序,就可以确保避免死锁。 以上我们介绍了死锁的避免策略。 #### 检测与恢复策略 -下面我们再来看第二个策略,那就是检测与恢复策略。 **什么是死锁检测算法** 它和之前避免死锁的策略不一样,避免死锁是通过逻辑让死锁不发生,而这里的检测与恢复策略,是 **先允许系统发生死锁,然后再解除** 。例如系统可以在每次调用锁的时候,都记录下来调用信息,形成一个“锁的调用链路图”,然后隔一段时间就用死锁检测算法来检测一下,搜索这个图中是否存在环路,一旦发生死锁,就可以用死锁恢复机制,比如剥夺某一个资源,来解开死锁,进行恢复。所以它的思路和之前的死锁避免策略是有很大不同的。 +下面我们再来看第二个策略,那就是检测与恢复策略。**什么是死锁检测算法** 它和之前避免死锁的策略不一样,避免死锁是通过逻辑让死锁不发生,而这里的检测与恢复策略,是 **先允许系统发生死锁,然后再解除** 。例如系统可以在每次调用锁的时候,都记录下来调用信息,形成一个“锁的调用链路图”,然后隔一段时间就用死锁检测算法来检测一下,搜索这个图中是否存在环路,一旦发生死锁,就可以用死锁恢复机制,比如剥夺某一个资源,来解开死锁,进行恢复。所以它的思路和之前的死锁避免策略是有很大不同的。 -在检测到死锁发生后,如何解开死锁呢? **方法1——线程终止** 第一种解开死锁的方法是线程(或进程,下同)终止,在这里,系统会逐个去终止已经陷入死锁的线程,线程被终止,同时释放资源,这样死锁就会被解开。 +在检测到死锁发生后,如何解开死锁呢?**方法1——线程终止** 第一种解开死锁的方法是线程(或进程,下同)终止,在这里,系统会逐个去终止已经陷入死锁的线程,线程被终止,同时释放资源,这样死锁就会被解开。 -当然这个终止是需要讲究顺序的,一般有以下几个考量指标。 **(1)优先级** 一般来说,终止时会考虑到线程或者进程的优先级,先终止优先级低的线程。例如,前台线程会涉及界面显示,这对用户而言是很重要的,所以前台线程的优先级往往高于后台线程。 **(2)已占用资源、还需要的资源** 同时也会考虑到某个线程占有的资源有多少,还需要的资源有多少?如果某线程已经占有了一大堆资源,只需要最后一点点资源就可以顺利完成任务,那么系统可能就不会优先选择终止这样的线程,会选择终止别的线程来优先促成该线程的完成。 **(3)已经运行时间** 另外还可以考虑的一个因素就是已经运行的时间,比如当前这个线程已经运行了很多个小时,甚至很多天了,很快就能完成任务了,那么终止这个线程可能不是一个明智的选择,我们可以让那些刚刚开始运行的线程终止,并在之后把它们重新启动起来,这样成本更低。 +当然这个终止是需要讲究顺序的,一般有以下几个考量指标。**(1)优先级** 一般来说,终止时会考虑到线程或者进程的优先级,先终止优先级低的线程。例如,前台线程会涉及界面显示,这对用户而言是很重要的,所以前台线程的优先级往往高于后台线程。**(2)已占用资源、还需要的资源** 同时也会考虑到某个线程占有的资源有多少,还需要的资源有多少?如果某线程已经占有了一大堆资源,只需要最后一点点资源就可以顺利完成任务,那么系统可能就不会优先选择终止这样的线程,会选择终止别的线程来优先促成该线程的完成。**(3)已经运行时间** 另外还可以考虑的一个因素就是已经运行的时间,比如当前这个线程已经运行了很多个小时,甚至很多天了,很快就能完成任务了,那么终止这个线程可能不是一个明智的选择,我们可以让那些刚刚开始运行的线程终止,并在之后把它们重新启动起来,这样成本更低。 -这里会有各种各样的算法和策略,我们根据实际业务去进行调整就可以了。 **方法2——资源抢占** 第二个解开死锁的方法就是资源抢占。其实,我们不需要把整个的线程终止,而是只需要把它已经获得的资源进行剥夺,比如让线程回退几步、 释放资源,这样一来就不用终止掉整个线程了,这样造成的后果会比刚才终止整个线程的后果更小一些, **成本更低** 。 +这里会有各种各样的算法和策略,我们根据实际业务去进行调整就可以了。**方法2——资源抢占** 第二个解开死锁的方法就是资源抢占。其实,我们不需要把整个的线程终止,而是只需要把它已经获得的资源进行剥夺,比如让线程回退几步、 释放资源,这样一来就不用终止掉整个线程了,这样造成的后果会比刚才终止整个线程的后果更小一些,**成本更低** 。 当然这种方式也有一个缺点,那就是如果算法不好的话,我们抢占的那个线程可能一直是同一个线程,就会造成 **线程饥饿** 。也就是说,这个线程一直被剥夺它已经得到的资源,那么它就长期得不到运行。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25471\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25471\350\256\262.md" index 95455248d..38749d54f 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25471\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25471\350\256\262.md" @@ -8,11 +8,11 @@ ![img](assets/Ciqah16ZbtKAZGebAAXzyFyJIXc351.png) -有 5 个哲学家,他们面前都有一双筷子,即左手有一根筷子,右手有一根筷子。当然,这个问题有多个版本的描述,可以说是筷子,也可以说是一刀一叉,因为吃牛排的时候,需要刀和叉,缺一不可,也有说是用两把叉子来吃意大利面。这里具体是刀叉还是筷子并不重要,重要的是 **必须要同时持有左右两边的两个才行** ,也就是说,哲学家左手要拿到一根筷子,右手也要拿到一根筷子,在这种情况下哲学家才能吃饭。为了方便理解,我们选取和我国传统最贴近的筷子来说明这个问题。 +有 5 个哲学家,他们面前都有一双筷子,即左手有一根筷子,右手有一根筷子。当然,这个问题有多个版本的描述,可以说是筷子,也可以说是一刀一叉,因为吃牛排的时候,需要刀和叉,缺一不可,也有说是用两把叉子来吃意大利面。这里具体是刀叉还是筷子并不重要,重要的是 **必须要同时持有左右两边的两个才行**,也就是说,哲学家左手要拿到一根筷子,右手也要拿到一根筷子,在这种情况下哲学家才能吃饭。为了方便理解,我们选取和我国传统最贴近的筷子来说明这个问题。 为什么选择哲学家呢?因为哲学家的特点是喜欢思考,所以我们可以把哲学家一天的行为抽象为 **思考,然后吃饭,并且他们吃饭的时候要用一双筷子,而不能只用一根筷子** 。 -**1. 主流程** 我们来看一下哲学家就餐的主流程。哲学家如果想吃饭,他会先尝试拿起左手的筷子,然后再尝试拿起右手的筷子,如果某一根筷子被别人使用了,他就得等待他人用完,用完之后他人自然会把筷子放回原位,接着他把筷子拿起来就可以吃了(不考虑卫生问题)。这就是哲学家就餐的最主要流程。 **2. 流程的伪代码** 我们来看一下这个流程的伪代码,如下所示: +**1. 主流程** 我们来看一下哲学家就餐的主流程。哲学家如果想吃饭,他会先尝试拿起左手的筷子,然后再尝试拿起右手的筷子,如果某一根筷子被别人使用了,他就得等待他人用完,用完之后他人自然会把筷子放回原位,接着他把筷子拿起来就可以吃了(不考虑卫生问题)。这就是哲学家就餐的最主要流程。**2. 流程的伪代码** 我们来看一下这个流程的伪代码,如下所示: ```java while(true) { @@ -38,7 +38,7 @@ while(true) 代表整个是一个无限循环。在每个循环中,哲学家 根据我们的逻辑规定,在拿起左手边的筷子之后,下一步是去拿右手的筷子。大部分情况下,右边的哲学家正在思考,所以当前哲学家的右手边的筷子是空闲的,或者如果右边的哲学家正在吃饭,那么当前的哲学家就等右边的哲学家吃完饭并释放筷子,于是当前哲学家就能拿到了他右手边的筷子了。 -但是,如果每个哲学家都同时拿起左手的筷子,那么就形成了环形依赖,在这种特殊的情况下, **每个人都拿着左手的筷子,都缺少右手的筷子,那么就没有人可以开始吃饭了** ,自然也就没有人会放下手中的筷子。这就陷入了死锁,形成了一个相互等待的情况。代码如下所示: +但是,如果每个哲学家都同时拿起左手的筷子,那么就形成了环形依赖,在这种特殊的情况下,**每个人都拿着左手的筷子,都缺少右手的筷子,那么就没有人可以开始吃饭了**,自然也就没有人会放下手中的筷子。这就陷入了死锁,形成了一个相互等待的情况。代码如下所示: ```java public class DiningPhilosophers { @@ -114,13 +114,13 @@ public class DiningPhilosophers { 哲学家2号 拿起左边的筷子 ``` -哲学家 1、3、2、4、5 几乎同时开始思考,然后,假设他们思考的时间比较相近,于是他们都 **在几乎同一时刻想开始吃饭,都纷纷拿起左手的筷子,这时就陷入了死锁状态** ,没有人可以拿到右手的筷子,也就没有人可以吃饭,于是陷入了无穷等待,这就是经典的哲学家就餐问题。 +哲学家 1、3、2、4、5 几乎同时开始思考,然后,假设他们思考的时间比较相近,于是他们都 **在几乎同一时刻想开始吃饭,都纷纷拿起左手的筷子,这时就陷入了死锁状态**,没有人可以拿到右手的筷子,也就没有人可以吃饭,于是陷入了无穷等待,这就是经典的哲学家就餐问题。 ### 多种解决方案 对于这个问题我们该如何解决呢?有多种解决方案,这里我们讲讲其中的几种。前面我们讲过,要想解决死锁问题,只要破坏死锁四个必要条件的任何一个都可以。 -**1. 服务员检查** 第一个解决方案就是引入服务员检查机制。比如我们引入一个服务员,当每次哲学家要吃饭时,他需要先询问服务员:我现在能否去拿筷子吃饭?此时,服务员先判断他拿筷子有没有发生死锁的可能,假如有的话,服务员会说:现在不允许你吃饭。这是一种解决方案。 **2. 领导调节** 我们根据上一讲的死锁 **检测和恢复策略** ,可以引入一个领导,这个领导进行定期巡视。如果他发现已经发生死锁了,就会剥夺某一个哲学家的筷子,让他放下。这样一来,由于这个人的牺牲,其他的哲学家就都可以吃饭了。这也是一种解决方案。 **3. 改变一个哲学家拿筷子的顺序** 我们还可以利用 **死锁避免** 策略,那就是从逻辑上去避免死锁的发生,比如改变其中一个哲学家拿筷子的顺序。我们可以让 4 个哲学家都先拿左边的筷子再拿右边的筷子,但是 **有一名哲学家与他们相反,他是先拿右边的再拿左边的** ,这样一来就不会出现循环等待同一边筷子的情况,也就不会发生死锁了。 +**1. 服务员检查** 第一个解决方案就是引入服务员检查机制。比如我们引入一个服务员,当每次哲学家要吃饭时,他需要先询问服务员:我现在能否去拿筷子吃饭?此时,服务员先判断他拿筷子有没有发生死锁的可能,假如有的话,服务员会说:现在不允许你吃饭。这是一种解决方案。**2. 领导调节** 我们根据上一讲的死锁 **检测和恢复策略**,可以引入一个领导,这个领导进行定期巡视。如果他发现已经发生死锁了,就会剥夺某一个哲学家的筷子,让他放下。这样一来,由于这个人的牺牲,其他的哲学家就都可以吃饭了。这也是一种解决方案。**3. 改变一个哲学家拿筷子的顺序** 我们还可以利用 **死锁避免** 策略,那就是从逻辑上去避免死锁的发生,比如改变其中一个哲学家拿筷子的顺序。我们可以让 4 个哲学家都先拿左边的筷子再拿右边的筷子,但是 **有一名哲学家与他们相反,他是先拿右边的再拿左边的**,这样一来就不会出现循环等待同一边筷子的情况,也就不会发生死锁了。 ### 死锁解决 @@ -146,7 +146,7 @@ public static void main(String[] args) { } ``` -在这里最主要的变化是,我们实例化哲学家对象的时候,传入的参数原本都是先传入左边的筷子再传入右边的,但是当我们发现他是最后一个哲学家的时候,也就是 if (i == philosophers.length - 1) ,在这种情况下,我们给它传入的筷子顺序恰好相反,这样一来,他拿筷子的顺序也就相反了, **他会先拿起右边的筷子,再拿起左边的筷子** 。那么这个程序运行的结果,是所有哲学家都可以正常地去进行思考和就餐了,并且不会发生死锁。 +在这里最主要的变化是,我们实例化哲学家对象的时候,传入的参数原本都是先传入左边的筷子再传入右边的,但是当我们发现他是最后一个哲学家的时候,也就是 if (i == philosophers.length - 1) ,在这种情况下,我们给它传入的筷子顺序恰好相反,这样一来,他拿筷子的顺序也就相反了,**他会先拿起右边的筷子,再拿起左边的筷子** 。那么这个程序运行的结果,是所有哲学家都可以正常地去进行思考和就餐了,并且不会发生死锁。 ### 总结 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25472\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25472\350\256\262.md" index 455012f94..d45725f03 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25472\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25472\350\256\262.md" @@ -4,7 +4,7 @@ ### final 的作用 -final 是 Java 中的一个关键字,简而言之,final 的作用意味着“ **这是无法改变的** ”。不过由于 final 关键字一共有三种用法,它可以用来修饰 **变量** 、 **方法** 或者 **类** ,而且在修饰不同的地方时,效果、含义和侧重点也会有所不同,所以我们需要把这三种情况分开介绍。 +final 是 Java 中的一个关键字,简而言之,final 的作用意味着“ **这是无法改变的** ”。不过由于 final 关键字一共有三种用法,它可以用来修饰 **变量** 、 **方法** 或者 **类**,而且在修饰不同的地方时,效果、含义和侧重点也会有所不同,所以我们需要把这三种情况分开介绍。 我们先来看一下 final 修饰变量的情况。 @@ -12,7 +12,7 @@ final 是 Java 中的一个关键字,简而言之,final 的作用意味着 #### 作用 -关键字 final 修饰变量的作用是很明确的,那就是意味着这个变量 **一旦被赋值就不能被修改了** ,也就是说只能被赋值一次,直到天涯海角也不会“变心”。如果我们尝试对一个已经赋值过 final 的变量再次赋值,就会报编译错误。 +关键字 final 修饰变量的作用是很明确的,那就是意味着这个变量 **一旦被赋值就不能被修改了**,也就是说只能被赋值一次,直到天涯海角也不会“变心”。如果我们尝试对一个已经赋值过 final 的变量再次赋值,就会报编译错误。 我们来看下面这段代码示例: @@ -43,7 +43,7 @@ public static final int YEAR = 2021; 这个时候其实 YEAR 是固定写死的,所以我们为了防止它被修改,就给它加上了 final 关键字,这样可以让这个常量更加清晰,也更不容易出错。 -第二个目的是从 **线程安全的角度** 去考虑的。 **不可变** 的对象天生就是线程安全的,所以不需要我们额外进行同步等处理,这些开销是没有的。如果 final 修饰的是 **基本数据类型** ,那么它自然就具备了不可变这个性质,所以自动保证了线程安全,这样的话,我们未来去使用它也就非常放心了。 +第二个目的是从 **线程安全的角度** 去考虑的。**不可变** 的对象天生就是线程安全的,所以不需要我们额外进行同步等处理,这些开销是没有的。如果 final 修饰的是 **基本数据类型**,那么它自然就具备了不可变这个性质,所以自动保证了线程安全,这样的话,我们未来去使用它也就非常放心了。 这就是我们使用 final 去修饰变量的两个目的。 @@ -151,7 +151,7 @@ class StaticFieldAssignment2 { 在这个类中有一个变量 private static final int a,然后有一个 static,接着是大括号,这是静态初始代码块的语法,在这里面我们对 a 进行了赋值,这种赋值时机也是允许的。以上就是静态 final 变量的两种赋值时机。 -需要注意的是,我们不能用普通的非静态初始代码块来给静态的 final 变量赋值。同样有一点比较特殊的是,这个 **static 的 final 变量不能在构造函数中进行赋值** 。 **(3)局部变量** 局部变量指的是方法中的变量,如果你把它修饰为了 final,它的含义依然是 **一旦赋值就不能改变** 。 +需要注意的是,我们不能用普通的非静态初始代码块来给静态的 final 变量赋值。同样有一点比较特殊的是,这个 **static 的 final 变量不能在构造函数中进行赋值** 。**(3)局部变量** 局部变量指的是方法中的变量,如果你把它修饰为了 final,它的含义依然是 **一旦赋值就不能改变** 。 但是它的赋值时机和前两种变量是不一样的,因为它是在方法中定义的,所以它没有构造函数,也同样不存在初始代码块,所以对应的这两种赋值时机就都不存在了。实际上,对于 final 的局部变量而言,它是不限定具体赋值时机的,只要求我们 **在使用之前必须对它进行赋值** 即可。 @@ -186,7 +186,7 @@ class LocalVarAssignment3 { 第三种情况就是先创造出一个 final int a,并且不在等号右边对它进行赋值,然后在使用之前对 a 进行赋值,最后再使用它,这也是允许的。 -总结一下,对于这种局部变量的 final 变量而言,它的赋值时机就是 **要求在使用之前进行赋值** ,否则使用一个未赋值的变量,自然会报错。 +总结一下,对于这种局部变量的 final 变量而言,它的赋值时机就是 **要求在使用之前进行赋值**,否则使用一个未赋值的变量,自然会报错。 #### 特殊用法:final 修饰参数 @@ -210,9 +210,9 @@ public class FinalPara { ### final 修饰方法 -下面来看一看 final 修饰方法的情况。选择用 final 修饰方法的原因之一是为了 **提高效率** ,因为在早期的 Java 版本中,会把 final 方法转为内嵌调用,可以消除方法调用的开销,以提高程序的运行效率。不过在后期的 Java 版本中,JVM 会对此自动进行优化,所以不需要我们程序员去使用 final 修饰方法来进行这些优化了,即便使用也不会带来性能上的提升。 +下面来看一看 final 修饰方法的情况。选择用 final 修饰方法的原因之一是为了 **提高效率**,因为在早期的 Java 版本中,会把 final 方法转为内嵌调用,可以消除方法调用的开销,以提高程序的运行效率。不过在后期的 Java 版本中,JVM 会对此自动进行优化,所以不需要我们程序员去使用 final 修饰方法来进行这些优化了,即便使用也不会带来性能上的提升。 -目前我们使用 final 去修饰方法的唯一原因,就是想把这个方法锁定,意味着任何继承类都不能修改这个方法的含义,也就是说,被 final 修饰的方法 **不可以被重写** ,不能被 override。我们来举一个代码的例子: +目前我们使用 final 去修饰方法的唯一原因,就是想把这个方法锁定,意味着任何继承类都不能修改这个方法的含义,也就是说,被 final 修饰的方法 **不可以被重写**,不能被 override。我们来举一个代码的例子: ```java /** @@ -260,7 +260,7 @@ class SubClass2 extends PrivateFinalMethod { 在这个代码例子中,首先有个 PrivateFinalMethod 类,它有个 final 修饰的方法,但是注意这个方法是 private 的,接下来,下面的 SubClass2 extends 第一个 PrivateFinalMethod 类,也就是说继承了第一个类;然后子类中又写了一个 private final void privateEat() 方法,而且这个时候编译是通过的,也就是说,子类有一个方法名字叫 privateEat,而且是 final 修饰的。同样的,这个方法一模一样的出现在了父类中,那是不是说这个子类 SubClass2 成功的重写了父类的 privateEat 方法呢?是不是意味着我们之前讲的“被 final 修饰的方法,不可被重写”,这个结论是有问题的呢? -其实我们之前讲的结论依然是对的,但是类中的所有 private 方法都是隐式的指定为自动被 final 修饰的,我们额外的给它加上 final 关键字并不能起到任何效果。由于我们这个方法是 private 类型的,所以对于子类而言,根本就获取不到父类的这个方法,就更别说重写了。在上面这个代码例子中,其实 **子类并没有真正意义上的去重写父类的 privateEat 方法** ,子类和父类的这两个 privateEat 方法彼此之间是独立的,只是方法名碰巧一样而已。 +其实我们之前讲的结论依然是对的,但是类中的所有 private 方法都是隐式的指定为自动被 final 修饰的,我们额外的给它加上 final 关键字并不能起到任何效果。由于我们这个方法是 private 类型的,所以对于子类而言,根本就获取不到父类的这个方法,就更别说重写了。在上面这个代码例子中,其实 **子类并没有真正意义上的去重写父类的 privateEat 方法**,子类和父类的这两个 privateEat 方法彼此之间是独立的,只是方法名碰巧一样而已。 为了证明这一点,我们尝试在子类的 privateEat 方法上加个 Override 注解,这个时候就会提示“Method does not override method from its superclass”,意思是“该方法没有重写父类的方法”,就证明了 **这不是一次真正的重写** 。 @@ -292,7 +292,7 @@ public final class FinalClassDemo { 这里有一个注意点,那就是如果我们真的要使用 final 类或者方法的话,需要注明原因。为什么呢?因为未来代码的维护者,他可能不是很理解为什么我们在这里使用了 final,因为使用后,对他来说是有影响的,比如用 final 修饰方法,那他就不能去重写了,或者说我们用 final 修饰了类,那他就不能去继承了。 -所以为了防止后续维护者有困惑,我们其实是 **有必要或者说有义务说明原因** ,这样也不至于发生后续维护上的一些问题。 +所以为了防止后续维护者有困惑,我们其实是 **有必要或者说有义务说明原因**,这样也不至于发生后续维护上的一些问题。 在很多情况下,我们并需要不急着把这个类或者方法声明为 final,可以到开发的中后期再去决定这件事情,这样的话,我们就能更清楚的明白各个类之间的交互方式,或者是各个方法之间的关系。所以你可能会发现根本就不需要去使用 final 来修饰,或者不需要把范围扩得太大,我们可以重构代码,把 final 应用在更小范围的类或方法上,这样造成更小的影响。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25473\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25473\350\256\262.md" index ccaac73c3..1c4d79289 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25473\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25473\350\256\262.md" @@ -4,7 +4,7 @@ #### 什么是不变性 -要想回答上面的问题,我们首先得知道什么是不变性(Immutable)。 **如果对象在被创建之后,其状态就不能修改了,那么它就具备“不变性”。** 我们举个例子,比如下面这个 Person 类: +要想回答上面的问题,我们首先得知道什么是不变性(Immutable)。**如果对象在被创建之后,其状态就不能修改了,那么它就具备“不变性”。** 我们举个例子,比如下面这个 Person 类: ```java public class Person { @@ -94,13 +94,13 @@ class Test { 这个 Test 类中有一个 int 类型的 p 属性,我们在 main 函数中新建了 Test 的实例 t 之后,把它用 final 修饰,然后去尝试改它里面成员变量 p 的值,并打印出结果,程序会打印出“30”。一开始 p 的值是 20,但是最后修改完毕变成了 30,说明这次修改是成功的。 -以上我们就得出了一个结论, **final 修饰一个指向对象的变量的时候,对象本身的内容依然是可以变化的** 。 +以上我们就得出了一个结论,**final 修饰一个指向对象的变量的时候,对象本身的内容依然是可以变化的** 。 #### final 和不可变的关系 这里就引申出一个问题,那就是 final 和不变性究竟是什么关系? -那我们就来具体对比一下 final 和不变性。关键字 final 可以确保变量的引用保持不变,但是不变性意味着对象一旦创建完毕就不能改变其状态, **它强调的是对象内容本身,而不是引用** ,所以 final 和不变性这两者是很不一样的。 +那我们就来具体对比一下 final 和不变性。关键字 final 可以确保变量的引用保持不变,但是不变性意味着对象一旦创建完毕就不能改变其状态,**它强调的是对象内容本身,而不是引用**,所以 final 和不变性这两者是很不一样的。 对于一个类的对象而言,你必须要保证它创建之后所有内部状态(包括它的成员变量的内部属性等)永远不变,才是具有不变性的,这就要求所有成员变量的状态都不允许发生变化。 @@ -117,7 +117,7 @@ Person 类里面有 final int id 和 final int age 两个属性,都是基本 但是如果一个类里面有一个 final 修饰的成员变量,并且这个成员变量不是基本类型,而是对象类型,那么情况就不一样了。有了前面基础之后,我们知道,对于对象类型的属性而言,我们如果给它加了 final,它内部的成员变量还是可以变化的,因为 final 只能保证其引用不变,不能保证其内容不变。所以这个时候 **若一旦某个对象类型的内容发生了变化,就意味着这整个类都不具备不变性了** 。 -所以我们就得出了这个结论: **不变性并不意味着,简单地使用 final 修饰所有类的属性,这个类的对象就具备不变性了。** 那就会有一个很大的疑问,假设我的类里面有一个 **对象类型的成员变量** ,那要怎样做才能保证整个对象是不可变的呢? +所以我们就得出了这个结论: **不变性并不意味着,简单地使用 final 修饰所有类的属性,这个类的对象就具备不变性了。** 那就会有一个很大的疑问,假设我的类里面有一个 **对象类型的成员变量**,那要怎样做才能保证整个对象是不可变的呢? 我们来举个例子,即 **一个包含对象类型的成员变量的类的对象,具备不可变性的例子** 。 @@ -139,7 +139,7 @@ public class ImmutableDemo { 在这个类中有一个 final 修饰的、且也是 private 修饰的的一个 Set 对象,叫作 lessons,它是个 HashSet;然后我们在构造函数中往这个 HashSet 里面加了三个值,分别是第 01、02、03 讲的题目;类中还有一个方法,即 isLesson,去判断传入的参数是不是属于本课前 3 讲的标题,isLesson 方法就是利用 lessons.contains 方法去判断的,如果包含就返回 true,否则返回 false。这个类的内容就是这些了,没有其他额外的代码了。 -在这种情况下,尽管 lessons 是 Set 类型的,尽管它是一个对象,但是对于 ImmutableDemo 类的对象而言,就是具备不变性的。因为 lessons 对象是 final 且 private 的,所以引用不会变,且外部也无法访问它,而且 ImmutableDemo 类也没有任何方法可以去修改 lessons 里包含的内容,只是在构造函数中对 lessons 添加了初始值,所以 ImmutableDemo 对象一旦创建完成,也就是一旦执行完构造方法,后面就再没有任何机会可以修改 lessons 里面的数据了。而对于 ImmutableDemo 类而言, **它就只有这么一个成员变量,而这个成员变量一旦构造完毕之后又不能变** ,所以就使得这个 ImmutableDemo 类的对象是具备不变性的,这就是一个很好的“ **包含对象类型的成员变量的类的对象,具备不可变性** ”的例子。 +在这种情况下,尽管 lessons 是 Set 类型的,尽管它是一个对象,但是对于 ImmutableDemo 类的对象而言,就是具备不变性的。因为 lessons 对象是 final 且 private 的,所以引用不会变,且外部也无法访问它,而且 ImmutableDemo 类也没有任何方法可以去修改 lessons 里包含的内容,只是在构造函数中对 lessons 添加了初始值,所以 ImmutableDemo 对象一旦创建完成,也就是一旦执行完构造方法,后面就再没有任何机会可以修改 lessons 里面的数据了。而对于 ImmutableDemo 类而言,**它就只有这么一个成员变量,而这个成员变量一旦构造完毕之后又不能变**,所以就使得这个 ImmutableDemo 类的对象是具备不变性的,这就是一个很好的“ **包含对象类型的成员变量的类的对象,具备不可变性** ”的例子。 #### 总结 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25474\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25474\350\256\262.md" index de63f4da8..e1c9a1cf8 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25474\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25474\350\256\262.md" @@ -4,7 +4,7 @@ ### String 是不可变的 -我们先来介绍一下“String 是不可变的”这件事。在 Java 中, **字符串是一个常量** ,我们一旦创建了一个 String 对象,就无法改变它的值,它的内容也就不可能发生变化(不考虑反射这种特殊行为)。 +我们先来介绍一下“String 是不可变的”这件事。在 Java 中,**字符串是一个常量**,我们一旦创建了一个 String 对象,就无法改变它的值,它的内容也就不可能发生变化(不考虑反射这种特殊行为)。 举个例子,比如我们给字符串 s 赋值为“lagou”,然后再尝试给它赋一个新值,正如下面这段代码所示: @@ -35,7 +35,7 @@ public final class String } ``` -首先,可以看到这里面有个非常重要的属性,即 **private final 的 char 数组** ,数组名字叫 value。它存储着字符串的每一位字符,同时 value 数组是被 final 修饰的,也就是说,这个 value 一旦被赋值,引用就不能修改了;并且在 String 的源码中可以发现,除了构造函数之外, **并没有任何其他方法会修改 value 数组里面的内容** ,而且 value 的权限是 private,外部的类也访问不到,所以最终使得 value 是不可变的。 +首先,可以看到这里面有个非常重要的属性,即 **private final 的 char 数组**,数组名字叫 value。它存储着字符串的每一位字符,同时 value 数组是被 final 修饰的,也就是说,这个 value 一旦被赋值,引用就不能修改了;并且在 String 的源码中可以发现,除了构造函数之外,**并没有任何其他方法会修改 value 数组里面的内容**,而且 value 的权限是 private,外部的类也访问不到,所以最终使得 value 是不可变的。 那么有没有可能存在这种情况:其他类继承了 String 类,然后重写相关的方法,就可以修改 value 的值呢?这样的话它不就是可变的了吗? @@ -81,7 +81,7 @@ LAGOU #### 用作 HashMap 的 key -String 不可变的第二个好处就是它可以很方便地用作 **HashMap (或者 HashSet) 的 key** 。通常建议把 **不可变对象作为 HashMap的 key** ,比如 String 就很合适作为 HashMap 的 key。 +String 不可变的第二个好处就是它可以很方便地用作 **HashMap (或者 HashSet) 的 key** 。通常建议把 **不可变对象作为 HashMap的 key**,比如 String 就很合适作为 HashMap 的 key。 对于 key 来说,最重要的要求就是它是不可变的,这样我们才能利用它去检索存储在 HashMap 里面的 value。由于 HashMap 的工作原理是 Hash,也就是散列,所以需要对象始终拥有相同的 Hash 值才能正常运行。如果 String 是可变的,这会带来很大的风险,因为一旦 String 对象里面的内容变了,那么 Hash 码自然就应该跟着变了,若再用这个 key 去查找的话,就找不回之前那个 value 了。 @@ -96,13 +96,13 @@ String 不可变的第三个好处就是 **缓存 HashCode** 。 private int hash; ``` -这是一个成员变量,保存的是 String 对象的 HashCode。因为 String 是不可变的,所以对象一旦被创建之后,HashCode 的值也就不可能变化了,我们就可以把 HashCode 缓存起来。这样的话,以后每次想要用到 HashCode 的时候, **不需要重新计算,直接返回缓存过的 hash 的值就可以了** ,因为它不会变,这样可以提高效率,所以这就使得字符串非常适合用作 HashMap 的 key。 +这是一个成员变量,保存的是 String 对象的 HashCode。因为 String 是不可变的,所以对象一旦被创建之后,HashCode 的值也就不可能变化了,我们就可以把 HashCode 缓存起来。这样的话,以后每次想要用到 HashCode 的时候,**不需要重新计算,直接返回缓存过的 hash 的值就可以了**,因为它不会变,这样可以提高效率,所以这就使得字符串非常适合用作 HashMap 的 key。 而对于其他的不具备不变性的普通类的对象而言,如果想要去获取它的 HashCode ,就必须每次都重新算一遍,相比之下,效率就低了。 #### 线程安全 -String 不可变的第四个好处就是 **线程安全** ,因为具备 **不变性的对象一定是线程安全的** ,我们不需要对其采取任何额外的措施,就可以天然保证线程安全。 +String 不可变的第四个好处就是 **线程安全**,因为具备 **不变性的对象一定是线程安全的**,我们不需要对其采取任何额外的措施,就可以天然保证线程安全。 由于 String 是不可变的,所以它就可以非常安全地被多个线程所共享,这对于多线程编程而言非常重要,避免了很多不必要的同步操作。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25475\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25475\350\256\262.md" index 4e9e5845c..f4a192c32 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25475\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25475\350\256\262.md" @@ -14,11 +14,11 @@ #### 学习 AQS 的思路 -接下来我想介绍一下我对于学习 AQS 的思路的理解。AQS 类的内部结构要比一般的类 **复杂得多** ,里面有很多细节,不容易完全掌握,所以如果我们一上来就直接看源码,容易把自己给绕晕,容易陷入细节不能自拔,导致最后铩羽而归。 +接下来我想介绍一下我对于学习 AQS 的思路的理解。AQS 类的内部结构要比一般的类 **复杂得多**,里面有很多细节,不容易完全掌握,所以如果我们一上来就直接看源码,容易把自己给绕晕,容易陷入细节不能自拔,导致最后铩羽而归。 -其实我们大多数的程序员都是业务开发者,而不是 JDK 开发者,所以平时并不需要自己来开发类似于 ReentrantLock 这样的工具类,所以通常而言,我们 **不会直接使用到 AQS 来进行开发** ,因为 JDK 已经提供了很多封装好的线程协作工具类,像前面讲解的 ReentrantLock、Semaphore 就是 JDK 提供给我们的,其内部就用到了 AQS,而这些工具类已经基本 **足够覆盖大部分的业务场景** 了,这就使得我们即便不了解 AQS,也能利用这些工具类顺利进行开发。 +其实我们大多数的程序员都是业务开发者,而不是 JDK 开发者,所以平时并不需要自己来开发类似于 ReentrantLock 这样的工具类,所以通常而言,我们 **不会直接使用到 AQS 来进行开发**,因为 JDK 已经提供了很多封装好的线程协作工具类,像前面讲解的 ReentrantLock、Semaphore 就是 JDK 提供给我们的,其内部就用到了 AQS,而这些工具类已经基本 **足够覆盖大部分的业务场景** 了,这就使得我们即便不了解 AQS,也能利用这些工具类顺利进行开发。 -既然我们学习 AQS 的目的不是进行代码开发,那我们为什么还需要学习 AQS 呢?我认为,我们学习 AQS 的目的主要是想理解其背后的 **原理** 、学习 **设计思想** ,以 **提高技术** 并 **应对面试** 。所以本课时的主要目的是从宏观的角度去解读 AQS,比如知道为什么需要 AQS、AQS 有什么作用,在了解了宏观思想之后,再去分析它的内部结构,学习起来就轻松多了。 +既然我们学习 AQS 的目的不是进行代码开发,那我们为什么还需要学习 AQS 呢?我认为,我们学习 AQS 的目的主要是想理解其背后的 **原理** 、学习 **设计思想**,以 **提高技术** 并 **应对面试** 。所以本课时的主要目的是从宏观的角度去解读 AQS,比如知道为什么需要 AQS、AQS 有什么作用,在了解了宏观思想之后,再去分析它的内部结构,学习起来就轻松多了。 #### 锁和协作类有共同点:阀门功能 @@ -26,7 +26,7 @@ 其实它们都可以当做一个阀门来使用。比如我们把 Semaphore 的许可证数量设置为 1,那么由于它只有一个许可证,所以只能允许一个线程通过,并且当之前的线程归还许可证后,会允许其他线程继续获得许可证。其实这点和 ReentrantLock 很像,只有一个线程能获得锁,并且当这个线程释放锁之后,会允许其他的线程获得锁。那如果线程发现当前没有额外的许可证时,或者当前得不到锁,那么线程就会被阻塞,并且等到后续有许可证或者锁释放出来后,被唤醒,所以这些环节都是比较类似的。 -除了上面讲的 ReentrantLock 和 Semaphore 之外,我们会发现 CountDownLatch、ReentrantReadWriteLock 等工具类都有 **类似的让线程“协作”的功能** ,其实它们背后都是利用 AQS 来实现的。 +除了上面讲的 ReentrantLock 和 Semaphore 之外,我们会发现 CountDownLatch、ReentrantReadWriteLock 等工具类都有 **类似的让线程“协作”的功能**,其实它们背后都是利用 AQS 来实现的。 #### 为什么需要 AQS @@ -42,7 +42,7 @@ 如果没有 AQS,那就需要每个线程协作工具类自己去实现至少以下内容,包括: -- **状态的原子性管理** - **线程的阻塞与解除阻塞** - **队列的管理** 这里的状态对于不同的工具类而言,代表不同的含义,比如对于 ReentrantLock 而言,它需要维护 **锁被重入的次数** ,但是保存重入次数的变量是会被多线程同时操作的,就需要进行处理,以便保证线程安全。不仅如此,对于那些未抢到锁的线程,还应该让它们陷入阻塞,并进行排队,并在合适的时机唤醒。所以说这些内容其实是比较繁琐的,而且也是比较重复的,而这些工作目前都由 AQS 来承担了。 +- **状态的原子性管理** - **线程的阻塞与解除阻塞** - **队列的管理** 这里的状态对于不同的工具类而言,代表不同的含义,比如对于 ReentrantLock 而言,它需要维护 **锁被重入的次数**,但是保存重入次数的变量是会被多线程同时操作的,就需要进行处理,以便保证线程安全。不仅如此,对于那些未抢到锁的线程,还应该让它们陷入阻塞,并进行排队,并在合适的时机唤醒。所以说这些内容其实是比较繁琐的,而且也是比较重复的,而这些工作目前都由 AQS 来承担了。 如果没有 AQS,就需要 ReentrantLock 等类来自己实现相关的逻辑,但是让每个线程协作工具类自己去正确并且高效地实现这些内容,是相当有难度的。AQS 可以帮我们把 “脏活累活” 都搞定,所以对于 ReentrantLock 和 Semaphore 等类而言,它们只需要关注自己特有的业务逻辑即可。正所谓是“哪有什么岁月静好,不过是有人替你负重前行”。 @@ -58,7 +58,7 @@ ![并发3.png](assets/Ciqah16meL6AWGzVAAEpniT-r2k348.png) -乍看起来,群面和单面的面试规则是很不一样的:前者是多人一起面试,而后者是逐个面试。但也其实, **群面和单面也有很多相同的地方** (或者称为流程或环节),而这些相同的地方往往都是由 HR 负责的。比如面试者来了,HR 需要安排候选人 **签到、就坐等待、排队,然后 HR 要按顺序叫号** ,从而避免发生多个候选人冲突的情况,同时 HR 还要确保等待的同学最终都会被叫到,这一系列的内容都由 HR 负责,而这些内容无论是单面还是群面都是一样的。这些 HR 在面试中所做的工作,其实就可以比作是 AQS 所干的活儿。 +乍看起来,群面和单面的面试规则是很不一样的:前者是多人一起面试,而后者是逐个面试。但也其实,**群面和单面也有很多相同的地方** (或者称为流程或环节),而这些相同的地方往往都是由 HR 负责的。比如面试者来了,HR 需要安排候选人 **签到、就坐等待、排队,然后 HR 要按顺序叫号**,从而避免发生多个候选人冲突的情况,同时 HR 还要确保等待的同学最终都会被叫到,这一系列的内容都由 HR 负责,而这些内容无论是单面还是群面都是一样的。这些 HR 在面试中所做的工作,其实就可以比作是 AQS 所干的活儿。 至于具体的面试规则,比如群面规则是 5 个人还是 10 个人一起?是单面还是群?这些是由面试官来安排的。对于面试官而言,他不会关心候选人是否号码冲突、如何等待、如何叫号,是否有休息的场地等,因为这是 HR 的职责范围。 @@ -66,11 +66,11 @@ 群面的流程类似于 CountDownLatch,CountDownLatch 会先设置需要倒数的初始值,假设为 10,每来一个候选人,计数减 1,如果 10 个人都到齐了,就开始面试。同样,单面可以理解为是 Semaphore 信号量,假设有 5 个许可证,每个线程每次获取 1 个许可证,这就类似于有 5 个面试官并行面试,候选人在面试之前需要先获得许可证,面试结束后归还许可证。 -对于 CountDownLatch 和 Semaphore 等工具类而言,它要确定自己的“要人”规则,是凑齐 10 个候选人一起面试,像群面一样呢?还是出 1 进 1,像单面一样呢?确定了规则之后,剩下的类似招呼面试者(类比于调度线程)等一系列工作可以交给 AQS 来做,这样一来, **各自的职责就非常独立且分明了** 。 +对于 CountDownLatch 和 Semaphore 等工具类而言,它要确定自己的“要人”规则,是凑齐 10 个候选人一起面试,像群面一样呢?还是出 1 进 1,像单面一样呢?确定了规则之后,剩下的类似招呼面试者(类比于调度线程)等一系列工作可以交给 AQS 来做,这样一来,**各自的职责就非常独立且分明了** 。 #### AQS 的作用 -好,在有了上面的理解之后,现在我们来总结一下 AQS 的作用。 **AQS 是一个用于构建锁、同步器等线程协作工具类的框架** ,有了 AQS 以后,很多用于线程协作的工具类就都可以很方便的被写出来,有了 AQS 之后,可以让更上层的开发极大的减少工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题,因为 AQS 把这些事情都做好了。总之,有了 AQS 之后,我们构建线程协作工具类就容易多了。 +好,在有了上面的理解之后,现在我们来总结一下 AQS 的作用。**AQS 是一个用于构建锁、同步器等线程协作工具类的框架**,有了 AQS 以后,很多用于线程协作的工具类就都可以很方便的被写出来,有了 AQS 之后,可以让更上层的开发极大的减少工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题,因为 AQS 把这些事情都做好了。总之,有了 AQS 之后,我们构建线程协作工具类就容易多了。 #### 总结 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25476\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25476\350\256\262.md" index 6d8b627e9..90998f51e 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25476\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25476\350\256\262.md" @@ -19,7 +19,7 @@ private volatile int state; ``` -而 state 的含义并不是一成不变的,它会 **根据具体实现类的作用不同而表示不同的含义** ,下面举几个例子。 +而 state 的含义并不是一成不变的,它会 **根据具体实现类的作用不同而表示不同的含义**,下面举几个例子。 比如说在信号量里面,state 表示的是剩余 **许可证的数量** 。如果我们最开始把 state 设置为 10,这就代表许可证初始一共有 10 个,然后当某一个线程取走一个许可证之后,这个 state 就会变为 9,所以信号量的 state 相当于是一个内部计数器。 @@ -55,7 +55,7 @@ protected final void setState(int newState) { 我们可以看到,它去修改 state 值的时候非常直截了当,直接把 state = newState,这样就直接赋值了。你可能会感到困惑,这里并没有进行任何的并发安全处理,没有加锁也没有 CAS,那如何能保证线程安全呢? -这里就要说到 volatile 的作用了,前面在学习 volatile 关键字的时候,知道了它适用于两种场景,其中一种场景就是,当 **对基本类型的变量进行直接赋值时** ,如果加了 volatile 就可以保证它的线程安全。注意,这是 volatile 的非常典型的使用场景。 +这里就要说到 volatile 的作用了,前面在学习 volatile 关键字的时候,知道了它适用于两种场景,其中一种场景就是,当 **对基本类型的变量进行直接赋值时**,如果加了 volatile 就可以保证它的线程安全。注意,这是 volatile 的非常典型的使用场景。 ```java /** @@ -70,7 +70,7 @@ private volatile int state; #### FIFO 队列 -下面我们再来看看 AQS 的第二个核心部分, **FIFO 队列** ,即先进先出队列,这个队列最主要的作用是存储等待的线程。假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,那怎么去处理这些抢不到锁的线程呢?就得需要有一个队列来存放、管理它们。所以 AQS 的一大功能就是充当线程的“ **排队管理器** ”。 +下面我们再来看看 AQS 的第二个核心部分,**FIFO 队列**,即先进先出队列,这个队列最主要的作用是存储等待的线程。假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,那怎么去处理这些抢不到锁的线程呢?就得需要有一个队列来存放、管理它们。所以 AQS 的一大功能就是充当线程的“ **排队管理器** ”。 当多个线程去竞争同一把锁的时候,就需要用 **排队机制** 把那些没能拿到锁的线程串在一起;而当前面的线程释放锁之后,这个管理器就会挑选一个合适的线程来尝试抢刚刚释放的那把锁。所以 AQS 就一直在维护这个队列,并把等待的线程都放到队列里面。 @@ -84,7 +84,7 @@ private volatile int state; #### 获取/释放方法 -下面我们就来看一看 AQS 的第三个核心部分,获取/释放方法。在 AQS 中除了刚才讲过的 state 和队列之外,还有一部分非常重要,那就是 **获取和释放相关的重要方法** ,这些方法是协作工具类的 **逻辑** 的 **具体体现** ,需要每一个协作工具类 **自己去实现** ,所以在不同的工具类中,它们的实现和含义各不相同。 +下面我们就来看一看 AQS 的第三个核心部分,获取/释放方法。在 AQS 中除了刚才讲过的 state 和队列之外,还有一部分非常重要,那就是 **获取和释放相关的重要方法**,这些方法是协作工具类的 **逻辑** 的 **具体体现**,需要每一个协作工具类 **自己去实现**,所以在不同的工具类中,它们的实现和含义各不相同。 ##### 获取方法 @@ -96,7 +96,7 @@ private volatile int state; 再举个例子,CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state 减为 0,此时就代表现在这个门闩放开了,所以之前阻塞的线程就会被唤醒。 -我们总结一下,“获取方法”在不同的类中代表不同的含义,但往往 **和 state 值相关** ,也经常会让线程进入 **阻塞** 状态,这也同样证明了 state 状态在 AQS 类中的重要地位。 +我们总结一下,“获取方法”在不同的类中代表不同的含义,但往往 **和 state 值相关**,也经常会让线程进入 **阻塞** 状态,这也同样证明了 state 状态在 AQS 类中的重要地位。 ##### 释放方法 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25477\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25477\350\256\262.md" index d2fc4179b..d9e3e2b35 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25477\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25477\350\256\262.md" @@ -6,9 +6,9 @@ 我们先讲一下 AQS 的用法。如果想使用 AQS 来写一个自己的线程协作工具类,通常而言是分为以下三步,这也是 JDK 里 **利用 AQS 类的主要步骤** : -- **第一步** ,新建一个自己的线程协作工具类,在内部写一个 Sync 类,该 Sync 类继承 AbstractQueuedSynchronizer,即 AQS; -- **第二步** ,想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则重写 tryAcquire 和 tryRelease 等方法;如果是非独占,则重写 tryAcquireShared 和 tryReleaseShared 等方法; -- **第三步** ,在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。 +- **第一步**,新建一个自己的线程协作工具类,在内部写一个 Sync 类,该 Sync 类继承 AbstractQueuedSynchronizer,即 AQS; +- **第二步**,想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则重写 tryAcquire 和 tryRelease 等方法;如果是非独占,则重写 tryAcquireShared 和 tryReleaseShared 等方法; +- **第三步**,在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。 通过这三步就可以实现对 AQS 的利用了。由于这三个步骤是经过浓缩和提炼的,所以现在你可能感觉有些不太容易理解,我们后面会有具体的实例来帮助理解,这里先有一个初步的印象即可。 @@ -79,7 +79,7 @@ public class CountDownLatch { 可以很明显看到最开始一个 Sync 类继承了 AQS,这正是上一节所讲的“第一步,新建一个自己的线程协作工具类,在内部写一个 Sync 类,该 Sync 类继承 AbstractQueuedSynchronizer,即 AQS”。而在 CountDownLatch 里面还有一个 sync 的变量,正是 Sync 类的一个对象。 -同时,我们看到,Sync 不但继承了 AQS 类,而且 **还重写了 tryAcquireShared 和 tryReleaseShared 方法** ,这正对应了“第二步,想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则重写 tryAcquire 或 tryRelease 等方法;如果是非独占,则重写 tryAcquireShared 和 tryReleaseShared 等方法”。 +同时,我们看到,Sync 不但继承了 AQS 类,而且 **还重写了 tryAcquireShared 和 tryReleaseShared 方法**,这正对应了“第二步,想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则重写 tryAcquire 或 tryRelease 等方法;如果是非独占,则重写 tryAcquireShared 和 tryReleaseShared 等方法”。 这里的 CountDownLatch 属于非独占的类型,因此它重写了 tryAcquireShared 和 tryReleaseShared 方法,那么这两个方法的具体含义是什么呢?别急,接下来就让我们对 CountDownLatch 类里面最重要的 4 个方法进行分析,逐步揭开它的神秘面纱。 @@ -112,7 +112,7 @@ protected final void setState(int newState) { } ``` -所以我们通过 CountDownLatch 构造函数将传入的 count **最终传递到 AQS 内部的 state 变量** ,给 state 赋值,state 就代表还需要倒数的次数。 +所以我们通过 CountDownLatch 构造函数将传入的 count **最终传递到 AQS 内部的 state 变量**,给 state 赋值,state 就代表还需要倒数的次数。 #### getCount @@ -220,7 +220,7 @@ protected int tryAcquireShared(int acquires) { getState 方法获取到的值是剩余需要倒数的次数,如果此时剩余倒数的次数大于 0,那么 getState 的返回值自然不等于 0,因此 tryAcquireShared 方法会返回 -1,一旦返回 -1,再看到 if (tryAcquireShared(arg) \< 0) 语句中,就会符合 if 的判断条件,并且去执行 doAcquireSharedInterruptibly 方法,然后会 **让线程进入阻塞状态** 。 -我们再来看下另一种情况,当 state 如果此时已经等于 0 了,那就意味着倒数其实结束了,不需要再去等待了,就是说门闩是打开状态,所以说此时 getState 返回 0,tryAcquireShared 方法返回 1 ,一旦返回 1,对于 acquireSharedInterruptibly 方法而言相当于立刻返回,也就意味着 await 方法会立刻返回,那么此时 **线程就不会进入阻塞状态了** ,相当于倒数已经结束,立刻放行了。 +我们再来看下另一种情况,当 state 如果此时已经等于 0 了,那就意味着倒数其实结束了,不需要再去等待了,就是说门闩是打开状态,所以说此时 getState 返回 0,tryAcquireShared 方法返回 1 ,一旦返回 1,对于 acquireSharedInterruptibly 方法而言相当于立刻返回,也就意味着 await 方法会立刻返回,那么此时 **线程就不会进入阻塞状态了**,相当于倒数已经结束,立刻放行了。 这里的 await 和 countDown 方法,正对应了本讲一开始所介绍的“第三步,在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。” diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25478\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25478\350\256\262.md" index b566e1895..803445467 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25478\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\21378\350\256\262/\347\254\25478\350\256\262.md" @@ -8,27 +8,27 @@ #### 线程基础升华 -首先对线程基础进行讲解和升华,在实现多线程上,讲解了为何本质只有 1 种 **实现线程** 的方法,并对于传统的 2 种或 3 种的说法进行了辨析;同时讲解了应该如何正确的 **停止线程** ,用 volatile 标记位的停止方法是不够全面的。 +首先对线程基础进行讲解和升华,在实现多线程上,讲解了为何本质只有 1 种 **实现线程** 的方法,并对于传统的 2 种或 3 种的说法进行了辨析;同时讲解了应该如何正确的 **停止线程**,用 volatile 标记位的停止方法是不够全面的。 -然后介绍了线程的 **6 种状态** ,即 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED,还介绍了转换路径。之后就把目光聚焦到了 **wait、notify/notifyAll、sleep** 相关的方法上,这也是面试中常考的内容,我们讲解了它们的注意事项,包括: +然后介绍了线程的 **6 种状态**,即 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED,还介绍了转换路径。之后就把目光聚焦到了 **wait、notify/notifyAll、sleep** 相关的方法上,这也是面试中常考的内容,我们讲解了它们的注意事项,包括: - 为什么 wait 方法必须在 synchronized 保护的同步代码中使用? - 为什么 wait / notify / notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中? -我们还把 wait / notify 和 sleep 进行了比较,并分析它们的异同。之后我们用三种方式实现了 **生产者和消费者模式** ,分别是 wait / notify、Condition、BlockingQueue 的方式,并对它们进行了对比。 +我们还把 wait / notify 和 sleep 进行了比较,并分析它们的异同。之后我们用三种方式实现了 **生产者和消费者模式**,分别是 wait / notify、Condition、BlockingQueue 的方式,并对它们进行了对比。 #### 线程安全 -在线程安全的相关课时中,首先讲解了 **什么是线程安全** ,线程 **不安全的场景** 包括运行结果错误、发布或初始化错误以及活跃性问题,而活跃性问题又包括死锁、活锁和饥饿。 +在线程安全的相关课时中,首先讲解了 **什么是线程安全**,线程 **不安全的场景** 包括运行结果错误、发布或初始化错误以及活跃性问题,而活跃性问题又包括死锁、活锁和饥饿。 -然后总结了 4 种特别需要 **注意线程安全的情况** ,分别是: +然后总结了 4 种特别需要 **注意线程安全的情况**,分别是: - 有操作共享资源或变量的时候; - 依赖时序的操作; - 不同数据之间存在绑定关系; - 使用的类没有声明自己是线程安全的。 -之后,讲解了多线程所带来的 **性能问题** ,包括线程调度所产生的上下文切换和缓存失效,以及线程协作带来的开销。 +之后,讲解了多线程所带来的 **性能问题**,包括线程调度所产生的上下文切换和缓存失效,以及线程协作带来的开销。 ### 模块二:玩转 JUC 并发工具 @@ -40,13 +40,13 @@ - 可以统筹内存和 CPU 的使用,避免资源使用不当; - 可以统一管理资源。 -在了解了线程池的好处之后,就需要掌握线程池的 **各个参数** 的含义,即 corePoolSize、maxPoolSize、keepAliveTime、workQueue、ThreadFactory、Handler,并且这也是 **面试中非常常见的考点** ,我们需要知道每个参数代表什么含义。 +在了解了线程池的好处之后,就需要掌握线程池的 **各个参数** 的含义,即 corePoolSize、maxPoolSize、keepAliveTime、workQueue、ThreadFactory、Handler,并且这也是 **面试中非常常见的考点**,我们需要知道每个参数代表什么含义。 而线程池也可能会 **拒绝** 我们提交的任务,我们讲解了 2 种拒绝的时机以及 4 种拒绝的策略,分别是 AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy,我们可以根据自己的业务需求去选择合适的拒绝策略。 -之后介绍了 **6 种常见的线程池** ,即 FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadExecutor、SingleThreadScheduledExecutor 和 ForkJoinPool,这 6 种线程池各有各的特点,它们所采用的的参数也各不相同。 +之后介绍了 **6 种常见的线程池**,即 FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadExecutor、SingleThreadScheduledExecutor 和 ForkJoinPool,这 6 种线程池各有各的特点,它们所采用的的参数也各不相同。 -接下来介绍了 **阻塞队列** ,在线程池中比较常用的是 3 种阻塞队列,即 LinkedBlockingQueue、SynchronousQueue、DelayedWorkQueue。然后讲解了为什么不应该自动创建线程池,主要原因是考虑到自动创建的线程池可能会发生 OOM 等风险,我们 **手动创建线程池** ,就可以更加明确其运行规则,也可以在必要的时候拒绝新的任务提交,所以是更加安全的。 +接下来介绍了 **阻塞队列**,在线程池中比较常用的是 3 种阻塞队列,即 LinkedBlockingQueue、SynchronousQueue、DelayedWorkQueue。然后讲解了为什么不应该自动创建线程池,主要原因是考虑到自动创建的线程池可能会发生 OOM 等风险,我们 **手动创建线程池**,就可以更加明确其运行规则,也可以在必要的时候拒绝新的任务提交,所以是更加安全的。 既然说到要手动去创建线程,那怎么设置线程池的参数呢?这里就需要考虑到 **合适的线程数量** 是多少,我们给出了一个通用的建议: @@ -54,7 +54,7 @@ - 线程的平均等待时间所占比例越高,则需要越多的线程; - 针对不同的程序,进行对应的压力测试就可以得到最合适的线程数。 -最后讲解了如何 **关闭线程池** ,讲解了和关闭线程池相关的 5 个方法,即 shutdown()、isShutdown()、isTerminated()、awaitTermination()、shutdownNow() 。其中的重点是 **shutdown() 和 shutdownNow()** 这两个方法的区别,前一个是优雅关闭,后一个则是立刻关闭。接着还对线程池实现“线程复用”的原理进行了讲解,同时分析了 **execute 方法的源码,这是线程池中一个非常重要的方法** 。 +最后讲解了如何 **关闭线程池**,讲解了和关闭线程池相关的 5 个方法,即 shutdown()、isShutdown()、isTerminated()、awaitTermination()、shutdownNow() 。其中的重点是 **shutdown() 和 shutdownNow()** 这两个方法的区别,前一个是优雅关闭,后一个则是立刻关闭。接着还对线程池实现“线程复用”的原理进行了讲解,同时分析了 **execute 方法的源码,这是线程池中一个非常重要的方法** 。 #### 各种各样的“锁” @@ -62,9 +62,9 @@ 如果可以,最好既不使用 Lock 也不使用 synchronized,而是优先使用 JUC 包中其他的成熟工具,因为它们通常会帮我们自动处理所有的加锁和解锁操作;如果必须使用锁,则优先使用 synchronized,因为它可以减少代码编写的数量以及降低出错的概率,因为一旦使用 Lock,就必须在 finally 中写上 unlock,不然代码可能会出很大的问题,而使用 synchronized 就不必考虑这些问题,因为它会自动解锁。当然如果 synchronized 不能满足我们的需求,就得考虑使用 Lock。 -所以接下来就是 Lock 相关的内容,它有很多强大的功能,比如尝试获取锁、有超时的获取等。我们介绍了 lock() 、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()、unlock() 这几个常用的方法,并且讲解了它们的作用。然后讲解了 **公平锁和非公平锁** ,其中公平锁会按照线程申请锁的顺序来依次获取锁,而非公平锁存在插队的情况,这在一定情况下可以提高整体的效率,通常默认也是非公平的。 +所以接下来就是 Lock 相关的内容,它有很多强大的功能,比如尝试获取锁、有超时的获取等。我们介绍了 lock() 、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()、unlock() 这几个常用的方法,并且讲解了它们的作用。然后讲解了 **公平锁和非公平锁**,其中公平锁会按照线程申请锁的顺序来依次获取锁,而非公平锁存在插队的情况,这在一定情况下可以提高整体的效率,通常默认也是非公平的。 -接着是读写锁内容。ReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率,它的规则是: **要么是一个或多个线程同时持有读锁,要么是一个线程持有写锁** ,但两者不会同时出现。也可以总结为读读共享、其他都互斥(包括写写互斥、读写互斥、写读互斥)。之后还讲解了读写锁的升降级和插队策略。 +接着是读写锁内容。ReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率,它的规则是: **要么是一个或多个线程同时持有读锁,要么是一个线程持有写锁**,但两者不会同时出现。也可以总结为读读共享、其他都互斥(包括写写互斥、读写互斥、写读互斥)。之后还讲解了读写锁的升降级和插队策略。 对于自旋锁而言,首先介绍了什么是自旋锁,然后对比了自旋和非自旋锁的获取锁的过程,讲解了自旋锁的好处,然后自己实现了一个可重入的自旋锁,最后还分析了自旋锁的缺点和适用场景。 @@ -78,7 +78,7 @@ #### 阻塞队列 -在并发容器里还有一个重点,那就是 **阻塞队列** ,首先介绍了什么是阻塞队列以及对于阻塞队列中的 3 组方法进行了辨析,同时还给出了代码演示。然后分别介绍了常见的 5 种阻塞队列,以及它们的特点,分别是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue 和 DelayQueue。 +在并发容器里还有一个重点,那就是 **阻塞队列**,首先介绍了什么是阻塞队列以及对于阻塞队列中的 3 组方法进行了辨析,同时还给出了代码演示。然后分别介绍了常见的 5 种阻塞队列,以及它们的特点,分别是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue 和 DelayQueue。 之后对比了阻塞和非阻塞队列的并发安全原理,其中阻塞队列主要利用了 ReentrantLock 以及它的 Condition 来实现的,而非阻塞队列则是利用了 CAS 保证线程安全。 @@ -101,7 +101,7 @@ 当然 ThreadLocal 并不是用来解决共享资源的多线程访问的问题的,因为它设计的本意是,资源并不是共享的,只是在每个线程内有个资源的副本而已,而每个副本都是各线程独享的。 -接下来还分析了 ThreadLocal 的内部结构,需要掌握 **Thread、ThreadLocal 及 ThreadLocalMap 三者之间的关系** ,同时还介绍了使用 ThreadLocal 之后要使用 remove 方法来防止内存泄漏。 +接下来还分析了 ThreadLocal 的内部结构,需要掌握 **Thread、ThreadLocal 及 ThreadLocalMap 三者之间的关系**,同时还介绍了使用 ThreadLocal 之后要使用 remove 方法来防止内存泄漏。 #### Future @@ -131,9 +131,9 @@ 接着首先介绍了 **重排序** 的相关内容,其好处是可以提高处理速度。 -接着介绍了 **原子性** ,包括什么是原子性、Java 中的原子操作有哪些、long 和 double 原子性的特殊性以及简单地把原子操作组合在一起,并不能保证整体依然具备原子性。 +接着介绍了 **原子性**,包括什么是原子性、Java 中的原子操作有哪些、long 和 double 原子性的特殊性以及简单地把原子操作组合在一起,并不能保证整体依然具备原子性。 -之后讲解了 **可见性** ,我们需要知道主内存和工作内存之间的关系,还需要知道 **happens-before** 关系:如果第一个操作 happens-before 第二个操作(也可以描述为第一个操作和第二个操作之间满足 happens-before 关系),那么我们就说第一个操作对于第二个操作一定是可见的,也就是第二个操作在执行时就一定能保证看见第一个操作执行的结果。 **这个关系非常重要,也是可见性内容的一个重点。** 最后介绍了 volatile 的两个作用,分别是保证可见性以及一定程度上禁止重排序,还分析了在单例模式的双重检查锁模式为什么必须加 volatile ?主要是为了保证线程安全。 +之后讲解了 **可见性**,我们需要知道主内存和工作内存之间的关系,还需要知道 **happens-before** 关系:如果第一个操作 happens-before 第二个操作(也可以描述为第一个操作和第二个操作之间满足 happens-before 关系),那么我们就说第一个操作对于第二个操作一定是可见的,也就是第二个操作在执行时就一定能保证看见第一个操作执行的结果。**这个关系非常重要,也是可见性内容的一个重点。** 最后介绍了 volatile 的两个作用,分别是保证可见性以及一定程度上禁止重排序,还分析了在单例模式的双重检查锁模式为什么必须加 volatile ?主要是为了保证线程安全。 #### CAS 原理 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25400\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25400\350\256\262.md" index 0d92cd3ed..328cfebc9 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25400\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25400\350\256\262.md" @@ -14,19 +14,19 @@ 但是深入想过之后,我坚定了写作的信心。这些年接触的大部分同学,都是工作几年后很多技术突飞猛进,却只有并发编程成为瓶颈,虽然并发相关的类库他们也熟悉,却总是写不出正确、高效的并发程序,原因在哪里?我发现很多人是因为某个地方有了盲点,忽略了一些细节,但恰恰是这些细节决定了程序的正确性和效率。 -而这个盲点有时候涉及对操作系统的理解,有时候又涉及一点硬件知识,非常复杂,如果要推荐相关图书,可能要推荐好几本,这就有点“大炮打蚊子”的感觉了,效率很差。同时图书更追求严谨性,却也因此失掉了形象性,所以阅读的过程也确实有点艰辛。 **我想,如果能够把这些问题解决,那么做这个事情应该是有意义的。** 例如,Java 里 synchronized、wait()/notify() 相关的知识很琐碎,看懂难,会用更难。但实际上 synchronized、wait()、notify() 不过是操作系统领域里管程模型的一种实现而已,Java SDK 并发包里的条件变量 Condition 也是管程里的概念,synchronized、wait()/notify()、条件变量这些知识如果单独理解,自然是管中窥豹。但是如果站在管程这个理论模型的高度,你就会发现这些知识原来这么简单,同时用起来也就得心应手了。 +而这个盲点有时候涉及对操作系统的理解,有时候又涉及一点硬件知识,非常复杂,如果要推荐相关图书,可能要推荐好几本,这就有点“大炮打蚊子”的感觉了,效率很差。同时图书更追求严谨性,却也因此失掉了形象性,所以阅读的过程也确实有点艰辛。**我想,如果能够把这些问题解决,那么做这个事情应该是有意义的。** 例如,Java 里 synchronized、wait()/notify() 相关的知识很琐碎,看懂难,会用更难。但实际上 synchronized、wait()、notify() 不过是操作系统领域里管程模型的一种实现而已,Java SDK 并发包里的条件变量 Condition 也是管程里的概念,synchronized、wait()/notify()、条件变量这些知识如果单独理解,自然是管中窥豹。但是如果站在管程这个理论模型的高度,你就会发现这些知识原来这么简单,同时用起来也就得心应手了。 管程作为一种解决并发问题的模型,是继信号量模型之后的一项重大创新,它与信号量在逻辑上是等价的(可以用管程实现信号量,也可以用信号量实现管程),但是相比之下管程更易用。而且,很多编程语言都支持管程,搞懂管程,对学习其他很多语言的并发编程有很大帮助。然而,很多人急于学习 Java 并发编程技术,却忽略了技术背后的理论和模型,而理论和模型却往往比具体的技术更为重要。 -此外,Java 经过这些年的发展,Java SDK 并发包提供了非常丰富的功能,对于初学者来说可谓是眼花缭乱,好多人觉得无从下手。但是,Java SDK 并发包乃是并发大师 Doug Lea 出品,堪称经典,它内部一定是有章可循的。那它的章法在哪里呢? **其实并发编程可以总结为三个核心问题:分工、同步、互斥。** 所谓 **分工** 指的是如何高效地拆解任务并分配给线程,而 **同步** 指的是线程之间如何协作, **互斥** 则是保证同一时刻只允许一个线程访问共享资源。Java SDK 并发包很大部分内容都是按照这三个维度组织的,例如 Fork/Join 框架就是一种分工模式,CountDownLatch 就是一种典型的同步方式,而可重入锁则是一种互斥手段。 +此外,Java 经过这些年的发展,Java SDK 并发包提供了非常丰富的功能,对于初学者来说可谓是眼花缭乱,好多人觉得无从下手。但是,Java SDK 并发包乃是并发大师 Doug Lea 出品,堪称经典,它内部一定是有章可循的。那它的章法在哪里呢?**其实并发编程可以总结为三个核心问题:分工、同步、互斥。** 所谓 **分工** 指的是如何高效地拆解任务并分配给线程,而 **同步** 指的是线程之间如何协作,**互斥** 则是保证同一时刻只允许一个线程访问共享资源。Java SDK 并发包很大部分内容都是按照这三个维度组织的,例如 Fork/Join 框架就是一种分工模式,CountDownLatch 就是一种典型的同步方式,而可重入锁则是一种互斥手段。 当把并发编程核心的问题搞清楚,再回过头来看 Java SDK 并发包,你会感觉豁然开朗,它不过是针对并发问题开发出来的工具而已,此时的 SDK 并发包可以任你“盘”了。 -而且,这三个核心问题是跨语言的,你如果要学习其他语言的并发编程类库,完全可以顺着这三个问题按图索骥。Java SDK 并发包其余的一部分则是并发容器和原子类,这些比较容易理解,属于辅助工具,其他语言里基本都能找到对应的。 **所以,你说并发编程难学吗?** 首先,难是肯定的。因为这其中涉及操作系统、CPU、内存等等多方面的知识,如果你缺少某一块,那理解起来自然困难。其次,难不难学也可能因人而异,就我的经验来看,很多人在学习并发编程的时候,总是喜欢从点出发,希望能从点里找到规律或者本质,最后却把自己绕晕了。 +而且,这三个核心问题是跨语言的,你如果要学习其他语言的并发编程类库,完全可以顺着这三个问题按图索骥。Java SDK 并发包其余的一部分则是并发容器和原子类,这些比较容易理解,属于辅助工具,其他语言里基本都能找到对应的。**所以,你说并发编程难学吗?** 首先,难是肯定的。因为这其中涉及操作系统、CPU、内存等等多方面的知识,如果你缺少某一块,那理解起来自然困难。其次,难不难学也可能因人而异,就我的经验来看,很多人在学习并发编程的时候,总是喜欢从点出发,希望能从点里找到规律或者本质,最后却把自己绕晕了。 我前面说过,并发编程并不是 Java 特有的语言特性,它是一个通用且早已成熟的领域。Java 只是根据自身情况做了实现罢了,当你理解或学习并发编程的时候,如果能够站在较高层面,系统且有体系地思考问题,那就会容易很多。 -所以,我希望这个专栏更多地谈及问题背后的本质、问题的起源,同时站在理论、模型的角度讲解 Java 并发,让你的知识更成体系,融会贯通。最终让你能够得心应手地解决各种并发难题,同时将这些知识用于其他编程语言,让你的一分辛劳三分收获。 **当然,我们要坚持下去,不能三天打鱼两天晒网,因为滴水穿石非一日之功。** +所以,我希望这个专栏更多地谈及问题背后的本质、问题的起源,同时站在理论、模型的角度讲解 Java 并发,让你的知识更成体系,融会贯通。最终让你能够得心应手地解决各种并发难题,同时将这些知识用于其他编程语言,让你的一分辛劳三分收获。**当然,我们要坚持下去,不能三天打鱼两天晒网,因为滴水穿石非一日之功。** 很多人都说学习是反人性的,开始容易,但是长久的坚持却很难。这个我也认同,我面试的时候,就经常问候选人一个问题:“工作中,有没有一件事你自己坚持了很久,并且从中获益?”如果候选人能够回答出来,那会是整个面试的加分项,因为我觉得,坚持真是一个可贵的品质,一件事情,有的人三分热度,而有的人,一做就能做一年,或者更久。你放长到时间的维度里看,这两种人,最后的成就绝对是指数级的差距。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25401\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25401\350\256\262.md" index 8a077eebe..54caba36c 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25401\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25401\350\256\262.md" @@ -44,7 +44,7 @@ 在我们了解性能指标之前,我们先来了解下哪些计算机资源会成为系统的性能瓶颈。 -**CPU** :有的应用需要大量计算,他们会长时间、不间断地占用 CPU 资源,导致其他资源无法争夺到 CPU 而响应缓慢,从而带来系统性能问题。例如,代码递归导致的无限循环,正则表达式引起的回溯,JVM 频繁的 FULL GC,以及多线程编程造成的大量上下文切换等,这些都有可能导致 CPU 资源繁忙。 **内存** :Java 程序一般通过 JVM 对内存进行分配管理,主要是用 JVM 中的堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常快,所以基本不存在读写性能瓶颈。但是由于内存成本要比磁盘高,相比磁盘,内存的存储空间又非常有限。所以当内存空间被占满,对象无法回收时,就会导致内存溢出、内存泄露等问题。 **磁盘 I/O** :磁盘相比内存来说,存储空间要大很多,但磁盘 I/O 读写的速度要比内存慢,虽然目前引入的 SSD 固态硬盘已经有所优化,但仍然无法与内存的读写速度相提并论。 **网络** :网络对于系统性能来说,也起着至关重要的作用。如果你购买过云服务,一定经历过,选择网络带宽大小这一环节。带宽过低的话,对于传输数据比较大,或者是并发量比较大的系统,网络就很容易成为性能瓶颈。 **异常** :Java 应用中,抛出异常需要构建异常栈,对异常进行捕获和处理,这个过程非常消耗系统性能。如果在高并发的情况下引发异常,持续地进行异常处理,那么系统的性能就会明显地受到影响。 **数据库** :大部分系统都会用到数据库,而数据库的操作往往是涉及到磁盘 I/O 的读写。大量的数据库读写操作,会导致磁盘 I/O 性能瓶颈,进而导致数据库操作的延迟性。对于有大量数据库读写操作的系统来说,数据库的性能优化是整个系统的核心。 **锁竞争** :在并发编程中,我们经常会需要多个线程,共享读写操作同一个资源,这个时候为了保持数据的原子性(即保证这个共享资源在一个线程写的时候,不被另一个线程修改),我们就会用到锁。锁的使用可能会带来上下文切换,从而给系统带来性能开销。JDK1.6 之后,Java 为了降低锁竞争带来的上下文切换,对 JVM 内部锁已经做了多次优化,例如,新增了偏向锁、自旋锁、轻量级锁、锁粗化、锁消除等。而如何合理地使用锁资源,优化锁资源,就需要你了解更多的操作系统知识、Java 多线程编程基础,积累项目经验,并结合实际场景去处理相关问题。 +**CPU** :有的应用需要大量计算,他们会长时间、不间断地占用 CPU 资源,导致其他资源无法争夺到 CPU 而响应缓慢,从而带来系统性能问题。例如,代码递归导致的无限循环,正则表达式引起的回溯,JVM 频繁的 FULL GC,以及多线程编程造成的大量上下文切换等,这些都有可能导致 CPU 资源繁忙。**内存** :Java 程序一般通过 JVM 对内存进行分配管理,主要是用 JVM 中的堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常快,所以基本不存在读写性能瓶颈。但是由于内存成本要比磁盘高,相比磁盘,内存的存储空间又非常有限。所以当内存空间被占满,对象无法回收时,就会导致内存溢出、内存泄露等问题。**磁盘 I/O** :磁盘相比内存来说,存储空间要大很多,但磁盘 I/O 读写的速度要比内存慢,虽然目前引入的 SSD 固态硬盘已经有所优化,但仍然无法与内存的读写速度相提并论。**网络** :网络对于系统性能来说,也起着至关重要的作用。如果你购买过云服务,一定经历过,选择网络带宽大小这一环节。带宽过低的话,对于传输数据比较大,或者是并发量比较大的系统,网络就很容易成为性能瓶颈。**异常** :Java 应用中,抛出异常需要构建异常栈,对异常进行捕获和处理,这个过程非常消耗系统性能。如果在高并发的情况下引发异常,持续地进行异常处理,那么系统的性能就会明显地受到影响。**数据库** :大部分系统都会用到数据库,而数据库的操作往往是涉及到磁盘 I/O 的读写。大量的数据库读写操作,会导致磁盘 I/O 性能瓶颈,进而导致数据库操作的延迟性。对于有大量数据库读写操作的系统来说,数据库的性能优化是整个系统的核心。**锁竞争** :在并发编程中,我们经常会需要多个线程,共享读写操作同一个资源,这个时候为了保持数据的原子性(即保证这个共享资源在一个线程写的时候,不被另一个线程修改),我们就会用到锁。锁的使用可能会带来上下文切换,从而给系统带来性能开销。JDK1.6 之后,Java 为了降低锁竞争带来的上下文切换,对 JVM 内部锁已经做了多次优化,例如,新增了偏向锁、自旋锁、轻量级锁、锁粗化、锁消除等。而如何合理地使用锁资源,优化锁资源,就需要你了解更多的操作系统知识、Java 多线程编程基础,积累项目经验,并结合实际场景去处理相关问题。 了解了上面这些基本内容,我们可以得到下面几个指标,来衡量一般系统的性能。 @@ -63,13 +63,13 @@ 在测试中,我们往往会比较注重系统接口的 TPS(每秒事务处理量),因为 TPS 体现了接口的性能,TPS 越大,性能越好。在系统中,我们也可以把吞吐量自下而上地分为两种:磁盘吞吐量和网络吞吐量。 -我们先来看 **磁盘吞吐量** ,磁盘性能有两个关键衡量指标。 +我们先来看 **磁盘吞吐量**,磁盘性能有两个关键衡量指标。 一种是 IOPS(Input/Output Per Second),即每秒的输入输出量(或读写次数),这种是指单位时间内系统能处理的 I/O 请求数量,I/O 请求通常为读或写数据操作请求,关注的是随机读写性能。适应于随机读写频繁的应用,如小文件存储(图片)、OLTP 数据库、邮件服务器。 另一种是数据吞吐量,这种是指单位时间内可以成功传输的数据量。对于大量顺序读写频繁的应用,传输大量连续数据,例如,电视台的视频编辑、视频点播 VOD(Video On Demand),数据吞吐量则是关键衡量指标。 -接下来看 **网络吞吐量** ,这个是指网络传输时没有帧丢失的情况下,设备能够接受的最大数据速率。网络吞吐量不仅仅跟带宽有关系,还跟 CPU 的处理能力、网卡、防火墙、外部接口以及 I/O 等紧密关联。而吞吐量的大小主要由网卡的处理能力、内部程序算法以及带宽大小决定。 +接下来看 **网络吞吐量**,这个是指网络传输时没有帧丢失的情况下,设备能够接受的最大数据速率。网络吞吐量不仅仅跟带宽有关系,还跟 CPU 的处理能力、网卡、防火墙、外部接口以及 I/O 等紧密关联。而吞吐量的大小主要由网卡的处理能力、内部程序算法以及带宽大小决定。 ### 计算机资源分配使用率 @@ -89,4 +89,4 @@ 回顾我自己的项目经验,有电商系统、支付系统以及游戏充值计费系统,用户级都是千万级别,且要承受各种大型抢购活动,所以我对系统的性能要求非常苛刻。除了通过观察以上指标来确定系统性能的好坏,还需要在更新迭代中,充分保障系统的稳定性。 -这里, **给你延伸一个方法,** 就是将迭代之前版本的系统性能指标作为参考标准,通过自动化性能测试,校验迭代发版之后的系统性能是否出现异常,这里就不仅仅是比较吞吐量、响应时间、负载能力等直接指标了,还需要比较系统资源的 CPU 占用率、内存使用率、磁盘 I/O、网络 I/O 等几项间接指标的变化。 +这里,**给你延伸一个方法,** 就是将迭代之前版本的系统性能指标作为参考标准,通过自动化性能测试,校验迭代发版之后的系统性能是否出现异常,这里就不仅仅是比较吞吐量、响应时间、负载能力等直接指标了,还需要比较系统资源的 CPU 占用率、内存使用率、磁盘 I/O、网络 I/O 等几项间接指标的变化。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25402\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25402\350\256\262.md" index 1bac7c767..83c074b1a 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25402\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25402\350\256\262.md" @@ -20,7 +20,7 @@ 最后看测试目标。我们的性能测试是要有目标的,这里可以通过吞吐量以及响应时间来衡量系统是否达标。不达标,就进行优化;达标,就继续加大测试的并发数,探底接口的 TPS(最大每秒事务处理量),这样做,可以深入了解到接口的性能。除了测试接口的吞吐量和响应时间以外,我们还需要循环测试可能导致性能问题的接口,观察各个服务器的 CPU、内存以及 I/O 使用率的变化。 -以上就是两种测试方法的详解。其中值得注意的是,性能测试存在干扰因子,会使测试结果不准确。所以, **我们在做性能测试时,还要注意一些问题。** ### **1. 热身问题** 当我们做性能测试时,我们的系统会运行得越来越快,后面的访问速度要比我们第一次访问的速度快上几倍。这是怎么回事呢? +以上就是两种测试方法的详解。其中值得注意的是,性能测试存在干扰因子,会使测试结果不准确。所以,**我们在做性能测试时,还要注意一些问题。** ### **1. 热身问题** 当我们做性能测试时,我们的系统会运行得越来越快,后面的访问速度要比我们第一次访问的速度快上几倍。这是怎么回事呢? 在 Java 编程语言和环境中,.java 文件编译成为 .class 文件后,机器还是无法直接运行 .class 文件中的字节码,需要通过解释器将字节码转换成本地机器码才能运行。为了节约内存和执行效率,代码最初被执行时,解释器会率先解释执行这段代码。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25403\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25403\350\256\262.md" index e981d8f04..752fd5be7 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25403\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25403\350\256\262.md" @@ -29,9 +29,9 @@ assertSame(str1==str3) ![img](assets/357f1cb1263fd0b5b3e4ccb6b971c96d.jpg) -**1. 在 Java6 以及之前的版本中** ,String 对象是对 char 数组进行了封装实现的对象,主要有四个成员变量:char 数组、偏移量 offset、字符数量 count、哈希值 hash。 +**1. 在 Java6 以及之前的版本中**,String 对象是对 char 数组进行了封装实现的对象,主要有四个成员变量:char 数组、偏移量 offset、字符数量 count、哈希值 hash。 -String 对象是通过 offset 和 count 两个属性来定位 char\[\] 数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间,但这种方式很有可能会导致内存泄漏。 **2. 从 Java7 版本开始到 Java8 版本** ,Java 对 String 类做了一些改变。String 类中不再有 offset 和 count 两个变量了。这样的好处是 String 对象占用的内存稍微少了些,同时,String.substring 方法也不再共享 char\[\],从而解决了使用该方法可能导致的内存泄漏问题。 +String 对象是通过 offset 和 count 两个属性来定位 char\[\] 数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间,但这种方式很有可能会导致内存泄漏。**2. 从 Java7 版本开始到 Java8 版本**,Java 对 String 类做了一些改变。String 类中不再有 offset 和 count 两个变量了。这样的好处是 String 对象占用的内存稍微少了些,同时,String.substring 方法也不再共享 char\[\],从而解决了使用该方法可能导致的内存泄漏问题。 **3. 从 Java9 版本开始,** 工程师将 char\[\] 字段改为了 byte\[\] 字段,又维护了一个新的属性 coder,它是一个编码格式的标识。 @@ -55,7 +55,7 @@ String 对象是通过 offset 和 count 两个属性来定位 char\[\] 数组, 当代码中使用第一种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。 -String str = new String(“abc”) 这种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象;最后,str 将引用 String 对象。 **这里附上一个你可能会想到的经典反例。** +String str = new String(“abc”) 这种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象;最后,str 将引用 String 对象。**这里附上一个你可能会想到的经典反例。** 平常编程时,对一个 String 对象 str 赋值“hello”,然后又让 str 值为“world”,这个时候 str 的值变成了“world”。那么 str 值确实改变了,为什么我还说 String 对象不可变呢? diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25404\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25404\350\256\262.md" index 69b0a123e..1aae0ba0f 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25404\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25404\350\256\262.md" @@ -110,13 +110,13 @@ text=“abbbc” regex=“ab{1,3}c” text=“abc” regex=“ab{1,3}?c” -匹配结果是“abc”,该模式下 NFA 自动机首先选择最小的匹配范围,即匹配 1 个 b 字符,因此就避免了回溯问题。 **3. 独占模式(Possessive)** 同贪婪模式一样,独占模式一样会最大限度地匹配更多内容;不同的是,在独占模式下,匹配失败就会结束匹配,不会发生回溯问题。 +匹配结果是“abc”,该模式下 NFA 自动机首先选择最小的匹配范围,即匹配 1 个 b 字符,因此就避免了回溯问题。**3. 独占模式(Possessive)** 同贪婪模式一样,独占模式一样会最大限度地匹配更多内容;不同的是,在独占模式下,匹配失败就会结束匹配,不会发生回溯问题。 还是上边的例子,在字符后面加一个“+”,就可以开启独占模式。 text=“abbc” regex=“ab{1,3}+bc” -结果是不匹配,结束匹配,不会发生回溯问题。讲到这里,你应该非常清楚了, **避免回溯的方法就是:使用懒惰模式和独占模式。** +结果是不匹配,结束匹配,不会发生回溯问题。讲到这里,你应该非常清楚了,**避免回溯的方法就是:使用懒惰模式和独占模式。** 还有开头那道“一个 split() 方法为什么会影响到 TPS”的存疑,你应该也清楚了吧? diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25407\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25407\350\256\262.md" index 47eca81e4..6fde524c7 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25407\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25407\350\256\262.md" @@ -10,11 +10,11 @@ HashMap 作为我们日常使用最频繁的容器之一,相信你一定不陌 我在 05 讲分享 List 集合类的时候,讲过 ArrayList 是基于数组的数据结构实现的,LinkedList 是基于链表的数据结构实现的,而我今天要讲的 HashMap 是基于哈希表的数据结构实现的。我们不妨一起来温习下常用的数据结构,这样也有助于你更好地理解后面地内容。 -**数组** :采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为 O(1),但在数组中间以及头部插入数据时,需要复制移动后面的元素。 **链表** :一种在物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 +**数组** :采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为 O(1),但在数组中间以及头部插入数据时,需要复制移动后面的元素。**链表** :一种在物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 链表由一系列结点(链表中每一个元素)组成,结点可以在运行时动态生成。每个结点都包含“存储数据单元的数据域”和“存储下一个结点地址的指针域”这两个部分。 -由于链表不用必须按顺序存储,所以链表在插入的时候可以达到 O(1) 的复杂度,但查找一个结点或者访问特定编号的结点需要 O(n) 的时间。 **哈希表** :根据关键码值(Key value)直接进行访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,存放记录的数组就叫做哈希表。 **树** :由 n(n≥1)个有限结点组成的一个具有层次关系的集合,就像是一棵倒挂的树。 +由于链表不用必须按顺序存储,所以链表在插入的时候可以达到 O(1) 的复杂度,但查找一个结点或者访问特定编号的结点需要 O(n) 的时间。**哈希表** :根据关键码值(Key value)直接进行访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,存放记录的数组就叫做哈希表。**树** :由 n(n≥1)个有限结点组成的一个具有层次关系的集合,就像是一棵倒挂的树。 ## HashMap 的实现结构 @@ -64,7 +64,7 @@ int threshold; final float loadFactor; ``` -LoadFactor 属性是用来间接设置 Entry 数组(哈希表)的内存空间大小,在初始 HashMap 不设置参数的情况下,默认 LoadFactor 值为 0.75。 **为什么是 0.75 这个值呢?** 这是因为对于使用链表法的哈希表来说,查找一个元素的平均时间是 O(1+n),这里的 n 指的是遍历链表的长度,因此加载因子越大,对空间的利用就越充分,这就意味着链表的长度越长,查找效率也就越低。如果设置的加载因子太小,那么哈希表的数据将过于稀疏,对空间造成严重浪费。 +LoadFactor 属性是用来间接设置 Entry 数组(哈希表)的内存空间大小,在初始 HashMap 不设置参数的情况下,默认 LoadFactor 值为 0.75。**为什么是 0.75 这个值呢?** 这是因为对于使用链表法的哈希表来说,查找一个元素的平均时间是 O(1+n),这里的 n 指的是遍历链表的长度,因此加载因子越大,对空间的利用就越充分,这就意味着链表的长度越长,查找效率也就越低。如果设置的加载因子太小,那么哈希表的数据将过于稀疏,对空间造成严重浪费。 那有没有什么办法来解决这个因链表过长而导致的查询时间复杂度高的问题呢?你可以先想想,我将在后面的内容中讲到。 @@ -95,7 +95,7 @@ Entry 数组的 Threshold 是通过初始容量和 LoadFactor 计算所得,在 假设要添加两个对象 a 和 b,如果数组长度是 16,这时对象 a 和 b 通过公式 (n - 1) & hash 运算,也就是 (16-1)&a.hashCode 和 (16-1)&b.hashCode,15 的二进制为 0000000000000000000000000001111,假设对象 A 的 hashCode 为 1000010001110001000001111000000,对象 B 的 hashCode 为 0111011100111000101000010100000,你会发现上述与运算结果都是 0。这样的哈希结果就太让人失望了,很明显不是一个好的哈希算法。 -但如果我们将 hashCode 值右移 16 位(h >>> 16 代表无符号右移 16 位),也就是取 int 类型的一半,刚好可以将该二进制数对半切开,并且使用位异或运算(如果两个数对应的位置相反,则结果为 1,反之为 0),这样的话,就能避免上面的情况发生。这就是 hash() 方法的具体实现方式。 **简而言之,就是尽量打乱 hashCode 真正参与运算的低 16 位。** +但如果我们将 hashCode 值右移 16 位(h >>> 16 代表无符号右移 16 位),也就是取 int 类型的一半,刚好可以将该二进制数对半切开,并且使用位异或运算(如果两个数对应的位置相反,则结果为 1,反之为 0),这样的话,就能避免上面的情况发生。这就是 hash() 方法的具体实现方式。**简而言之,就是尽量打乱 hashCode 真正参与运算的低 16 位。** 我再来解释下 (n - 1) & hash 是怎么设计的,这里的 n 代表哈希表的长度,哈希表习惯将长度设置为 2 的 n 次方,这样恰好可以保证 (n - 1) & hash 的计算得到的索引值总是位于 table 数组的索引之内。例如:hash=15,n=16 时,结果为 15;hash=17,n=16 时,结果为 1。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25408\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25408\350\256\262.md" index c6ab780cf..286206bab 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25408\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25408\350\256\262.md" @@ -55,7 +55,7 @@ Reader/Writer 是字符流的抽象类,这两个抽象类也派生出了若干 ### 2. 阻塞 -在传统 I/O 中,InputStream 的 read() 是一个 while 循环操作,它会一直等待数据读取,直到数据就绪才会返回。 **这就意味着如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态。** +在传统 I/O 中,InputStream 的 read() 是一个 while 循环操作,它会一直等待数据读取,直到数据就绪才会返回。**这就意味着如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态。** 在少量连接请求的情况下,使用这种方式没有问题,响应速度也很高。但在发生大量连接请求时,就需要创建大量监听线程,这时如果线程没有数据就绪就会被挂起,然后进入阻塞状态。一旦发生线程阻塞,这些线程将会不断地抢夺 CPU 资源,从而导致大量的 CPU 上下文切换,增加系统的性能开销。 @@ -95,7 +95,7 @@ NIO 很多人也称之为 Non-block I/O,即非阻塞 I/O,因为这样叫, 最开始,在应用程序调用操作系统 I/O 接口时,是由 CPU 完成分配,这种方式最大的问题是“发生大量 I/O 请求时,非常消耗 CPU“;之后,操作系统引入了 DMA(直接存储器存储),内核空间与磁盘之间的存取完全由 DMA 负责,但这种方式依然需要向 CPU 申请权限,且需要借助 DMA 总线来完成数据的复制操作,如果 DMA 总线过多,就会造成总线冲突。 -通道的出现解决了以上问题,Channel 有自己的处理器,可以完成内核空间和磁盘之间的 I/O 操作。在 NIO 中,我们读取和写入数据都要通过 Channel,由于 Channel 是双向的,所以读、写可以同时进行。 **多路复用器(Selector)** +通道的出现解决了以上问题,Channel 有自己的处理器,可以完成内核空间和磁盘之间的 I/O 操作。在 NIO 中,我们读取和写入数据都要通过 Channel,由于 Channel 是双向的,所以读、写可以同时进行。**多路复用器(Selector)** Selector 是 Java NIO 编程的基础。用于检查一个或多个 NIO Channel 的状态是否处于可读、可写。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25409\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25409\350\256\262.md" index 08e701b8e..752a03916 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25409\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25409\350\256\262.md" @@ -2,7 +2,7 @@ 你好,我是刘超。 -当前大部分后端服务都是基于微服务架构实现的。服务按照业务划分被拆分,实现了服务的解偶,但同时也带来了新的问题,不同业务之间通信需要通过接口实现调用。两个服务之间要共享一个数据对象,就需要从对象转换成二进制流,通过网络传输,传送到对方服务,再转换回对象,供服务方法调用。 **这个编码和解码过程我们称之为序列化与反序列化。** 在大量并发请求的情况下,如果序列化的速度慢,会导致请求响应时间增加;而序列化后的传输数据体积大,会导致网络吞吐量下降。所以一个优秀的序列化框架可以提高系统的整体性能。 +当前大部分后端服务都是基于微服务架构实现的。服务按照业务划分被拆分,实现了服务的解偶,但同时也带来了新的问题,不同业务之间通信需要通过接口实现调用。两个服务之间要共享一个数据对象,就需要从对象转换成二进制流,通过网络传输,传送到对方服务,再转换回对象,供服务方法调用。**这个编码和解码过程我们称之为序列化与反序列化。** 在大量并发请求的情况下,如果序列化的速度慢,会导致请求响应时间增加;而序列化后的传输数据体积大,会导致网络吞吐量下降。所以一个优秀的序列化框架可以提高系统的整体性能。 我们知道,Java 提供了 RMI 框架可以实现服务与服务之间的接口暴露和调用,RMI 中对数据对象的序列化采用的是 Java 序列化。而目前主流的微服务框架却几乎没有用到 Java 序列化,SpringCloud 用的是 Json 序列化,Dubbo 虽然兼容了 Java 序列化,但默认使用的是 Hessian 序列化。这是为什么呢? @@ -67,7 +67,7 @@ for (int i = 0; i < 100; i++) { 其实,Apache Commons Collections 就是一个第三方基础库,它扩展了 Java 标准库里的 Collection 结构,提供了很多强有力的数据结构类型,并且实现了各种集合工具类。 -实现攻击的原理就是:Apache Commons Collections 允许链式的任意的类函数反射调用,攻击者通过“实现了 Java 序列化协议”的端口,把攻击代码上传到服务器上,再由 Apache Commons Collections 里的 TransformedMap 来执行。 **那么后来是如何解决这个漏洞的呢?** 很多序列化协议都制定了一套数据结构来保存和获取对象。例如,JSON 序列化、ProtocolBuf 等,它们只支持一些基本类型和数组数据类型,这样可以避免反序列化创建一些不确定的实例。虽然它们的设计简单,但足以满足当前大部分系统的数据传输需求。 +实现攻击的原理就是:Apache Commons Collections 允许链式的任意的类函数反射调用,攻击者通过“实现了 Java 序列化协议”的端口,把攻击代码上传到服务器上,再由 Apache Commons Collections 里的 TransformedMap 来执行。**那么后来是如何解决这个漏洞的呢?** 很多序列化协议都制定了一套数据结构来保存和获取对象。例如,JSON 序列化、ProtocolBuf 等,它们只支持一些基本类型和数组数据类型,这样可以避免反序列化创建一些不确定的实例。虽然它们的设计简单,但足以满足当前大部分系统的数据传输需求。 我们也可以通过反序列化对象白名单来控制反序列化对象,可以重写 resolveClass 方法,并在该方法中校验对象名字。代码如下所示: @@ -167,9 +167,9 @@ ByteBuffer 序列化时间:6 ## 使用 Protobuf 序列化替换 Java 序列化 -目前业内优秀的序列化框架有很多,而且大部分都避免了 Java 默认序列化的一些缺陷。例如,最近几年比较流行的 FastJson、Kryo、Protobuf、Hessian 等。 **我们完全可以找一种替换掉 Java 序列化,这里我推荐使用 Protobuf 序列化框架。** Protobuf 是由 Google 推出且支持多语言的序列化框架,目前在主流网站上的序列化框架性能对比测试报告中,Protobuf 无论是编解码耗时,还是二进制流压缩大小,都名列前茅。 +目前业内优秀的序列化框架有很多,而且大部分都避免了 Java 默认序列化的一些缺陷。例如,最近几年比较流行的 FastJson、Kryo、Protobuf、Hessian 等。**我们完全可以找一种替换掉 Java 序列化,这里我推荐使用 Protobuf 序列化框架。** Protobuf 是由 Google 推出且支持多语言的序列化框架,目前在主流网站上的序列化框架性能对比测试报告中,Protobuf 无论是编解码耗时,还是二进制流压缩大小,都名列前茅。 -Protobuf 以一个 .proto 后缀的文件为基础,这个文件描述了字段以及字段类型,通过工具可以生成不同语言的数据结构文件。在序列化该数据对象的时候,Protobuf 通过.proto 文件描述来生成 Protocol Buffers 格式的编码。 **这里拓展一点,我来讲下什么是 Protocol Buffers 存储格式以及它的实现原理。** +Protobuf 以一个 .proto 后缀的文件为基础,这个文件描述了字段以及字段类型,通过工具可以生成不同语言的数据结构文件。在序列化该数据对象的时候,Protobuf 通过.proto 文件描述来生成 Protocol Buffers 格式的编码。**这里拓展一点,我来讲下什么是 Protocol Buffers 存储格式以及它的实现原理。** Protocol Buffers 是一种轻便高效的结构化数据存储格式。它使用 T-L-V(标识 - 长度 - 字段值)的数据格式来存储数据,T 代表字段的正数序列 (tag),Protocol Buffers 将对象中的每个字段和正数序列对应起来,对应关系的信息是由生成的代码来保证的。在序列化的时候用整数值来代替字段名称,于是传输流量就可以大幅缩减;L 代表 Value 的字节长度,一般也只占一个字节;V 则代表字段值经过编码后的值。这种数据格式不需要分隔符,也不需要空格,同时减少了冗余字段名。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25410\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25410\350\256\262.md" index d33808fe0..3e689460f 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25410\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25410\350\256\262.md" @@ -32,7 +32,7 @@ ## 什么是 RPC 通信 -一提到 RPC,你是否还想到 MVC、SOA 这些概念呢?如果你没有经历过这些架构的演变,这些概念就很容易混淆。 **你可以通过下面这张图来了解下这些架构的演变史。** +一提到 RPC,你是否还想到 MVC、SOA 这些概念呢?如果你没有经历过这些架构的演变,这些概念就很容易混淆。**你可以通过下面这张图来了解下这些架构的演变史。** ![img](assets/e43a8f81d76927948a73a9977643daa5.jpg) diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25411\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25411\350\256\262.md" index e44d5ab6d..d560eccf0 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25411\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25411\350\256\262.md" @@ -18,7 +18,7 @@ Tomcat 中经常被提到的一个调优就是修改线程的 I/O 模型。Tomca 网络通信中,最底层的就是内核中的网络 I/O 模型了。随着技术的发展,操作系统内核的网络模型衍生出了五种 I/O 模型,《UNIX 网络编程》一书将这五种 I/O 模型分为阻塞式 I/O、非阻塞式 I/O、I/O 复用、信号驱动式 I/O 和异步 I/O。每一种 I/O 模型的出现,都是基于前一种 I/O 模型的优化升级。 -最开始的阻塞式 I/O,它在每一个连接创建时,都需要一个用户线程来处理,并且在 I/O 操作没有就绪或结束时,线程会被挂起,进入阻塞等待状态,阻塞式 I/O 就成为了导致性能瓶颈的根本原因。 **那阻塞到底发生在套接字(socket)通信的哪些环节呢?** +最开始的阻塞式 I/O,它在每一个连接创建时,都需要一个用户线程来处理,并且在 I/O 操作没有就绪或结束时,线程会被挂起,进入阻塞等待状态,阻塞式 I/O 就成为了导致性能瓶颈的根本原因。**那阻塞到底发生在套接字(socket)通信的哪些环节呢?** 在《Unix 网络编程》中,套接字通信可以分为流式套接字(TCP)和数据报套接字(UDP)。其中 TCP 连接是我们最常用的,一起来了解下 TCP 服务端的工作流程(由于 TCP 的数据传输比较复杂,存在拆包和装包的可能,这里我只假设一次最简单的 TCP 数据传输): @@ -55,7 +55,7 @@ Tomcat 中经常被提到的一个调优就是修改线程的 I/O 模型。Tomca 如果使用用户线程轮询查看一个 I/O 操作的状态,在大量请求的情况下,这对于 CPU 的使用率无疑是种灾难。 那么除了这种方式,还有其它方式可以实现非阻塞 I/O 套接字吗? -Linux 提供了 I/O 复用函数 select/poll/epoll,进程将一个或多个读操作通过系统调用函数,阻塞在函数操作上。这样,系统内核就可以帮我们侦测多个读操作是否处于就绪状态。 **select() 函数** :它的用途是,在超时时间内,监听用户感兴趣的文件描述符上的可读可写和异常事件的发生。Linux 操作系统的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个文件描述符(fd)。 +Linux 提供了 I/O 复用函数 select/poll/epoll,进程将一个或多个读操作通过系统调用函数,阻塞在函数操作上。这样,系统内核就可以帮我们侦测多个读操作是否处于就绪状态。**select() 函数** :它的用途是,在超时时间内,监听用户感兴趣的文件描述符上的可读可写和异常事件的发生。Linux 操作系统的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个文件描述符(fd)。 ```javascript int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout) @@ -146,7 +146,7 @@ Linux 内核中的 mmap 函数可以代替 read、write 的 I/O 读写操作, ## 线程模型优化 -除了内核对网络 I/O 模型的优化,NIO 在用户层也做了优化升级。NIO 是基于事件驱动模型来实现的 I/O 操作。Reactor 模型是同步 I/O 事件处理的一种常见模型,其核心思想是将 I/O 事件注册到多路复用器上,一旦有 I/O 事件触发,多路复用器就会将事件分发到事件处理器中,执行就绪的 I/O 事件操作。 **该模型有以下三个主要组件:** +除了内核对网络 I/O 模型的优化,NIO 在用户层也做了优化升级。NIO 是基于事件驱动模型来实现的 I/O 操作。Reactor 模型是同步 I/O 事件处理的一种常见模型,其核心思想是将 I/O 事件注册到多路复用器上,一旦有 I/O 事件触发,多路复用器就会将事件分发到事件处理器中,执行就绪的 I/O 事件操作。**该模型有以下三个主要组件:** - 事件接收器 Acceptor:主要负责接收请求连接; - 事件分离器 Reactor:接收请求后,会将建立的连接注册到分离器中,依赖于循环监听多路复用器 Selector,一旦监听到事件,就会将事件 dispatch 到事件处理器; diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25412\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25412\350\256\262.md" index aadb7d2a9..777093898 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25412\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25412\350\256\262.md" @@ -150,7 +150,7 @@ Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的 偏向锁主要用来优化同一线程多次申请同一个锁的竞争。在某些情况下,大部分时间是同一个线程竞争锁资源,例如,在创建一个线程并在线程中执行循环监听的场景下,或单线程操作一个线程安全集合时,同一线程每次都需要获取和释放锁,每次操作都会发生用户态与内核态的切换。 -偏向锁的作用就是,当一个线程再次访问这个同步代码或方法时,该线程只需去对象头的 Mark Word 中去判断一下是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。 **当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标志位设置为 1,并且记录抢到锁的线程 ID,表示进入偏向锁状态。** 一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占。 **下图中红线流程部分为偏向锁获取和撤销流程:** ![img](assets/43f90d5e5ec3e9d311a84027caf44e24.png) +偏向锁的作用就是,当一个线程再次访问这个同步代码或方法时,该线程只需去对象头的 Mark Word 中去判断一下是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。**当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标志位设置为 1,并且记录抢到锁的线程 ID,表示进入偏向锁状态。** 一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占。**下图中红线流程部分为偏向锁获取和撤销流程:**![img](assets/43f90d5e5ec3e9d311a84027caf44e24.png) 因此,在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生 stop the word 后, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加 JVM 参数关闭偏向锁来调优系统性能,示例代码如下: @@ -168,7 +168,7 @@ Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的 当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。 -轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。 **下图中红线流程部分为升级轻量级锁及操作流程:** ![img](assets/6bdab4d622bec1806526425bcc3724df.png) +轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。**下图中红线流程部分为升级轻量级锁及操作流程:**![img](assets/6bdab4d622bec1806526425bcc3724df.png) ### 3. 自旋锁与重量级锁 @@ -178,7 +178,7 @@ JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁, 从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定,这里我不建议设置的重试次数过多,因为 CAS 重试操作意味着长时间地占用 CPU。 -自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 \_WaitSet 队列中。 **下图中红线流程部分为自旋后升级为重量级锁的流程:** +自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 \_WaitSet 队列中。**下图中红线流程部分为自旋后升级为重量级锁的流程:** ![img](assets/fa85ab7b61a1a7ad410b3e7158e1c05d.png) diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25418\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25418\350\256\262.md" index a6f7571ed..e911714fc 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25418\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25418\350\256\262.md" @@ -202,7 +202,7 @@ public class IOTypeTest implements Runnable { 通过测试结果,我们可以看到每个线程所花费的时间。当线程数量在 8 时,线程平均执行时间是最佳的,这个线程数量和我们的计算公式所得的结果就差不多。 -看完以上两种情况下的线程计算方法,你可能还想说,在平常的应用场景中,我们常常遇不到这两种极端情况, **那么碰上一些常规的业务操作,比如,通过一个线程池实现向用户定时推送消息的业务,我们又该如何设置线程池的数量呢?** +看完以上两种情况下的线程计算方法,你可能还想说,在平常的应用场景中,我们常常遇不到这两种极端情况,**那么碰上一些常规的业务操作,比如,通过一个线程池实现向用户定时推送消息的业务,我们又该如何设置线程池的数量呢?** 此时我们可以参考以下公式来计算线程数: diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25421\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25421\350\256\262.md" index 7a275c8d3..6e1e1cea9 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25421\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25421\350\256\262.md" @@ -148,7 +148,7 @@ C2 编译器是为长期运行的服务器端应用程序做性能调优的编 在 Java7 之前,需要根据程序的特性来选择对应的 JIT,虚拟机默认采用解释器和其中一个编译器配合工作。 -Java7 引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,我们也可以通过参数 “-client”“-server” 强制指定虚拟机的即时编译模式。 **分层编译将 JVM 的执行状态分为了 5 个层次:** +Java7 引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,我们也可以通过参数 “-client”“-server” 强制指定虚拟机的即时编译模式。**分层编译将 JVM 的执行状态分为了 5 个层次:** - 第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译; - 第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling; @@ -292,7 +292,7 @@ static class Student { ![img](assets/259bd540cca1120813146cebbebef763.jpg) -这其实是因为由于 HotSpot 虚拟机目前的实现导致栈上分配实现比较复杂,可以说,在 HotSpot 中暂时没有实现这项优化。随着即时编译器的发展与逃逸分析技术的逐渐成熟,相信不久的将来 HotSpot 也会实现这项优化功能。 **锁消除** 在非线程安全的情况下,尽量不要使用线程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性能下降。 +这其实是因为由于 HotSpot 虚拟机目前的实现导致栈上分配实现比较复杂,可以说,在 HotSpot 中暂时没有实现这项优化。随着即时编译器的发展与逃逸分析技术的逐渐成熟,相信不久的将来 HotSpot 也会实现这项优化功能。**锁消除** 在非线程安全的情况下,尽量不要使用线程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性能下降。 但实际上,在以下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。 ``` diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25442\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25442\350\256\262.md" index fd37cb8ff..84a472358 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25442\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25442\350\256\262.md" @@ -64,7 +64,7 @@ XA 规范实现的分布式事务属于二阶提交事务,顾名思义就是 一旦资源管理器挂了,就会出现一直阻塞等待的情况。类似问题,我们可以通过设置事务超时时间来解决。 -第二,仍然存在数据不一致的可能性,例如,在最后通知提交全局事务时,由于网络故障,部分节点有可能收不到通知,由于这部分节点没有提交事务,就会导致数据不一致的情况出现。 **而三阶事务(3PC)的出现就是为了减少此类问题的发生。** +第二,仍然存在数据不一致的可能性,例如,在最后通知提交全局事务时,由于网络故障,部分节点有可能收不到通知,由于这部分节点没有提交事务,就会导致数据不一致的情况出现。**而三阶事务(3PC)的出现就是为了减少此类问题的发生。** 3PC 把 2PC 的准备阶段分为了准备阶段和预处理阶段,在第一阶段只是询问各个资源节点是否可以执行事务,而在第二阶段,所有的节点反馈可以执行事务,才开始执行事务操作,最后在第三阶段执行提交或回滚操作。并且在事务管理器和资源管理器中都引入了超时机制,如果在第三阶段,资源节点一直无法收到来自资源管理器的提交或回滚请求,它就会在超时之后,继续提交事务。 diff --git "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25449\350\256\262.md" "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25449\350\256\262.md" index f1d44ef88..f5c8e4b80 100644 --- "a/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25449\350\256\262.md" +++ "b/docs/Java/Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/\347\254\25449\350\256\262.md" @@ -18,13 +18,13 @@ 你有没有发现,网络通信配置参数在 TCP 通信框架中也有。在配置 Netty 的默认参数时,我就发现很多人把 ServerSocketChannel 的配置参数配置到了 SocketChannel 中,这样做虽然不会造成什么严重的 Bug,但这也体现出了我们对技术的态度。 -所以说,在工作中如果你发现了一些不熟悉的知识点,就一定要深挖,了解其具体原理和作用。如果你发现这个知识点所属的知识面是自己所不熟悉的领域,我很建议你从点到面地系统学习一下。 **然后,有意识地锻炼我们的综合素质,以实践能力为重。** 系统性能调优,考验的不仅是我们的基础知识,还包括开发者的综合素质。首当其冲就是我们的实践能力了,善于动手去实践所学的知识点,不仅可以更深刻地理解其中的原理,还能在实践中发现更多的问题。 +所以说,在工作中如果你发现了一些不熟悉的知识点,就一定要深挖,了解其具体原理和作用。如果你发现这个知识点所属的知识面是自己所不熟悉的领域,我很建议你从点到面地系统学习一下。**然后,有意识地锻炼我们的综合素质,以实践能力为重。** 系统性能调优,考验的不仅是我们的基础知识,还包括开发者的综合素质。首当其冲就是我们的实践能力了,善于动手去实践所学的知识点,不仅可以更深刻地理解其中的原理,还能在实践中发现更多的问题。 其实我们身边从来都不缺“知道先生”,缺乏的是这种动手实践的人。 深挖和动手实践结合是很高效的学习方法,但我相信大部分人都很难做到这两点。烦杂的工作已经占据了我们大部分的时间,当我们发现陌生技术点的时候,很可能会因为这个功能还能用,没有爆出什么严重的性能问题而直接忽略。 -这种习惯会让我们在技术成长的道路上越来越浮躁,总是停留在“会用”的阶段。我的方法是,协调时间,做紧急项排序。当我看到陌生技术点时,如果恰好没有紧急需求,我会适当地放下工作,先把这些技术问题理解透彻,渠道就有很多了,比如阅读源码、官方说明文档或者搜索相关技术论坛等。但如果是陌生技术点带出了陌生的知识面,那就需要规划下学习时间和路线了。 **最后,学会分享,践行“费曼学习方法论”。** +这种习惯会让我们在技术成长的道路上越来越浮躁,总是停留在“会用”的阶段。我的方法是,协调时间,做紧急项排序。当我看到陌生技术点时,如果恰好没有紧急需求,我会适当地放下工作,先把这些技术问题理解透彻,渠道就有很多了,比如阅读源码、官方说明文档或者搜索相关技术论坛等。但如果是陌生技术点带出了陌生的知识面,那就需要规划下学习时间和路线了。**最后,学会分享,践行“费曼学习方法论”。** 我发现这样一个现象,只要是我分享过的知识点,我自己会理解地非常深刻,而且经过朋友或者同事的几番提问之后,我对所学习技术边边角角的知识点都能囊括到。这一点我也要感谢一直在专栏中给我留言,和我做技术交流的你,我非常喜欢这样的精进方式,希望你也是。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25400\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25400\350\256\262.md" index 8347f336e..39f01c760 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25400\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25400\350\256\262.md" @@ -11,18 +11,18 @@ 但是作为过来人,我发现很多学习者和实践者在 Java 性能优化上面临着很多的困扰,比如: - 工作场景中遇到“性能优化”难题,往往只能靠盲猜和感觉,用 **临时性的补救措施** 去掩盖,看似解决了问题,但下次同样的问题又会发作,原因则是 **缺乏方法论、思路的指引,以及工具支持** ; -- 能力修炼中,由于常年接触 CRUD,缺乏高并发这一实践环境,对“性能优化”只能通过理论知识进行想象, **无法认识其在工作实战中的真实面目和实操过程** ; -- 职场晋升中, **只管功能开发,不了解组件设计原理,缺少深入地思考与总结** ,无法完成高并发、高性能系统设计这类高阶工作,难以在工作中大展拳脚,而有挑战的工作往往留给有准备的人。 +- 能力修炼中,由于常年接触 CRUD,缺乏高并发这一实践环境,对“性能优化”只能通过理论知识进行想象,**无法认识其在工作实战中的真实面目和实操过程** ; +- 职场晋升中,**只管功能开发,不了解组件设计原理,缺少深入地思考与总结**,无法完成高并发、高性能系统设计这类高阶工作,难以在工作中大展拳脚,而有挑战的工作往往留给有准备的人。 # 总之,一旦遇到“性能优化”问题,很少人能够由点及面逆向分析,最终找到瓶颈点和优化方法,而 **性能优化是软件工程的深水区,也是衡量一个程序员能力高低的标准。** 进行 Java 性能优化的关键 俗话说,知己知彼百战百胜,想要克服“性能优化”这一难题,先要了解性能优化的特点,并抓住其关键和本质。 -作为面试必考内容,很多应聘者反映说面试官的一些问题会让其陷入模棱两可的境地,不知如何作答,比如很多人就搞不懂缓冲与缓存的区别。这种问题的答案,只能靠体系化的整理,依靠零零散散的知识是行不通的。 **你需要具备触类旁通的能力** ,才能对面试的散点知识既有深度又有广度地做进一步升华,才会让面试官眼前一亮。 **性能优化是个系统性工程,对工程师的技术广度和深度都有要求** 。它不仅需要你精通编程语言,还需要深刻理解操作系统、JVM 以及框架原理的相互作用关系,需要你多维度、全方面地去分析排查。 +作为面试必考内容,很多应聘者反映说面试官的一些问题会让其陷入模棱两可的境地,不知如何作答,比如很多人就搞不懂缓冲与缓存的区别。这种问题的答案,只能靠体系化的整理,依靠零零散散的知识是行不通的。**你需要具备触类旁通的能力**,才能对面试的散点知识既有深度又有广度地做进一步升华,才会让面试官眼前一亮。**性能优化是个系统性工程,对工程师的技术广度和深度都有要求** 。它不仅需要你精通编程语言,还需要深刻理解操作系统、JVM 以及框架原理的相互作用关系,需要你多维度、全方面地去分析排查。 此外,很多人能够遇到问题解决问题,但救火式治理只能临时补救表面问题,无法真正找出病灶,这次的解决只是为下次发作埋下了伏笔。事实上,很多性能问题往往隐藏的很深,比如,spring-aop 所引起的性能问题就比较难以排查。 -再比如,有人细致到会关注 switch 语句速度快还是 if 语句快,但并不能真正解决性能问题。原因是什么呢?他虽然做了“性能优化”这个动作,但思路方向却错了。这种极细微级别的优化对性能提升的影响面是很小的;而且,细节上极度地追求性能,反而会把代码写得晦涩难懂,难以维护,导致最后舍本逐末。其实, **性能优化更多要求我们关注整体效果,兼顾可靠性、扩展性,以及极端的异常场景,这样才能体现性能优化的价值** 。 **实践比理论重要** 。性能优化并不是对固定、单一场景的优化,场景不同,方法也会不同。比如,如果你的业务是串行的,耗时很长,就不能简单地通过增加 CPU 资源进行性能提升;如果你的业务是并行的,也不能钻牛角尖地优化每一行代码,要照顾各个资源的协调,对短板着重进行优化,以便达到最优效果。 +再比如,有人细致到会关注 switch 语句速度快还是 if 语句快,但并不能真正解决性能问题。原因是什么呢?他虽然做了“性能优化”这个动作,但思路方向却错了。这种极细微级别的优化对性能提升的影响面是很小的;而且,细节上极度地追求性能,反而会把代码写得晦涩难懂,难以维护,导致最后舍本逐末。其实,**性能优化更多要求我们关注整体效果,兼顾可靠性、扩展性,以及极端的异常场景,这样才能体现性能优化的价值** 。**实践比理论重要** 。性能优化并不是对固定、单一场景的优化,场景不同,方法也会不同。比如,如果你的业务是串行的,耗时很长,就不能简单地通过增加 CPU 资源进行性能提升;如果你的业务是并行的,也不能钻牛角尖地优化每一行代码,要照顾各个资源的协调,对短板着重进行优化,以便达到最优效果。 在过去你面临以上情况时,可能会仅凭感觉入手,或者先动手才思考,无法发现抓住本质,但在本课程中,我会向你讲解正确的思路,让你进行性能优化时有理可依。 @@ -32,18 +32,18 @@ 课程分为 5 个模块,共 21 篇,我将从理论分析、工具支持、案例与面试点,以及 JVM 优化四大方面展开系统讲解: -- **模块一:理论分析** ,针对平常对性能优化的盲猜问题,我们会首先讲解大量的衡量指标,然后以此为依据,盘点一下常用的优化方法,包括业务优化、复用优化、计算优化、结果集优化、资源冲突优化、算法优化、高效实现等方面。学完后,你将会了解如何描述性能,并对性能优化有个整体的印象。 -- **模块二:工具支持** ,工欲善其事,必先利其器。此部分将介绍一些评估操作系统设备性能的工具,包含大量实用的命令行解析;还会介绍 Java 中最有效的基准测试工具 JMH,以及一些监测 JVM 性能的应用。本模块的目的,是为大家提供一些测量性能的工具,为实践环节做准备。 -- **模块三:实战案例与高频面试点** ,该模块为课程的主要内容,结合之前模块的理论分析和工具支持,通过海量实战案例,深入专项性能场景,并将每个场景下的高频面试点逐一击破,点拨调优思路,目标是能够做到举一反三,在遇到相似的性能问题时,能够快速想到合适的切入点进行优化。 -- **模块四:JVM 优化** ,该模块对系统的性能提升是巨大的。本部分主要介绍垃圾回收的一些基本知识,看一下 JIT 在性能提升上所做的文章;最后列举了一些常见的优化参数,以及对编码方面的要求。学完本模块,你将掌握和 JVM 相关的常见优化措施。 -- **模块五:特别放送** ,最后,针对工作中最常用的服务和框架,我想和你介绍一个 SpringBoot 服务的优化案例,涵盖 Tomcat、Undertow、JVM、网络等场景,同时再进行优化方法和求职面经总结,希望以一个全局的案例,帮助你掌握从系统层到应用层的整个优化技巧。 +- **模块一:理论分析**,针对平常对性能优化的盲猜问题,我们会首先讲解大量的衡量指标,然后以此为依据,盘点一下常用的优化方法,包括业务优化、复用优化、计算优化、结果集优化、资源冲突优化、算法优化、高效实现等方面。学完后,你将会了解如何描述性能,并对性能优化有个整体的印象。 +- **模块二:工具支持**,工欲善其事,必先利其器。此部分将介绍一些评估操作系统设备性能的工具,包含大量实用的命令行解析;还会介绍 Java 中最有效的基准测试工具 JMH,以及一些监测 JVM 性能的应用。本模块的目的,是为大家提供一些测量性能的工具,为实践环节做准备。 +- **模块三:实战案例与高频面试点**,该模块为课程的主要内容,结合之前模块的理论分析和工具支持,通过海量实战案例,深入专项性能场景,并将每个场景下的高频面试点逐一击破,点拨调优思路,目标是能够做到举一反三,在遇到相似的性能问题时,能够快速想到合适的切入点进行优化。 +- **模块四:JVM 优化**,该模块对系统的性能提升是巨大的。本部分主要介绍垃圾回收的一些基本知识,看一下 JIT 在性能提升上所做的文章;最后列举了一些常见的优化参数,以及对编码方面的要求。学完本模块,你将掌握和 JVM 相关的常见优化措施。 +- **模块五:特别放送**,最后,针对工作中最常用的服务和框架,我想和你介绍一个 SpringBoot 服务的优化案例,涵盖 Tomcat、Undertow、JVM、网络等场景,同时再进行优化方法和求职面经总结,希望以一个全局的案例,帮助你掌握从系统层到应用层的整个优化技巧。 你将收获 -==== **建立完整的性能优化知识体系。你可以系统地学习相关知识,而不是碎片化获取,基础理论实用性强,直入主题,让你在工作实战时有理可依,有据可循** 。 **能够对线上应用输出优化思路** 。掌握各种实战排查工具,并灵活应用,定位至应用中的症结瓶颈点,并输出优化思路方案。正确的方法比努力更重要,有了正确的思路方法,才能在实际工作中避免跑偏,避免把大力气花在一些细枝末节上。我还会分享大量的操作系统方面的知识,让你对应用性能有更好的评测。 **收获海量实战经验分享** 。作为这门课最硬核内容,我将从流行的中间件介绍到常用的工具类,再到 JDK 中的知识点,用实战分析和经验分享高度还原真实的业务场景,带你了解性能优化的全过程。 **获得面试 Offer 收割利器** 。本课程的大多数案例,都是 Java 面试题的重灾区,我将直接指出高频考点,让你既能在整体上对性能优化提供建议,也能深入细节进行针对性优化。 +==== **建立完整的性能优化知识体系。你可以系统地学习相关知识,而不是碎片化获取,基础理论实用性强,直入主题,让你在工作实战时有理可依,有据可循** 。**能够对线上应用输出优化思路** 。掌握各种实战排查工具,并灵活应用,定位至应用中的症结瓶颈点,并输出优化思路方案。正确的方法比努力更重要,有了正确的思路方法,才能在实际工作中避免跑偏,避免把大力气花在一些细枝末节上。我还会分享大量的操作系统方面的知识,让你对应用性能有更好的评测。**收获海量实战经验分享** 。作为这门课最硬核内容,我将从流行的中间件介绍到常用的工具类,再到 JDK 中的知识点,用实战分析和经验分享高度还原真实的业务场景,带你了解性能优化的全过程。**获得面试 Offer 收割利器** 。本课程的大多数案例,都是 Java 面试题的重灾区,我将直接指出高频考点,让你既能在整体上对性能优化提供建议,也能深入细节进行针对性优化。 # 讲师寄语 -最后, **性能优化既是工程师们进阶的“拦路虎”,也可以是你能力的炼金石** 。希望这个专栏可以让 这个非常难啃的老大难问题,变得“平易近人”“通俗易懂”“一点就通”,希望可以让你体会到“哦,原来如此简单!”的感觉,体会到久违的学习的快乐,并能学有所用。 +最后,**性能优化既是工程师们进阶的“拦路虎”,也可以是你能力的炼金石** 。希望这个专栏可以让 这个非常难啃的老大难问题,变得“平易近人”“通俗易懂”“一点就通”,希望可以让你体会到“哦,原来如此简单!”的感觉,体会到久违的学习的快乐,并能学有所用。 另外,我去年就与拉勾教育平台合作了\[《深入浅出 Java 虚拟机》(已完结)\]课程,用户口碑还不错,Java 虚拟机这门课可作为 Java 性能优化课程的一个补充,我也推荐你去学习了解。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25401\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25401\350\256\262.md" index db6eaf0c9..7c6cdedf8 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25401\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25401\350\256\262.md" @@ -6,7 +6,7 @@ 这个道理大家都懂,但一旦到了性能优化上,却往往因为缺乏理论依据而选择了错误的优化方向,陷入了盲猜的窘境。在衡量一项优化是否能达到目的之时,不能仅靠感觉,它同样有一系列的指标来衡量你的改进。如果在改动之后,性能不升反降,那就不能叫性能优化了。 -所谓性能,就是使用 **有限的资源** 在 **有限的时间内** 完成工作。最主要的衡量因素就是时间,所以很多衡量指标,都可以把时间作为横轴。 **加载缓慢的网站,会受到搜索排名算法的惩罚,从而导致网站排名下降。** 因此加载的快慢是性能优化是否合理的一个非常直观的判断因素,但性能指标不仅仅包括单次请求的速度,它还包含更多因素。 +所谓性能,就是使用 **有限的资源** 在 **有限的时间内** 完成工作。最主要的衡量因素就是时间,所以很多衡量指标,都可以把时间作为横轴。**加载缓慢的网站,会受到搜索排名算法的惩罚,从而导致网站排名下降。** 因此加载的快慢是性能优化是否合理的一个非常直观的判断因素,但性能指标不仅仅包括单次请求的速度,它还包含更多因素。 接下来看一下,都有哪些衡量指标能够帮我们进行决策。 @@ -24,7 +24,7 @@ ![image](assets/CgqCHl8L02KAdjZ_AAB-AStGwkw402.png) -像我们平常开发中经常提到的,QPS 代表每秒查询的数量,TPS 代表每秒事务的数量,HPS 代表每秒的 HTTP 请求数量等,这都是常用的与吞吐量相关的量化指标。 **在性能优化的时候,我们要搞清楚优化的目标,到底是吞吐量还是响应速度。** 有些时候,虽然响应速度比较慢,但整个吞吐量却非常高,比如一些数据库的批量操作、一些缓冲区的合并等。虽然信息的延迟增加了,但如果我们的目标就是吞吐量,那么这显然也可以算是比较大的性能提升。 +像我们平常开发中经常提到的,QPS 代表每秒查询的数量,TPS 代表每秒事务的数量,HPS 代表每秒的 HTTP 请求数量等,这都是常用的与吞吐量相关的量化指标。**在性能优化的时候,我们要搞清楚优化的目标,到底是吞吐量还是响应速度。** 有些时候,虽然响应速度比较慢,但整个吞吐量却非常高,比如一些数据库的批量操作、一些缓冲区的合并等。虽然信息的延迟增加了,但如果我们的目标就是吞吐量,那么这显然也可以算是比较大的性能提升。 一般情况下,我们认为: @@ -33,19 +33,19 @@ 我们平常的优化主要侧重于响应速度,因为一旦响应速度提升了,那么整个吞吐量自然也会跟着提升。 -## **但对于高并发的互联网应用来说,响应速度和吞吐量两者都需要** 。这些应用都会标榜为高吞吐、高并发的场景,用户对系统的延迟忍耐度很差, **我们需要使用有限的硬件资源,从中找到一个平衡点。** 2. 响应时间衡量 +## **但对于高并发的互联网应用来说,响应速度和吞吐量两者都需要** 。这些应用都会标榜为高吞吐、高并发的场景,用户对系统的延迟忍耐度很差,**我们需要使用有限的硬件资源,从中找到一个平衡点。** 2. 响应时间衡量 -既然响应时间这么重要,我们就着重看一下响应时间的衡量方法。 **(1)平均响应时间** 我们最常用的指标,即 **平均响应时间(AVG)** ,该指标能够体现服务接口的平均处理能力。它的本质是把所有的请求耗时加起来,然后除以请求的次数。举个最简单的例子,有 10 个请求,其中有 2 个 1ms、3 个 5ms、5 个 10ms,那么它的平均耗时就是(21+35+5\*10)/10=6.7ms。 +既然响应时间这么重要,我们就着重看一下响应时间的衡量方法。**(1)平均响应时间** 我们最常用的指标,即 **平均响应时间(AVG)**,该指标能够体现服务接口的平均处理能力。它的本质是把所有的请求耗时加起来,然后除以请求的次数。举个最简单的例子,有 10 个请求,其中有 2 个 1ms、3 个 5ms、5 个 10ms,那么它的平均耗时就是(21+35+5\*10)/10=6.7ms。 除非服务在一段时间内出现了严重的问题,否则平均响应时间都会比较平缓。因为高并发应用请求量都特别大,所以长尾请求的影响会被很快平均,导致很多用户的请求变慢,但这不能体现在平均耗时指标中。 为了解决这个问题,另外一个比较常用的指标,就是 **百分位数(Percentile)** 。 -**(2)百分位数** ![image](assets/Ciqc1F8L032AC_6sAABe7N44eqs490.png) +**(2)百分位数**![image](assets/Ciqc1F8L032AC_6sAABe7N44eqs490.png) 这个也比较好理解。我们圈定一个时间范围,把每次请求的耗时加入一个列表中,然后按照从小到大的顺序将这些时间进行排序。这样,我们取出特定百分位的耗时,这个数字就是 TP 值。可以看到,TP 值(Top Percentile)和中位数、平均数等是类似的,都是一个统计学里的术语。 -它的意义是,超过 N% 的请求都在 X 时间内返回。比如 TP90 = 50ms,意思是超过 90th 的请求,都在 50ms 内返回。 **这个指标也是非常重要的,它能够反映出应用接口的整体响应情况** 。比如,某段时间若发生了长时间的 GC,那它的某个时间段之上的指标就会产生严重的抖动,但一些低百分位的数值却很少有变化。 +它的意义是,超过 N% 的请求都在 X 时间内返回。比如 TP90 = 50ms,意思是超过 90th 的请求,都在 50ms 内返回。**这个指标也是非常重要的,它能够反映出应用接口的整体响应情况** 。比如,某段时间若发生了长时间的 GC,那它的某个时间段之上的指标就会产生严重的抖动,但一些低百分位的数值却很少有变化。 我们一般分为 TP50、TP90、TP95、TP99、TP99.9 等多个段,对高百分位的值要求越高,对系统响应能力的稳定性要求越高。 @@ -70,7 +70,7 @@ 但等应用真正上线时,却发生了重大事故,这是因为接口返回的都是无法使用的数据。 -# 其问题原因也比较好定位,就是项目中使用了熔断。在压测的时候,接口直接超出服务能力,触发熔断了,但是压测并没有对接口响应的正确性做判断,造成了非常低级的错误。 **所以在进行性能评估的时候,不要忘记正确性这一关键要素。** 有哪些理论方法? +# 其问题原因也比较好定位,就是项目中使用了熔断。在压测的时候,接口直接超出服务能力,触发熔断了,但是压测并没有对接口响应的正确性做判断,造成了非常低级的错误。**所以在进行性能评估的时候,不要忘记正确性这一关键要素。** 有哪些理论方法? 性能优化有很多理论方法,比如木桶理论、基础测试、Amdahl 定律等。下面我们简单地讲解一下最常用的两个理论。 @@ -80,7 +80,7 @@ 能够装多少水,取决于最短的那块木板,而不是最长的那一块。 -木桶效应在解释系统性能上,也非常适合。组成系统的组件,在速度上是良莠不齐的。 **系统的整体性能,就取决于系统中最慢的组件。** 比如,在数据库应用中,制约性能最严重的是落盘的 I/O 问题,也就是说,硬盘是这个场景下的短板,我们首要的任务就是补齐这个短板。 +木桶效应在解释系统性能上,也非常适合。组成系统的组件,在速度上是良莠不齐的。**系统的整体性能,就取决于系统中最慢的组件。** 比如,在数据库应用中,制约性能最严重的是落盘的 I/O 问题,也就是说,硬盘是这个场景下的短板,我们首要的任务就是补齐这个短板。 ## 2. 基准测试、预热 @@ -118,7 +118,7 @@ 时间要花在刀刃上,我们需要找到最迫切需要解决的性能点,然后将其击破。比如,一个系统主要是慢在了数据库查询上,结果你却花了很大的精力去优化 Java 编码规范,这就是偏离目标的典型情况。 -一般地,性能优化后的代码,由于太过于追求执行速度,读起来都比较晦涩,在结构上也会有很多让步。很显然,过早优化会让这种难以维护的特性过早介入到你的项目中,等代码重构的时候,就会花更大的力气去解决它。 **正确的做法是,项目开发和性能优化,应该作为两个独立的步骤进行,要做性能优化,要等到整个项目的架构和功能大体进入稳定状态时再进行。** +一般地,性能优化后的代码,由于太过于追求执行速度,读起来都比较晦涩,在结构上也会有很多让步。很显然,过早优化会让这种难以维护的特性过早介入到你的项目中,等代码重构的时候,就会花更大的力气去解决它。**正确的做法是,项目开发和性能优化,应该作为两个独立的步骤进行,要做性能优化,要等到整个项目的架构和功能大体进入稳定状态时再进行。** ## 4. 保持良好的编码习惯 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25402\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25402\350\256\262.md" index 71b29f83a..44ee96369 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25402\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25402\350\256\262.md" @@ -24,8 +24,8 @@ 在软件系统中,谈到数据复用,我们首先想到的就是 **缓冲和缓存** 。注意这两个词的区别,它们的意义是完全不同的,很多同学很容易搞混,在这里简单地介绍一下(后续 06 和 07 课时会再详细讲解)。 -- 缓冲(Buffer),常见于对数据的暂存,然后批量传输或者写入。多使用顺序方式,用来缓解不同设备之间频繁地、缓慢地随机写, **缓冲主要针对的是写操作** 。 -- 缓存(Cache),常见于对已读取数据的复用,通过将它们缓存在相对高速的区域, **缓存主要针对的是读操作** 。 +- 缓冲(Buffer),常见于对数据的暂存,然后批量传输或者写入。多使用顺序方式,用来缓解不同设备之间频繁地、缓慢地随机写,**缓冲主要针对的是写操作** 。 +- 缓存(Cache),常见于对已读取数据的复用,通过将它们缓存在相对高速的区域,**缓存主要针对的是读操作** 。 与之类似的,是对于对象的池化操作,比如数据库连接池、线程池等,在 Java 中使用得非常频繁。由于这些对象的创建和销毁成本都比较大,我们在使用之后,也会将这部分对象暂时存储,下次用的时候,就不用再走一遍耗时的初始化操作了。 @@ -37,17 +37,17 @@ 现在的 CPU 发展速度很快,绝大多数硬件,都是多核。要想加快某个任务的执行,最快最优的解决方式,就是让它并行执行。并行执行有以下三种模式。 -第一种模式是 **多机** ,采用负载均衡的方式,将流量或者大的计算拆分成多个部分,同时进行处理。比如,Hadoop 通过 MapReduce 的方式,把任务打散,多机同时进行计算。 +第一种模式是 **多机**,采用负载均衡的方式,将流量或者大的计算拆分成多个部分,同时进行处理。比如,Hadoop 通过 MapReduce 的方式,把任务打散,多机同时进行计算。 第二种模式是 **采用多进程** 。比如 Nginx,采用 NIO 编程模型,Master 统一管理 Worker 进程,然后由 Worker 进程进行真正的请求代理,这也能很好地利用硬件的多个 CPU。 -第三种模式是 **使用多线程** ,这也是 Java 程序员接触最多的。比如 Netty,采用 Reactor 编程模型,同样使用 NIO,但它是基于线程的。Boss 线程用来接收请求,然后调度给相应的 Worker 线程进行真正的业务计算。 +第三种模式是 **使用多线程**,这也是 Java 程序员接触最多的。比如 Netty,采用 Reactor 编程模型,同样使用 NIO,但它是基于线程的。Boss 线程用来接收请求,然后调度给相应的 Worker 线程进行真正的业务计算。 像 Golang 这样的语言,有更加轻量级的协程(Coroutine),协程是一种比线程更加轻量级的存在,但目前在 Java 中还不太成熟,就不做过多介绍了,但本质上,它也是对于多核的应用,使得任务并行执行。 (2)变同步为异步 -再一种对于计算的优化,就是 **变同步为异步** ,这通常涉及编程模型的改变。同步方式,请求会一直阻塞,直到有成功,或者失败结果的返回。虽然它的编程模型简单,但应对突发的、时间段倾斜的流量,问题就特别大,请求很容易失败。 +再一种对于计算的优化,就是 **变同步为异步**,这通常涉及编程模型的改变。同步方式,请求会一直阻塞,直到有成功,或者失败结果的返回。虽然它的编程模型简单,但应对突发的、时间段倾斜的流量,问题就特别大,请求很容易失败。 异步操作可以方便地支持横向扩容,也可以缓解瞬时压力,使请求变得平滑。同步请求,就像拳头打在钢板上;异步请求,就像拳头打在海绵上。你可以想象一下这个过程,后者肯定是富有弹性的,体验更加友好。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25403\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25403\350\256\262.md" index 9a48827be..c9910abb6 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25403\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25403\350\256\262.md" @@ -86,7 +86,7 @@ nonvoluntary_ctxt_switches: 171204 我们在平常写完代码后,比如写了一个 C++ 程序,去查看它的汇编,如果看到其中的内存地址,并不是实际的物理内存地址,那么应用程序所使用的,就是 **逻辑内存** 。学过计算机组成结构的同学应该都有了解。 -逻辑地址可以映射到两个内存段上: **物理内存** 和 **虚拟内存** ,那么整个系统可用的内存就是两者之和。比如你的物理内存是 4GB,分配了 8GB 的 SWAP 分区,那么应用可用的总内存就是 12GB。 +逻辑地址可以映射到两个内存段上: **物理内存** 和 **虚拟内存**,那么整个系统可用的内存就是两者之和。比如你的物理内存是 4GB,分配了 8GB 的 SWAP 分区,那么应用可用的总内存就是 12GB。 ## 1. top 命令 @@ -161,7 +161,7 @@ I/O 设备可能是计算机里速度最慢的组件了,它指的不仅仅是 ![Drawing 8.png](assets/Ciqc1F8VRxaAK34SAAHTZp7R44c733.png) -如上图所示,可以看到普通磁盘的随机写与顺序写相差非常大,但顺序写与 CPU 内存依旧不在一个数量级上。 **缓冲区依然是解决速度差异的唯一工具** ,但在极端情况下,比如断电时,就产生了太多的不确定性,这时这些缓冲区,都容易丢。由于这部分内容的篇幅比较大,我将在第 06 课时专门讲解。 +如上图所示,可以看到普通磁盘的随机写与顺序写相差非常大,但顺序写与 CPU 内存依旧不在一个数量级上。**缓冲区依然是解决速度差异的唯一工具**,但在极端情况下,比如断电时,就产生了太多的不确定性,这时这些缓冲区,都容易丢。由于这部分内容的篇幅比较大,我将在第 06 课时专门讲解。 ## 1. iostat @@ -183,7 +183,7 @@ I/O 设备可能是计算机里速度最慢的组件了,它指的不仅仅是 ## 2. 零拷贝 -硬盘上的数据,在发往网络之前,需要经过多次缓冲区的拷贝,以及用户空间和内核空间的多次切换。如果能减少一些拷贝的过程,效率就能提升,所以零拷贝应运而生。 **零拷贝** 是一种非常重要的性能优化手段,比如常见的 Kafka、Nginx 等,就使用了这种技术。我们来看一下有无零拷贝之间的区别。 +硬盘上的数据,在发往网络之前,需要经过多次缓冲区的拷贝,以及用户空间和内核空间的多次切换。如果能减少一些拷贝的过程,效率就能提升,所以零拷贝应运而生。**零拷贝** 是一种非常重要的性能优化手段,比如常见的 Kafka、Nginx 等,就使用了这种技术。我们来看一下有无零拷贝之间的区别。 (1)没有采取零拷贝手段 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25404\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25404\350\256\262.md" index 6e14d92be..db9666926 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25404\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25404\350\256\262.md" @@ -87,7 +87,7 @@ jcmd JFR.dump filename=recording.jfr jcmd JFR.stop ``` -JFR 功能是建在 JVM 内部的,不需要额外依赖,可以直接使用,它能够监测大量数据。比如,我们提到的锁竞争、延迟、阻塞等;甚至在 JVM 内部,比如 SafePoint、JIT 编译等,也能去分析。 **JMC 集成了 JFR 的功能** ,下面介绍一下 JMC 的使用。 +JFR 功能是建在 JVM 内部的,不需要额外依赖,可以直接使用,它能够监测大量数据。比如,我们提到的锁竞争、延迟、阻塞等;甚至在 JVM 内部,比如 SafePoint、JIT 编译等,也能去分析。**JMC 集成了 JFR 的功能**,下面介绍一下 JMC 的使用。 ## 1.录制 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25405\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25405\350\256\262.md" index 89d23ed26..d56748aa1 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25405\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25405\350\256\262.md" @@ -149,7 +149,7 @@ Iteration 5: 1423.000 ns/op 虽然经过预热之后,代码都能表现出它的最优状态,但一般和实际应用场景还是有些出入。如果你的测试机器性能很高,或者你的测试机资源利用已经达到了极限,都会影响测试结果的数值。 -所以,通常情况下,我都会在测试时,给机器充足的资源,保持一个稳定的环境。在分析结果时,也会更加关注不同代码实现方式下的 **性能差异** ,而不是测试数据本身。 +所以,通常情况下,我都会在测试时,给机器充足的资源,保持一个稳定的环境。在分析结果时,也会更加关注不同代码实现方式下的 **性能差异**,而不是测试数据本身。 ## 3. @BenchmarkMode @@ -351,7 +351,7 @@ Options opt = new OptionsBuilder() ![Drawing 5.png](assets/Ciqc1F8ebi2AdAAbAALlvsHgcKk925.png) 2\. 结果图形化制图工具 -------------- **JMH Visualizer** 这里有一个开源的项目,通过导出 json 文件,上传至 [JMH Visualizer](https://jmh.morethan.io/)(点击链接跳转),可得到简单的统计结果。由于很多操作需要鼠标悬浮在上面进行操作,所以个人认为它的展示方式并不是很好。 **JMH Visual Chart** 相比较而言, [JMH Visual Chart](http://deepoove.com/jmh-visual-chart)(点击链接跳转)这个工具,就相对直观一些。 +------------- **JMH Visualizer** 这里有一个开源的项目,通过导出 json 文件,上传至 [JMH Visualizer](https://jmh.morethan.io/)(点击链接跳转),可得到简单的统计结果。由于很多操作需要鼠标悬浮在上面进行操作,所以个人认为它的展示方式并不是很好。**JMH Visual Chart** 相比较而言, [JMH Visual Chart](http://deepoove.com/jmh-visual-chart)(点击链接跳转)这个工具,就相对直观一些。 ![Drawing 6.png](assets/CgqCHl8ebkmAbujsAAHK-g94ooM905.png) **meta-chart** diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25406\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25406\350\256\262.md" index e6a69f9a7..74d915b9b 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25406\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25406\350\256\262.md" @@ -26,7 +26,7 @@ 接下来,我会以文件读取和写入字符流为例进行讲解。 -Java 的 I/O 流设计,采用的是 **装饰器模式** ,当需要给类添加新的功能时,就可以将被装饰者通过参数传递到装饰者,封装成新的功能方法。下图是装饰器模式的典型示意图,就增加功能来说,装饰模式比生成子类更为灵活。 +Java 的 I/O 流设计,采用的是 **装饰器模式**,当需要给类添加新的功能时,就可以将被装饰者通过参数传递到装饰者,封装成新的功能方法。下图是装饰器模式的典型示意图,就增加功能来说,装饰模式比生成子类更为灵活。 ![image](assets/Ciqc1F8hIqCAXF-UAACYqHfWtqs495.png) @@ -141,7 +141,7 @@ int n = getInIfOpen().read(buffer, pos, buffer.length - pos); SLF4J 是 Java 里标准的日志记录库,它是一个允许你使用任何 Java 日志记录库的抽象适配层,最常用的实现是 Logback,支持修改后自动 reload,它比 Java 自带的 JUL 还要流行。 -**Logback** 性能也很高,其中一个原因就是 **异步日志** ,它在记录日志时,使用了一个缓冲队列,当缓冲的内容达到一定的阈值时,才会把缓冲区的内容写到文件里。使用异步日志有两个考虑: +**Logback** 性能也很高,其中一个原因就是 **异步日志**,它在记录日志时,使用了一个缓冲队列,当缓冲的内容达到一定的阈值时,才会把缓冲区的内容写到文件里。使用异步日志有两个考虑: - 同步日志的写入,会阻塞业务,导致服务接口的耗时增加; - 日志写入磁盘的代价是昂贵的,如果每产生一条日志就写入一次,CPU 会花很多时间在磁盘 I/O 上。 @@ -163,17 +163,17 @@ Logback 的异步日志也比较好配置,我们需要在正常配置的基础 上图中有三个关键参数: -- **queueSize** ,代表了队列的大小,默认是256。如果这个值设置的太大,大日志量下突然断电,会丢掉缓冲区的内容; -- **maxFlushTime** ,关闭日志上下文后,继续执行写任务的时间,这是通过调用 Thread 类的 join 方法来实现的(worker.join(maxFlushTime)); -- **discardingThreshold** ,当 queueSize 快达到上限时,可以通过配置,丢弃一些级别比较低的日志,这个值默认是队列长度的 80%;但若你担心可能会丢失业务日志,则可以将这个值设置成 0,表示所有的日志都要打印。 +- **queueSize**,代表了队列的大小,默认是256。如果这个值设置的太大,大日志量下突然断电,会丢掉缓冲区的内容; +- **maxFlushTime**,关闭日志上下文后,继续执行写任务的时间,这是通过调用 Thread 类的 join 方法来实现的(worker.join(maxFlushTime)); +- **discardingThreshold**,当 queueSize 快达到上限时,可以通过配置,丢弃一些级别比较低的日志,这个值默认是队列长度的 80%;但若你担心可能会丢失业务日志,则可以将这个值设置成 0,表示所有的日志都要打印。 # 缓冲区优化思路 -毫无疑问缓冲区是可以提高性能的, **但它通常会引入一个异步的问题** ,使得编程模型变复杂。 +毫无疑问缓冲区是可以提高性能的,**但它通常会引入一个异步的问题**,使得编程模型变复杂。 通过文件读写流和 Logback 两个例子,我们来看一下对于缓冲区设计的一些常规操作。 -如下图所示,资源 A 读取或写入一些操作到资源 B,这本是一个正常的操作流程,但由于中间插入了一个额外的 **存储层** ,所以这个流程被生生截断了,这时就需要你手动处理被截断两方的资源协调问题。 +如下图所示,资源 A 读取或写入一些操作到资源 B,这本是一个正常的操作流程,但由于中间插入了一个额外的 **存储层**,所以这个流程被生生截断了,这时就需要你手动处理被截断两方的资源协调问题。 ![image](assets/Ciqc1F8hIuqATvhSAAB9F5pMiOE699.png) @@ -181,9 +181,9 @@ Logback 的异步日志也比较好配置,我们需要在正常配置的基础 ## 1.同步操作 -同步操作的编程模型相对简单,在一个线程中就可完成,你只需要控制缓冲区的大小,并把握处理的时机。比如,缓冲区 **大小达到阈值** ,或者缓冲区的元素在缓冲区的 **停留时间超时** ,这时就会触发 **批量操作** 。 +同步操作的编程模型相对简单,在一个线程中就可完成,你只需要控制缓冲区的大小,并把握处理的时机。比如,缓冲区 **大小达到阈值**,或者缓冲区的元素在缓冲区的 **停留时间超时**,这时就会触发 **批量操作** 。 -由于所有的操作又都在 **单线程** ,或者同步方法块中完成,再加上资源 B 的处理能力有限,那么很多操作就会阻塞并等待在调用线程上。比如写文件时,需要等待前面的数据写入完毕,才能处理后面的请求。 +由于所有的操作又都在 **单线程**,或者同步方法块中完成,再加上资源 B 的处理能力有限,那么很多操作就会阻塞并等待在调用线程上。比如写文件时,需要等待前面的数据写入完毕,才能处理后面的请求。 ![image](assets/CgqCHl8hIvuAILAKAABaDCSPRRw546.png) @@ -197,7 +197,7 @@ Logback 的异步日志也比较好配置,我们需要在正常配置的基础 许多应用系统还会有更复杂的策略,比如在用户线程等待,设置一个超时时间,以及成功进入缓冲区之后的回调函数等。 -对缓冲区的 **消费** ,一般采用开启线程的方式,如果有多个线程消费缓冲区,还会存在信息同步和顺序问题。 +对缓冲区的 **消费**,一般采用开启线程的方式,如果有多个线程消费缓冲区,还会存在信息同步和顺序问题。 ![image](assets/CgqCHl8hIwaAQl3SAACaljNt5Fs553.png) @@ -236,9 +236,9 @@ Logback 的异步日志也比较好配置,我们需要在正常配置的基础 虽然缓冲区可以帮我们大大地提高应用程序的性能,但同时它也有不少问题,在我们设计时,要注意这些异常情况。 -其中, **比较严重就是缓冲区内容的丢失** 。即使你使用 addShutdownHook 做了优雅关闭,有些情形依旧难以防范避免,比如机器突然间断电,应用程序进程突然死亡等。这时,缓冲区内未处理完的信息便会丢失,尤其金融信息,电商订单信息的丢失都是比较严重的。 +其中,**比较严重就是缓冲区内容的丢失** 。即使你使用 addShutdownHook 做了优雅关闭,有些情形依旧难以防范避免,比如机器突然间断电,应用程序进程突然死亡等。这时,缓冲区内未处理完的信息便会丢失,尤其金融信息,电商订单信息的丢失都是比较严重的。 -所以, **内容写入缓冲区之前,需要先预写日志** ,故障后重启时,就会根据这些日志进行数据恢复。在数据库领域,文件缓冲的场景非常多,一般都是采用 WAL 日志(Write-Ahead Logging)解决。对数据完整性比较严格的系统,甚至会通过电池或者 UPS 来保证缓冲区的落地。这就是性能优化带来的新问题,必须要解决。 +所以,**内容写入缓冲区之前,需要先预写日志**,故障后重启时,就会根据这些日志进行数据恢复。在数据库领域,文件缓冲的场景非常多,一般都是采用 WAL 日志(Write-Ahead Logging)解决。对数据完整性比较严格的系统,甚至会通过电池或者 UPS 来保证缓冲区的落地。这就是性能优化带来的新问题,必须要解决。 # 小结 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25407\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25407\350\256\262.md" index 85a533b40..1ef0de0e0 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25407\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25407\350\256\262.md" @@ -6,7 +6,7 @@ 缓存的优化效果是非常好的,它既可以让原本载入非常缓慢的页面,瞬间秒开,也能让本是压力山大的数据库,瞬间清闲下来。 -**缓存** , **本质** 上是为了协调两个速度差异非常大的组件,如下图所示,通过加入一个中间层,将常用的数据存放在相对高速的设备中。 +**缓存**,**本质** 上是为了协调两个速度差异非常大的组件,如下图所示,通过加入一个中间层,将常用的数据存放在相对高速的设备中。 ![Drawing 1.png](assets/CgqCHl8nuCKAad7oAAAk6v90xvo900.png) @@ -96,7 +96,7 @@ static String slowMethod(String key) throws Exception { #### 4.缓存造成内存故障 -LC 可以通过 recordStats 函数,对缓存加载和命中率等情况进行监控。 **值得注意的是:LC 是基于数据条数而不是基于缓存物理大小的,所以如果你缓存的对象特别大,就会造成不可预料的内存占用。** +LC 可以通过 recordStats 函数,对缓存加载和命中率等情况进行监控。**值得注意的是:LC 是基于数据条数而不是基于缓存物理大小的,所以如果你缓存的对象特别大,就会造成不可预料的内存占用。** 围绕这点,我分享一个由于不正确使用缓存导致的常见内存故障。 @@ -114,7 +114,7 @@ LC 可以通过 recordStats 函数,对缓存加载和命中率等情况进行 - **FIFO** 这是一种先进先出的模式。如果缓存容量满了,将会 **移除最先加入的元素** 。这种缓存实现方式简单,但符合先进先出的队列模式场景的功能不多,应用场景较少。 -- **LRU** LRU 是最近最少使用的意思,当缓存容量达到上限,它会 **优先移除那些最久未被使用的数据** ,LRU是目前 **最常用** 的缓存算法,稍后我们会使用 Java 的 API 简单实现一个。 +- **LRU** LRU 是最近最少使用的意思,当缓存容量达到上限,它会 **优先移除那些最久未被使用的数据**,LRU是目前 **最常用** 的缓存算法,稍后我们会使用 Java 的 API 简单实现一个。 - **LFU** LFU 是最近最不常用的意思。相对于 LRU 的时间维度,LFU 增加了访问次数的维度。如果缓存满的时候,将 **优先移除访问次数最少的元素** ;而当有多个访问次数相同的元素时,则 **优先移除最久未被使用的元素** 。 @@ -164,15 +164,15 @@ public class LRU extends LinkedHashMap { 预读算法有三个关键点: -- **预测性** ,能够根据应用的使用数据,提前预测应用后续的操作目标; -- **提前** ,能够将这些数据提前加载到缓存中,保证命中率; -- **批量** ,将小块的、频繁的读取操作,合并成顺序的批量读取,提高性能。 +- **预测性**,能够根据应用的使用数据,提前预测应用后续的操作目标; +- **提前**,能够将这些数据提前加载到缓存中,保证命中率; +- **批量**,将小块的、频繁的读取操作,合并成顺序的批量读取,提高性能。 预读技术一般都是比较智能的,能够覆盖大多数后续的读取操作。举个极端的例子,如果我们的数据集合比较小,访问频率又非常高,就可以使用完全载入的方式,来替换懒加载的方式。在系统启动的时候,将数据加载到缓存中。 ### 缓存优化的一般思路 -一般,缓存针对的主要是读操作。 **当你的功能遇到下面的场景时** ,就可以选择使用缓存组件进行性能优化: +一般,缓存针对的主要是读操作。**当你的功能遇到下面的场景时**,就可以选择使用缓存组件进行性能优化: - 存在数据热点,缓存的数据能够被频繁使用; - 读操作明显比写操作要多; @@ -184,7 +184,7 @@ public class LRU extends LinkedHashMap { - 缓冲,数据一般只使用一次,等待缓冲区满了,就执行 flush 操作; - 缓存,数据被载入之后,可以多次使用,数据将会共享多次。 -**缓存最重要的指标就是命中率** ,有以下几个因素会影响命中率。 **(1)缓存容量** 缓存的容量总是有限制的,所以就存在一些冷数据的逐出问题。但缓存也不是越大越好,它不能明显挤占业务的内存。 **(2)数据集类型** 如果缓存的数据是非热点数据,或者是操作几次就不再使用的冷数据,那命中率肯定会低,缓存也会失去了它的作用。 **(3)缓存失效策略** 缓存算法也会影响命中率和性能,目前效率最高的算法是 Caffeine 使用的 **W-TinyLFU 算法** ,它的命中率非常高,内存占用也更小。新版本的 spring-cache,已经默认支持 Caffeine。 +**缓存最重要的指标就是命中率**,有以下几个因素会影响命中率。**(1)缓存容量** 缓存的容量总是有限制的,所以就存在一些冷数据的逐出问题。但缓存也不是越大越好,它不能明显挤占业务的内存。**(2)数据集类型** 如果缓存的数据是非热点数据,或者是操作几次就不再使用的冷数据,那命中率肯定会低,缓存也会失去了它的作用。**(3)缓存失效策略** 缓存算法也会影响命中率和性能,目前效率最高的算法是 Caffeine 使用的 **W-TinyLFU 算法**,它的命中率非常高,内存占用也更小。新版本的 spring-cache,已经默认支持 Caffeine。 下图展示了这个算法的性能,[从官网的 github 仓库](https://github.com/ben-manes/caffeine)就可以找到 JMH 的测试代码。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25408\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25408\350\256\262.md" index 493ed4325..c0e7991c3 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25408\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25408\350\256\262.md" @@ -4,9 +4,9 @@ 那什么叫 **分布式缓存** 呢?它其实是一种 **集中管理** 的思想。如果我们的服务有多个节点,堆内缓存在每个节点上都会有一份;而分布式缓存,所有的节点,共用一份缓存,既节约了空间,又减少了管理成本。 -在分布式缓存领域,使用最多的就是 Redis。 **Redis** 支持非常丰富的数据类型,包括字符串(string)、列表(list)、集合(set)、有序集合(zset)、哈希表(hash)等常用的数据结构。当然,它也支持一些其他的比如位图(bitmap)一类的数据结构。 +在分布式缓存领域,使用最多的就是 Redis。**Redis** 支持非常丰富的数据类型,包括字符串(string)、列表(list)、集合(set)、有序集合(zset)、哈希表(hash)等常用的数据结构。当然,它也支持一些其他的比如位图(bitmap)一类的数据结构。 -说到 Redis,就不得不提一下另外一个分布式缓存 **Memcached** (以下简称 MC)。MC 现在已经很少用了,但 **面试的时候经常会问到它们之间的区别** ,这里简单罗列一下: +说到 Redis,就不得不提一下另外一个分布式缓存 **Memcached** (以下简称 MC)。MC 现在已经很少用了,但 **面试的时候经常会问到它们之间的区别**,这里简单罗列一下: ![Drawing 0.png](assets/CgqCHl8qaxiATTH1AAB10CrXXk8295.png) @@ -14,7 +14,7 @@ Redis 在互联网中,几乎是标配。我们接下来,先简单看一下 R ### SpringBoot 如何使用 Redis -使用 SpringBoot 可以很容易地对 Redis 进行操作([完整代码见仓库](https://gitee.com/xjjdog/tuning-lagou-res/tree/master/tuning-008/cache-redis))。Java 的 Redis的客户端,常用的有三个:jedis、redisson 和 lettuce,Spring 默认使用的是 lettuce。 **lettuce** 是使用 netty 开发的,操作是异步的,性能比常用的 jedis 要高; **redisson** 也是异步的,但它对常用的业务操作进行了封装,适合书写有业务含义的代码。 +使用 SpringBoot 可以很容易地对 Redis 进行操作([完整代码见仓库](https://gitee.com/xjjdog/tuning-lagou-res/tree/master/tuning-008/cache-redis))。Java 的 Redis的客户端,常用的有三个:jedis、redisson 和 lettuce,Spring 默认使用的是 lettuce。**lettuce** 是使用 netty 开发的,操作是异步的,性能比常用的 jedis 要高; **redisson** 也是异步的,但它对常用的业务操作进行了封装,适合书写有业务含义的代码。 通过加入下面的 jar 包即可方便地使用 Redis。 @@ -64,7 +64,7 @@ Redis 在互联网中,几乎是标配。我们接下来,先简单看一下 R 如果参与秒杀的人,等待很长时间,体验就非常差,想象一下拥堵的高速公路收费站,就能理解秒杀者的心情。同时,被秒杀的资源会成为热点,发生并发争抢的后果。比如 12306 的抢票,如果单纯使用数据库来接受这些请求,就会产生严重的锁冲突,这也是秒杀业务难的地方。 -大家可以回忆一下上一课时的内容,此时,秒杀前端需求与数据库之间的速度是严重不匹配的,而且秒杀的资源是热点资源。这种场景下,采用缓存是非常合适的。 **处理秒杀业务有三个绝招:** +大家可以回忆一下上一课时的内容,此时,秒杀前端需求与数据库之间的速度是严重不匹配的,而且秒杀的资源是热点资源。这种场景下,采用缓存是非常合适的。**处理秒杀业务有三个绝招:** - 第一,选择速度最快的内存作为数据写入; - 第二,使用异步处理代替同步请求; @@ -76,9 +76,9 @@ Redis 在互联网中,几乎是标配。我们接下来,先简单看一下 R 一个秒杀系统是非常复杂的,一般来说,秒杀可以分为一下三个阶段: -- **准备阶段** ,会提前载入一些必需的数据到缓存中,并提前预热业务数据,用户会不断刷新页面,来查看秒杀是否开始; -- **抢购阶段** ,就是我们通常说的秒杀,会产生瞬时的高并发流量,对资源进行集中操作; -- **结束清算** ,主要完成数据的一致性,处理一些异常情况和回仓操作。 +- **准备阶段**,会提前载入一些必需的数据到缓存中,并提前预热业务数据,用户会不断刷新页面,来查看秒杀是否开始; +- **抢购阶段**,就是我们通常说的秒杀,会产生瞬时的高并发流量,对资源进行集中操作; +- **结束清算**,主要完成数据的一致性,处理一些异常情况和回仓操作。 ![Drawing 4.png](assets/Ciqc1F8qa1eAfW9ZAADONsLsuh4160.png) @@ -98,7 +98,7 @@ seckill:goods:${goodsId}{ - **total** 是一个静态值,表示要秒杀商品的数量,在秒杀开始前,会将这个数值载入到缓存中。 - **start** 是一个布尔值。秒杀开始前的值为 0;通过后台或者定时,将这个值改为 1,则表示秒杀开始。 -- 此时, **alloc** 将会记录已经被秒杀的商品数量,直到它的值达到 total 的上限。 +- 此时,**alloc** 将会记录已经被秒杀的商品数量,直到它的值达到 total 的上限。 ```java static final String goodsId = "seckill:goods:%s"; @@ -117,7 +117,7 @@ public void prepare(String id, int total) { 秒杀的时候,首先需要判断库存,才能够对库存进行锁定。这两步动作并不是原子的,在分布式环境下,多台机器同时对 Redis 进行操作,就会发生同步问题。 -为了 **解决同步问题** ,一种方式就是使用 Lua 脚本,把这些操作封装起来,这样就能保证原子性;另外一种方式就是使用分布式锁,分布式锁我们将在 13、14 课时介绍。 +为了 **解决同步问题**,一种方式就是使用 Lua 脚本,把这些操作封装起来,这样就能保证原子性;另外一种方式就是使用分布式锁,分布式锁我们将在 13、14 课时介绍。 下面是一个调试好的 Lua 脚本,可以看到一些关键的比较动作,和 HINCRBY 命令,能够成为一个原子操作。 @@ -160,15 +160,15 @@ public int secKill(String id, int number) { 第一个比较大的问题就是缓存穿透。这个概念比较好理解,和我们上一课时提到的命中率有关。如果命中率很低,那么压力就会集中在数据库持久层。 -假如能找到相关数据,我们就可以把它缓存起来。但问题是, **本次请求,在缓存和持久层都没有命中,这种情况就叫缓存的穿透。** ![Drawing 7.png](assets/CgqCHl8qa32AXy2GAACsgw1i8As520.png) +假如能找到相关数据,我们就可以把它缓存起来。但问题是,**本次请求,在缓存和持久层都没有命中,这种情况就叫缓存的穿透。**![Drawing 7.png](assets/CgqCHl8qa32AXy2GAACsgw1i8As520.png) 举个例子,如上图,在一个登录系统中,有外部攻击,一直尝试使用不存在的用户进行登录,这些用户都是虚拟的,不能有效地被缓存起来,每次都会到数据库中查询一次,最后就会造成服务的性能故障。 -解决这个问题有多种方案,我们来简单介绍一下。 **第一种** 就是把空对象缓存起来。不是持久层查不到数据吗?那么我们就可以把本次请求的结果设置为 null,然后放入到缓存中。通过设置合理的过期时间,就可以保证后端数据库的安全。 +解决这个问题有多种方案,我们来简单介绍一下。**第一种** 就是把空对象缓存起来。不是持久层查不到数据吗?那么我们就可以把本次请求的结果设置为 null,然后放入到缓存中。通过设置合理的过期时间,就可以保证后端数据库的安全。 缓存空对象会占用额外的缓存空间,还会有数据不一致的时间窗口,所以 **第二种** 方法就是针对大数据量的、有规律的键值,使用布隆过滤器进行处理。 -一条记录存在与不存在,是一个 Bool 值,只需要使用 1 比特就可存储。 **布隆过滤器** 就可以把这种是、否操作,压缩到一个数据结构中。比如手机号,用户性别这种数据,就非常适合使用布隆过滤器。 +一条记录存在与不存在,是一个 Bool 值,只需要使用 1 比特就可存储。**布隆过滤器** 就可以把这种是、否操作,压缩到一个数据结构中。比如手机号,用户性别这种数据,就非常适合使用布隆过滤器。 #### 2.缓存击穿 @@ -203,14 +203,14 @@ public int secKill(String id, int number) { 由于业务逻辑大多数情况下,是比较复杂的。其中的更新操作,就非常昂贵,比如一个用户的余额,就是通过计算一系列的资产算出来的一个数。如果这些关联的资产,每个地方改动的时候,都去刷新缓存,那代码结构就会非常混乱,以至于无法维护。 -我推荐使用 **触发式的缓存一致性方式** ,使用懒加载的方式,可以让缓存的同步变得非常简单: +我推荐使用 **触发式的缓存一致性方式**,使用懒加载的方式,可以让缓存的同步变得非常简单: - 当读取缓存的时候,如果缓存里没有相关数据,则执行相关的业务逻辑,构造缓存数据存入到缓存系统; - 当与缓存项相关的资源有变动,则先删除相应的缓存项,然后再对资源进行更新,这个时候,即使是资源更新失败,也是没有问题的。 这种操作,除了编程模型简单,有一个明显的好处。我只有在用到这个缓存的时候,才把它加载到缓存系统中。如果每次修改 都创建、更新资源,那缓存系统中就会存在非常多的冷数据。 -但这样还是有问题。 **接下来介绍的场景,也是面试中经常提及的问题。** 我们上面提到的缓存删除动作,和数据库的更新动作,明显是不在一个事务里的。如果一个请求删除了缓存,同时有另外一个请求到来,此时发现没有相关的缓存项,就从数据库里加载了一份到缓存系统。接下来,数据库的更新操作也完成了,此时数据库的内容和缓存里的内容,就产生了不一致。 +但这样还是有问题。**接下来介绍的场景,也是面试中经常提及的问题。** 我们上面提到的缓存删除动作,和数据库的更新动作,明显是不在一个事务里的。如果一个请求删除了缓存,同时有另外一个请求到来,此时发现没有相关的缓存项,就从数据库里加载了一份到缓存系统。接下来,数据库的更新操作也完成了,此时数据库的内容和缓存里的内容,就产生了不一致。 下面这张图,直观地解释了这种不一致的情况,此时,缓存读取 B 操作以及之后的读取操作,都会读到错误的缓存值。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25409\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25409\350\256\262.md" index 55c467398..720e4b5bc 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25409\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25409\350\256\262.md" @@ -4,7 +4,7 @@ 并且这些对象都有一个显著的特征,就是通过轻量级的重置工作,可以循环、重复地使用。这个时候,我们就可以 **使用一个虚拟的池子,将这些资源保存起来,当使用的时候,我们就从池子里快速获取一个即可** 。 -在 Java 中, **池化技术** 应用非常广泛,常见的就有数据库连接池、线程池等,本节课主讲连接池,线程池我们将在 12 课时进行介绍。 +在 Java 中,**池化技术** 应用非常广泛,常见的就有数据库连接池、线程池等,本节课主讲连接池,线程池我们将在 12 课时进行介绍。 ### 公用池化包 Commons Pool 2.0 @@ -18,7 +18,7 @@ public GenericObjectPool( final GenericObjectPoolConfig config) ``` -**Redis 的常用客户端 Jedis** ,就是使用 Commons Pool 管理连接池的,可以说是一个最佳实践。下图是 Jedis 使用工厂 **创建对象** 的主要代码块。对象工厂类最主要的方法就是makeObject,它的返回值是 PooledObject 类型,可以将对象使用 new DefaultPooledObject\<>(obj) 进行简单包装返回。 +**Redis 的常用客户端 Jedis**,就是使用 Commons Pool 管理连接池的,可以说是一个最佳实践。下图是 Jedis 使用工厂 **创建对象** 的主要代码块。对象工厂类最主要的方法就是makeObject,它的返回值是 PooledObject 类型,可以将对象使用 new DefaultPooledObject\<>(obj) 进行简单包装返回。 ![Drawing 0.png](assets/CgqCHl8xKV-AHSvoAAX4BkEi8aQ783.png) @@ -51,7 +51,7 @@ private long timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_ private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED; ``` -参数很多,要想了解参数的意义,我们首先来看一下一个池化对象在整个池子中的生命周期。如下图所示,池子的操作主要有两个:一个是 **业务线程** ,一个是 **检测线程** 。 +参数很多,要想了解参数的意义,我们首先来看一下一个池化对象在整个池子中的生命周期。如下图所示,池子的操作主要有两个:一个是 **业务线程**,一个是 **检测线程** 。 ![Drawing 3.png](assets/CgqCHl8xKYKAdvm7AADGC-6LsfE257.png) @@ -63,7 +63,7 @@ private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED; 其中 **maxTotal** 和业务线程有关,当业务线程想要获取对象时,会首先检测是否有空闲的对象。如果有,则返回一个;否则进入创建逻辑。此时,如果池中个数已经达到了最大值,就会创建失败,返回空对象。 -对象在获取的时候,有一个非常重要的参数,那就是 **最大等待时间(maxWaitMillis)** ,这个参数对应用方的性能影响是比较大的。该参数默认为 -1,表示永不超时,直到有对象空闲。 +对象在获取的时候,有一个非常重要的参数,那就是 **最大等待时间(maxWaitMillis)**,这个参数对应用方的性能影响是比较大的。该参数默认为 -1,表示永不超时,直到有对象空闲。 如下图,如果对象创建非常缓慢或者使用非常繁忙,业务线程会持续阻塞 (blockWhenExhausted 默认为 true),进而导致正常服务也不能运行。 @@ -71,15 +71,15 @@ private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED; 一般面试官会问:你会把超时参数设置成多大呢? -我一般都会把最大等待时间,设置成接口可以忍受的最大延迟。比如,一个正常服务响应时间 10ms 左右,达到 1 秒钟就会感觉到卡顿,那么这个参数设置成 500~1000ms 都是可以的。超时之后,会抛出 NoSuchElementException 异常,请求 **会快速失败** ,不会影响其他业务线程,这种 Fail Fast 的思想,在互联网应用非常广泛。 +我一般都会把最大等待时间,设置成接口可以忍受的最大延迟。比如,一个正常服务响应时间 10ms 左右,达到 1 秒钟就会感觉到卡顿,那么这个参数设置成 500~1000ms 都是可以的。超时之后,会抛出 NoSuchElementException 异常,请求 **会快速失败**,不会影响其他业务线程,这种 Fail Fast 的思想,在互联网应用非常广泛。 -带有 **evcit** 字样的参数,主要是处理对象逐出的。池化对象除了初始化和销毁的时候比较昂贵,在运行时也会占用系统资源。比如, **连接池** 会占用多条连接, **线程池** 会增加调度开销等。业务在突发流量下,会申请到超出正常情况的对象资源,放在池子中。等这些对象不再被使用,我们就需要把它清理掉。 +带有 **evcit** 字样的参数,主要是处理对象逐出的。池化对象除了初始化和销毁的时候比较昂贵,在运行时也会占用系统资源。比如,**连接池** 会占用多条连接,**线程池** 会增加调度开销等。业务在突发流量下,会申请到超出正常情况的对象资源,放在池子中。等这些对象不再被使用,我们就需要把它清理掉。 超出 **minEvictableIdleTimeMillis** 参数指定值的对象,就会被强制回收掉,这个值默认是 30 分钟; **softMinEvictableIdleTimeMillis** 参数类似,但它只有在当前对象数量大于 minIdle 的时候才会执行移除,所以前者的动作要更暴力一些。 -还有 4 个 test 参数: **testOnCreate** 、 **testOnBorrow** 、 **testOnReturn** 、 **testWhileIdle** ,分别指定了在创建、获取、归还、空闲检测的时候,是否对池化对象进行有效性检测。 +还有 4 个 test 参数: **testOnCreate** 、 **testOnBorrow** 、 **testOnReturn** 、 **testWhileIdle**,分别指定了在创建、获取、归还、空闲检测的时候,是否对池化对象进行有效性检测。 -开启这些检测,能保证资源的有效性,但它会耗费性能,所以默认为 false。生产环境上,建议只将 **testWhileIdle** 设置为 true,并通过调整 **空闲检测时间间隔(timeBetweenEvictionRunsMillis)** ,比如 1 分钟,来保证资源的可用性,同时也保证效率。 +开启这些检测,能保证资源的有效性,但它会耗费性能,所以默认为 false。生产环境上,建议只将 **testWhileIdle** 设置为 true,并通过调整 **空闲检测时间间隔(timeBetweenEvictionRunsMillis)**,比如 1 分钟,来保证资源的可用性,同时也保证效率。 ### Jedis JMH 测试 @@ -124,7 +124,7 @@ public class JedisPoolVSJedisBenchmark { HikariCP 对性能的一些优化操作,是非常值得我们借鉴的,在之后的 16 课时,我们将详细分析几个优化场景。 -数据库连接池同样面临一个最大值(maximumPoolSize)和最小值(minimumIdle)的问题。 **这里同样有一个非常高频的面试题:你平常会把连接池设置成多大呢?** 很多同学认为, **连接池的大小设置得越大越好,有的同学甚至把这个值设置成 1000 以上,这是一种误解** 。根据经验,数据库连接,只需要 20~50 个就够用了。具体的大小,要根据业务属性进行调整,但大得离谱肯定是不合适的。 +数据库连接池同样面临一个最大值(maximumPoolSize)和最小值(minimumIdle)的问题。**这里同样有一个非常高频的面试题:你平常会把连接池设置成多大呢?** 很多同学认为,**连接池的大小设置得越大越好,有的同学甚至把这个值设置成 1000 以上,这是一种误解** 。根据经验,数据库连接,只需要 20~50 个就够用了。具体的大小,要根据业务属性进行调整,但大得离谱肯定是不合适的。 HikariCP 官方是不推荐设置 minimumIdle 这个值的,它将被默认设置成和 maximumPoolSize 一样的大小。如果你的数据库Server端连接资源空闲较大,不妨也可以去掉连接池的动态调整功能。 @@ -142,17 +142,17 @@ HikariCP 还提到了另外一个知识点,在 JDBC4 的协议中,通过 Con 到了这里你可能会发现池(Pool)与缓存(Cache)有许多相似之处。 -它们之间的一个共同点,就是将对象加工后,存储在相对高速的区域。我习惯性将 **缓存** 看作是 **数据对象** ,而把 **池中的对象** 看作是 **执行对象** 。缓存中的数据有一个命中率问题,而池中的对象一般都是对等的。 +它们之间的一个共同点,就是将对象加工后,存储在相对高速的区域。我习惯性将 **缓存** 看作是 **数据对象**,而把 **池中的对象** 看作是 **执行对象** 。缓存中的数据有一个命中率问题,而池中的对象一般都是对等的。 考虑下面一个场景,jsp 提供了网页的动态功能,它可以在执行后,编译成 class 文件,加快执行速度;再或者,一些媒体平台,会将热门文章,定时转化成静态的 html 页面,仅靠 nginx 的负载均衡即可应对高并发请求(动静分离)。 -这些时候,你很难说清楚, **这是针对缓存的优化,还是针对对象进行了池化,它们在本质** 上只是保存了某个执行步骤的结果,使得下次访问时不需要从头再来。我通常把这种技术叫作 **结果缓存池** (Result Cache Pool),属于多种优化手段的综合。 +这些时候,你很难说清楚,**这是针对缓存的优化,还是针对对象进行了池化,它们在本质** 上只是保存了某个执行步骤的结果,使得下次访问时不需要从头再来。我通常把这种技术叫作 **结果缓存池** (Result Cache Pool),属于多种优化手段的综合。 ### 小结 下面我来简单总结一下该课时的内容重点: -我们从 Java 中最通用的公用池化包 **Commons Pool 2.0** 说起,介绍了它的一些实现细节,并对一些重要参数的应用做了讲解; **Jedis** 就是在 Commons Pool 2.0 的基础上封装的,通过 JMH 测试,我们发现对象池化之后,有了接近 5 倍的性能提升;接下来介绍了数据库连接池中速度速快的 **HikariCP** ,它在池化技术之上,又通过编码技巧进行了进一步的性能提升,HikariCP 是我重点研究的类库之一,我也建议你加入自己的任务清单中。 **总体来说,当你遇到下面的场景,就可以考虑使用池化来增加系统性能:** +我们从 Java 中最通用的公用池化包 **Commons Pool 2.0** 说起,介绍了它的一些实现细节,并对一些重要参数的应用做了讲解; **Jedis** 就是在 Commons Pool 2.0 的基础上封装的,通过 JMH 测试,我们发现对象池化之后,有了接近 5 倍的性能提升;接下来介绍了数据库连接池中速度速快的 **HikariCP**,它在池化技术之上,又通过编码技巧进行了进一步的性能提升,HikariCP 是我重点研究的类库之一,我也建议你加入自己的任务清单中。**总体来说,当你遇到下面的场景,就可以考虑使用池化来增加系统性能:** - 对象的创建或者销毁,需要耗费较多的系统资源; - 对象的创建或者销毁,耗时长,需要繁杂的操作和较长时间的等待; @@ -166,6 +166,6 @@ HikariCP 还提到了另外一个知识点,在 JDBC4 的协议中,通过 Con 平常的编码中,有很多类似的场景。比如 Http 连接池,Okhttp 和 Httpclient 就都提供了连接池的概念,你可以类比着去分析一下,关注点也是在连接大小和超时时间上;在底层的中间件,比如 RPC,也通常使用连接池技术加速资源获取,比如 Dubbo 连接池、 Feign 切换成 httppclient 的实现等技术。 -你会发现,在不同资源层面的池化设计也是类似的。比如 **线程池** ,通过队列对任务进行了二层缓冲,提供了多样的拒绝策略等,线程池我们将在 12 课时进行介绍。线程池的这些特性,你同样可以借鉴到连接池技术中,用来缓解请求溢出,创建一些溢出策略。现实情况中,我们也会这么做。那么具体怎么做?有哪些做法?这部分内容就留给大家思考了,欢迎你在下方留言,与大家一起分享讨论,我也会针对你的思考进行一一点评。 +你会发现,在不同资源层面的池化设计也是类似的。比如 **线程池**,通过队列对任务进行了二层缓冲,提供了多样的拒绝策略等,线程池我们将在 12 课时进行介绍。线程池的这些特性,你同样可以借鉴到连接池技术中,用来缓解请求溢出,创建一些溢出策略。现实情况中,我们也会这么做。那么具体怎么做?有哪些做法?这部分内容就留给大家思考了,欢迎你在下方留言,与大家一起分享讨论,我也会针对你的思考进行一一点评。 但无论以何种方式处理对象,让对象保持精简,提高它的复用度,都是我们的目标,所以下一课时,我将系统讲解大对象的复用和注意点。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25410\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25410\350\256\262.md" index 901721a55..3b132e2b2 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25410\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25410\350\256\262.md" @@ -2,8 +2,8 @@ 本课时我们将讲解一下对于“ **大对象** ”的优化。这里的“大对象”,是一个泛化概念,它可能存放在 JVM 中,也可能正在网络上传输,也可能存在于数据库中。那么为什么大对象会影响我们的应用性能呢? -- 第一,大对象 **占用的资源多** ,垃圾回收器要花一部分精力去对它进行回收; -- 第二,大对象在不同的 **设备** 之间 **交换** ,会耗费网络流量,以及昂贵的 I/O; +- 第一,大对象 **占用的资源多**,垃圾回收器要花一部分精力去对它进行回收; +- 第二,大对象在不同的 **设备** 之间 **交换**,会耗费网络流量,以及昂贵的 I/O; - 第三,对大对象的解析和处理操作是 **耗时** 的,对象职责不聚焦,就会承担额外的性能开销。 结合我们前面提到的缓存,以及对象的池化操作,加上对一些中间结果的保存,我们能够对大对象进行初步的提速。 @@ -118,7 +118,7 @@ Entry[] oldTable = table; List 的代码大家可自行查看,也是阻塞性的,扩容策略是原长度的 1.5 倍。 -由于集合在代码中使用的频率非常高,如果你知道具体的数据项上限,那么不妨设置一个合理的初始化大小。比如,HashMap 需要 1024 个元素,需要 7 次扩容,会影响应用的性能。 **面试中会频繁出现这个问题,你需要了解这些扩容操作对性能的影响。** 但是要注意,像 HashMap 这种有负载因子的集合(0.75), **初始化大小 = 需要的个数/负载因子+1** ,如果你不是很清楚底层的结构,那就不妨保持默认。 +由于集合在代码中使用的频率非常高,如果你知道具体的数据项上限,那么不妨设置一个合理的初始化大小。比如,HashMap 需要 1024 个元素,需要 7 次扩容,会影响应用的性能。**面试中会频繁出现这个问题,你需要了解这些扩容操作对性能的影响。** 但是要注意,像 HashMap 这种有负载因子的集合(0.75),**初始化大小 = 需要的个数/负载因子+1**,如果你不是很清楚底层的结构,那就不妨保持默认。 接下来,我将从数据的 **结构纬度** 和 **时间维度** 出发,讲解一下应用层面的优化。 @@ -199,7 +199,7 @@ return sexSet.get(userId) ? "female" : "male"; } ``` -这些数据,放在堆内内存中,还是过大了。幸运的是,Redis 也支持 Bitmap 结构,如果内存有压力,我们可以把这个结构放到 Redis 中,判断逻辑也是类似的。 **再插一道面试算法题:给出一个 1GB 内存的机器,提供 60亿 int 数据,如何快速判断有哪些数据是重复的?** 大家可以类比思考一下。Bitmap 是一个比较底层的结构,在它之上还有一个叫作布隆过滤器的结构(Bloom Filter),布隆过滤器可以判断一个值不存在,或者可能存在。 +这些数据,放在堆内内存中,还是过大了。幸运的是,Redis 也支持 Bitmap 结构,如果内存有压力,我们可以把这个结构放到 Redis 中,判断逻辑也是类似的。**再插一道面试算法题:给出一个 1GB 内存的机器,提供 60亿 int 数据,如何快速判断有哪些数据是重复的?** 大家可以类比思考一下。Bitmap 是一个比较底层的结构,在它之上还有一个叫作布隆过滤器的结构(Bloom Filter),布隆过滤器可以判断一个值不存在,或者可能存在。 ![Drawing 3.png](assets/Ciqc1F8zkZWAKUhuAACFphHz8XU285.png) @@ -207,15 +207,15 @@ return sexSet.get(userId) ? "female" : "male"; Guava 中有一个 BloomFilter 的类,可以方便地实现相关功能。 -上面这种优化方式, **本质上也是把大对象变成小对象的方式** ,在软件设计中有很多类似的思路。比如像一篇新发布的文章,频繁用到的是摘要数据,就不需要把整个文章内容都查询出来;用户的 feed 信息,也只需要保证可见信息的速度,而把完整信息存放在速度较慢的大型存储里。 +上面这种优化方式,**本质上也是把大对象变成小对象的方式**,在软件设计中有很多类似的思路。比如像一篇新发布的文章,频繁用到的是摘要数据,就不需要把整个文章内容都查询出来;用户的 feed 信息,也只需要保证可见信息的速度,而把完整信息存放在速度较慢的大型存储里。 ### 数据的冷热分离 -数据除了横向的结构纬度,还有一个纵向的 **时间维度** ,对时间维度的优化,最有效的方式就是 **冷热分离** 。 +数据除了横向的结构纬度,还有一个纵向的 **时间维度**,对时间维度的优化,最有效的方式就是 **冷热分离** 。 -所谓 **热数据** ,就是靠近用户的,被频繁使用的数据;而 **冷数据** 是那些访问频率非常低,年代非常久远的数据。 +所谓 **热数据**,就是靠近用户的,被频繁使用的数据;而 **冷数据** 是那些访问频率非常低,年代非常久远的数据。 -同一句复杂的 SQL,运行在几千万的数据表上,和运行在几百万的数据表上,前者的效果肯定是很差的。所以,虽然你的系统刚开始上线时速度很快,但随着时间的推移,数据量的增加,就会渐渐变得很慢。 **冷热分离** 是把数据分成两份,如下图,一般都会保持一份全量数据,用来做一些耗时的统计操作。 +同一句复杂的 SQL,运行在几千万的数据表上,和运行在几百万的数据表上,前者的效果肯定是很差的。所以,虽然你的系统刚开始上线时速度很快,但随着时间的推移,数据量的增加,就会渐渐变得很慢。**冷热分离** 是把数据分成两份,如下图,一般都会保持一份全量数据,用来做一些耗时的统计操作。 ![Drawing 4.png](assets/CgqCHl8zkaGALD0uAADj7bx0YMY053.png) **由于冷热分离在工作中经常遇到,所以面试官会频繁问到数据冷热分离的方案。下面简单介绍三种:** @@ -235,11 +235,11 @@ Guava 中有一个 BloomFilter 的类,可以方便地实现相关功能。 对于结果集的操作,我们可以再发散一下思维。可以将一个简单冗余的结果集,改造成复杂高效的数据结构。这个复杂的数据结构可以代理我们的请求,有效地转移耗时操作。 -* 比如,我们常用的 **数据库索引** ,就是一种对数据的重新组织、加速。 +* 比如,我们常用的 **数据库索引**,就是一种对数据的重新组织、加速。 B+ tree 可以有效地减少数据库与磁盘交互的次数,它通过类似 B+ tree 的数据结构,将最常用的数据进行索引,存储在有限的存储空间中。 -* 还有就是, **在 RPC 中常用的序列化** 。 +* 还有就是,**在 RPC 中常用的序列化** 。 有的服务是采用的 SOAP 协议的 WebService,它是基于 XML 的一种协议,内容大传输慢,效率低下。现在的 Web 服务中,大多数是使用 json 数据进行交互的,json 的效率相比 SOAP 就更高一些。 @@ -257,7 +257,7 @@ protobuf 的设计是值得借鉴的,它通过 tag|leng|value 三段对数据 针对大对象,我们有结构纬度的优化和时间维度的优化两种方法: -从 **结构纬度** 来说,通过把对象 **切分成合适的粒度** ,可以把操作集中在小数据结构上,减少时间处理成本;通过把对象进行 **压缩、转换** ,或者 **提取热点数据** ,就可以避免大对象的存储和传输成本。 +从 **结构纬度** 来说,通过把对象 **切分成合适的粒度**,可以把操作集中在小数据结构上,减少时间处理成本;通过把对象进行 **压缩、转换**,或者 **提取热点数据**,就可以避免大对象的存储和传输成本。 从 **时间纬度** 来说,就可以通过 **冷热分离** 的手段,将常用的数据存放在高速设备中,减少数据处理的集合,加快处理速度。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25411\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25411\350\256\262.md" index 2e55f5d67..b2f1fd7da 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25411\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25411\350\256\262.md" @@ -10,7 +10,7 @@ Spring 广泛使用了代理模式,它使用 CGLIB 对 Java 的字节码进行了增强。在复杂的项目中,会有非常多的 AOP 代码,比如权限、日志等切面。在方便了编码的同时,AOP 也给不熟悉项目代码的同学带来了很多困扰。 -下面我将分析一个 **使用 arthas 找到动态代理慢逻辑的具体原因** ,这种方式在复杂项目中,非常有效,你不需要熟悉项目的代码,就可以定位到性能瓶颈点。 +下面我将分析一个 **使用 arthas 找到动态代理慢逻辑的具体原因**,这种方式在复杂项目中,非常有效,你不需要熟悉项目的代码,就可以定位到性能瓶颈点。 首先,我们创建一个最简单的 Bean([代码见仓库](https://gitee.com/xjjdog/tuning-lagou-res/tree/master/tuning-011/design-pattern))。 @@ -90,7 +90,7 @@ Java 中实现动态代理主要有两种模式:一种是使用 JDK,另外 - 其中,JDK 方式是面向接口的,主 要的相关类是 InvocationHandler 和 Proxy; - CGLib 可以代理普通类,主要的相关类是 MethodInterceptor 和 Enhancer。 -**这个知识点面试频率非常高** ,仓库中有这两个实现的完整代码,这里就不贴出来了。 +**这个知识点面试频率非常高**,仓库中有这两个实现的完整代码,这里就不贴出来了。 下面是 JDK 方式和 CGLib 方式代理速度的 JMH 测试结果: @@ -132,7 +132,7 @@ private static Singleton instace = new Singleton(); 饿汉模式在代码里用的很少,它会造成资源的浪费,生成很多可能永远不会用到的对象。 而对象初始化就不一样了。通常,我们在 new 一个新对象的时候,都会调用它的构造方法,就是,用来初始化对象的属性。由于在同一时刻,多个线程可以同时调用函数,我们就需要使用 synchronized 关键字对生成过程进行同步。 -目前, **公认的兼顾线程安全和效率的单例模式,就是 double check。很多面试官,会要求你手写,并分析 double check 的原理。** +目前,**公认的兼顾线程安全和效率的单例模式,就是 double check。很多面试官,会要求你手写,并分析 double check 的原理。** ![15957667011612.jpg](assets/Ciqc1F86TauAd8scAAB4D1n3djc759.jpg) @@ -143,7 +143,7 @@ private static Singleton instace = new Singleton(); - 第二次检查才是关键。如果不加这次判空动作,可能会有多个线程进入同步代码块,进而生成多个实例。 - 最后一个关键点是 volatile 关键字。在一些低版本的 Java 里,由于指令重排的缘故,可能会导致单例被 new 出来后,还没来得及执行构造函数,就被其他线程使用。 这个关键字,可以阻止字节码指令的重排序,在写 double check 代码时,习惯性会加上 volatile。 -可以看到, **double check 的写法繁杂,注意点很多,它现在其实是一种反模式,已经不推荐使用了** ,我也不推荐你用在自己的代码里。但它能够考察面试者对并发的理解,所以这个问题经常被问到。 +可以看到,**double check 的写法繁杂,注意点很多,它现在其实是一种反模式,已经不推荐使用了**,我也不推荐你用在自己的代码里。但它能够考察面试者对并发的理解,所以这个问题经常被问到。 推荐使用 enum 实现懒加载的单例,代码片段如下: @@ -172,7 +172,7 @@ public class EnumSingleton { 上面的描述,我们非常熟悉,因为在过去的一些课时中,我们就能看到很多享元模式的身影,比如《09 | 案例分析:池化对象的应用场景》里的池化对象和《10 | 案例分析:大对象复用的目标和注意点》里的对象复用等。 -设计模式对这我们平常的编码进行了抽象,从不同的角度去解释设计模式,都会找到设计思想的一些共通点。比如,单例模式就是享元模式的一种特殊情况,它通过共享 **单个实例** ,达到对象的复用。 +设计模式对这我们平常的编码进行了抽象,从不同的角度去解释设计模式,都会找到设计思想的一些共通点。比如,单例模式就是享元模式的一种特殊情况,它通过共享 **单个实例**,达到对象的复用。 值得一提的是,同样的代码,不同的解释,会产生不同的效果。比如下面这段代码: @@ -194,11 +194,11 @@ strategys.put("b",new BStrategy()); 实现深拷贝,还有序列化等手段,比如实现 Serializable 接口,或者把对象转化成 JSON。 -所以,在现实情况下, **原型模式变成了一种思想,而不是加快对象创建速度的工具** 。 +所以,在现实情况下,**原型模式变成了一种思想,而不是加快对象创建速度的工具** 。 ### 小结 -本课时,我们主要看了几个与性能相关的设计模式,包括一些高频的考点。我们了解到了 **Java 实现动态代理** 的两种方式,以及他们的区别,在现版本的 JVM 中,性能差异并不大;我们还了解到 **单例模式的三种创建方式** ,并看了一个 double check 的反例,平常编码中,推荐使用枚举去实现单例;最后,我们学习了 **享元模式** 和 **原型模式** ,它们概念性更强一些,并没有固定的编码模式。 +本课时,我们主要看了几个与性能相关的设计模式,包括一些高频的考点。我们了解到了 **Java 实现动态代理** 的两种方式,以及他们的区别,在现版本的 JVM 中,性能差异并不大;我们还了解到 **单例模式的三种创建方式**,并看了一个 double check 的反例,平常编码中,推荐使用枚举去实现单例;最后,我们学习了 **享元模式** 和 **原型模式**,它们概念性更强一些,并没有固定的编码模式。 我们还顺便学习了 arthas 使用 trace 命令,寻找耗时代码块的方法,最终将问题定位到 Spring 的 AOP 功能模块里,而这种场景在复杂项目中经常发生,需要你特别注意。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25412\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25412\350\256\262.md" index ff0f3773c..a97f91fbc 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25412\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25412\350\256\262.md" @@ -2,7 +2,7 @@ 现在的电脑,往往都有多颗核,即使是一部手机,也往往配备了并行处理器,通过多进程和多线程的手段,就可以让多个 CPU 核同时工作,加快任务的执行。 -Java 提供了非常丰富的 API,来支持多线程开发。对我们 Java 程序员来说, **多线程是面试和工作中必备的技能** 。但它如何应用到业务场景中?又有哪些注意事项?本课时将从一个并行获取数据的例子开始,逐步讲解这个面试中最频繁的知识点。 +Java 提供了非常丰富的 API,来支持多线程开发。对我们 Java 程序员来说,**多线程是面试和工作中必备的技能** 。但它如何应用到业务场景中?又有哪些注意事项?本课时将从一个并行获取数据的例子开始,逐步讲解这个面试中最频繁的知识点。 ### 并行获取数据 @@ -89,7 +89,7 @@ public static void main(String[] args) { 首先,latch 的数量加上 map 的 size,总数应该是 20,但运行之后,大概率不是,我们丢失了部分数据。原因就是,main 方法里使用了 HashMap 类,它并不是线程安全的,在并发执行时发生了错乱,造成了错误的结果,将 HashMap 换成 ConcurrentHashMap 即可解决问题。 -从这个小问题我们就可以看出:并发编程并不是那么友好,一不小心就会踏进陷阱。如果你对集合的使用场景并不是特别在行, **直接使用线程安全的类** ,出错的概率会更少一点。 +从这个小问题我们就可以看出:并发编程并不是那么友好,一不小心就会踏进陷阱。如果你对集合的使用场景并不是特别在行,**直接使用线程安全的类**,出错的概率会更少一点。 我们再来看一下线程池的设置,里面有非常多的参数,最大池数量达到了 200 个。那线程数到底设置多少合适呢?按照我们的需求,每次请求需要执行 20 个线程,200 个线程就可以支持 10 个并发量,按照最悲观的 50ms 来算的话,这个接口支持的最小 QPS 就是:1000/50\*10=200。这就是说,如果访问量增加,这个线程数还可以调大。 @@ -125,7 +125,7 @@ public ThreadPoolExecutor(int corePoolSize, ![Drawing 2.png](assets/Ciqc1F8858qAEUHZAACDI8Y5Ehc385.png) -我们来看一下 Executors 工厂类中默认的几个快捷线程池代码。 **1.固定大小线程池** +我们来看一下 Executors 工厂类中默认的几个快捷线程池代码。**1.固定大小线程池** ```java public static ExecutorService newFixedThreadPool(int nThreads) { @@ -139,7 +139,7 @@ new LinkedBlockingQueue()); } ``` -FixedThreadPool 的最大最小线程数是相等的,其实设置成不等的也不会起什么作用。主要原因就是它所采用的任务队列 LinkedBlockingQueue 是无界的,代码走不到判断最大线程池的逻辑。keepAliveTime 参数的设置,也没有意义,因为线程池回收的是corePoolSize和maximumPoolSize 之间的线程。 这个线程池的问题是,由于队列是无界的,在任务较多的情况下,会造成内存使用不可控,同时任务也会在队列里长时间等待。 **2.无限大小线程池** +FixedThreadPool 的最大最小线程数是相等的,其实设置成不等的也不会起什么作用。主要原因就是它所采用的任务队列 LinkedBlockingQueue 是无界的,代码走不到判断最大线程池的逻辑。keepAliveTime 参数的设置,也没有意义,因为线程池回收的是corePoolSize和maximumPoolSize 之间的线程。 这个线程池的问题是,由于队列是无界的,在任务较多的情况下,会造成内存使用不可控,同时任务也会在队列里长时间等待。**2.无限大小线程池** ```java public static ExecutorService newCachedThreadPool() { @@ -155,9 +155,9 @@ return new ThreadPoolExecutor(0, Integer.MAX_VALUE, CachedThreadPool 是另外一个极端,它的最小线程数是 0,线程空闲 1 分钟的都会被回收。在提交任务时,使用了 SynchronousQueue,不缓存任何任务,直接创建新的线程。这种方式同样会有问题,因为它同样无法控制资源的使用,很容易造成内存溢出和过量的线程创建。 一般在线上,这两种方式都不推荐,我们需要根据具体的需求,使用 ThreadPoolExecutor 自行构建线程池,这也是阿里开发规范中推荐的方式。 * 如果任务可以接受一定时间的延迟,那么使用 LinkedBlockingQueue 指定一个队列的上限,缓存一部分任务是合理的; -* 如果任务对实时性要求很高,比如 RPC 服务,就可以使用 SynchronousQueue 队列对任务进行传递,而不是缓存它们。 **3.拒绝策略** 默认的拒绝策略,就是抛出异常的 AbortPolicy,与之类似的是 DiscardPolicy,它什么都不做,连异常都不抛出,这个非常不推荐。 +* 如果任务对实时性要求很高,比如 RPC 服务,就可以使用 SynchronousQueue 队列对任务进行传递,而不是缓存它们。**3.拒绝策略** 默认的拒绝策略,就是抛出异常的 AbortPolicy,与之类似的是 DiscardPolicy,它什么都不做,连异常都不抛出,这个非常不推荐。 还有一个叫作 CallerRunsPolicy,当线程池饱和时,它会使用用户的线程执行任务。比如,在Controller 里的线程池满了,会阻塞在 Tomcat 的线程池里对任务进行执行,这很容易会将用户线程占满,造成用户业务长时间等待。具体用不用这种策略,还是要看客户对等待时间的忍受程度。 -最后一个策略叫作 **DiscardOldestPolicy** ,它在遇到线程饱和时,会先弹出队列里最旧的任务,然后把当前的任务添加到队列中。 +最后一个策略叫作 **DiscardOldestPolicy**,它在遇到线程饱和时,会先弹出队列里最旧的任务,然后把当前的任务添加到队列中。 ### 在 SpringBoot 中如何使用异步? SpringBoot 中可以非常容易地实现异步任务。 首先,我们需要在启动类上加上 @EnableAsync 注解,然后在需要异步执行的方法上加上 @Async 注解。一般情况下,我们的任务直接在后台运行就可以,但有些任务需要返回一些数据,这个时候,就可以使用 Future 返回一个代理,供其他的代码使用。 @@ -185,14 +185,14 @@ return taskExecutor; ``` ### 多线程资源盘点 **1.线程安全的类** 我们在上面谈到了 HashMap 和 ConcurrentHashMap,后者相对于前者,是线程安全的。多线程的细节非常多,下面我们就来盘点一下,一些常见的线程安全的类。 -注意,下面的每一个对比, **都是面试中的知识点** ,想要更加深入地理解,你需要阅读 JDK 的源码。 +注意,下面的每一个对比,**都是面试中的知识点**,想要更加深入地理解,你需要阅读 JDK 的源码。 * StringBuilder 对应着 StringBuffer。后者主要是通过 synchronized 关键字实现了线程的同步。值得注意的是,在单个方法区域里,这两者是没有区别的,JIT 的编译优化会去掉 synchronized 关键字的影响。 * HashMap 对应着 ConcurrentHashMap。ConcurrentHashMap 的话题很大,这里提醒一下 JDK1.7 和 1.8 之间的实现已经不一样了。1.8 已经去掉了分段锁的概念(锁分离技术),并且使用 synchronized 来代替了 ReentrantLock。 * ArrayList 对应着 CopyOnWriteList。后者是写时复制的概念,适合读多写少的场景。 * LinkedList 对应着 ArrayBlockingQueue。ArrayBlockingQueue 对默认是不公平锁,可以修改构造参数,将其改成公平阻塞队列,它在 concurrent 包里使用得非常频繁。 * HashSet 对应着 CopyOnWriteArraySet。 下面以一个经常发生问题的案例,来说一下线程安全的重要性。 -SimpleDateFormat 是我们经常用到的日期处理类,但它本身不是线程安全的,在多线程运行环境下,会产生很多问题,在以往的工作中,通过 sonar 扫描,我发现这种误用的情况特别的多。 **在面试中,我也会专门问到 SimpleDateFormat,用来判断面试者是否具有基本的多线程编程意识。** ![Drawing 5.png](assets/CgqCHl885-eAAm9sAACoGWQZ14E564.png) +SimpleDateFormat 是我们经常用到的日期处理类,但它本身不是线程安全的,在多线程运行环境下,会产生很多问题,在以往的工作中,通过 sonar 扫描,我发现这种误用的情况特别的多。**在面试中,我也会专门问到 SimpleDateFormat,用来判断面试者是否具有基本的多线程编程意识。**![Drawing 5.png](assets/CgqCHl885-eAAm9sAACoGWQZ14E564.png) 执行上图的代码,可以看到,时间已经错乱了。 ```plaintext @@ -221,12 +221,12 @@ Sun Jul 13 01:55:40 CST 20220200 * 使用 Concurrent 包里的可重入锁 ReentrantLock。使用 CAS 方式实现的可重入锁。 * 使用 volatile 关键字控制变量的可见性,这个关键字保证了变量的可见性,但不能保证它的原子性。 * 使用线程安全的阻塞队列完成线程同步。比如,使用 LinkedBlockingQueue 实现一个简单的生产者消费者。 -* 使用原子变量。 **Atomic** \* 系列方法,也是使用 CAS 实现的,关于 CAS,我们将在下一课时介绍。 +* 使用原子变量。**Atomic** \* 系列方法,也是使用 CAS 实现的,关于 CAS,我们将在下一课时介绍。 * 使用 Thread 类的 join 方法,可以让多线程按照指定的顺序执行。 -下面的截图,是使用 LinkedBlockingQueue 实现的一个简单生产者和消费者实例, **在很多互联网的笔试环节,这个题目会经常出现。** 可以看到,我们还使用了一个 volatile 修饰的变量,来决定程序是否继续运行,这也是 volatile 变量的常用场景。 +下面的截图,是使用 LinkedBlockingQueue 实现的一个简单生产者和消费者实例,**在很多互联网的笔试环节,这个题目会经常出现。** 可以看到,我们还使用了一个 volatile 修饰的变量,来决定程序是否继续运行,这也是 volatile 变量的常用场景。 ![Drawing 7.png](assets/Ciqc1F885_aAbF8qAAEC6dLMPo0828.png) ### FastThreadLocal -在我们平常的编程中,使用最多的就是 ThreadLocal 类了。拿最常用的 Spring 来说,它事务管理的传播机制,就是使用 ThreadLocal 实现的。因为 ThreadLocal 是线程私有的,所以 Spring 的事务传播机制是不能够跨线程的。 **在问到 Spring 事务管理是否包含子线程时,要能够想到面试官的真实意图。** +在我们平常的编程中,使用最多的就是 ThreadLocal 类了。拿最常用的 Spring 来说,它事务管理的传播机制,就是使用 ThreadLocal 实现的。因为 ThreadLocal 是线程私有的,所以 Spring 的事务传播机制是不能够跨线程的。**在问到 Spring 事务管理是否包含子线程时,要能够想到面试官的真实意图。** ```plaintext /** @@ -276,7 +276,7 @@ ThreadLocalMap getMap(Thread t) { 还记得《03 | 深入剖析:哪些资源,容易成为瓶颈?》提到的伪共享问题吗?底层的 InternalThreadLocalMap对cacheline 也做了相应的优化。 ![Drawing 9.png](assets/Ciqc1F886AqAGmwzAAJh0-ZJljI401.png) ### 你在多线程使用中都遇到过哪些问题? -通过上面的知识总结,可以看到多线程相关的编程,是属于比较高阶的技能。面试中, **面试官会经常问你在多线程使用中遇到的一些问题,以此来判断你实际的应用情况。** 我们先总结一下文中已经给出的示例: +通过上面的知识总结,可以看到多线程相关的编程,是属于比较高阶的技能。面试中,**面试官会经常问你在多线程使用中遇到的一些问题,以此来判断你实际的应用情况。** 我们先总结一下文中已经给出的示例: * 线程池的不正确使用,造成了资源分配的不可控; * I/O 密集型场景下,线程池开得过小,造成了请求的频繁失败; * 线程池使用了 CallerRunsPolicy 饱和策略,造成了业务线程的阻塞; @@ -325,14 +325,14 @@ protected void afterExecute(Runnable r, Throwable t) { } 只有使用 execute 方法提交的任务才会走到这行异常处理代码。如果你想要默认打印异常,推荐使用 execute 方法提交任务,它和 submit 方法的区别,也不仅仅是返回值不一样那么简单。 ### 关于异步 曾经有同事问我:“异步,并没有减少任务的执行步骤,也没有算法上的改进,那么为什么说异步的速度更快呢?” -其实这是部分同学对“异步作用”的错误理解。 **异步是一种编程模型,它通过将耗时的操作转移到后台线程运行,从而减少对主业务的堵塞,所以我们说异步让速度变快了** 。但如果你的系统资源使用已经到了极限,异步就不能产生任何效果了,它主要优化的是那些阻塞性的等待。 +其实这是部分同学对“异步作用”的错误理解。**异步是一种编程模型,它通过将耗时的操作转移到后台线程运行,从而减少对主业务的堵塞,所以我们说异步让速度变快了** 。但如果你的系统资源使用已经到了极限,异步就不能产生任何效果了,它主要优化的是那些阻塞性的等待。 在我们前面的课程里,缓冲、缓存、池化等优化方法,都是用到了异步。它能够起到转移冲突,优化请求响应的作用。由于合理地利用了资源,我们的系统响应确实变快了, 之后的《15 | 案例分析:从 BIO 到 NIO,再到 AIO》会对此有更多讲解。 异步还能够对业务进行解耦,如下图所示,它比较像是生产者消费者模型。主线程负责生产任务,并将它存放在待执行列表中;消费线程池负责任务的消费,进行真正的业务逻辑处理。 ![Drawing 11.png](assets/CgqCHl886B6ADAe8AAFW13_eF1Q541.png) ### 小结 多线程的话题很大,本课时的内容稍微多,我们简单总结一下课时重点。 本课时默认你已经有了多线程的基础知识(否则看起来会比较吃力),所以我们从 CountDownLatch 的一个实际应用场景说起,谈到了线程池的两个重点: **阻塞队列** 和 **拒绝策略** 。 -接下来,我们学习了如何在常见的框架 **SpringBoot 中配置任务异步执行** 。我们还对多线程的一些重要知识点进行了盘点,尤其看了一些线程安全的工具,以及线程的同步方式。最后,我们对最常用的 **ThreadLocal** 进行了介绍,并了解了 Netty 对这个工具类的优化。 **本课时的所有问题,都是面试高频考点。** 多线程编程的难点除了 API 繁多复杂外,还在于异步编程的模式很难调试。 +接下来,我们学习了如何在常见的框架 **SpringBoot 中配置任务异步执行** 。我们还对多线程的一些重要知识点进行了盘点,尤其看了一些线程安全的工具,以及线程的同步方式。最后,我们对最常用的 **ThreadLocal** 进行了介绍,并了解了 Netty 对这个工具类的优化。**本课时的所有问题,都是面试高频考点。** 多线程编程的难点除了 API 繁多复杂外,还在于异步编程的模式很难调试。 我们也对比较难回答的使用经验问题,进行了专题讨论,例如“你在多线程使用中遇到的一些问题以及解决方法”,这种问题被问到的概率还是很高的。 ``` diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25413\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25413\350\256\262.md" index f965d9ce5..fb13e0c9c 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25413\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25413\350\256\262.md" @@ -20,7 +20,7 @@ SynchronizedNormalBenchmark.threadLocal thrpt 10 7041876.244 ± 355598.686 可以看到,使用同步锁的方式,性能是比较低的。如果去掉业务本身逻辑的影响(删掉执行逻辑),这个差异会更大。代码执行的次数越多,锁的累加影响越大,对锁本身的速度优化,是非常重要的。 -我们都知道,Java 中有两种加锁的方式:一种就是常见的 **synchronized 关键字** ,另外一种,就是 **使用 concurrent 包里面的 Lock** 。针对这两种锁,JDK 自身做了很多的优化,它们的实现方式也是不同的。本课时将从这两种锁讲起,看一下对锁的一些优化方式。 +我们都知道,Java 中有两种加锁的方式:一种就是常见的 **synchronized 关键字**,另外一种,就是 **使用 concurrent 包里面的 Lock** 。针对这两种锁,JDK 自身做了很多的优化,它们的实现方式也是不同的。本课时将从这两种锁讲起,看一下对锁的一些优化方式。 ### synchronied @@ -84,7 +84,7 @@ void syncBlock(); 10 13 10 any ``` -这两者虽然显示效果不同,但他们都是通过 monitor 来实现同步的。我们可以通过下面这张图,来看一下 monitor 的原理。 **注意了,下面是面试题目高发地。比如,你能描述一下 monitor 锁的实现原理吗?** ![1.png](assets/CgqCHl9Dl-mAHYlWAACjjjqUdwE492.png) +这两者虽然显示效果不同,但他们都是通过 monitor 来实现同步的。我们可以通过下面这张图,来看一下 monitor 的原理。**注意了,下面是面试题目高发地。比如,你能描述一下 monitor 锁的实现原理吗?**![1.png](assets/CgqCHl9Dl-mAHYlWAACjjjqUdwE492.png) 如上图所示,我们可以把运行时的对象锁抽象地分成三部分。其中,EntrySet 和 WaitSet 是两个队列,中间虚线部分是当前持有锁的线程,我们可以想象一下线程的执行过程。 @@ -140,7 +140,7 @@ synchronized (lock){ #### 2.分级锁 -在 JDK 1.8 中,synchronized 的速度已经有了显著的提升,它都做了哪些优化呢?答案就是分级锁。JVM 会根据使用情况,对 synchronized 的锁,进行升级,它大体可以按照下面的路径进行升级:偏向锁 — 轻量级锁 — 重量级锁。 **锁只能升级,不能降级** ,所以一旦升级为重量级锁,就只能依靠操作系统进行调度。 +在 JDK 1.8 中,synchronized 的速度已经有了显著的提升,它都做了哪些优化呢?答案就是分级锁。JVM 会根据使用情况,对 synchronized 的锁,进行升级,它大体可以按照下面的路径进行升级:偏向锁 — 轻量级锁 — 重量级锁。**锁只能升级,不能降级**,所以一旦升级为重量级锁,就只能依靠操作系统进行调度。 要想了解锁升级的过程,需要先看一下对象在内存里的结构。 @@ -156,7 +156,7 @@ synchronized (lock){ 01 也是锁默认的状态,线程一旦获取了这把锁,就会把自己的线程 ID 写到 MarkWord 中,在其他线程来获取这把锁之前,锁都处于偏向锁状态。 -当下一个线程参与到偏向锁竞争时,会先判断 MarkWord 中保存的线程 ID 是否与这个线程 ID 相等, **如果不相等,会立即撤销偏向锁,升级为轻量级锁** 。 +当下一个线程参与到偏向锁竞争时,会先判断 MarkWord 中保存的线程 ID 是否与这个线程 ID 相等,**如果不相等,会立即撤销偏向锁,升级为轻量级锁** 。 - **轻量级锁** 轻量级锁的获取是怎么进行的呢?它们使用的是自旋方式。 @@ -253,7 +253,7 @@ sync = fair ? new FairSync() : new NonfairSync(); 由于所有的线程都需要排队,需要在多核的场景下维护一个同步队列,在多个线程争抢锁的时候,吞吐量就很低。 -下面是 20 个并发之下,锁的 JMH 测试结果,可以看到, **非公平锁比公平锁的性能高出两个数量级。** +下面是 20 个并发之下,锁的 JMH 测试结果,可以看到,**非公平锁比公平锁的性能高出两个数量级。** ```plaintext Benchmark Mode Cnt Score Error Units diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25414\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25414\350\256\262.md" index 1a1052bf2..29464a4dd 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25414\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25414\350\256\262.md" @@ -4,7 +4,7 @@ **synchronized** 的方式加锁,会让线程在 BLOCKED 状态和 RUNNABLE 状态之间切换,在操作系统上,就会造成用户态和内核态的频繁切换,效率就比较低。 -与 synchronized 的实现方式不同, **AQS** 中很多数据结构的变化,都是依赖 CAS 进行操作的,而 **CAS 就是乐观锁的一种实现** 。 +与 synchronized 的实现方式不同,**AQS** 中很多数据结构的变化,都是依赖 CAS 进行操作的,而 **CAS 就是乐观锁的一种实现** 。 ### CAS @@ -45,7 +45,7 @@ return v; } ``` -追踪到 JVM 内部,在 linux 机器上参照 os\_cpu/linux\_x86/atomic\_linux\_x86.hpp。可以看到,最底层的调用,是汇编语言,而最重要的,就是 **cmpxchgl** 指令。到这里没法再往下找代码了, **因为 CAS 的原子性实际上是硬件 CPU 直接保证的。** +追踪到 JVM 内部,在 linux 机器上参照 os\_cpu/linux\_x86/atomic\_linux\_x86.hpp。可以看到,最底层的调用,是汇编语言,而最重要的,就是 **cmpxchgl** 指令。到这里没法再往下找代码了,**因为 CAS 的原子性实际上是硬件 CPU 直接保证的。** ```cpp template\<> @@ -92,11 +92,11 @@ private volatile int value; ### 乐观锁 -从上面的描述可以看出, **乐观锁** 严格来说,并不是一种锁,它提供了一种检测冲突的机制,并在有冲突的时候,采取重试的方法完成某项操作。假如没有重试操作,乐观锁就仅仅是一个判断逻辑而已。 +从上面的描述可以看出,**乐观锁** 严格来说,并不是一种锁,它提供了一种检测冲突的机制,并在有冲突的时候,采取重试的方法完成某项操作。假如没有重试操作,乐观锁就仅仅是一个判断逻辑而已。 从这里可以看出乐观锁与悲观锁的一些区别。悲观锁每次操作数据的时候,都会认为别人会修改,所以每次在操作数据的时候,都会加锁,除非别人释放掉锁。 -乐观锁在检测到冲突的时候,会有多次重试操作,所以之前我们说,乐观锁适合用在读多写少的场景;而在资源冲突比较严重的场景,乐观锁会出现多次失败的情况,造成 CPU 的空转,所以悲观锁在这种场景下,会有更好的性能。 **为什么读多写少的情况,就适合使用乐观锁呢?悲观锁在读多写少的情况下,不也是有很少的冲突吗?** 其实,问题不在于冲突的频繁性,而在于 **加锁这个动作** 上。 +乐观锁在检测到冲突的时候,会有多次重试操作,所以之前我们说,乐观锁适合用在读多写少的场景;而在资源冲突比较严重的场景,乐观锁会出现多次失败的情况,造成 CPU 的空转,所以悲观锁在这种场景下,会有更好的性能。**为什么读多写少的情况,就适合使用乐观锁呢?悲观锁在读多写少的情况下,不也是有很少的冲突吗?** 其实,问题不在于冲突的频繁性,而在于 **加锁这个动作** 上。 * 悲观锁需要遵循下面三种模式:一锁、二读、三更新,即使在没有冲突的情况下,执行也会非常慢; * 如之前所说,乐观锁本质上不是锁,它只是一个判断逻辑,资源冲突少的情况下,它不会产生任何开销。 @@ -351,7 +351,7 @@ Disruptor 是一个无锁、有界的队列框架,它的性能非常高。它 ### 小结 -本课时,我们从 CAS 出发,逐步了解了乐观锁的一些概念和使用场景。 **乐观锁** 严格来说,并不是一种锁。它提供了一种检测冲突的机制,并在有冲突的时候,采取重试的方法完成某项操作。假如没有重试操作,乐观锁就仅仅是一个判断逻辑而已。 **悲观锁** 每次操作数据的时候,都会认为别人会修改,所以每次在操作数据的时候,都会加锁,除非别人释放掉锁。 +本课时,我们从 CAS 出发,逐步了解了乐观锁的一些概念和使用场景。**乐观锁** 严格来说,并不是一种锁。它提供了一种检测冲突的机制,并在有冲突的时候,采取重试的方法完成某项操作。假如没有重试操作,乐观锁就仅仅是一个判断逻辑而已。**悲观锁** 每次操作数据的时候,都会认为别人会修改,所以每次在操作数据的时候,都会加锁,除非别人释放掉锁。 乐观锁在读多写少的情况下,之所以比悲观锁快,是因为悲观锁需要进行很多额外的操作,并且乐观锁在没有冲突的情况下,也根本不耗费资源。但乐观锁在冲突比较严重的情况下,由于不断地重试,其性能在大多数情况下,是不如悲观锁的。 @@ -361,7 +361,7 @@ Disruptor 是一个无锁、有界的队列框架,它的性能非常高。它 当然,乐观锁有它的使用场景。当冲突非常严重的情况下,会进行大量的无效计算;它也只能保护单一的资源,处理多个资源的情况下就捉襟见肘;它还会有 ABA 问题,使用带版本号的乐观锁变种可以解决这个问题。 -这些经验,我们都可以从 CAS 中进行借鉴。多线程环境和分布式环境有很多相似之处,对于乐观锁来说,我们找到一种检测冲突的机制,就基本上实现了。 **下面留一个问题,请你分析解答:** +这些经验,我们都可以从 CAS 中进行借鉴。多线程环境和分布式环境有很多相似之处,对于乐观锁来说,我们找到一种检测冲突的机制,就基本上实现了。**下面留一个问题,请你分析解答:** 一个接口的写操作,大约会花费 5 分钟左右的时间。它在开始写时,会把数据库里的一个字段值更新为 start,写入完成后,更新为 done。有另外一个用户也想写入一些数据,但需要等待状态为 done。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25415\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25415\350\256\262.md" index 39b398b0b..245b88738 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25415\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25415\350\256\262.md" @@ -8,7 +8,7 @@ Reactor 是 NIO 的基础。为什么 NIO 的性能就能够比传统的阻塞 I ![Drawing 1.png](assets/CgqCHl9MynKADFW4AAB9PAD7ZA0902.png) -如上图,是典型的 **BIO 模型** ,每当有一个连接到来,经过协调器的处理,就开启一个对应的线程进行接管。如果连接有 1000 条,那就需要 1000 个线程。 +如上图,是典型的 **BIO 模型**,每当有一个连接到来,经过协调器的处理,就开启一个对应的线程进行接管。如果连接有 1000 条,那就需要 1000 个线程。 线程资源是非常昂贵的,除了占用大量的内存,还会占用非常多的 CPU 调度时间,所以 BIO 在连接非常多的情况下,效率会变得非常低。 @@ -70,7 +70,7 @@ PONG:nice 其实,在处理 I/O 动作时,有大部分时间是在等待。比如,socket 连接要花费很长时间进行连接操作,在完成连接的这段时间内,它并没有占用额外的系统资源,但它只能阻塞等待在线程中。这种情况下,系统资源并不能被合理利用。 -Java 的 NIO,在 Linux 上底层是使用 epoll 实现的。epoll 是一个高性能的多路复用 I/O 工具,改进了 select 和 poll 等工具的一些功能。 **在网络编程中,对 epoll 概念的一些理解,几乎是面试中必问的问题。** +Java 的 NIO,在 Linux 上底层是使用 epoll 实现的。epoll 是一个高性能的多路复用 I/O 工具,改进了 select 和 poll 等工具的一些功能。**在网络编程中,对 epoll 概念的一些理解,几乎是面试中必问的问题。** epoll 的数据结构是直接在内核上进行支持的,通过 epoll_create 和 epoll_ctl 等函数的操作,可以构造描述符(fd)相关的事件组合(event)。 @@ -165,7 +165,7 @@ ssc.register(selector, ssc.validOps()); ![Drawing 3.png](assets/Ciqc1F9MyqmAdmlrAAMSNPAj_F4698.png) -接下来,在 while 循环里,使用 select 函数,阻塞在主线程里。所谓 **阻塞** ,就是操作系统不再分配 CPU 时间片到当前线程中,所以 select 函数是几乎不占用任何系统资源的。 +接下来,在 while 循环里,使用 select 函数,阻塞在主线程里。所谓 **阻塞**,就是操作系统不再分配 CPU 时间片到当前线程中,所以 select 函数是几乎不占用任何系统资源的。 ```plaintext int num = selector.select(); @@ -248,7 +248,7 @@ NIO 是基于事件机制的,有一个叫作 Selector 的选择器,阻塞获 ### AIO -关于 NIO 的概念,误解还是比较多的。 **面试官可能会问你:为什么我在使用 NIO 时,使用 Channel 进行读写,socket 的操作依然是阻塞的?NIO 的作用主要体现在哪里?** +关于 NIO 的概念,误解还是比较多的。**面试官可能会问你:为什么我在使用 NIO 时,使用 Channel 进行读写,socket 的操作依然是阻塞的?NIO 的作用主要体现在哪里?** ```plaintext //这行代码是阻塞的 @@ -317,7 +317,7 @@ AIO 是 Java 1.7 加入的,理论上性能会有提升,但实际测试并不 ![image.png](assets/Ciqc1F9My2WAeCGbAACrOS4gYGA066.png) Spring WebFlux 的底层使用的是 Netty,所以操作是异步非阻塞的,类似的组件还有 vert.x、akka、rxjava 等。 WebFlux 是运行在 project reactor 之上的一个封装,其根本特性是后者提供的,至于再底层的非阻塞模型,就是由 Netty 保证的了。 -非阻塞的特性我们可以理解,那响应式又是什么概念呢? **响应式编程** 是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值,通过数据流进行传播。 +非阻塞的特性我们可以理解,那响应式又是什么概念呢?**响应式编程** 是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值,通过数据流进行传播。 这段话很晦涩,在编程方面,它表达的意思就是: **把生产者消费者模式,使用简单的API 表示出来,并自动处理背压(Backpressure)问题。** 背压,指的是生产者与消费者之间的流量控制,通过将操作全面异步化,来减少无效的等待和资源消耗。 Java 的 Lambda 表达式可以让编程模型变得非常简单,Java 9 更是引入了响应式流(Reactive Stream),方便了我们的操作。 比如,下面是 Spring Cloud GateWay 的 Fluent API 写法,响应式编程的 API 都是类似的。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25416\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25416\350\256\262.md" index f3ce19448..3d6885396 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25416\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25416\350\256\262.md" @@ -105,7 +105,7 @@ public String test() { #### 6.HashMap 等集合初始化的时候,指定初始值大小 -这个原则参见 **“10 | 案例分析:大对象复用的目标和注意点”** ,这样的对象有很多,比如 ArrayList,StringBuilder 等,通过指定初始值大小可减少扩容造成的性能损耗。 +这个原则参见 **“10 | 案例分析:大对象复用的目标和注意点”**,这样的对象有很多,比如 ArrayList,StringBuilder 等,通过指定初始值大小可减少扩容造成的性能损耗。 #### 7.遍历 Map 的时候,使用 EntrySet 方法 @@ -115,7 +115,7 @@ public String test() { Random 类的 seed 会在并发访问的情况下发生竞争,造成性能降低,建议在多线程环境下使用 ThreadLocalRandom 类。 -在 Linux 上,通过加入 JVM 配置 **-Djava.security.egd=file:/dev/./urandom** ,使用 urandom 随机生成器,在进行随机数获取时,速度会更快。 +在 Linux 上,通过加入 JVM 配置 **-Djava.security.egd=file:/dev/./urandom**,使用 urandom 随机生成器,在进行随机数获取时,速度会更快。 #### 9.自增推荐使用 LongAddr @@ -555,7 +555,7 @@ private final java.sql.PreparedStatement prepareStatement(java.lang.String, java 大多数普通方法调用,使用的是 **invokevirtual** 指令,属于虚方法调用。 -很多时候,JVM 需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比, **invokestatic** 指令,就属于静态绑定过程,能够直接识别目标方法,效率会高那么一点点。 +很多时候,JVM 需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比,**invokestatic** 指令,就属于静态绑定过程,能够直接识别目标方法,效率会高那么一点点。 虽然 HikariCP 的这些优化有点吹毛求疵,但我们能够从中看到 HikariCP 这些追求性能极致的编码技巧。 @@ -565,4 +565,4 @@ private final java.sql.PreparedStatement prepareStatement(java.lang.String, java 其实语言层面的性能优化,都是在各个资源之间的权衡(比如开发时间、代码复杂度、扩展性等)。这些法则也不是一成不变的教条,这就要求我们在编码中选择合适的工具,根据实际的工作场景进行灵活变动。 -接下来,我们将进入“模块四:JVM 优化”,下一课时我将讲解 **“17 | 高级进阶:JVM 如何完成垃圾回收?”** ,带你向高级进阶。 +接下来,我们将进入“模块四:JVM 优化”,下一课时我将讲解 **“17 | 高级进阶:JVM 如何完成垃圾回收?”**,带你向高级进阶。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25417\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25417\350\256\262.md" index 6cbd71d68..af9153870 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25417\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25417\350\256\262.md" @@ -30,7 +30,7 @@ Java 虚拟机栈图 如上图,Java 虚拟机栈里的每一个元素,叫作栈帧。每一个栈帧,包含四个区域: 局部变量表 、操作数栈、动态连接和返回地址。 -其中, **操作数栈** 就是具体的字节码指令所操作的栈区域,考虑到下面这段代码: +其中,**操作数栈** 就是具体的字节码指令所操作的栈区域,考虑到下面这段代码: ```java public void test(){ @@ -84,7 +84,7 @@ JVM 将会为 test 方法生成一个栈帧,然后入栈,等 test 方法执 把不再使用的对象及时地从堆空间清理出去,是避免 OOM 有效的方法。那 JVM 是如何判断哪些对象应该被清理,哪些对象需要被继续使用呢? -这里首先强调一个概念,这对理解垃圾回收的过程非常有帮助,面试时也能很好地展示自己。 **垃圾回收** ,并不是找到不再使用的对象,然后将这些对象清除掉。它的过程正好相反,JVM 会找到正在使用的对象,对这些使用的对象进行标记和追溯,然后一股脑地把剩下的对象判定为垃圾,进行清理。 +这里首先强调一个概念,这对理解垃圾回收的过程非常有帮助,面试时也能很好地展示自己。**垃圾回收**,并不是找到不再使用的对象,然后将这些对象清除掉。它的过程正好相反,JVM 会找到正在使用的对象,对这些使用的对象进行标记和追溯,然后一股脑地把剩下的对象判定为垃圾,进行清理。 了解了这个概念,我们就可以看下一些基本的衍生分析: @@ -92,7 +92,7 @@ JVM 将会为 test 方法生成一个栈帧,然后入栈,等 test 方法执 - GC 的速度与堆的大小无关,32GB 的堆和 4GB 的堆,只要存活对象是一样的,垃圾回收速度也会差不多; - 垃圾回收不必每次都把垃圾清理得干干净净,最重要的是不要把正在使用的对象判定为垃圾。 -**那么,如何找到这些存活对象,也就是哪些对象是正在被使用的,就成了问题的核心。** 大家可以想一下写代码的时候,如果想要保证一个 HashMap 能够被持续使用,可以把它声明成静态变量,这样就不会被垃圾回收器回收掉。 **我们把这些正在使用的引用的入口,叫作GC Roots。** 这种使用 tracing 方式寻找存活对象的方法,还有一个好听的名字,叫作 **可达性分析法** 。 +**那么,如何找到这些存活对象,也就是哪些对象是正在被使用的,就成了问题的核心。** 大家可以想一下写代码的时候,如果想要保证一个 HashMap 能够被持续使用,可以把它声明成静态变量,这样就不会被垃圾回收器回收掉。**我们把这些正在使用的引用的入口,叫作GC Roots。** 这种使用 tracing 方式寻找存活对象的方法,还有一个好听的名字,叫作 **可达性分析法** 。 概括来讲,GC Roots 包括: @@ -123,7 +123,7 @@ Java 对象与对象之间的引用,存在着四种不同的引用级别,强 一般情况下,JVM 在做这些事情的时候,都会停止业务线程的所有工作,进入 SafePoint 状态,这也就是我们通常说的 Stop the World。所以,现在的垃圾回收器,有一个主要目标,就是减少 STW 的时间。 -其中一种有效的方式,就是采用 **分代垃圾回收** ,减少单次回收区域的大小。这是因为,大部分对象,可以分为两类: +其中一种有效的方式,就是采用 **分代垃圾回收**,减少单次回收区域的大小。这是因为,大部分对象,可以分为两类: - 大部分对象的生命周期都很短 - 其他对象则很可能会存活很长时间 @@ -180,7 +180,7 @@ G1 的配置非常简单,我们只需要配置三个参数,一般就可以 ### 小结 -本课时,我们主要介绍了 JVM 的内存区域划分, **面试的时候,经常有同学把这个概念和 Java 的内存模型(JMM)搞混,这需要你特别注意。** > JMM 指的是与多线程协作相关的主存与工作内存方面的内容,一定要和面试官确认好要问的问题。 +本课时,我们主要介绍了 JVM 的内存区域划分,**面试的时候,经常有同学把这个概念和 Java 的内存模型(JMM)搞混,这需要你特别注意。** > JMM 指的是与多线程协作相关的主存与工作内存方面的内容,一定要和面试官确认好要问的问题。 这一课时我们主要介绍了堆、Java 虚拟机栈、程序计数器、本地方法栈、元空间、直接内存这六个概念。 @@ -192,4 +192,4 @@ JVM 的垃圾回收器更新很快,也有非常多的 JVM 版本,比如 Zing 正如我刚刚所言,垃圾回收器的主要目标是保证回收效果的同时,提高吞吐量,减少 STW 的时间。 -从 CMS 垃圾回收器,到 G1 垃圾回收器,再到现在支持 16TB 大小的 ZGC,垃圾回收器的演变越来越智能,配置参数也越来越少,能够达到开箱即用的效果。但无论使用哪种垃圾回收器,我们的编码方式还是会影响垃圾回收的效果,减少对象的创建,及时切断与不再使用对象的联系,是我们平常编码中一定要注意的。 **最后留一个思考题:我们常说的对象,除了基本数据类型,一定是在堆上分配的吗?欢迎你在留言区与大家分享探讨,我将一一点评解答。** \ No newline at end of file +从 CMS 垃圾回收器,到 G1 垃圾回收器,再到现在支持 16TB 大小的 ZGC,垃圾回收器的演变越来越智能,配置参数也越来越少,能够达到开箱即用的效果。但无论使用哪种垃圾回收器,我们的编码方式还是会影响垃圾回收的效果,减少对象的创建,及时切断与不再使用对象的联系,是我们平常编码中一定要注意的。**最后留一个思考题:我们常说的对象,除了基本数据类型,一定是在堆上分配的吗?欢迎你在留言区与大家分享探讨,我将一一点评解答。** \ No newline at end of file diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25418\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25418\350\256\262.md" index 1ca403bcf..4008474dd 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25418\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25418\350\256\262.md" @@ -57,7 +57,7 @@ public class A{ 在 **“05 | 工具实践:基准测试 JMH,精确测量方法性能”** 提到 JMH 的时候,我们就了解到 CompilerControl 注解可以控制 JIT 编译器的一些行为。 -其中,有一个模式叫作 **inline** ,就是内联的意思,它会把一些短小的方法体,直接纳入目标方法的作用范围之内,就像是直接在代码块中追加代码。这样,就少了一次方法调用,执行速度就能够得到提升,这就是方法内联的概念。 +其中,有一个模式叫作 **inline**,就是内联的意思,它会把一些短小的方法体,直接纳入目标方法的作用范围之内,就像是直接在代码块中追加代码。这样,就少了一次方法调用,执行速度就能够得到提升,这就是方法内联的概念。 可以使用 -XX:-Inline 参数来禁用方法内联,如果想要更细粒度的控制,可以使用 CompileCommand 参数,例如: @@ -162,11 +162,11 @@ public class EscapeReturn { } ``` -那逃逸分析有什么好处呢? **1. 栈上分配** 如果一个对象在子程序中被分配,指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再分配到堆空间,可以有效地减少 GC 的压力。 **2. 分离对象或标量替换** 但对象结构通常都比较复杂,如何将对象保存在栈上呢? +那逃逸分析有什么好处呢?**1. 栈上分配** 如果一个对象在子程序中被分配,指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再分配到堆空间,可以有效地减少 GC 的压力。**2. 分离对象或标量替换** 但对象结构通常都比较复杂,如何将对象保存在栈上呢? JIT 可以将对象打散,全部替换为一个个小的局部变量,这个打散的过程,就叫作标量替换(标量就是不能被进一步分割的变量,比如 int、long 等基本类型)。也就是说,标量替换后的对象,全部变成了局部变量,可以方便地进行栈上分配,而无须改动其他的代码。 -从上面的描述我们可以看到,并不是所有的对象或者数组,都会在堆上分配。由于JIT的存在,如果发现某些对象没有逃逸出方法,那么就有可能被优化成栈分配。 **3.同步消除** 如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 +从上面的描述我们可以看到,并不是所有的对象或者数组,都会在堆上分配。由于JIT的存在,如果发现某些对象没有逃逸出方法,那么就有可能被优化成栈分配。**3.同步消除** 如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 注意这是针对 synchronized 来说的,JUC 中的 Lock 并不能被消除。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25419\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25419\350\256\262.md" index f1efc5569..7427a32d1 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25419\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25419\350\256\262.md" @@ -51,7 +51,7 @@ ElasticSearch(简称 ES)是一个高性能的开源分布式搜索引擎。E ![Drawing 1.png](assets/CgqCHl9fOACAW_TIAAClqw0re70194.png) -但我们在配置 ES 的堆内存时,通常把堆的初始化大小,设置成物理内存的一半。这是因为 ES 是存储类型的服务,我们需要预留一半的内存给文件缓存(理论参见 **“07 | 案例分析:无处不在的缓存,高并发系统的法宝”** ),等下次用到相同的文件时,就不用与磁盘进行频繁的交互。这一块区域一般叫作 **PageCache** ,占用的空间很大。 +但我们在配置 ES 的堆内存时,通常把堆的初始化大小,设置成物理内存的一半。这是因为 ES 是存储类型的服务,我们需要预留一半的内存给文件缓存(理论参见 **“07 | 案例分析:无处不在的缓存,高并发系统的法宝”** ),等下次用到相同的文件时,就不用与磁盘进行频繁的交互。这一块区域一般叫作 **PageCache**,占用的空间很大。 对于计算型节点来说,比如我们普通的 Web 服务,通常会把堆内存设置为物理内存的 2/3,剩下的 1/3 就是给堆外内存使用的。 @@ -119,7 +119,7 @@ ES 默认使用 CMS 垃圾回收器,它有以下三行主要的配置。 下面介绍一下这两个参数: -- **UseConcMarkSweepGC** ,表示年轻代使用 ParNew,老年代的用 CMS 垃圾回收器 +- **UseConcMarkSweepGC**,表示年轻代使用 ParNew,老年代的用 CMS 垃圾回收器 - **-XX:CMSInitiatingOccupancyFraction** 由于 CMS 在执行过程中,用户线程还需要运行,那就需要保证有充足的内存空间供用户使用。如果等到老年代空间快满了,再开启这个回收过程,用户线程可能会产生“Concurrent Mode Failure”的错误,这时会临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了(STW)。 这部分空间预留,一般在 30% 左右即可,那么能用的大概只有 70%。参数 -XX:CMSInitiatingOccupancyFraction 用来配置这个比例,但它首先必须配置 -XX:+UseCMSInitiatingOccupancyOnly 参数。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25420\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25420\350\256\262.md" index a2bff5b5d..7a3b9f70c 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25420\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25420\350\256\262.md" @@ -58,7 +58,7 @@ public String test() { test_total{from="127.0.0.1",method="test",} 5.0 ``` -这里简单介绍一下流行的 **Prometheus 监控体系** ,Prometheus 使用拉的方式获取监控数据,这个暴露数据的过程可以交给功能更加齐全的 telegraf 组件。 +这里简单介绍一下流行的 **Prometheus 监控体系**,Prometheus 使用拉的方式获取监控数据,这个暴露数据的过程可以交给功能更加齐全的 telegraf 组件。 ![Drawing 1.png](assets/CgqCHl9htdiAO89HAAK1NRYCNZE604.png) @@ -96,7 +96,7 @@ SpringBoot 默认使用内嵌的 tomcat 作为 Web 容器,使用典型的 MVC ### HTTP 优化 -下面我们举例来看一下,哪些动作能够加快网页的获取。为了描述方便,我们仅讨论 HTTP1.1 协议的。 **1.使用 CDN 加速文件获取** 比较大的文件,尽量使用 CDN(Content Delivery Network)分发,甚至是一些常用的前端脚本、样式、图片等,都可以放到 CDN 上。CDN 通常能够加快这些文件的获取,网页加载也更加迅速。 **2.合理设置 Cache-Control 值** +下面我们举例来看一下,哪些动作能够加快网页的获取。为了描述方便,我们仅讨论 HTTP1.1 协议的。**1.使用 CDN 加速文件获取** 比较大的文件,尽量使用 CDN(Content Delivery Network)分发,甚至是一些常用的前端脚本、样式、图片等,都可以放到 CDN 上。CDN 通常能够加快这些文件的获取,网页加载也更加迅速。**2.合理设置 Cache-Control 值** 浏览器会判断 HTTP 头 Cache-Control 的内容,用来决定是否使用浏览器缓存,这在管理一些静态文件的时候,非常有用,相同作用的头信息还有 Expires。Cache-Control 表示多久之后过期;Expires 则表示什么时候过期。 @@ -111,7 +111,7 @@ location ~* ^.+\.(ico|gif|jpg|jpeg|png)$ { **3.减少单页面请求域名的数量** 减少每个页面请求的域名数量,尽量保证在 4 个之内。这是因为,浏览器每次访问后端的资源,都需要先查询一次 DNS,然后找到 DNS 对应的 IP 地址,再进行真正的调用。 -DNS 有多层缓存,比如浏览器会缓存一份、本地主机会缓存、ISP 服务商缓存等。从 DNS 到 IP 地址的转变,通常会花费 20-120ms 的时间。减少域名的数量,可加快资源的获取。 **4.开启 gzip** 开启 gzip,可以先把内容压缩后,浏览器再进行解压。由于减少了传输的大小,会减少带宽的使用,提高传输效率。 +DNS 有多层缓存,比如浏览器会缓存一份、本地主机会缓存、ISP 服务商缓存等。从 DNS 到 IP 地址的转变,通常会花费 20-120ms 的时间。减少域名的数量,可加快资源的获取。**4.开启 gzip** 开启 gzip,可以先把内容压缩后,浏览器再进行解压。由于减少了传输的大小,会减少带宽的使用,提高传输效率。 在 nginx 中可以很容易地开启,配置如下: @@ -124,7 +124,7 @@ gzip_http_version 1.1; gzip_types text/plain application/javascript text/css; ``` -**5.对资源进行压缩** 对 JavaScript 和 CSS,甚至是 HTML 进行压缩。道理类似,现在流行的前后端分离模式,一般都是对这些资源进行压缩的。 **6.使用 keepalive** 由于连接的创建和关闭,都需要耗费资源。用户访问我们的服务后,后续也会有更多的互动,所以保持长连接可以显著减少网络交互,提高性能。 +**5.对资源进行压缩** 对 JavaScript 和 CSS,甚至是 HTML 进行压缩。道理类似,现在流行的前后端分离模式,一般都是对这些资源进行压缩的。**6.使用 keepalive** 由于连接的创建和关闭,都需要耗费资源。用户访问我们的服务后,后续也会有更多的互动,所以保持长连接可以显著减少网络交互,提高性能。 nginx 默认开启了对客户端的 keep avlide 支持,你可以通过下面两个参数来调整它的行为。 @@ -336,7 +336,7 @@ controller 层用于接收前端的查询参数,然后构造查询结果。现 我见过很多案例,由于返回对象的嵌套层次太深、引用了不该引用的对象(比如非常大的 byte\[\] 对象),造成了内存使用的飙升。 -所以, **对于一般的服务,保持结果集的精简,是非常有必要的** ,这也是 DTO(data transfer object)存在的必要。如果你的项目,返回的结果结构比较复杂,对结果集进行一次转换是非常有必要的。 +所以,**对于一般的服务,保持结果集的精简,是非常有必要的**,这也是 DTO(data transfer object)存在的必要。如果你的项目,返回的结果结构比较复杂,对结果集进行一次转换是非常有必要的。 #### 2.Service 层 @@ -356,7 +356,7 @@ service 层会频繁使用更底层的资源,通过组合的方式获取我们 ![Drawing 7.png](assets/Ciqc1F9htx6ADeh6AAFoqvxy4eM753.png) -如上图,分布式事务要在改造成本、性能、时效等方面进行综合考虑。有一个介于分布式事务和非事务之间的名词,叫作 **柔性事务** 。柔性事务的理念是将业务逻辑和互斥操作,从资源层上移至业务层面。 **关于传统事务和柔性事务,我们来简单比较一下。** **ACID** +如上图,分布式事务要在改造成本、性能、时效等方面进行综合考虑。有一个介于分布式事务和非事务之间的名词,叫作 **柔性事务** 。柔性事务的理念是将业务逻辑和互斥操作,从资源层上移至业务层面。**关于传统事务和柔性事务,我们来简单比较一下。** **ACID** 关系数据库, 最大的特点就是事务处理, 即满足 ACID。 diff --git "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25421\350\256\262.md" "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25421\350\256\262.md" index b6b48bc66..325afb81e 100644 --- "a/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25421\350\256\262.md" +++ "b/docs/Java/Java \346\200\247\350\203\275\344\274\230\345\214\226\345\256\236\346\210\230/\347\254\25421\350\256\262.md" @@ -14,7 +14,7 @@ 再举一个 **硬件层面** 的例子。有一个定时任务,可以算是 CPU 密集型的,每次都将 CPU 用得满满的。由于系统有架构上的硬伤,无法做到横向扩容。技术经过评估,如果改造成按照数据分片执行的模式,则需要耗费长达 1 个月的工时。 -其实在这种情况下,我们通过增加硬件配置的方式,便能解决性能瓶颈问题,为业务改进赢得更多的时间。 **举这两个例子的目的是想要说明,性能优化有很多优化途径,如果这个性能问题可以通过其他方式解决,那就尽量不要采用调整软件代码的方式,我们尽可能地在效果、工时、手段这三方面之间进行权衡。** +其实在这种情况下,我们通过增加硬件配置的方式,便能解决性能瓶颈问题,为业务改进赢得更多的时间。**举这两个例子的目的是想要说明,性能优化有很多优化途径,如果这个性能问题可以通过其他方式解决,那就尽量不要采用调整软件代码的方式,我们尽可能地在效果、工时、手段这三方面之间进行权衡。** ### 如何找到优化目标? @@ -147,7 +147,7 @@ server: ![3.png](assets/CgqCHl9obM2AUI9qAAFueXY-U4s279.png) -有了上面的信息收集和初步优化,我想你脑海里应该对要优化的系统已经有了非常详细的了解,是时候改变一些现有代码的设计了。 **可以说如果上面的基本解决方式面向的是“面”,那么代码层面的优化,面向的就是具体的“性能瓶颈点”。** ### 代码层面 +有了上面的信息收集和初步优化,我想你脑海里应该对要优化的系统已经有了非常详细的了解,是时候改变一些现有代码的设计了。**可以说如果上面的基本解决方式面向的是“面”,那么代码层面的优化,面向的就是具体的“性能瓶颈点”。** ### 代码层面 代码层面的优化是我们课程的重点,我们花了非常大的篇幅在整个“模块三:实战案例与高频面试点”部分进行这方面的讲解,在这一课时我再简单地总结一下。 @@ -179,7 +179,7 @@ server: #### 3.组织优化 -另外一种有效的方式是通过 **重构** ,改变我们代码的组织结构。 +另外一种有效的方式是通过 **重构**,改变我们代码的组织结构。 通过设计模式,可以让我们的代码逻辑更加清晰,在性能优化的时候,可以直接定位到要优化的代码。我曾见过很多需要性能调优的应用代码,由于对象的关系复杂和代码组织的混乱,想要加入一个中间层是相当困难的。这个时候,首要的任务是梳理、重构这些代码,否则很难进行进一步的性能优化。 @@ -203,7 +203,7 @@ server: ![5.png](assets/CgqCHl9obOqAFQ2CAABk4i6nXkU801.png) -如上图, **PDCA 循环** 的方法论可以支持我们管理性能优化的过程,它有 4 个步骤: +如上图,**PDCA 循环** 的方法论可以支持我们管理性能优化的过程,它有 4 个步骤: - P(Planning)计划阶段,找出存在的性能问题,收集性能指标信息,确定要改进的目标,准备达到这些目标的具体措施; - D(do)执行阶段,按照设计,将优化措施付诸实践; diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25401\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25401\350\256\262.md" index 3e5ff0b66..44d7149ab 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25401\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25401\350\256\262.md" @@ -27,7 +27,7 @@ - **系统高可用性差** 。因为所有的功能开发最后都部署到同一个 WAR 包里,运行在同一个 Tomcat 进程之中,一旦某一功能涉及的代码或者资源有问题,那就会影响整个 WAR 包中部署的功能。比如我经常遇到的一个问题,某段代码不断在内存中创建大对象,并且没有回收,部署到线上运行一段时间后,就会造成 JVM 内存泄露,异常退出,那么部署在同一个 JVM 进程中的所有服务都不可用,后果十分严重。 - **线上发布变慢** 。特别是对于 Java 应用来说,一旦代码膨胀,服务启动的时间就会变长,有些甚至超过 10 分钟以上,如果机器规模超过 100 台以上,假设每次发布的步长为 10%,单次发布需要就需要 100 分钟之久。因此,急需一种方法能够将应用的不同模块的解耦,降低开发和部署成本。 -想要解决上面这些问题, **服务化** 的思想也就应运而生。 +想要解决上面这些问题,**服务化** 的思想也就应运而生。 ## 什么是服务化? diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25402\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25402\350\256\262.md" index 376392ffe..e0d3ffba3 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25402\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25402\350\256\262.md" @@ -20,9 +20,9 @@ 那么服务化拆分具体该如何实施呢?一个最有效的手段就是将不同的功能模块服务化,独立部署和运维。以前面提到的社交 App 为例,你可以认为首页信息流是一个服务,评论是一个服务,消息通知是一个服务,个人主页也是一个服务。 -这种服务化拆分方式是 **纵向拆分** ,是从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。 +这种服务化拆分方式是 **纵向拆分**,是从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。 -还有一种服务化拆分方式是 **横向拆分** ,是从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。 +还有一种服务化拆分方式是 **横向拆分**,是从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。 继续以前面提到的社交 App 举例,无论是首页信息流、评论、消息箱还是个人主页,都需要显示用户的昵称。假如用户的昵称功能有产品需求的变更,你需要上线几乎所有的服务,这个成本就有点高了。显而易见,如果我把用户的昵称功能单独部署成一个独立的服务,那么有什么变更我只需要上线这个服务即可,其他服务不受影响,开发和上线成本就大大降低了。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25403\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25403\350\256\262.md" index f80986791..7ebc5888b 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25403\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25403\350\256\262.md" @@ -99,7 +99,7 @@ IDL 文件方式通常用作 Thrift 和 gRPC 这类跨语言服务调用框架 通过前面的讲解,相信你已经对微服务架构有了基本的认识,对微服务架构的基本组件也有了初步了解。 -这几个基本组件共同组成了微服务架构,在生产环境下缺一不可,所以在引入微服务架构之前,你的团队必须掌握这些基本组件的原理并具备相应的开发能力。实现方式上,可以引入开源方案;如果有充足的资深技术人员,也可以选择自行研发微服务架构的每个组件。但对于大部分中小团队来说,我认为采用开源实现方案是一个更明智的选择,一方面你可以节省相关技术人员的投入从而更专注于业务,另一方面也可以少走弯路少踩坑。不管你是采用开源方案还是自行研发, **都必须吃透每个组件的工作原理并能在此基础上进行二次开发** 。 +这几个基本组件共同组成了微服务架构,在生产环境下缺一不可,所以在引入微服务架构之前,你的团队必须掌握这些基本组件的原理并具备相应的开发能力。实现方式上,可以引入开源方案;如果有充足的资深技术人员,也可以选择自行研发微服务架构的每个组件。但对于大部分中小团队来说,我认为采用开源实现方案是一个更明智的选择,一方面你可以节省相关技术人员的投入从而更专注于业务,另一方面也可以少走弯路少踩坑。不管你是采用开源方案还是自行研发,**都必须吃透每个组件的工作原理并能在此基础上进行二次开发** 。 专栏后面的内容,我会带你对这几个微服务架构的基本组件进行详细剖析,让你能知其然也知其所以然。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25404\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25404\350\256\262.md" index 8080d7597..6f207617a 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25404\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25404\350\256\262.md" @@ -2,7 +2,7 @@ 从这期开始,我将陆续给你讲解微服务各个基本组件的原理和实现方式。 -今天我要与你分享的第一个组件是服务发布和引用。我在前面说过,想要构建微服务,首先要解决的问题是, **服务提供者如何发布一个服务,服务消费者如何引用这个服务** 。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。 +今天我要与你分享的第一个组件是服务发布和引用。我在前面说过,想要构建微服务,首先要解决的问题是,**服务提供者如何发布一个服务,服务消费者如何引用这个服务** 。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。 我前面说过,最常见的服务发布和引用的方式有三种: @@ -14,7 +14,7 @@ ## RESTful API -首先来说说 RESTful API 的方式,主要被 **用作 HTTP 或者 HTTPS 协议的接口定义** ,即使在非微服务架构体系下,也被广泛采用。 +首先来说说 RESTful API 的方式,主要被 **用作 HTTP 或者 HTTPS 协议的接口定义**,即使在非微服务架构体系下,也被广泛采用。 下面是开源服务化框架[Motan](https://github.com/weibocom/motan)发布 RESTful API 的例子,它发布了三个 RESTful 格式的 API,接口声明如下: @@ -174,7 +174,7 @@ public class Client { IDL 就是接口描述语言(interface description language)的缩写,通过一种中立的方式来描述接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。比如你用 Java 语言实现提供的一个服务,也能被 PHP 语言调用。 -也就是说 IDL 主要是 **用作跨语言平台的服务之间的调用** ,有两种最常用的 IDL:一个是 Facebook 开源的 **Thrift 协议** ,另一个是 Google 开源的 **gRPC 协议** 。无论是 Thrift 协议还是 gRPC 协议,它们的工作原理都是类似的。 +也就是说 IDL 主要是 **用作跨语言平台的服务之间的调用**,有两种最常用的 IDL:一个是 Facebook 开源的 **Thrift 协议**,另一个是 Google 开源的 **gRPC 协议** 。无论是 Thrift 协议还是 gRPC 协议,它们的工作原理都是类似的。 接下来,我以 gRPC 协议为例,给你讲讲如何使用 IDL 文件方式来描述接口。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25406\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25406\350\256\262.md" index 0c5983e6e..89001b712 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25406\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25406\350\256\262.md" @@ -6,7 +6,7 @@ 在介绍 RPC 调用的原理之前,先来想象一下一次电话通话的过程。首先,呼叫者 A 通过查询号码簿找到被呼叫者 B 的电话号码,然后拨打 B 的电话。B 接到来电提示时,如果方便接听的话就会接听;如果不方便接听的话,A 就得一直等待。当等待超过一段时间后,电话会因超时被挂断,这个时候 A 需要再次拨打电话,一直等到 B 空闲的时候,才能接听。 -RPC 调用的原理与此类似,我习惯把服务消费者叫作 **客户端** ,服务提供者叫作 **服务端** ,两者通常位于网络上两个不同的地址,要完成一次 RPC 调用,就必须先建立网络连接。建立连接后,双方还必须按照某种约定的协议进行网络通信,这个协议就是通信协议。双方能够正常通信后,服务端接收到请求时,需要以某种方式进行处理,处理成功后,把请求结果返回给客户端。为了减少传输的数据大小,还要对数据进行压缩,也就是对数据进行序列化。 +RPC 调用的原理与此类似,我习惯把服务消费者叫作 **客户端**,服务提供者叫作 **服务端**,两者通常位于网络上两个不同的地址,要完成一次 RPC 调用,就必须先建立网络连接。建立连接后,双方还必须按照某种约定的协议进行网络通信,这个协议就是通信协议。双方能够正常通信后,服务端接收到请求时,需要以某种方式进行处理,处理成功后,把请求结果返回给客户端。为了减少传输的数据大小,还要对数据进行压缩,也就是对数据进行序列化。 上面就是 RPC 调用的过程,由此可见,想要完成调用,你需要解决四个问题: @@ -61,7 +61,7 @@ Socket 通信是基于 TCP/IP 协议的封装,建立一次 Socket 连接至少 - NIO 适用于连接数比较多并且请求消耗比较轻的业务场景,比如聊天服务器。这种方式相比 BIO,相对来说编程比较复杂。 - AIO 适用于连接数比较多而且请求消耗比较重的业务场景,比如涉及 I/O 操作的相册服务器。这种方式相比另外两种,编程难度最大,程序也不易于理解。 -上面两个问题就是“通信框架”要解决的问题,你可以基于现有的 Socket 通信,在服务消费者和服务提供者之间建立网络连接,然后在服务提供者一侧基于 BIO、NIO 和 AIO 三种方式中的任意一种实现服务端请求处理,最后再花费一些精力去解决服务消费者和服务提供者之间的网络可靠性问题。这种方式对于 Socket 网络编程、多线程编程知识都要求比较高,感兴趣的话可以尝试自己实现一个通信框架。 **但我建议最为稳妥的方式是使用成熟的开源方案** ,比如 Netty、MINA 等,它们都是经过业界大规模应用后,被充分论证是很可靠的方案。 +上面两个问题就是“通信框架”要解决的问题,你可以基于现有的 Socket 通信,在服务消费者和服务提供者之间建立网络连接,然后在服务提供者一侧基于 BIO、NIO 和 AIO 三种方式中的任意一种实现服务端请求处理,最后再花费一些精力去解决服务消费者和服务提供者之间的网络可靠性问题。这种方式对于 Socket 网络编程、多线程编程知识都要求比较高,感兴趣的话可以尝试自己实现一个通信框架。**但我建议最为稳妥的方式是使用成熟的开源方案**,比如 Netty、MINA 等,它们都是经过业界大规模应用后,被充分论证是很可靠的方案。 假设客户端和服务端的连接已经建立了,服务端也能正确地处理请求了,接下来完成一次正常地 RPC 调用还需要解决两个问题,即数据传输采用什么协议以及数据该如何序列化和反序列化。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25407\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25407\350\256\262.md" index a7a364694..dff8f1276 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25407\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25407\350\256\262.md" @@ -31,13 +31,13 @@ - 时间维度。同一个监控对象,在每天的同一时刻各种指标通常也不会一样,这种差异要么是由业务变更导致,要么是运营活动导致。为了了解监控对象各种指标的变化,通常需要与一天前、一周前、一个月前,甚至三个月前做比较。 - 核心维度。根据我的经验,业务上一般会依据重要性程度对监控对象进行分级,最简单的是分成核心业务和非核心业务。核心业务和非核心业务在部署上必须隔离,分开监控,这样才能对核心业务做重点保障。 -讲到这里先小结一下, **对于一个微服务来说,你必须明确要监控哪些对象、哪些指标,并且还要从不同的维度进行监控,才能掌握微服务的调用情况** 。明确了这几个关键的问题后,那么该如何搭建一个监控系统,来完成上面这些监控功能呢? +讲到这里先小结一下,**对于一个微服务来说,你必须明确要监控哪些对象、哪些指标,并且还要从不同的维度进行监控,才能掌握微服务的调用情况** 。明确了这几个关键的问题后,那么该如何搭建一个监控系统,来完成上面这些监控功能呢? ## 监控系统原理 显然,我们要对服务调用进行监控,首先要能收集到每一次调用的详细信息,包括调用的响应时间、调用是否成功、调用的发起者和接收者分别是谁,这个过程叫作数据采集。采集到数据之后,要把数据通过一定的方式传输给数据处理中心进行处理,这个过程叫作数据传输。数据传输过来后,数据处理中心再按照服务的维度进行聚合,计算出不同服务的请求量、响应时间以及错误率等信息并存储起来,这个过程叫作数据处理。最后再通过接口或者 Dashboard 的形式对外展示服务的调用情况,这个过程叫作数据展示。 -可见, **监控系统主要包括四个环节:数据采集、数据传输、数据处理和数据展示** ,下面我来给你讲解下每一个环节的实现原理。 +可见,**监控系统主要包括四个环节:数据采集、数据传输、数据处理和数据展示**,下面我来给你讲解下每一个环节的实现原理。 ### 1. 数据采集 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25408\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25408\350\256\262.md" index 0670cc7c3..20d6dc1a8 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25408\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25408\350\256\262.md" @@ -12,11 +12,11 @@ 在介绍追踪原理与实现之前,我们先来看看服务追踪的作用。除了刚才说的能够快速定位请求失败的原因以外,我这里再列出四点,它们可以帮你在微服务改造过程中解决不少问题。 -**第一,优化系统瓶颈。** 通过记录调用经过的每一条链路上的耗时,我们能快速定位整个系统的瓶颈点在哪里。比如你访问微博首页发现很慢,肯定是由于某种原因造成的,有可能是运营商网络延迟,有可能是网关系统异常,有可能是某个服务异常,还有可能是缓存或者数据库异常。通过服务追踪,可以从全局视角上去观察,找出整个系统的瓶颈点所在,然后做出针对性的优化。 **第二,优化链路调用。** 通过服务追踪可以分析调用所经过的路径,然后评估是否合理。比如一个服务调用下游依赖了多个服务,通过调用链分析,可以评估是否每个依赖都是必要的,是否可以通过业务优化来减少服务依赖。 +**第一,优化系统瓶颈。** 通过记录调用经过的每一条链路上的耗时,我们能快速定位整个系统的瓶颈点在哪里。比如你访问微博首页发现很慢,肯定是由于某种原因造成的,有可能是运营商网络延迟,有可能是网关系统异常,有可能是某个服务异常,还有可能是缓存或者数据库异常。通过服务追踪,可以从全局视角上去观察,找出整个系统的瓶颈点所在,然后做出针对性的优化。**第二,优化链路调用。** 通过服务追踪可以分析调用所经过的路径,然后评估是否合理。比如一个服务调用下游依赖了多个服务,通过调用链分析,可以评估是否每个依赖都是必要的,是否可以通过业务优化来减少服务依赖。 还有就是,一般业务都会在多个数据中心都部署服务,以实现异地容灾,这个时候经常会出现一种状况就是服务 A 调用了另外一个数据中心的服务 B,而没有调用同处于一个数据中心的服务 B。 -根据我的经验,跨数据中心的调用视距离远近都会有一定的网络延迟,像北京和广州这种几千公里距离的网络延迟可能达到 30ms 以上,这对于有些业务几乎是不可接受的。通过对调用链路进行分析,可以找出跨数据中心的服务调用,从而进行优化,尽量规避这种情况出现。 **第三,生成网络拓扑。** 通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系是什么样的,可以一目了然。除此之外,在网络拓扑图上还可以把服务调用的详细信息也标出来,也能起到服务监控的作用。 **第四,透明传输数据。** 除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息。比如业务想做一些 A/B 测试,这时候就想通过服务追踪系统,把 A/B 测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能够统一进行 A/B 测试。 +根据我的经验,跨数据中心的调用视距离远近都会有一定的网络延迟,像北京和广州这种几千公里距离的网络延迟可能达到 30ms 以上,这对于有些业务几乎是不可接受的。通过对调用链路进行分析,可以找出跨数据中心的服务调用,从而进行优化,尽量规避这种情况出现。**第三,生成网络拓扑。** 通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系是什么样的,可以一目了然。除此之外,在网络拓扑图上还可以把服务调用的详细信息也标出来,也能起到服务监控的作用。**第四,透明传输数据。** 除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息。比如业务想做一些 A/B 测试,这时候就想通过服务追踪系统,把 A/B 测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能够统一进行 A/B 测试。 ## 服务追踪系统原理 @@ -95,7 +95,7 @@ 下面以一张 Zipkin 的调用链路图为例,通过这张图可以看出下面几个信息。 -**服务整体情况** :服务总耗时、服务调用的网络深度、每一层经过的系统,以及多少次调用。下图展示的一次调用,总共耗时 209.323ms,经过了 5 个不同的系统模块,调用深度为 7 层,共发生了 24 次系统调用。 **每一层的情况** :每一层发生了几次调用,以及每一层调用的耗时。 +**服务整体情况** :服务总耗时、服务调用的网络深度、每一层经过的系统,以及多少次调用。下图展示的一次调用,总共耗时 209.323ms,经过了 5 个不同的系统模块,调用深度为 7 层,共发生了 24 次系统调用。**每一层的情况** :每一层发生了几次调用,以及每一层调用的耗时。 ![img](assets/13ea5d45ff39006f14368f44169e5813.png)(图片来源:[https://zipkin.io/public/img/web-screenshot.png](https://zipkin.io/public/img/web-screenshot.png) ) diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25411\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25411\350\256\262.md" index 2251bf181..ae49725dd 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25411\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25411\350\256\262.md" @@ -92,7 +92,7 @@ http://www.springframework.org/schema/aop http://www.springframework.org/schema/ ## 服务发布和引用的那些坑 -根据我的项目经验,在一个服务被多个服务消费者引用的情况下,由于业务经验的参差不齐,可能不同的服务消费者对服务的认知水平不一,比如某个服务可能调用超时了,最好可以重试来提供调用成功率。但可能有的服务消费者会忽视这一点,并没有在服务引用配置文件中配置接口调用超时重试的次数, **因此最好是可以在服务发布的配置文件中预定义好类似超时重试次数** ,即使服务消费者没有在服务引用配置文件中定义,也能继承服务提供者的定义。这就是下面要讲的服务发布预定义配置。 +根据我的项目经验,在一个服务被多个服务消费者引用的情况下,由于业务经验的参差不齐,可能不同的服务消费者对服务的认知水平不一,比如某个服务可能调用超时了,最好可以重试来提供调用成功率。但可能有的服务消费者会忽视这一点,并没有在服务引用配置文件中配置接口调用超时重试的次数,**因此最好是可以在服务发布的配置文件中预定义好类似超时重试次数**,即使服务消费者没有在服务引用配置文件中定义,也能继承服务提供者的定义。这就是下面要讲的服务发布预定义配置。 ### 1. 服务发布预定义配置 @@ -123,7 +123,7 @@ http://www.springframework.org/schema/aop http://www.springframework.org/schema/ 这里就存在一种风险,当服务提供者发生节点变更,尤其是在网络频繁抖动的情况下,所有的服务消费者都会从注册中心拉取最新的服务节点信息,就包括了服务发布配置中预定的各项接口信息,这个信息不加限制的话可能达到 1M 以上,如果同时有上百个服务消费者从注册中心拉取服务节点信息,在注册中心机器部署为百兆带宽的情况下,很有可能会导致网络带宽打满的情况发生。 -面对这种情况, **最好的办法是把服务发布端的详细服务配置信息转移到服务引用端** ,这样的话注册中心中就不需要存储服务提供者发布的详细服务配置信息了。这就是下面要讲的服务引用定义配置。 +面对这种情况,**最好的办法是把服务发布端的详细服务配置信息转移到服务引用端**,这样的话注册中心中就不需要存储服务提供者发布的详细服务配置信息了。这就是下面要讲的服务引用定义配置。 ### 2. 服务引用定义配置 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25417\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25417\350\256\262.md" index 6b0b0c010..6576f0c9e 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25417\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25417\350\256\262.md" @@ -12,7 +12,7 @@ 在网络频繁抖动的情况下,注册中心中可用的节点会不断变化,这时候服务消费者会频繁收到服务提供者节点变更的信息,于是就不断地请求注册中心来拉取最新的可用服务节点信息。当有成百上千个服务消费者,同时请求注册中心获取最新的服务提供者的节点信息时,可能会把注册中心的带宽给占满,尤其是注册中心是百兆网卡的情况下。 -所以针对这种情况, **需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息** 。 +所以针对这种情况,**需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息** 。 我曾经就遇到过这种情况,一个可行的解决方案就是给注册中心设置一个开关,当开关打开时,即使网络频繁抖动,注册中心也不会通知所有的服务消费者有服务节点信息变更,比如只给 10% 的服务消费者返回变更,这样的话就能将注册中心的请求量减少到原来的 1/10。 @@ -22,7 +22,7 @@ 服务提供者在进程启动时,会注册服务到注册中心,并每隔一段时间,汇报心跳给注册中心,以标识自己的存活状态。如果隔了一段固定时间后,服务提供者仍然没有汇报心跳给注册中心,注册中心就会认为该节点已经处于“dead”状态,于是从服务的可用节点信息中移除出去。 -如果遇到网络问题,大批服务提供者节点汇报给注册中心的心跳信息都可能会传达失败,注册中心就会把它们都从可用节点列表中移除出去,造成剩下的可用节点难以承受所有的调用,引起“雪崩”。但是这种情况下,可能大部分服务提供者节点是可用的,仅仅因为网络原因无法汇报心跳给注册中心就被“无情”的摘除了。 **这个时候就需要根据实际业务的情况,设定一个阈值比例,即使遇到刚才说的这种情况,注册中心也不能摘除超过这个阈值比例的节点** 。 +如果遇到网络问题,大批服务提供者节点汇报给注册中心的心跳信息都可能会传达失败,注册中心就会把它们都从可用节点列表中移除出去,造成剩下的可用节点难以承受所有的调用,引起“雪崩”。但是这种情况下,可能大部分服务提供者节点是可用的,仅仅因为网络原因无法汇报心跳给注册中心就被“无情”的摘除了。**这个时候就需要根据实际业务的情况,设定一个阈值比例,即使遇到刚才说的这种情况,注册中心也不能摘除超过这个阈值比例的节点** 。 这个阈值比例可以根据实际业务的冗余度来确定,我通常会把这个比例设定在 20%,就是说注册中心不能摘除超过 20% 的节点。因为大部分情况下,节点的变化不会这么频繁,只有在网络抖动或者业务明确要下线大批量节点的情况下才有可能发生。而业务明确要下线大批量节点的情况是可以预知的,这种情况下可以关闭阈值保护;而正常情况下,应该打开阈值保护,以防止网络抖动时,大批量可用的服务节点被摘除。 @@ -32,7 +32,7 @@ 可见,无论是心跳开关保护机制还是服务节点摘除保护机制,都是因为注册中心里的节点信息是随时可能发生变化的,所以也可以把注册中心叫作动态注册中心。 -那么是不是可以换个思路, **服务消费者并不严格以注册中心中的服务节点信息为准,而是更多的以服务消费者实际调用信息来判断服务提供者节点是否可用** 。这就是下面我要讲的静态注册中心。 +那么是不是可以换个思路,**服务消费者并不严格以注册中心中的服务节点信息为准,而是更多的以服务消费者实际调用信息来判断服务提供者节点是否可用** 。这就是下面我要讲的静态注册中心。 ## 静态注册中心 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25422\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25422\350\256\262.md" index 8a0a4b010..db34d8882 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25422\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25422\350\256\262.md" @@ -18,7 +18,7 @@ public Optional getProductInventoryByCode(String produ } ``` -还有一种方案就是 **把配置都抽离到单独的配置文件当中,使配置与代码分离** ,比如下面这段代码。 +还有一种方案就是 **把配置都抽离到单独的配置文件当中,使配置与代码分离**,比如下面这段代码。 ```java @HystrixCommand(commandKey = "inventory-by-productcode", fallbackMethod = "getDefaultProductInventoryByCode") diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25423\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25423\350\256\262.md" index 3c03aac2a..17a3c07e9 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25423\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25423\350\256\262.md" @@ -6,7 +6,7 @@ ## 微服务治理平台的基本功能 -你可能先会问,到底什么是微服务治理平台?根据我的理解,微服务治理平台就是 **与服务打交道的统一入口** ,无论是开发人员还是运维人员,都能通过这个平台对服务进行各种操作,比如开发人员可以通过这个平台对服务进行降级操作,运维人员可以通过这个平台对服务进行上下线操作,而不需要关心这个操作背后的具体实现。 +你可能先会问,到底什么是微服务治理平台?根据我的理解,微服务治理平台就是 **与服务打交道的统一入口**,无论是开发人员还是运维人员,都能通过这个平台对服务进行各种操作,比如开发人员可以通过这个平台对服务进行降级操作,运维人员可以通过这个平台对服务进行上下线操作,而不需要关心这个操作背后的具体实现。 接下来我就结合下面这张图,给你介绍一下一个微服务治理平台应该具备哪些基本功能。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25424\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25424\350\256\262.md" index 674d9aa0e..326a68872 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25424\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25424\350\256\262.md" @@ -8,13 +8,13 @@ 经过我前面的讲解,你应该认识到微服务架构相比于单体应用来说复杂度提升了很多,这其中涉及很多组件,比如注册中心、配置中心、RPC 框架、监控系统、追踪系统、服务治理等,每个组件都需要专门的人甚至专家把控才能 hold 住,不然微服务架构的落地就相当于空中楼阁,虚无缥缈。 -所以想要落地微服务,首先需要合适的人,也就是组建一支合适的技术团队。你一定很容易想到,是不是只有架构师适合做微服务架构的开发?一定程度上,这是合理的,因为微服务架构所涉及的具体技术,比如 CAP 理论、底层网络可靠性保证、Netty 高并发框架等,都对技术的深度要求比较高,一般有经验的架构师才能掌握,所以这个技术团队必须包含技术能力很强的架构师。但是还要考虑到微服务架构最后还是要落地到业务当中,既要满足业务的需求,也要防止一种情况的发生,那就是全部由架构人员组成技术团队,根据自己的设想,脱离了实际的业务场景,最后开发出来的架构中看不中用,业务无法实际落地,既打击了团队人员积极性,又对业务没有实际价值,劳民伤财。所以这支技术团队,也 **必须包含做业务懂业务的开发人员** ,只有他们了解业务的实际痛点以及落地过程中的难点,这样才能保证最后设计出的微服务架构是贴合业务实际的,并且最后是能够实际落地的。 +所以想要落地微服务,首先需要合适的人,也就是组建一支合适的技术团队。你一定很容易想到,是不是只有架构师适合做微服务架构的开发?一定程度上,这是合理的,因为微服务架构所涉及的具体技术,比如 CAP 理论、底层网络可靠性保证、Netty 高并发框架等,都对技术的深度要求比较高,一般有经验的架构师才能掌握,所以这个技术团队必须包含技术能力很强的架构师。但是还要考虑到微服务架构最后还是要落地到业务当中,既要满足业务的需求,也要防止一种情况的发生,那就是全部由架构人员组成技术团队,根据自己的设想,脱离了实际的业务场景,最后开发出来的架构中看不中用,业务无法实际落地,既打击了团队人员积极性,又对业务没有实际价值,劳民伤财。所以这支技术团队,也 **必须包含做业务懂业务的开发人员**,只有他们了解业务的实际痛点以及落地过程中的难点,这样才能保证最后设计出的微服务架构是贴合业务实际的,并且最后是能够实际落地的。 ## 从一个案例入手 当你的团队决定要对业务进行微服务架构改造时,要避免一上来就妄想将整个业务进行服务化拆分、追求完美。这种想法是很危险的,一切的技术改造都应当以给业务创造价值为宗旨,所以业务的稳定性要放在第一位,切忌好高骛远。 -正确的方法是 **首先从众多业务中找到一个小的业务进行试点** ,前期的技术方案以满足这个小的业务需求为准,力求先把这个小业务的微服务架构落地实施,从中发现各种问题并予以解决,然后才可以继续考虑更大规模的推广。这样的话,即使微服务架构的改造因为技术方案不成熟,对业务造成了影响,也只是局限在一个小的业务之中,不会对整体业务造成太大影响。否则的话,如果因为微服务架构的改造给业务带来灾难性的后果,在许多技术团队的决策者来看,可能微服务架构的所带来的种种好处也不足以抵消其带来的风险,最后整个微服务架构的改造可能就夭折了。 +正确的方法是 **首先从众多业务中找到一个小的业务进行试点**,前期的技术方案以满足这个小的业务需求为准,力求先把这个小业务的微服务架构落地实施,从中发现各种问题并予以解决,然后才可以继续考虑更大规模的推广。这样的话,即使微服务架构的改造因为技术方案不成熟,对业务造成了影响,也只是局限在一个小的业务之中,不会对整体业务造成太大影响。否则的话,如果因为微服务架构的改造给业务带来灾难性的后果,在许多技术团队的决策者来看,可能微服务架构的所带来的种种好处也不足以抵消其带来的风险,最后整个微服务架构的改造可能就夭折了。 回想一下微博业务的微服务改造,从 2013 年开始进行微服务架构的研发,到 2014 年用户关系服务开始进行微服务改造,再到 2015 年 Feed 业务开始进行微服务改造,从几个服务上线后经过春晚流量的考验后,逐步推广到上百个服务的上线,整个过程持续了两年多时间。虽然周期比较长,但是对于大流量的业务系统来说,稳定性永远是在第一位的,业务架构改造追求的是稳步推进,中间可以有小的波折,但对整体架构的演进方向不会产生影响。 @@ -31,7 +31,7 @@ ## 采用 DevOps -微服务架构带来的不光是业务开发模式的改变,对测试和运维的影响也是根本性的。以往在单体应用架构时,开发只需要整体打包成一个服务,交给测试去做自动化测试、交给运维去部署发布就可以了。但是微服务架构下,一个单体应用被拆分成多个细的微服务,并且需要独自开发、测试和上线,如果继续按照之前的单体应用模式运维,那么测试和运维的工作量相当于成倍的增加。因此迫切需要对以往的开发、测试和运维模式进行升级,从我的经验来看, **最好的方案就是采用 DevOps** ,对微服务架构进行一站式开发、测试、上线和运维。 +微服务架构带来的不光是业务开发模式的改变,对测试和运维的影响也是根本性的。以往在单体应用架构时,开发只需要整体打包成一个服务,交给测试去做自动化测试、交给运维去部署发布就可以了。但是微服务架构下,一个单体应用被拆分成多个细的微服务,并且需要独自开发、测试和上线,如果继续按照之前的单体应用模式运维,那么测试和运维的工作量相当于成倍的增加。因此迫切需要对以往的开发、测试和运维模式进行升级,从我的经验来看,**最好的方案就是采用 DevOps**,对微服务架构进行一站式开发、测试、上线和运维。 在单体应用架构下,开发、测试和运维这三者角色的区分是十分比较明显的,分属于不同的部门。而在微服务架构下,由于服务被拆分得足够细,每个服务都需要完成独立的开发、测试和运维工作,有自己完整的生命周期,所以需要将一个服务从代码开发、单元测试、集成测试以及服务发布都自动化起来。这样的话,测试人员就可以从众多微服务的测试中解放出来,着重进行自动化测试用例的维护;运维人员也可以从众多微服务的上线发布工作中解放出来,着重进行 DevOps 体系工具的建设。而每个服务的开发负责人,需要对服务的整个生命周期负责,无论是在代码检查阶段出现问题,还是测试阶段和发布阶段出现问题,都需要去解决。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25425\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25425\350\256\262.md" index d282a42e1..f664b10c8 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25425\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25425\350\256\262.md" @@ -18,7 +18,7 @@ DevOps 可以简单理解为开发和运维的结合,服务的开发者不再 Docker 是容器技术的一种,事实上已经成为业界公认的容器标准,要理解 Docker 的工作原理首先得知道什么是容器。 -容器翻译自英文的 Container 一词,而 Container 又可以翻译成集装箱。我们都知道, **集装箱的作用就是,在港口把货物用集装箱封装起来,然后经过货轮从海上运输到另一个港口,再在港口卸载后通过大货车运送到目的地。这样的话,货物在世界的任何地方流转时,都是在集装箱里封装好的,不需要根据是在货轮上还是大货车上而对货物进行重新装配** 。同样,在软件的世界里,容器也起到了相同的作用,只不过它封装的是软件的运行环境。容器的本质就是 Linux 操作系统里的进程,但与操作系统中运行的一般进程不同的是,容器通过[Namespace](https://en.wikipedia.org/wiki/Linux_namespaces)和[Cgroups](https://zh.wikipedia.org/wiki/Cgroups)这两种机制,可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至是自己的用户 ID 空间,这样的话容器里的进程就像是运行在宿主机上的另外一个单独的操作系统内,从而实现与宿主机操作系统里运行的其他进程隔离。 +容器翻译自英文的 Container 一词,而 Container 又可以翻译成集装箱。我们都知道,**集装箱的作用就是,在港口把货物用集装箱封装起来,然后经过货轮从海上运输到另一个港口,再在港口卸载后通过大货车运送到目的地。这样的话,货物在世界的任何地方流转时,都是在集装箱里封装好的,不需要根据是在货轮上还是大货车上而对货物进行重新装配** 。同样,在软件的世界里,容器也起到了相同的作用,只不过它封装的是软件的运行环境。容器的本质就是 Linux 操作系统里的进程,但与操作系统中运行的一般进程不同的是,容器通过[Namespace](https://en.wikipedia.org/wiki/Linux_namespaces)和[Cgroups](https://zh.wikipedia.org/wiki/Cgroups)这两种机制,可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至是自己的用户 ID 空间,这样的话容器里的进程就像是运行在宿主机上的另外一个单独的操作系统内,从而实现与宿主机操作系统里运行的其他进程隔离。 Docker 也是基于 Linux 内核的 Cgroups、Namespace 机制来实现进程的封装和隔离的,那么 Docker 为何能把容器技术推向一个新的高度呢?这就要从 Docker 在容器技术上的一项创新 Docker 镜像说起。虽然容器解决了应用程序运行时隔离的问题,但是要想实现应用能够从一台机器迁移到另外一台机器上还能正常运行,就必须保证另外一台机器上的操作系统是一致的,而且应用程序依赖的各种环境也必须是一致的。Docker 镜像恰恰就解决了这个痛点,具体来讲,就是 **Docker 镜像不光可以打包应用程序本身,而且还可以打包应用程序的所有依赖,甚至可以包含整个操作系统** 。这样的话,你在你自己本机上运行通过的应用程序,就可以使用 Docker 镜像把应用程序文件、所有依赖的软件以及操作系统本身都打包成一个镜像,可以在任何一个安装了 Docker 软件的地方运行。 @@ -28,7 +28,7 @@ Docker 镜像解决了 DevOps 中微服务运行的环境难以在本地环境 ## 微服务容器化实践 -Docker 能帮助解决服务运行环境可迁移问题的关键,就在于 Docker 镜像的使用上,实际在使用 Docker 镜像的时候往往并不是把业务代码、依赖的软件环境以及操作系统本身直接都打包成一个镜像,而是利用 Docker 镜像的 **分层机制** ,在每一层通过编写 Dockerfile 文件来逐层打包镜像。这是因为虽然不同的微服务依赖的软件环境不同,但是还是存在大大小小的相同之处,因此在打包 Docker 镜像的时候,可以分层设计、逐层复用,这样的话可以减少每一层镜像文件的大小。 +Docker 能帮助解决服务运行环境可迁移问题的关键,就在于 Docker 镜像的使用上,实际在使用 Docker 镜像的时候往往并不是把业务代码、依赖的软件环境以及操作系统本身直接都打包成一个镜像,而是利用 Docker 镜像的 **分层机制**,在每一层通过编写 Dockerfile 文件来逐层打包镜像。这是因为虽然不同的微服务依赖的软件环境不同,但是还是存在大大小小的相同之处,因此在打包 Docker 镜像的时候,可以分层设计、逐层复用,这样的话可以减少每一层镜像文件的大小。 下面我就以微博的业务 Docker 镜像为例,来实际讲解下生产环境中如何使用 Docker 镜像。正如下面这张图所描述的那样,微博的 Docker 镜像大致分为四层。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25426\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25426\350\256\262.md" index 41cc2366b..fb3ea371b 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25426\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25426\350\256\262.md" @@ -6,7 +6,7 @@ 对于大部分业务团队来说,在进行容器化以前,服务都是部署在物理机或者虚拟机上,运维往往有一套既有的运维平台来发布服务。我就以微博的运维平台 JPool 来举例,当有服务要发布的时候,JPool 会根据服务所属的集群(一般一个业务线是一个集群)运行在哪个服务池(一般一个业务线有多个服务池),找到对应的物理机或者虚拟机 IP,然后把最新的应用程序代码通过 Puppet 等工具分批逐次地发布到这些物理机或者虚拟机上,然后重新启动服务,这样就完成一个服务的发布流程。 -但是现在情况变了, **业务容器化后,运维面对的不再是一台台实实在在的物理机或者虚拟机了,而是一个个 Docker 容器,它们可能都没有固定的 IP** ,这个时候要想服务发布该怎么做呢? +但是现在情况变了,**业务容器化后,运维面对的不再是一台台实实在在的物理机或者虚拟机了,而是一个个 Docker 容器,它们可能都没有固定的 IP**,这个时候要想服务发布该怎么做呢? 这时候就需要一个面向容器的新型运维平台,它能够在现有的物理机或者虚拟机上创建容器,并且能够像运维物理机或者虚拟机一样,对容器的生命周期进行管理,通常我们叫它“容器运维平台”。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25432\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25432\350\256\262.md" index 9bb8b874a..831db8483 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25432\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25432\350\256\262.md" @@ -66,7 +66,7 @@ 当有流量上涨,超出了内部私有云机房部署所能承受的范围时,可以扩容阿里云机房的机器,然后把流量切换到阿里云机房,这个过程请看下面这张图。切流量也有两种方案:一是在 DNS 层切换,把原先解析到私有云机房 VIP 的流量,解析到阿里云机房的 SLB,这时候阿里云机房部署的 SLB、Nginx 和 Java Web 都需要扩容;一种是在 Nginx 层切换,把原先转发到私有云机房 Nginx 的流量,转发到阿里云机房的 Java Web,这个时候只需要扩容阿里云的 Java Web。 -**这两种方案应对的业务场景不同** ,DNS 层的切换主要是针对大规模流量增长的情况,这个时候一般四层 VIP、七层 Nginx 和 Java Web 的容量都不足以应对,就需要在 DNS 层就把流量切到阿里云机房,在阿里云扩容 SLB、Nginx 和 Java Web;而 Nginx 层的切换主要是针对私有云内某个机房的 Java Web 容量不足或者服务有问题的时候,需要把这个机房的一部分流量切换到其他机房,这个时候就可以只扩容阿里云机房的 Java Web,然后从 Nginx 层把流量切换到阿里云机房。 +**这两种方案应对的业务场景不同**,DNS 层的切换主要是针对大规模流量增长的情况,这个时候一般四层 VIP、七层 Nginx 和 Java Web 的容量都不足以应对,就需要在 DNS 层就把流量切到阿里云机房,在阿里云扩容 SLB、Nginx 和 Java Web;而 Nginx 层的切换主要是针对私有云内某个机房的 Java Web 容量不足或者服务有问题的时候,需要把这个机房的一部分流量切换到其他机房,这个时候就可以只扩容阿里云机房的 Java Web,然后从 Nginx 层把流量切换到阿里云机房。 ![img](assets/33de6e4eee9b98cf980956f4522d279c.png) diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25435\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25435\350\256\262.md" index ce2c585de..95f05b763 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25435\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25435\350\256\262.md" @@ -12,7 +12,7 @@ 从上面这张图可以看出,一次 HTTP 调用的链路相当长,从我的实践来看,经常会遇到好几个问题。 -**第一个问题:中间链路损耗大** 。由于一次 HTTP 调用要经过 DNS、LVS、Nginx 这三个基础设施,每一层都会带来相应的损耗。我曾经在线上就碰到过因为 DNS 解析延迟、LVS 带宽打满引起的网络延迟,以及 Nginx 本地磁盘写满引起的转发延迟等各种情况,造成接口响应在中间链路的损耗甚至超过了接口本身业务逻辑执行的时间。 **第二个问题:全链路扩容难** 。由于微博业务经常要面临突发热点事件带来的流量冲击,所以需要能够随时随地动态扩缩容。其实在应用本身这一层扩容并不是难点,比较麻烦的是四七层负载均衡设备的动态扩缩容,它涉及如何评估容量、如何动态申请节点并及时修改生效等,要完成一次全链路扩容的话,复杂度非常高,所以最后往往采取的办法是给四七层负载均衡设备预备足够的冗余度,在峰值流量到来时,只扩容应用本身。 **第三个问题:混合云部署难** 。专栏前面我讲过微博的业务目前采用的是混合云部署,也就是在内网私有云和公有云上都有业务部署,同样也需要部署四七层负载均衡设备,并且要支持公有云上的请求经过 DNS 解析后要能够转发到公有云上的负载均衡设备上去,避免跨专线访问带来不必要的网络延迟和专线带宽占用。 +**第一个问题:中间链路损耗大** 。由于一次 HTTP 调用要经过 DNS、LVS、Nginx 这三个基础设施,每一层都会带来相应的损耗。我曾经在线上就碰到过因为 DNS 解析延迟、LVS 带宽打满引起的网络延迟,以及 Nginx 本地磁盘写满引起的转发延迟等各种情况,造成接口响应在中间链路的损耗甚至超过了接口本身业务逻辑执行的时间。**第二个问题:全链路扩容难** 。由于微博业务经常要面临突发热点事件带来的流量冲击,所以需要能够随时随地动态扩缩容。其实在应用本身这一层扩容并不是难点,比较麻烦的是四七层负载均衡设备的动态扩缩容,它涉及如何评估容量、如何动态申请节点并及时修改生效等,要完成一次全链路扩容的话,复杂度非常高,所以最后往往采取的办法是给四七层负载均衡设备预备足够的冗余度,在峰值流量到来时,只扩容应用本身。**第三个问题:混合云部署难** 。专栏前面我讲过微博的业务目前采用的是混合云部署,也就是在内网私有云和公有云上都有业务部署,同样也需要部署四七层负载均衡设备,并且要支持公有云上的请求经过 DNS 解析后要能够转发到公有云上的负载均衡设备上去,避免跨专线访问带来不必要的网络延迟和专线带宽占用。 因此,迫切需要一种支持跨语言调用的服务化框架,使得跨语言应用之间的调用能够像 Java 应用之间的调用一样,不需要经过其他中间链路转发,做到直接交互,就像下图描述的那样。 @@ -24,7 +24,7 @@ ![img](assets/d7d21afa6d37bf5f55a831a25fdef83c.png) -但这种架构主要存在两个问题。 **第一个问题** :Motan 协议与 Yar 协议在基本数据结构和序列化方式的支持有所不同,需要经过复杂的协议转换。 **第二个问题** :服务调用还必须依赖 Nginx,所以调用链路多了一层,在应用部署和扩容时都要考虑 Nginx。 +但这种架构主要存在两个问题。**第一个问题** :Motan 协议与 Yar 协议在基本数据结构和序列化方式的支持有所不同,需要经过复杂的协议转换。**第二个问题** :服务调用还必须依赖 Nginx,所以调用链路多了一层,在应用部署和扩容时都要考虑 Nginx。 ## gRPC 会是救命稻草吗 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25436\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25436\350\256\262.md" index 9dc1f57cb..01c6dd8c0 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25436\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25436\350\256\262.md" @@ -14,7 +14,7 @@ **Filter Chain 模块** 是以请求处理链的组合方式,来实现 AccessLog(请求日志记录)、Metric(监控统计)、CircuitBreaker(熔断)、Switcher(降级)、Tracing(服务追踪)、Mock(单元测试)、ActiveLimit(限流)等功能。 -![img](assets/8464472dced2bf74304f08963205cb03.png) **High Available 模块** 是用来保证高可用性,默认集成了 Failover、Backup Request 等故障处理手段。 **Load Balance 模块** 负载均衡,默认集成了 Random、Roundrobin 等负载均衡算法。 **EndPoint 模块** 的作用是封装请求来调用远程的 Server 端,默认可以封装 Motan 请求和 gRPC 请求。 **Serialize 模块** 负责实现不同类型的序列化方式,默认支持 Simple 序列化。 **Server 模块** 实现不同类型的 Server,要么是采用 Motan 协议实现,要么是采用 gRPC 协议。 +![img](assets/8464472dced2bf74304f08963205cb03.png) **High Available 模块** 是用来保证高可用性,默认集成了 Failover、Backup Request 等故障处理手段。**Load Balance 模块** 负载均衡,默认集成了 Random、Roundrobin 等负载均衡算法。**EndPoint 模块** 的作用是封装请求来调用远程的 Server 端,默认可以封装 Motan 请求和 gRPC 请求。**Serialize 模块** 负责实现不同类型的序列化方式,默认支持 Simple 序列化。**Server 模块** 实现不同类型的 Server,要么是采用 Motan 协议实现,要么是采用 gRPC 协议。 Motan-go Agent 每个模块都是功能可扩展的,你可以在 Filter Chain 模块加上自己实现的 Trace 功能,这样请求在经过 Filter Chain 处理时,就会自动加载你加上的 Trace 功能。当然,你也可以在 High Available 模块添加自己实现的故障处理手段,在 Load Balance 模块里实现自己的负载均衡算法,在 EndPoint 模块封装 HTTP 协议的请求,在 Serialize 模块添加 PB 序列化,在 Server 模块实现 HTTP 协议等。 diff --git "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25437\350\256\262.md" "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25437\350\256\262.md" index a66d23a21..03e832026 100644 --- "a/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25437\350\256\262.md" +++ "b/docs/Java/\344\273\216 0 \345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241/\347\254\25437\350\256\262.md" @@ -2,10 +2,10 @@ 时间过得好快,终于到了该说再见的时候,仿佛下笔写下专栏开篇词就在昨日。回想当初,我与极客时间团队讨论专栏的主题,希望可以面向零基础用户,给一些中小团队的微服务架构落地提供参考和帮助。但是微服务确实有一定的技术门槛,对于团队也有一定的要求,“从入门到放弃”这句话用在微服务上也不是耸人听闻,因此我在构思开篇词时写下了“微服务,从放弃到入门”这个标题,希望通过专栏我们可以一起走入微服务的大门。现在专栏正文已经更新完毕,和我一起走到这里的同学,也请不要松气,从入门到精通还有很长一段路要走。 -回想起一开始学习微服务时,我对注册中心、RPC 调用、熔断、限流等概念也是一知半解,虽然也在实际项目中应用过,但对它们背后的工作原理并不是很了解。我记得当时在微服务架构中为什么要使用注册中心,注册中心是如何判断服务提供者节点存活的,这个问题也一直困扰着我。后来随着服务化改造项目越来越多,我在项目中遇到的问题也越来越多,难度也越来越大。当时为了排查线上问题,我只能逼自己深入进代码细节,去理解注册中心背后的工作原理。 **从学习到实践再到学习的过程** ,才让我真正对注册中心的原理和架构有了深刻地理解,做到了不仅知其然,也知其所以然。之前困扰我的问题以及线上的故障也都一一有了解决方案,仿佛一切都是水到渠成。 **所以在入门到精通这段路上,一定少不了实践的过程** 。只有在掌握基础知识以后,通过具体业务项目的实践,才能深刻体会到这些知识点的原理,真正理解专栏中讲述的那些架构取舍的根本原因所在。这也是我在过去一年的时间里,作为微博跨语言服务化改造的主导者之一,参与推进多个重要业务线微服务架构落地后所得出的体会。同时也希望通过专栏可以把整个实践过程做个总结,分享一下我的实践经验。 +回想起一开始学习微服务时,我对注册中心、RPC 调用、熔断、限流等概念也是一知半解,虽然也在实际项目中应用过,但对它们背后的工作原理并不是很了解。我记得当时在微服务架构中为什么要使用注册中心,注册中心是如何判断服务提供者节点存活的,这个问题也一直困扰着我。后来随着服务化改造项目越来越多,我在项目中遇到的问题也越来越多,难度也越来越大。当时为了排查线上问题,我只能逼自己深入进代码细节,去理解注册中心背后的工作原理。**从学习到实践再到学习的过程**,才让我真正对注册中心的原理和架构有了深刻地理解,做到了不仅知其然,也知其所以然。之前困扰我的问题以及线上的故障也都一一有了解决方案,仿佛一切都是水到渠成。**所以在入门到精通这段路上,一定少不了实践的过程** 。只有在掌握基础知识以后,通过具体业务项目的实践,才能深刻体会到这些知识点的原理,真正理解专栏中讲述的那些架构取舍的根本原因所在。这也是我在过去一年的时间里,作为微博跨语言服务化改造的主导者之一,参与推进多个重要业务线微服务架构落地后所得出的体会。同时也希望通过专栏可以把整个实践过程做个总结,分享一下我的实践经验。 再回到专栏,专栏内容的安排是由浅入深,从基础知识讲起,逐渐深入到业务实践中去。但是微服务发展至今,涵盖的知识点越来越多,所以我挑选了其中最为核心的部分给你详细讲解。更新完全部正文我们再回过头来看,你在回顾这个专栏时可以把它分为两部分,上半部分是微服务架构的基础知识,包括基本原理和基础组件;下半部分是微博在微服务架构方面的具体实践,包括容器运维平台以及 Service Mesh 的具体实践。对于大部分微服务的初学者来说,通过专栏上半部分的学习可以对微服务架构有全面的认识;而对于有一定经验的微服务开发者来说,专栏下半部分的具体实践,能给你提供一些工作中可能会用到的方法论和实战指引。 -做好一件事从来都不是容易的,就好像我写专栏的过程,需要花费大量的时间和精力一次次推翻自己的想法、突破认知的边界。就这样从酷暑写到寒冬,几乎每个工作日的夜晚和周末,都用在学习、写作、录音上。这个过程虽然很痛苦,但对我来说收获是巨大的。同样,学习微服务也是一个循序渐进的过程,就像打怪升级一样,刚开始的初级阶段好像比较容易,但越往后难度越大,尤其到了具体实践环节,对我们构建的知识体系有了一定的要求,不少同学走到这里可能就放弃了。 **但是相信我,在遇到难以理解的知识时,不要轻言放弃,通过反复阅读和理解,并结合具体实践去体会,你的收获会越来越大,对微服务的理解也会越来越深** 。 +做好一件事从来都不是容易的,就好像我写专栏的过程,需要花费大量的时间和精力一次次推翻自己的想法、突破认知的边界。就这样从酷暑写到寒冬,几乎每个工作日的夜晚和周末,都用在学习、写作、录音上。这个过程虽然很痛苦,但对我来说收获是巨大的。同样,学习微服务也是一个循序渐进的过程,就像打怪升级一样,刚开始的初级阶段好像比较容易,但越往后难度越大,尤其到了具体实践环节,对我们构建的知识体系有了一定的要求,不少同学走到这里可能就放弃了。**但是相信我,在遇到难以理解的知识时,不要轻言放弃,通过反复阅读和理解,并结合具体实践去体会,你的收获会越来越大,对微服务的理解也会越来越深** 。 专栏虽然结束了,但我想你一定还有很多疑问,不用担心,我还会继续帮助你答疑。同时针对专栏前面没有来得及回复的留言,我也会专门挑选一些典型的问题深入解答。最后考虑到很多同学在留言中提到想了解一些微博的基础架构,我还会给你赠送特别福利,写几篇关于微博基础架构的文章,敬请期待! diff --git "a/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25420\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25420\350\256\262.md" index 0456b3380..0a93d6322 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25420\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25420\350\256\262.md" @@ -92,7 +92,7 @@ public static int bar(boolean); 这就意味着,生成的机器码越长,越容易填满 Code Cache,从而出现 Code Cache 已满,即时编译已被关闭的警告信息(CodeCache is full. Compiler has been disabled)。 -因此,即时编译器不会无限制地进行方法内联。下面我便列举即时编译器的部分内联规则。(其他的特殊规则,如自动拆箱总会被内联、Throwable 类的方法不能被其他类中的方法所内联,你可以直接参考[JDK 的源代码](http://hg.openjdk.java.net/jdk/jdk/file/da387726a4f5/src/hotspot/share/opto/bytecodeInfo.cpp#l197)。) **首先,由 -XX:CompileCommand 中的 inline 指令指定的方法,以及由 @ForceInline 注解的方法(仅限于 JDK 内部方法),会被强制内联。** 而由 -XX:CompileCommand 中的 dontinline 指令或 exclude 指令(表示不编译)指定的方法,以及由 @DontInline 注解的方法(仅限于 JDK 内部方法),则始终不会被内联。 **其次,如果调用字节码对应的符号引用未被解析、目标方法所在的类未被初始化,或者目标方法是 native 方法,都将导致方法调用无法内联。** **再次,C2 不支持内联超过 9 层的调用(可以通过虚拟机参数 -XX:MaxInlineLevel 调整),以及 1 层的直接递归调用(可以通过虚拟机参数 -XX:MaxRecursiveInlineLevel 调整)。** +因此,即时编译器不会无限制地进行方法内联。下面我便列举即时编译器的部分内联规则。(其他的特殊规则,如自动拆箱总会被内联、Throwable 类的方法不能被其他类中的方法所内联,你可以直接参考[JDK 的源代码](http://hg.openjdk.java.net/jdk/jdk/file/da387726a4f5/src/hotspot/share/opto/bytecodeInfo.cpp#l197)。) **首先,由 -XX:CompileCommand 中的 inline 指令指定的方法,以及由 @ForceInline 注解的方法(仅限于 JDK 内部方法),会被强制内联。** 而由 -XX:CompileCommand 中的 dontinline 指令或 exclude 指令(表示不编译)指定的方法,以及由 @DontInline 注解的方法(仅限于 JDK 内部方法),则始终不会被内联。**其次,如果调用字节码对应的符号引用未被解析、目标方法所在的类未被初始化,或者目标方法是 native 方法,都将导致方法调用无法内联。** **再次,C2 不支持内联超过 9 层的调用(可以通过虚拟机参数 -XX:MaxInlineLevel 调整),以及 1 层的直接递归调用(可以通过虚拟机参数 -XX:MaxRecursiveInlineLevel 调整)。** > 如果方法 a 调用了方法 b,而方法 b 调用了方法 c,那么我们称 b 为 a 的 1 层调用,而 c 为 a 的 2 层调用。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25421\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25421\350\256\262.md" index 2bf10fec7..613cf30df 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25421\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25421\350\256\262.md" @@ -4,7 +4,7 @@ 然而,对于需要动态绑定的虚方法调用来说,即时编译器则需要先对虚方法调用进行去虚化(devirtualize),即转换为一个或多个直接调用,然后才能进行方法内联。 -**即时编译器的去虚化方式可分为完全去虚化以及条件去虚化(guarded devirtualization)。** **完全去虚化** 是通过类型推导或者类层次分析(class hierarchy analysis),识别虚方法调用的唯一目标方法,从而将其转换为直接调用的一种优化手段。它的关键在于证明虚方法调用的目标方法是唯一的。 **条件去虚化** 则是将虚方法调用转换为若干个类型测试以及直接调用的一种优化手段。它的关键在于找出需要进行比较的类型。 +**即时编译器的去虚化方式可分为完全去虚化以及条件去虚化(guarded devirtualization)。** **完全去虚化** 是通过类型推导或者类层次分析(class hierarchy analysis),识别虚方法调用的唯一目标方法,从而将其转换为直接调用的一种优化手段。它的关键在于证明虚方法调用的目标方法是唯一的。**条件去虚化** 则是将虚方法调用转换为若干个类型测试以及直接调用的一种优化手段。它的关键在于找出需要进行比较的类型。 在介绍具体的去虚化方式之前,我们先来看一段代码。这里我定义了一个抽象类 BinaryOp,其中包含一个抽象方法 apply。BinaryOp 类有两个子类 Add 和 Sub,均实现了 apply 方法。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25436\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25436\350\256\262.md" index f95ad936f..901b08fe5 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25436\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\213\206\350\247\243 Java \350\231\232\346\213\237\346\234\272/\347\254\25436\350\256\262.md" @@ -4,7 +4,7 @@ 先来介绍一下 AOT 编译,所谓 AOT 编译,是与即时编译相对立的一个概念。我们知道,即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。 -而 AOT 编译指的则是,在 **程序运行之前** ,便将字节码转换为机器码的过程。它的成果可以是需要链接至托管环境中的动态共享库,也可以是独立运行的可执行文件。 +而 AOT 编译指的则是,在 **程序运行之前**,便将字节码转换为机器码的过程。它的成果可以是需要链接至托管环境中的动态共享库,也可以是独立运行的可执行文件。 狭义的 AOT 编译针对的目标代码需要与即时编译的一致,也就是针对那些原本可以被即时编译的代码。不过,我们也可以简单地将 AOT 编译理解为类似于 GCC 的静态编译器。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25403\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25403\350\256\262.md" index 718ea39f5..971f291e8 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25403\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25403\350\256\262.md" @@ -18,7 +18,7 @@ ![img](assets/Cgq2xl4cQNeAO_j6AABZKdVbw1w802.png) -如图所示。 **大多数情况下** ,类会按照图中给出的顺序进行加载。下面我们就来分别介绍下这个过程。 +如图所示。**大多数情况下**,类会按照图中给出的顺序进行加载。下面我们就来分别介绍下这个过程。 #### 加载 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25404\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25404\350\256\262.md" index 0c126a5df..905a8925c 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25404\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25404\350\256\262.md" @@ -114,7 +114,7 @@ javap -p -v B 1: invokespecial #1 // Method java/lang/Object."":()V ``` -可以看到对象的初始化,首先是调用了 Object 类的初始化方法。注意这里是 而不是 。 **<2>** +可以看到对象的初始化,首先是调用了 Object 类的初始化方法。注意这里是 而不是 。**<2>** ```java #2 = Fieldref #6.#27 // B.a:I @@ -145,9 +145,9 @@ javap -p -v B 我们注意到 code 区域,有非常多的二进制指令。如果你接触过汇编语言,会发现它们之间其实有一定的相似性。但这些二进制指令,并不是操作系统能够认识的,它们是提供给 JVM 运行的源材料。 #### 可视化查看字节码 接下来,我们就可以使用更加直观的工具 jclasslib,来查看字节码中的具体内容了。 -我们以 B.class 文件为例,来查看它的内容。 **<1>** 首先,我们能够看到 Constant Pool(常量池),这些内容,就存放于我们的 Metaspace 区域,属于非堆。 +我们以 B.class 文件为例,来查看它的内容。**<1>** 首先,我们能够看到 Constant Pool(常量池),这些内容,就存放于我们的 Metaspace 区域,属于非堆。 ![img](assets/Cgq2xl4ezeKAWB30AADZXqT3TjQ870.jpg) -常量池包含 .class 文件常量池、运行时常量池、String 常量池等部分,大多是一些静态内容。 **<2>** 接下来,可以看到两个默认的 和 方法。以下截图是 test 方法的 code 区域,比命令行版的更加直观。 +常量池包含 .class 文件常量池、运行时常量池、String 常量池等部分,大多是一些静态内容。**<2>** 接下来,可以看到两个默认的 和 方法。以下截图是 test 方法的 code 区域,比命令行版的更加直观。 ![img](assets/CgpOIF4ezeKAVmSnAACExsXdgtg544.jpg) **<3>** 继续往下看,我们看到了 LocalVariableTable 的三个变量。其中,slot 0 指向的是 this 关键字。该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。如果没有这些信息,那么在 IDE 中引用这个方法时,将无法获取到方法名,取而代之的则是 arg0 这样的变量名。 ![img](assets/Cgq2xl4ezeKASWJHAAB5Ptt1JsM137.jpg) 本地变量表的 slot 是可以复用的。注意一个有意思的地方,index 的最大值为 3,证明了本地变量表同时最多能够存放 4 个变量。 @@ -182,7 +182,7 @@ public long test(long); 12 2 3 ret J ``` -我们介绍一下比较重要的 3 三个数值。 **<1>** 首先,注意 stack 字样,它此时的数值为 4,表明了 test 方法的最大操作数栈深度为 4。JVM 运行时,会根据这个数值,来分配栈帧中操作栈的深度。 **<2>** 相对应的,locals 变量存储了局部变量的存储空间。它的单位是 Slot(槽),可以被重用。其中存放的内容,包括: +我们介绍一下比较重要的 3 三个数值。**<1>** 首先,注意 stack 字样,它此时的数值为 4,表明了 test 方法的最大操作数栈深度为 4。JVM 运行时,会根据这个数值,来分配栈帧中操作栈的深度。**<2>** 相对应的,locals 变量存储了局部变量的存储空间。它的单位是 Slot(槽),可以被重用。其中存放的内容,包括: - this - 方法参数 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25406\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25406\350\256\262.md" index 140bdc157..3364957ec 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25406\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25406\350\256\262.md" @@ -28,7 +28,7 @@ JVM 规范并没有规定垃圾回收器怎么实现,它只需要保证不要 ![img](assets/Ciqah16G0T-AG78xAAFEMVAUqPU670.png) -如图所示,圆圈代表的是对象。绿色的代表 GC Roots,红色的代表可以追溯到的对象。可以看到标记之后,仍然有多个灰色的圆圈,它们都是被回收的对象。 **清除(Sweep)** ------------- +如图所示,圆圈代表的是对象。绿色的代表 GC Roots,红色的代表可以追溯到的对象。可以看到标记之后,仍然有多个灰色的圆圈,它们都是被回收的对象。**清除(Sweep)** ------------- 清除阶段就是把未被标记的对象回收掉。 @@ -46,7 +46,7 @@ JVM 规范并没有规定垃圾回收器怎么实现,它只需要保证不要 这个时候,我应该有足足 6k 的空闲空间。接下来,我打算申请另外一个 5k 的空间,结果系统告诉我内存不足了。系统运行时间越长,这种碎片就越多。 -在很久之前使用 Windows 系统时,有一个非常有用的功能,就是内存整理和磁盘整理,运行之后有可能会显著提高系统性能。这个出发点是一样的。 **复制(Copy)** ------------ +在很久之前使用 Windows 系统时,有一个非常有用的功能,就是内存整理和磁盘整理,运行之后有可能会显著提高系统性能。这个出发点是一样的。**复制(Copy)** ------------ 解决碎片问题没有银弹,只有老老实实的进行内存整理。 @@ -58,7 +58,7 @@ JVM 规范并没有规定垃圾回收器怎么实现,它只需要保证不要 ![img](assets/Cgq2xl4lQueABnuaAABW19PzhdM953.jpg) -这种方式看似非常完美的,解决了碎片问题。但是,它的弊端也非常明显。它浪费了几乎一半的内存空间来做这个事情,如果资源本来就很有限,这就是一种无法容忍的浪费。 **整理(Compact)** --------------- +这种方式看似非常完美的,解决了碎片问题。但是,它的弊端也非常明显。它浪费了几乎一半的内存空间来做这个事情,如果资源本来就很有限,这就是一种无法容忍的浪费。**整理(Compact)** --------------- 其实,不用分配一个对等的额外空间,也是可以完成内存的整理工作。 @@ -81,7 +81,7 @@ for(i=0;i&1 | grep MaxTenuringThreshold -java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep MaxTenuringThreshold **PretenureSizeThreshold** 这个参数默认值是 0,意味着所有的对象年轻代优先分配。我们把这个值调小一点,再观测 JVM 的行为。追加参数 -XX:PretenureSizeThreshold=1024,可以看到 VisualVm 中老年代的区域增长。 **TargetSurvivorRatio** 默认值为 50。在动态计算对象提升阈值的时候使用。计算时,会从年龄最小的对象开始累加,如果累加的对象大小大于幸存区的一半,则将当前的对象 age 作为新的阈值,年龄大于此阈值的对象直接进入老年代。工作中不建议调整这个值,如果要调,请调成比 50 大的值。 +java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep MaxTenuringThreshold **PretenureSizeThreshold** 这个参数默认值是 0,意味着所有的对象年轻代优先分配。我们把这个值调小一点,再观测 JVM 的行为。追加参数 -XX:PretenureSizeThreshold=1024,可以看到 VisualVm 中老年代的区域增长。**TargetSurvivorRatio** 默认值为 50。在动态计算对象提升阈值的时候使用。计算时,会从年龄最小的对象开始累加,如果累加的对象大小大于幸存区的一半,则将当前的对象 age 作为新的阈值,年龄大于此阈值的对象直接进入老年代。工作中不建议调整这个值,如果要调,请调成比 50 大的值。 -你可以尝试着更改其他参数,比如垃圾回收器的种类,动态看一下效果。尤其注意每一项内存区域的内容变动,你会对垃圾回收器有更好的理解。 **UseAdaptiveSizePolicy** ,因为它和 CMS 不兼容,所以 CMS 下默认为 false,但 G1 下默认为 true。这是一个非常智能的参数,它是用来自适应调整空间大小的参数。它会在每次 GC 之后,重新计算 Eden、From、To 的大小。很多人在 Java 8 的一些配置中会见到这个参数,但其实在 CMS 和 G1 中是不需要显式设置的。 +你可以尝试着更改其他参数,比如垃圾回收器的种类,动态看一下效果。尤其注意每一项内存区域的内容变动,你会对垃圾回收器有更好的理解。**UseAdaptiveSizePolicy**,因为它和 CMS 不兼容,所以 CMS 下默认为 false,但 G1 下默认为 true。这是一个非常智能的参数,它是用来自适应调整空间大小的参数。它会在每次 GC 之后,重新计算 Eden、From、To 的大小。很多人在 Java 8 的一些配置中会见到这个参数,但其实在 CMS 和 G1 中是不需要显式设置的。 值的注意的是,Java 8 默认垃圾回收器是 Parallel Scavenge,它的这个参数是默认开启的,有可能会发生把幸存区自动调小的可能,造成一些问题,显式的设置 SurvivorRatio 可以解决这个问题。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25412\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25412\350\256\262.md" index 8ffa7101b..352982546 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25412\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25412\350\256\262.md" @@ -1,6 +1,6 @@ # 12 第11讲:动手实践:遇到问题不要慌,轻松搞定内存泄漏 -当一个系统在发生 OOM 的时候,行为可能会让你感到非常困惑。因为 JVM 是运行在操作系统之上的,操作系统的一些限制,会严重影响 JVM 的行为。 **故障排查是一个综合性的技术问题,在日常工作中要增加自己的知识广度** 。多总结、多思考、多记录,这才是正确的晋级方式。 +当一个系统在发生 OOM 的时候,行为可能会让你感到非常困惑。因为 JVM 是运行在操作系统之上的,操作系统的一些限制,会严重影响 JVM 的行为。**故障排查是一个综合性的技术问题,在日常工作中要增加自己的知识广度** 。多总结、多思考、多记录,这才是正确的晋级方式。 现在的互联网服务,一般都做了负载均衡。如果一个实例发生了问题,不要着急去重启。万能的重启会暂时缓解问题,但如果不保留现场,可能就错失了解决问题的根本,担心的事情还会到来。 @@ -12,7 +12,7 @@ 我们有个线上应用,单节点在运行一段时间后,CPU 的使用会飙升,一旦飙升,一般怀疑某个业务逻辑的计算量太大,或者是触发了死循环(比如著名的 HashMap 高并发引起的死循环),但排查到最后其实是 GC 的问题。 -在 Linux 上,分析哪个线程引起的 CPU 问题,通常有一个固定的步骤。我们下面来分解这个过程, **这是面试频率极高的一个问题** 。 ![img](assets/CgpOIF5GcZ-AcGzzAAAmNdRr-Xo623.jpg)- +在 Linux 上,分析哪个线程引起的 CPU 问题,通常有一个固定的步骤。我们下面来分解这个过程,**这是面试频率极高的一个问题** 。 ![img](assets/CgpOIF5GcZ-AcGzzAAAmNdRr-Xo623.jpg)- (1)使用 top 命令,查找到使用 CPU 最多的某个进程,记录它的 pid。使用 Shift + P 快捷键可以按 CPU 的使用率进行排序。 ```top @@ -254,7 +254,7 @@ find / | grep "x" ![img](assets/Cgq2xl5GcZ-AP5pOAAAhN4DWbUQ769.jpg) -我们再来聊一下内存溢出和内存泄漏的区别。 **内存溢出是一个结果,而内存泄漏是一个原因** 。内存溢出的原因有内存空间不足、配置错误等因素。 +我们再来聊一下内存溢出和内存泄漏的区别。**内存溢出是一个结果,而内存泄漏是一个原因** 。内存溢出的原因有内存空间不足、配置错误等因素。 不再被使用的对象、没有被回收、没有及时切断与 GC Roots 的联系,这就是内存泄漏。内存泄漏是一些错误的编程方式,或者过多的无用对象创建引起的。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25413\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25413\350\256\262.md" index 6999a3e6d..15be3ae7a 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25413\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25413\350\256\262.md" @@ -40,7 +40,7 @@ MAT 工具是基于 Eclipse 平台开发的,本身是一个 Java 程序,所 浅堆代表了对象本身的内存占用,包括对象自身的内存占用,以及“为了引用”其他对象所占用的内存。 -深堆是一个统计结果,会循环计算引用的具体对象所占用的内存。但是深堆和“对象大小”有一点不同, **深堆指的是一个对象被垃圾回收后,能够释放的内存大小,这些被释放的对象集合,叫做保留集** (Retained Set)。 +深堆是一个统计结果,会循环计算引用的具体对象所占用的内存。但是深堆和“对象大小”有一点不同,**深堆指的是一个对象被垃圾回收后,能够释放的内存大小,这些被释放的对象集合,叫做保留集** (Retained Set)。 ![img](assets/CgpOIF5NRsmAEeWTAABDIx8RWa4815.png) @@ -125,7 +125,7 @@ public class Objects4MAT { ### 2.1. 代码介绍 -我们以一段代码示例 **Objects4MAT** ,来具体看一下 MAT 工具的使用。代码创建了一个新的线程 "huge-thread",并建立了一个引用的层级关系,总的内存大约占用 100 MB。同时,demo1 和 demo2 展示了一个循环引用的关系。最后,使用 sleep 函数,让线程永久阻塞住,此时整个堆处于一个相对“静止”的状态。 +我们以一段代码示例 **Objects4MAT**,来具体看一下 MAT 工具的使用。代码创建了一个新的线程 "huge-thread",并建立了一个引用的层级关系,总的内存大约占用 100 MB。同时,demo1 和 demo2 展示了一个循环引用的关系。最后,使用 sleep 函数,让线程永久阻塞住,此时整个堆处于一个相对“静止”的状态。 ![img](assets/Cgq2xl5NRsmAC91XAABbohWGa5g179.png) diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25415\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25415\350\256\262.md" index d44dbae37..c4070fc7b 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25415\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25415\350\256\262.md" @@ -6,7 +6,7 @@ 你可能见过在地铁上抱着电脑处理故障的照片,由此可见,大部分程序员都是随身携带电脑的,它体现了两个问题:第一,自动化应急处理机制并不完善;第二,缺乏能够跟踪定位问题的工具,只能靠“苦力”去解决。 -我们在前面第 11 课时中提到的一系列命令,就是一个被分解的典型脚本,这个脚本能够在问题发生的时候,自动触发并保存顺时态的现场。除了这些工具,我们还需要有一个与时间序列相关的监控系统。 **这就是监控工具的必要性** 。 +我们在前面第 11 课时中提到的一系列命令,就是一个被分解的典型脚本,这个脚本能够在问题发生的时候,自动触发并保存顺时态的现场。除了这些工具,我们还需要有一个与时间序列相关的监控系统。**这就是监控工具的必要性** 。 我们来盘点一下对于问题的排查,现在都有哪些资源: diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25416\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25416\350\256\262.md" index 9481429cd..f0ca8f0d4 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25416\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25416\350\256\262.md" @@ -30,9 +30,9 @@ 我们前面算过,6GB 大小的内存,年轻代大约是 2GB,在高峰期,每几秒钟则需要进行一次 MinorGC。报表系统和高并发系统不太一样,它的对象,存活时长大得多,并不能仅仅通过增加年轻代来解决;而且,如果增加了年轻代,那么必然减少了老年代的大小,由于 CMS 的碎片和浮动垃圾问题,我们可用的空间就更少了。虽然服务能够满足目前的需求,但还有一些不太确定的风险。 -第一,了解到程序中有很多缓存数据和静态统计数据,为了减少 MinorGC 的次数,通过分析 GC 日志打印的对象年龄分布,把 MaxTenuringThreshold 参数调整到了 3(请根据你自己的应用情况设置)。 **这个参数是让年轻代的这些对象,赶紧回到老年代去,不要老呆在年轻代里** 。 +第一,了解到程序中有很多缓存数据和静态统计数据,为了减少 MinorGC 的次数,通过分析 GC 日志打印的对象年龄分布,把 MaxTenuringThreshold 参数调整到了 3(请根据你自己的应用情况设置)。**这个参数是让年轻代的这些对象,赶紧回到老年代去,不要老呆在年轻代里** 。 -第二,我们的 GC 时间比较长,就一块开了参数 **CMSScavengeBeforeRemark** ,使得在 CMS remark 前,先执行一次 Minor GC 将新生代清掉。同时配合上个参数,其效果还是比较好的,一方面,对象很快晋升到了老年代,另一方面,年轻代的对象在这种情况下是有限的,在整个 MajorGC 中占的时间也有限。 +第二,我们的 GC 时间比较长,就一块开了参数 **CMSScavengeBeforeRemark**,使得在 CMS remark 前,先执行一次 Minor GC 将新生代清掉。同时配合上个参数,其效果还是比较好的,一方面,对象很快晋升到了老年代,另一方面,年轻代的对象在这种情况下是有限的,在整个 MajorGC 中占的时间也有限。 第三,由于缓存的使用,有大量的弱引用,拿一次长达 10 秒的 GC 来说。我们发现在 GC 日志里,处理 **weak refs** 的时间较长,达到了 4.5 秒。 @@ -51,7 +51,7 @@ epGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccu pancyOnly -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M ``` -优化之后,效果不错,但并不是特别明显。经过评估,针对高峰时期的情况进行调研,我们决定再次提升机器性能,改用 8core16g 的机器。但是,这会带来另外一个问题。 **高性能的机器带来了非常大的服务吞吐量** ,通过 jstat 进行监控,能够看到年轻代的分配速率明显提高,但随之而来的 MinorGC 时长却变的不可控,有时候会超过 1 秒。累积的请求造成了更加严重的后果。 +优化之后,效果不错,但并不是特别明显。经过评估,针对高峰时期的情况进行调研,我们决定再次提升机器性能,改用 8core16g 的机器。但是,这会带来另外一个问题。**高性能的机器带来了非常大的服务吞吐量**,通过 jstat 进行监控,能够看到年轻代的分配速率明显提高,但随之而来的 MinorGC 时长却变的不可控,有时候会超过 1 秒。累积的请求造成了更加严重的后果。 这是由于堆空间明显加大造成的回收时间加长。为了获取较小的停顿时间,我们在堆上采用了 G1 垃圾回收器,把它的目标设定在 200ms。G1 是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的 GC 暂停目标,就能得到不错的性能。所以为了照顾大对象的生成,我们把小堆区的大小修改为 16 M。修改之后,虽然 GC 更加频繁了一些,但是停顿时间都比较小,应用的运行较为平滑。 @@ -89,7 +89,7 @@ pancyOnly -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M 如上图所示,接口 A 通过 HttpClient 访问服务 2,响应 100ms 后返回;接口 B 访问服务 3,耗时 2 秒。HttpClient 本身是有一个最大连接数限制的,如果服务 3 迟迟不返回,就会造成 HttpClient 的连接数达到上限,最上层的 Tomcat 线程也会一直阻塞在这里,进而连响应速度比较快的接口 A 也无法正常提供服务。 -这是出现频率非常高的的一类故障,在工作中你会大概率遇见。 **概括来讲,就是同一服务,由于一个耗时非常长的接口,进而引起了整体的服务不可用。** 这个时候,通过 jstack 打印栈信息,会发现大多数竟然阻塞在了接口 A 上,而不是耗时更长的接口 B。这是一种错觉,其实是因为接口 A 的速度比较快,在问题发生点进入了更多的请求,它们全部都阻塞住了。 +这是出现频率非常高的的一类故障,在工作中你会大概率遇见。**概括来讲,就是同一服务,由于一个耗时非常长的接口,进而引起了整体的服务不可用。** 这个时候,通过 jstack 打印栈信息,会发现大多数竟然阻塞在了接口 A 上,而不是耗时更长的接口 B。这是一种错觉,其实是因为接口 A 的速度比较快,在问题发生点进入了更多的请求,它们全部都阻塞住了。 证据本身具有非常强的迷惑性。由于这种问题发生的频率很高,排查起来又比较困难,我这里专门做了一个小工程,用于还原解决这种问题的一个方式,参见 **report-demo** 工程。 @@ -577,7 +577,7 @@ select * from user where 1=1 ```plaintext 数据库中的所有记录,都会被查询出来,载入到 JVM 的内存中。由于数据库记录实在太多,直接把内存给撑爆了。 -在工作中,由于这种原因引起的内存溢出,发生的频率非常高。通常的解决方式是强行加入 **分页功能** ,或者对一些必填的参数进行校验,但不总是有效。因为上面的示例仅展示了一个非常简单的 SQL 语句,而在实际工作中,这个 SQL 语句会非常长,每个条件对结果集的影响也会非常大,在进行数据筛选的时候,一定要小心。 +在工作中,由于这种原因引起的内存溢出,发生的频率非常高。通常的解决方式是强行加入 **分页功能**,或者对一些必填的参数进行校验,但不总是有效。因为上面的示例仅展示了一个非常简单的 SQL 语句,而在实际工作中,这个 SQL 语句会非常长,每个条件对结果集的影响也会非常大,在进行数据筛选的时候,一定要小心。 内存使用问题 ------ diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25417\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25417\350\256\262.md" index 0f9e6b1e0..2af218bd7 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25417\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25417\350\256\262.md" @@ -36,7 +36,7 @@ select * from user where 1=1 数据库中的所有记录,都会被查询出来,载入到 JVM 的内存中。由于数据库记录实在太多,直接把内存给撑爆了。 -在工作中,由于这种原因引起的内存溢出,发生的频率非常高。通常的解决方式是强行加入 **分页功能** ,或者对一些必填的参数进行校验,但不总是有效。因为上面的示例仅展示了一个非常简单的 SQL 语句,而在实际工作中,这个 SQL 语句会非常长,每个条件对结果集的影响也会非常大,在进行数据筛选的时候,一定要小心。 +在工作中,由于这种原因引起的内存溢出,发生的频率非常高。通常的解决方式是强行加入 **分页功能**,或者对一些必填的参数进行校验,但不总是有效。因为上面的示例仅展示了一个非常简单的 SQL 语句,而在实际工作中,这个 SQL 语句会非常长,每个条件对结果集的影响也会非常大,在进行数据筛选的时候,一定要小心。 ## 内存使用问题 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25418\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25418\350\256\262.md" index d7ef4428e..fa2556af9 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25418\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25418\350\256\262.md" @@ -16,7 +16,7 @@ **minor_version:** 副版本号,这两个版本号用来标识编译时的 JDK 版本,常见的一个异常比如 Unsupported major.minor version 52.0 就是因为运行时的 JDK 版本低于编译时的 JDK 版本(52 是 Java 8 的主版本号)。 -**constant_pool_count** :常量池计数器,等于常量池中的成员数加 1。 **constant_pool** :常量池,是一种表结构,包含 class 文件结构和子结构中引用的所有字符串常量,类或者接口名,字段名和其他常量。 **access_flags** :表示某个类或者接口的访问权限和属性。 **this_class** :类索引,该值必须是对常量池中某个常量的一个有效索引值,该索引处的成员必须是一个 CONSTANT_Class_info 类型的结构体,表示这个 class 文件所定义的类和接口。 **super_class** :父类索引。 **interfaces_count** :接口计数器,表示当前类或者接口直接继承接口的数量。 **interfaces** :接口表,是一个表结构,成员同 this_class,是对常量池中 CONSTANT_Class_info 类型的一个有效索引值。 **fields_count** :字段计数器,当前 class 文件所有字段的数量。 **fields** :字段表,是一个表结构,表中每个成员必须是 filed_info 数据结构,用于表示当前类或者接口的某个字段的完整描述,但它不包含从父类或者父接口继承的字段。 **methods_count** :方法计数器,表示当前类方法表的成员个数。 **methods** :方法表,是一个表结构,表中每个成员必须是 method_info 数据结构,用于表示当前类或者接口的某个方法的完整描述。 **attributes_count** :属性计数器,表示当前 class 文件 attributes 属性表的成员个数。 **attributes** :属性表,是一个表结构,表中每个成员必须是 attribute_info 数据结构,这里的属性是对 class 文件本身,方法或者字段的补充描述,比如 SourceFile 属性用于表示 class 文件的源代码文件名。 +**constant_pool_count** :常量池计数器,等于常量池中的成员数加 1。**constant_pool** :常量池,是一种表结构,包含 class 文件结构和子结构中引用的所有字符串常量,类或者接口名,字段名和其他常量。**access_flags** :表示某个类或者接口的访问权限和属性。**this_class** :类索引,该值必须是对常量池中某个常量的一个有效索引值,该索引处的成员必须是一个 CONSTANT_Class_info 类型的结构体,表示这个 class 文件所定义的类和接口。**super_class** :父类索引。**interfaces_count** :接口计数器,表示当前类或者接口直接继承接口的数量。**interfaces** :接口表,是一个表结构,成员同 this_class,是对常量池中 CONSTANT_Class_info 类型的一个有效索引值。**fields_count** :字段计数器,当前 class 文件所有字段的数量。**fields** :字段表,是一个表结构,表中每个成员必须是 filed_info 数据结构,用于表示当前类或者接口的某个字段的完整描述,但它不包含从父类或者父接口继承的字段。**methods_count** :方法计数器,表示当前类方法表的成员个数。**methods** :方法表,是一个表结构,表中每个成员必须是 method_info 数据结构,用于表示当前类或者接口的某个方法的完整描述。**attributes_count** :属性计数器,表示当前 class 文件 attributes 属性表的成员个数。**attributes** :属性表,是一个表结构,表中每个成员必须是 attribute_info 数据结构,这里的属性是对 class 文件本身,方法或者字段的补充描述,比如 SourceFile 属性用于表示 class 文件的源代码文件名。 ![img](assets/CgpOIF5h7KeAKhUAAAFE99wUPW0675.jpg) @@ -94,13 +94,13 @@ public class InvokeDemo extends Abs implements I { ![img](assets/Cgq2xl5h7KeAVTC2AACpnNi6GE0282.jpg) -attach 启动 Java 进程后,可以在 **Class Browser** 菜单中查看加载的所有类信息。我们在搜索框中输入 **InvokeDemo** ,找到要查看的类。 +attach 启动 Java 进程后,可以在 **Class Browser** 菜单中查看加载的所有类信息。我们在搜索框中输入 **InvokeDemo**,找到要查看的类。 ![img](assets/CgpOIF5h7KeAQIsAAACaFyeUVPI476.jpg) **@** 符号后面的,就是具体的内存地址,我们可以复制一个,然后在 **Inspector** 视图中查看具体的属性,可以 **大体** 认为这就是类在方法区的具体存储。 ![img](assets/Cgq2xl5h7KiAWuv-AAGcKB6dCE4406.jpg) -在 Inspector 视图中,我们找到方法相关的属性 **_methods** ,可惜它无法点开,也无法查看。 +在 Inspector 视图中,我们找到方法相关的属性 **_methods**,可惜它无法点开,也无法查看。 ![img](assets/Cgq2xl5h7KiALPruAAD5Er51lCo505.jpg) @@ -138,7 +138,7 @@ examine 0x000000010e650570/10 回想一下,第 03 课时讲到的类加载机制,在 class 文件被加载到方法区以后,就完成了从符号引用到具体地址的转换过程。 -我们可以看一下编译后的 main 方法字节码,尤其需要注意的是对于接口方法的调用。使用实例对象直接调用,和强制转化成接口调用,所调用的字节码指令分别是 **invokevirtual** 和 **invokeinterface** ,它们是有所不同的。 +我们可以看一下编译后的 main 方法字节码,尤其需要注意的是对于接口方法的调用。使用实例对象直接调用,和强制转化成接口调用,所调用的字节码指令分别是 **invokevirtual** 和 **invokeinterface**,它们是有所不同的。 ```java public static void main(java.lang.String[]); @@ -179,9 +179,9 @@ invokevirtual 指令有多态查找的机制,该指令运行时,解析过程 - 否则,按照继承关系从下往上依次对 c 的各个父类进行第二步的搜索和验证过程; - 如果始终没找到合适的方法,则抛出 java.lang.AbstractMethodError 异常,这就是 Java 语言中方法重写的本质。 -相对比, **invokestatic** 指令加上 **invokespecial** 指令,就属于静态绑定过程。 +相对比,**invokestatic** 指令加上 **invokespecial** 指令,就属于静态绑定过程。 -所以 **静态绑定** ,指的是能够直接识别目标方法的情况,而 **动态绑定** 指的是需要在运行过程中根据调用者的类型来确定目标方法的情况。 +所以 **静态绑定**,指的是能够直接识别目标方法的情况,而 **动态绑定** 指的是需要在运行过程中根据调用者的类型来确定目标方法的情况。 可以想象,相对于静态绑定的方法调用来说,动态绑定的调用会更加耗时一些。由于方法的调用非常的频繁,JVM 对动态调用的代码进行了比较多的优化,比如使用方法表来加快对具体方法的寻址,以及使用更快的缓冲区来直接寻址( 内联缓存)。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25419\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25419\350\256\262.md" index 3c5acd52b..3c384b7ef 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25419\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25419\350\256\262.md" @@ -125,7 +125,7 @@ JMM 分为主存储器(Main Memory)和工作存储器(Working Memory)两 ### 三大特征 **(1)原子性** JMM 保证了 read、load、assign、use、store 和 write 六个操作具有原子性,可以认为除了 long 和 double 类型以外,对其他基本数据类型所对应的内存单元的访问读写都是原子的 -如果想要一个颗粒度更大的原子性保证,就可以使用 lock 和 unlock 这两个操作。 **(2)可见性** 可见性是指当一个线程修改了共享变量的值,其他线程也能立即感知到这种变化。 +如果想要一个颗粒度更大的原子性保证,就可以使用 lock 和 unlock 这两个操作。**(2)可见性** 可见性是指当一个线程修改了共享变量的值,其他线程也能立即感知到这种变化。 我们从前面的图中可以看到,要保证这种效果,需要经历多次操作。一个线程对变量的修改,需要先同步给主内存,赶在另外一个线程的读取之前刷新变量值。 @@ -133,7 +133,7 @@ volatile、synchronized、final 和锁,都是保证可见性的方式。 这里要着重提一下 volatile,因为它的特点最显著。使用了 volatile 关键字的变量,每当变量的值有变动时,都会把更改立即同步到主内存中;而如果某个线程想要使用这个变量,则先要从主存中刷新到工作内存上,这样就确保了变量的可见性。 -而锁和同步关键字就比较好理解一些,它是把更多个操作强制转化为原子化的过程。由于只有一把锁,变量的可见性就更容易保证。 **(3)有序性** Java 程序很有意思,从上面的 add 操作可以看出,如果在线程中观察,则所有的操作都是有序的;而如果在另一个线程中观察,则所有的操作都是无序的。 +而锁和同步关键字就比较好理解一些,它是把更多个操作强制转化为原子化的过程。由于只有一把锁,变量的可见性就更容易保证。**(3)有序性** Java 程序很有意思,从上面的 add 操作可以看出,如果在线程中观察,则所有的操作都是有序的;而如果在另一个线程中观察,则所有的操作都是无序的。 除了多线程这种无序性的观测,无序的产生还来源于 **指令重排** 。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25421\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25421\350\256\262.md" index 9e54db6cb..56a701046 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25421\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25421\350\256\262.md" @@ -388,7 +388,7 @@ void loop(List paramList) { ## 注解 -注解在 Java 中得到了广泛的应用,Spring 框架更是由于注解的存在而起死回生。注解在开发中的作用就是做数据约束和标准定义,可以将其理解成代码的规范标准,并帮助我们写出方便、快捷、简洁的代码。 那么注解信息是存放在哪里的呢?我们使用两个 Java 文件来看一下其中的一种情况。 **MyAnnotation.java** +注解在 Java 中得到了广泛的应用,Spring 框架更是由于注解的存在而起死回生。注解在开发中的作用就是做数据约束和标准定义,可以将其理解成代码的规范标准,并帮助我们写出方便、快捷、简洁的代码。 那么注解信息是存放在哪里的呢?我们使用两个 Java 文件来看一下其中的一种情况。**MyAnnotation.java** ```plaintext public @interface MyAnnotation { diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25422\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25422\350\256\262.md" index 578f1e22e..88da36bbb 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25422\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25422\350\256\262.md" @@ -172,7 +172,7 @@ public class JvmAttach { **(1)jar 包依赖方式** 一般,Agent 的 jar 包会以 fatjar 的方式提供,即将所有的依赖打包到一个大的 jar 包中。如果你的功能复杂、依赖多,那么这个 jar 包将会特别的大。 -使用独立的 **bom** 文件维护这些依赖是另外一种方法。使用方自行管理依赖问题,但这通常会发生一些找不到 jar 包的错误,更糟糕的是,大多数在运行时才发现。 **(2)类名称重复** 不要使用和 jdk 及 instrument 包中相同的类名(包括包名),有时候你能够侥幸过关,但也会陷入无法控制的异常中。 **(3)做有限的功能** 可以看到,给系统动态的增加功能是非常酷的,但大多数情况下非常耗费性能。你会发现,一些简单的诊断工具,会占用你 1 核的 CPU,这是很平常的事情。 **(4)ClassLoader** +使用独立的 **bom** 文件维护这些依赖是另外一种方法。使用方自行管理依赖问题,但这通常会发生一些找不到 jar 包的错误,更糟糕的是,大多数在运行时才发现。**(2)类名称重复** 不要使用和 jdk 及 instrument 包中相同的类名(包括包名),有时候你能够侥幸过关,但也会陷入无法控制的异常中。**(3)做有限的功能** 可以看到,给系统动态的增加功能是非常酷的,但大多数情况下非常耗费性能。你会发现,一些简单的诊断工具,会占用你 1 核的 CPU,这是很平常的事情。**(4)ClassLoader** 如果你用的 JVM 比较旧,频繁地生成大量的代理类,会造成元空间的膨胀,容易发生内存占用问题。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25423\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25423\350\256\262.md" index 9379915ed..d97eef827 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25423\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25423\350\256\262.md" @@ -206,9 +206,9 @@ public void fig(){ 那逃逸分析有什么好处呢? -- **同步省略** ,如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 -- **栈上分配** ,如果一个对象在子程序中被分配,那么指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配。 -- **分离对象或标量替换** ,有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。标量是指无法再分解的数据类型,比如原始数据类型及 reference 类型。 +- **同步省略**,如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 +- **栈上分配**,如果一个对象在子程序中被分配,那么指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配。 +- **分离对象或标量替换**,有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。标量是指无法再分解的数据类型,比如原始数据类型及 reference 类型。 ![img](assets/Cgq2xl57A7yAZKc2AAFu39pb5SE629.jpg) diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25424\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25424\350\256\262.md" index 7dba6c47e..98a11b65e 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25424\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25424\350\256\262.md" @@ -10,7 +10,7 @@ ![img](assets/Cgq2xl5956uAEXorAAA7ycYosaA257.png) -**数据库优化** : 数据库是最容易成为瓶颈的组件,研发会从 SQL 优化或者数据库本身去提高它的性能。如果瓶颈依然存在,则会考虑分库分表将数据打散,如果这样也没能解决问题,则可能会选择缓存组件进行优化。这个过程与本课时相关的知识点,可以使用 jstack 获取阻塞的执行栈,进行辅助分析。 **集群最优** :存储节点的问题解决后,计算节点也有可能发生问题。一个集群系统如果获得了水平扩容的能力,就会给下层的优化提供非常大的时间空间,这也是弹性扩容的魅力所在。我接触过一个服务,由最初的 3 个节点,扩容到最后的 200 多个节点,但由于人力问题,服务又没有什么新的需求,下层的优化就一直被搁置着。 **硬件升级** :水平扩容不总是有效的,原因在于单节点的计算量比较集中,或者 JVM 对内存的使用超出了宿主机的承载范围。在动手进行代码优化之前,我们会对节点的硬件配置进行升级。升级容易,降级难,降级需要依赖代码和调优层面的优化。 **代码优化** :出于成本的考虑,上面的这些问题,研发团队并不总是坐视不管。代码优化是提高性能最有效的方式,但需要收集一些数据,这个过程可能是服务治理,也有可能是代码流程优化。我在第 21 课时介绍的 JavaAgent 技术,会无侵入的收集一些 profile 信息,供我们进行决策。像 Sonar 这种质量监控工具,也可以在此过程中帮助到我们。 **并行优化** :并行优化的对象是这样一种接口,它占用的资源不多,计算量也不大,就是速度太慢。所以我们通常使用 ContDownLatch 对需要获取的数据进行并行处理,效果非常不错,比如在 200ms 内返回对 50 个耗时 100ms 的下层接口的调用。 **JVM 优化** :虽然对 JVM 进行优化,有时候会获得巨大的性能提升,但在 JVM 不发生问题时,我们一般不会想到它。原因就在于,相较于上面 5 层所达到的效果来说,它的优化效果有限。但在代码优化、并行优化、JVM 优化的过程中,JVM 的知识却起到了关键性的作用,是一些根本性的影响因素。 **操作系统优化** :操作系统优化是解决问题的杀手锏,比如像 HugePage、Luma、“CPU 亲和性”这种比较底层的优化。但就计算节点来说,对操作系统进行优化并不是很常见。运维在背后会做一些诸如文件句柄的调整、网络参数的修改,这对于我们来说就已经够用了。 +**数据库优化** : 数据库是最容易成为瓶颈的组件,研发会从 SQL 优化或者数据库本身去提高它的性能。如果瓶颈依然存在,则会考虑分库分表将数据打散,如果这样也没能解决问题,则可能会选择缓存组件进行优化。这个过程与本课时相关的知识点,可以使用 jstack 获取阻塞的执行栈,进行辅助分析。**集群最优** :存储节点的问题解决后,计算节点也有可能发生问题。一个集群系统如果获得了水平扩容的能力,就会给下层的优化提供非常大的时间空间,这也是弹性扩容的魅力所在。我接触过一个服务,由最初的 3 个节点,扩容到最后的 200 多个节点,但由于人力问题,服务又没有什么新的需求,下层的优化就一直被搁置着。**硬件升级** :水平扩容不总是有效的,原因在于单节点的计算量比较集中,或者 JVM 对内存的使用超出了宿主机的承载范围。在动手进行代码优化之前,我们会对节点的硬件配置进行升级。升级容易,降级难,降级需要依赖代码和调优层面的优化。**代码优化** :出于成本的考虑,上面的这些问题,研发团队并不总是坐视不管。代码优化是提高性能最有效的方式,但需要收集一些数据,这个过程可能是服务治理,也有可能是代码流程优化。我在第 21 课时介绍的 JavaAgent 技术,会无侵入的收集一些 profile 信息,供我们进行决策。像 Sonar 这种质量监控工具,也可以在此过程中帮助到我们。**并行优化** :并行优化的对象是这样一种接口,它占用的资源不多,计算量也不大,就是速度太慢。所以我们通常使用 ContDownLatch 对需要获取的数据进行并行处理,效果非常不错,比如在 200ms 内返回对 50 个耗时 100ms 的下层接口的调用。**JVM 优化** :虽然对 JVM 进行优化,有时候会获得巨大的性能提升,但在 JVM 不发生问题时,我们一般不会想到它。原因就在于,相较于上面 5 层所达到的效果来说,它的优化效果有限。但在代码优化、并行优化、JVM 优化的过程中,JVM 的知识却起到了关键性的作用,是一些根本性的影响因素。**操作系统优化** :操作系统优化是解决问题的杀手锏,比如像 HugePage、Luma、“CPU 亲和性”这种比较底层的优化。但就计算节点来说,对操作系统进行优化并不是很常见。运维在背后会做一些诸如文件句柄的调整、网络参数的修改,这对于我们来说就已经够用了。 虽然本课程是针对比较底层的 JVM,但我还是想谈一下一个研发对技术体系的整体演进方向。 @@ -56,9 +56,9 @@ java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep UseAdaptiveSizePolicy ### 垃圾回收器优化 -接下来看一下主要的垃圾回收器。 **CMS 垃圾回收器** +接下来看一下主要的垃圾回收器。**CMS 垃圾回收器** -- **-XX:+UseCMSInitiatingOccupancyOnly** :这个参数需要加上 **-** **XX:CMSInitiatingOccupancyFraction** ,注意后者需要和前者一块配合才能完成工作,它们指定了 MajorGC 的发生时机。 +- **-XX:+UseCMSInitiatingOccupancyOnly** :这个参数需要加上 **-** **XX:CMSInitiatingOccupancyFraction**,注意后者需要和前者一块配合才能完成工作,它们指定了 MajorGC 的发生时机。 - **-XX:ExplicitGCInvokesConcurrent** :当代码里显示调用了 System.gc(),实际上是想让回收器进行 FullGC,如果发生这种情况,则使用这个参数开始并行 FullGC,建议加上这个参数。 @@ -66,7 +66,7 @@ java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep UseAdaptiveSizePolicy - **-XX:CMSScavengeBeforeRemark** :表示开启或关闭在 CMS 重新标记阶段之前的清除(YGC)尝试,它可以降低 remark 时间,建议加上。 -- **-XX:+ParallelRefProcEnabled** :可以用来并行处理 Reference,以加快处理速度,缩短耗时,具体用法见第 15 课时。 **G1 垃圾回收器** +- **-XX:+ParallelRefProcEnabled** :可以用来并行处理 Reference,以加快处理速度,缩短耗时,具体用法见第 15 课时。**G1 垃圾回收器** - **-XX:MaxGCPauseMillis** :用于设置目标停顿时间,G1 会尽力达成。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25425\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25425\350\256\262.md" index 585b34710..fd428baf0 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25425\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25425\350\256\262.md" @@ -28,7 +28,7 @@ JVM 的版本非常之多,比较牛的公司都搞了自己的 JVM,但当时 WebSphere 就是这样一个以“巨无霸”的形式存在,当年的中间件指的就是它,和现在的中间件完全不是一个概念。 -WebSphere 是 IBM 的产品,开发语言是 Java。但是它运行时的 JVM,却是一个叫做 J9 的虚拟机,依稀记得当年,有非常多的 jar 包,由于引用了一些非常偏门的 API,却不能运行(现在应该好了很多)。 **Zing VM** Zing JVM 是 Azul 公司传统风格的产品,它在 HotSpot 上做了不少的定制及优化,主打低延迟、高实时服务器端 JDK 市场。它代表了一类商业化的定制,比如 JRockit,都比较贵。 **IKVM** 这个以前在写一些游戏的时候,使用过 LibGDX,相当于使用了 Java,最后却能跑在 .net 环境上,使用的方式是 IKVM 。它包含了一个使用 .net 语言实现的 Java 虚拟机,配合 Mono 能够完成 Java 和 .net 的交互,让人认识到语言之间的鸿沟是那么的渺小。 **Dalvik** +WebSphere 是 IBM 的产品,开发语言是 Java。但是它运行时的 JVM,却是一个叫做 J9 的虚拟机,依稀记得当年,有非常多的 jar 包,由于引用了一些非常偏门的 API,却不能运行(现在应该好了很多)。**Zing VM** Zing JVM 是 Azul 公司传统风格的产品,它在 HotSpot 上做了不少的定制及优化,主打低延迟、高实时服务器端 JDK 市场。它代表了一类商业化的定制,比如 JRockit,都比较贵。**IKVM** 这个以前在写一些游戏的时候,使用过 LibGDX,相当于使用了 Java,最后却能跑在 .net 环境上,使用的方式是 IKVM 。它包含了一个使用 .net 语言实现的 Java 虚拟机,配合 Mono 能够完成 Java 和 .net 的交互,让人认识到语言之间的鸿沟是那么的渺小。**Dalvik** Android 的 JVM,就是让 Google 吃官司的那个,从现在 Android 的流行度上也能看出来,Dalvik 优化的很好。 diff --git "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25426\350\256\262.md" "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25426\350\256\262.md" index 7cf47b1c9..1db9d2830 100644 --- "a/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25426\350\256\262.md" +++ "b/docs/Java/\346\267\261\345\205\245\346\265\205\345\207\272 Java \350\231\232\346\213\237\346\234\272/\347\254\25426\350\256\262.md" @@ -26,9 +26,9 @@ JVM 中有多个常量池: -- **字符串常量池** ,存放在堆上,也就是执行 intern 方法后存的地方,class 文件的静态常量池,如果是字符串,则也会被装到字符串常量池中。 -- **运行时常量池** ,存放在方法区,属于元空间,是类加载后的一些存储区域,大多数是类中 constant_pool 的内容。 -- **类文件常量池** ,也就是 constant_pool,这个是概念性的,并没有什么实际存储区域。 +- **字符串常量池**,存放在堆上,也就是执行 intern 方法后存的地方,class 文件的静态常量池,如果是字符串,则也会被装到字符串常量池中。 +- **运行时常量池**,存放在方法区,属于元空间,是类加载后的一些存储区域,大多数是类中 constant_pool 的内容。 +- **类文件常量池**,也就是 constant_pool,这个是概念性的,并没有什么实际存储区域。 在平常的交流过程中,聊的最多的是字符串常量池,[具体可参考官网](https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-2.html#jvms-2.5.5)。 @@ -42,7 +42,7 @@ Java 13 增加到 16TB,Java 11 还是 4 TB,技术在发展,请保持关注 参考代码 share/gc/shared/ageTable.cpp 中的 compute_tenuring_threshold 函数,重新表述为:程序从年龄最小的对象开始累加,如果累加的对象大小,大于幸存区的一半,则将当前的对象 age 作为新的阈值,年龄大于此阈值的对象则直接进入老年代。 -这里说的 **一半** ,是通过 TargetSurvivorRatio 参数进行设置的。 +这里说的 **一半**,是通过 TargetSurvivorRatio 参数进行设置的。 ### 永久代 diff --git "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25404\350\256\262.md" "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25404\350\256\262.md" index 09d0ce214..2aa17a299 100644 --- "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25404\350\256\262.md" +++ "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25404\350\256\262.md" @@ -21,13 +21,13 @@ KIND(Kubernetes in Docker)是为了能提供更加简单,高效的方式 - 可以直接在项目的 [Release 页面](https://github.com/kubernetes-sigs/kind/releases) 下载已经编译好的二进制文件。(下文中使用的是 v0.1.0 版本的二进制包) -注意:如果不直接使用二进制包,而是使用 `go get sigs.k8s.io/kind` 的方式下载,则与下文中的配置文件不兼容。 **请参考使用 Kind 搭建你的本地 Kubernetes 集群** 这篇文章。 +注意:如果不直接使用二进制包,而是使用 `go get sigs.k8s.io/kind` 的方式下载,则与下文中的配置文件不兼容。**请参考使用 Kind 搭建你的本地 Kubernetes 集群** 这篇文章。 更新(2020年2月5日):KIND 已经发布了 v0.7.0 版本,如果你想使用新版本,建议参考 [使用 Kind 在离线环境创建 K8S 集群](https://zhuanlan.zhihu.com/p/105173589) ,这篇文章使用了最新版本的 KIND。 ![img](assets/16898b9e3d57fcab) -### 创建集群 **在使用 KIND 之前,你需要本地先安装好 Docker 的环境** ,此处暂不做展开 +### 创建集群 **在使用 KIND 之前,你需要本地先安装好 Docker 的环境**,此处暂不做展开 由于网络问题,我们此处也需要写一个配置文件,以便让 `kind` 可以使用国内的镜像源。(KIND 最新版本中已经内置了所有需要的镜像,无需此操作) @@ -124,7 +124,7 @@ _如果你是在 Linux 系统上面,其实还有一个选择,便是将 Minik 上一节我们已经学到 K8S 集群是典型的 C/S 架构,有一个官方提供的名为 `kubectl` 的 CLI 工具。在这里,我们要安装 `kubectl` 以便后续我们可以对搭建好的集群进行管理。 -**注:由于 API 版本兼容的问题,尽量保持 kubectl 版本与 K8S 集群版本保持一致,或版本相差在在一个小版本内。** 官方文档提供了 `macOS`, `Linux`, `Windows` 等操作系统上的安装方式,且描述很详细,这里不过多赘述,[文档地址](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl)。 **此处提供一个不同于官方文档中的安装方式。** +**注:由于 API 版本兼容的问题,尽量保持 kubectl 版本与 K8S 集群版本保持一致,或版本相差在在一个小版本内。** 官方文档提供了 `macOS`, `Linux`, `Windows` 等操作系统上的安装方式,且描述很详细,这里不过多赘述,[文档地址](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl)。**此处提供一个不同于官方文档中的安装方式。** - 访问 K8S 主仓库的 [CHANGELOG 文件](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG.md.html) 找到你所需要的版本。 由于我们将要使用的 Minikube 是官方最新的稳定版 v0.28.2,而它内置的 Kubernetes 的版本是 v1.10 所以,我们选择使用对应的 [1.10 版本](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.10.md.html)的 `kubectl`。当然,我们也可以通过传递参数的方式来创建不同版本的集群。如 `minikube start --kubernetes-version v1.11.3` 用此命令创建 `v1.11.3` 版本的集群,当然 `kubectl` 的版本也需要相应升级。 diff --git "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25405\350\256\262.md" "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25405\350\256\262.md" index 450b38c20..675acaa05 100644 --- "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25405\350\256\262.md" +++ "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25405\350\256\262.md" @@ -204,7 +204,7 @@ func portForward(client libdocker.Interface, podSandboxID string, port int32, st ### 初始化集群 -所有的准备工作已经完成,我们开始真正创建一个 K8S 集群。 **注意:如果需要配置 Pod 网络方案,请先阅读本章最后的部分 配置集群网络** +所有的准备工作已经完成,我们开始真正创建一个 K8S 集群。**注意:如果需要配置 Pod 网络方案,请先阅读本章最后的部分 配置集群网络** ```javascript [[email protected] ~]# kubeadm init diff --git "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25414\350\256\262.md" "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25414\350\256\262.md" index 9076c09c2..0380d7da7 100644 --- "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25414\350\256\262.md" +++ "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25414\350\256\262.md" @@ -129,7 +129,7 @@ master 这是使用 `kubeadm` 搭建的集群中的 `kube-controller-manager` 的 `Pod`,首先可以看到它所使用的镜像,其次可以看到它使用的一系列参数,最后它在 `10252` 端口提供了健康检查的接口。稍后我们再展开。 -- 控制循环:这里拆解为两部分: **控制** 和 **循环** ,它所控制的是集群的状态;至于循环它当然是会有个循环间隔的,这里有个参数可以进行控制。 +- 控制循环:这里拆解为两部分: **控制** 和 **循环**,它所控制的是集群的状态;至于循环它当然是会有个循环间隔的,这里有个参数可以进行控制。 - 守护进程:这个就不单独展开了。 ## `kube-controller-manager` 有什么作用 diff --git "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25419\350\256\262.md" "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25419\350\256\262.md" index 1daa2237a..c73d21809 100644 --- "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25419\350\256\262.md" +++ "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25419\350\256\262.md" @@ -131,7 +131,7 @@ master - 在线修改配置 - 使用 `kubectl -n work edit deploy/saythx-redis`,会打开默认的编辑器,我们可以将使用的镜像及 tag 修正为 `redis:5` 保存退出,便会自动应用新的配置。这种做法比较适合比较紧急或者资源是直接通过命令行创建等情况。 **非特殊情况尽量不要在线修改。** 且这样修改并不利于后期维护。 + 使用 `kubectl -n work edit deploy/saythx-redis`,会打开默认的编辑器,我们可以将使用的镜像及 tag 修正为 `redis:5` 保存退出,便会自动应用新的配置。这种做法比较适合比较紧急或者资源是直接通过命令行创建等情况。**非特殊情况尽量不要在线修改。** 且这样修改并不利于后期维护。 ### 通过详细内容排查错误 diff --git "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25420\350\256\262.md" "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25420\350\256\262.md" index 22d6d639a..c051ce4d9 100644 --- "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25420\350\256\262.md" +++ "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25420\350\256\262.md" @@ -123,7 +123,7 @@ namespace: 11 bytes token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZC10b2tlbi02Y2sybCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjdlMDFkZGRhLTA0N2MtMTFlOS1iNTVjLTAyNDJhYzExMDAyYSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTprdWJlcm5ldGVzLWRhc2hib2FyZCJ9.WZ5YRUkGlKRSpkBFCk3BrZ6p2t1qVxEs7Kb18DP5X2C2lfMhDrB931PeN05uByLD6biz_4IQvKh4xmvY2RqekfV1BLCfcIiMUbc1lcXGbhH4g4vrsjYx3NZifaBh_5HuBlEL5zs5e_zFkPEhhIqjsY3KueFEuGwxTAsqGBQwawc-v6wqzB3Gzb01o1iN5aTb37PVG5gTTE8cQLih_urKhvdNEKBSRg_zHQlYjFrtUUWYRYMlYz_sWmamYVXHy_7NvKrBfw44WU5tLxMITkoUEGVwROBnHf_BcWVedozLg2uLVontB12YvhmTfJCDEAJ8o937bS-Fq3tLfu_xM40fqw ``` -将此处的 token 填入输入框内便可登录, **注意这里使用的是 describe。** +将此处的 token 填入输入框内便可登录,**注意这里使用的是 describe。** ![img](assets/167d6b926b4eed4d) diff --git "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25421\350\256\262.md" "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25421\350\256\262.md" index 72a925202..cc622aa57 100644 --- "a/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25421\350\256\262.md" +++ "b/docs/Linux/Kubernetes \344\273\216\344\270\212\346\211\213\345\210\260\345\256\236\350\267\265/\347\254\25421\350\256\262.md" @@ -119,7 +119,7 @@ saythx-redis.work.svc.cluster.local. 5 IN A 10.109.215.147 - 域名解析是可跨 `Namespace` 的 - 刚才的示例中,我们没有指定 `Namespace` 所以刚才我们所在的 `Namespace` 是 `default`。而我们的解析实验成功了。说明 CoreDNS 的解析是全局的。 **虽然解析是全局的,但不代表网络互通** + 刚才的示例中,我们没有指定 `Namespace` 所以刚才我们所在的 `Namespace` 是 `default`。而我们的解析实验成功了。说明 CoreDNS 的解析是全局的。**虽然解析是全局的,但不代表网络互通** - 域名有特定格式 diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25400\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25400\350\256\262.md" index d527ac7b3..91d5d3ed3 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25400\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25400\350\256\262.md" @@ -1,14 +1,14 @@ # 00 为什么我们要学习 Kubernetes 技术 -Kubernetes 是谷歌开源的分布式容器编排和资源管理系统。因为它的英文术语字数太长,社区专门给它定义了一个缩写单词:K8s。从 2014 年发布至今,已经成为 GitHub 社区中最炙手可热的开源项目。因为以 K8s 为核心的云原生技术已经成为业界企业数字化转型的事实标准技术栈。国内企业纷纷效仿并开始计划落地 K8s 集群作为企业应用发布的默认基础设施,但是具体怎么落实这项云原生技术其实并没有特别好实施的工具,大部分情况下我们必须结合现有企业的实际情况来落地企业应用。当然,这个说起来容易,真正开始落地的时候,技术人员就会发现遇到一点问题能在网上查到的都是一些碎片化的知识,很难系统的解决实际应用发布和部署问题。所以,笔者借着这个场景机会,秉着布道云原生技术的信心带着大家来一起探讨 **K8s 落地的各项技术细节和实际的决策思路** ,让 K8s 的用户可以从容自如的应对落地容器集群编排技术。 +Kubernetes 是谷歌开源的分布式容器编排和资源管理系统。因为它的英文术语字数太长,社区专门给它定义了一个缩写单词:K8s。从 2014 年发布至今,已经成为 GitHub 社区中最炙手可热的开源项目。因为以 K8s 为核心的云原生技术已经成为业界企业数字化转型的事实标准技术栈。国内企业纷纷效仿并开始计划落地 K8s 集群作为企业应用发布的默认基础设施,但是具体怎么落实这项云原生技术其实并没有特别好实施的工具,大部分情况下我们必须结合现有企业的实际情况来落地企业应用。当然,这个说起来容易,真正开始落地的时候,技术人员就会发现遇到一点问题能在网上查到的都是一些碎片化的知识,很难系统的解决实际应用发布和部署问题。所以,笔者借着这个场景机会,秉着布道云原生技术的信心带着大家来一起探讨 **K8s 落地的各项技术细节和实际的决策思路**,让 K8s 的用户可以从容自如的应对落地容器集群编排技术。 在学习 K8s 技术之前,我想给大家梳理下当前社区在学习 K8s 过程中遇到的几个问题: **选择多:** K8s 系统是一套专注容器应用管理的集群系统,它的组件一般按功能分别部署在主控节点(master node)和计算节点(agent node)。对于主控节点,主要包含有 etcd 集群,controller manager 组件,scheduler 组件,api-server 组件。对于计算节点,主要包含 kubelet 组件和 kubelet-proxy 组件。初学者会发现其实 **K8s 的组件并不是特别多,为什么给人的印象就是特别难安装呢?** 这里需要特别强调的是,即使到了 2020 年,我们基础软硬件设施并不能保证装完就是最优的配置,仍然需要系统工程师解决一些兼容性问题。所以当你把这些 K8s 系统组件安装到物理机、虚拟机中,并不能保证就是最优的部署配置。因为这个原因,当你作为用户在做一份新的集群部署的方案的时候,需要做很多选择题才能调优到最优解。 另外,企业业务系统的发布,并不止依赖于 K8s,它还需要包括网络、存储等。我们知道容器模型是基于单机设计的,当初设计的时候并没有考虑大规模的容器在跨主机的情况下通信问题。Pod 和 Pod 之间的网络只定义了接口标准,具体实现还要依赖第三方的网络解决方案。一直发展到今天,你仍然需要面对选择,选择适合的网络方案和网络存储。 -这里特别强调的是,目前容器网络并没有完美方案出现,它需要结合你的现有环境和基础硬件的情况来做选择。但是,当前很多书籍资料只是介绍当前最流行的开源解决方案,至于这个方案是否能在你的系统里面跑的更好是不承担责任的。这个给系统运维人员带来的痛苦是非常巨大的。一直到现在,我遇到很多维护 K8s 系统的开发运维还是对这种选择题很头疼。是的,开源社区的方案是多头驱动并带有竞争关系的,我们不能拍脑袋去选择一个容器网络之后就不在关心它的发展的。今天的最优网络方案可能过半年就不是最优的了。同理这种问题在应对选择容器存储解决方案过程中也是一样的道理。 **排错难:** 当前 K8s 社区提供了各种各样的 K8s 运维工具,有 ansible 的,dind 容器化的,有 mac-desktop 桌面版本的,还有其他云原生的部署工具。每种工具都不是简单的几行代码就能熟悉,用户需要投入很大的精力来学习和试用。因为各种底层系统的多样性,你会遇到各种各样的问题,比如容器引擎 Docker 版本低,时间同步组件 ntp 没有安装,容器网络不兼容底层网络等。任何一个点出了问题,你都需要排错。加上企业的系统环境本来就很复杂,很多场景下都是没有互联网可以查资料的,对排错来说即使所有的日志都收集起来做分析也很难轻易的排错。 +这里特别强调的是,目前容器网络并没有完美方案出现,它需要结合你的现有环境和基础硬件的情况来做选择。但是,当前很多书籍资料只是介绍当前最流行的开源解决方案,至于这个方案是否能在你的系统里面跑的更好是不承担责任的。这个给系统运维人员带来的痛苦是非常巨大的。一直到现在,我遇到很多维护 K8s 系统的开发运维还是对这种选择题很头疼。是的,开源社区的方案是多头驱动并带有竞争关系的,我们不能拍脑袋去选择一个容器网络之后就不在关心它的发展的。今天的最优网络方案可能过半年就不是最优的了。同理这种问题在应对选择容器存储解决方案过程中也是一样的道理。**排错难:** 当前 K8s 社区提供了各种各样的 K8s 运维工具,有 ansible 的,dind 容器化的,有 mac-desktop 桌面版本的,还有其他云原生的部署工具。每种工具都不是简单的几行代码就能熟悉,用户需要投入很大的精力来学习和试用。因为各种底层系统的多样性,你会遇到各种各样的问题,比如容器引擎 Docker 版本低,时间同步组件 ntp 没有安装,容器网络不兼容底层网络等。任何一个点出了问题,你都需要排错。加上企业的系统环境本来就很复杂,很多场景下都是没有互联网可以查资料的,对排错来说即使所有的日志都收集起来做分析也很难轻易的排错。 -你可能会觉得这是公司的基础设施没有建设好,可以考虑专家看看。用户倒是想解决这个问题,但是不管是商业方案还是开源方案都只是片面的考虑到 K8s 核心组件的排错,而真正企业关心的应用容器,集群,主机,网络,存储,监控日志,持续集成发布等方面的排错实践就只能靠自己摸索,你很难系统的学习到。还有,K8s 集群的版本是每个季度有一个大版本的更新。对于企业用户来说 **怎么才能在保证业务没有影响的情况下平滑更新 K8s 组件呢?** 头疼的问题就是这么出来的。一旦发生不可知问题,如何排错和高效的解决问题呢。这就是本系列专栏和大家探讨的问题。 **场景多:** 在早期的应用编排场景,主要是为了削峰填谷,高效利用浪费的主机资源。一个不公开的企业运维秘密就是生产中主机资源平均利用率不超过 20%。这不是因为运维傻,这是因为如果遇到峰值,主机系统需要能平滑应对,需要给业务容量留有余地。因为容器的引入让原来主机仅可以部署 3-4 个进程的系统,现在可以充分利用容器进程隔离的技术在主机上部署 20-40 个进程系统,并且各自还不受影响。这就是容器应用的最大好处。 +你可能会觉得这是公司的基础设施没有建设好,可以考虑专家看看。用户倒是想解决这个问题,但是不管是商业方案还是开源方案都只是片面的考虑到 K8s 核心组件的排错,而真正企业关心的应用容器,集群,主机,网络,存储,监控日志,持续集成发布等方面的排错实践就只能靠自己摸索,你很难系统的学习到。还有,K8s 集群的版本是每个季度有一个大版本的更新。对于企业用户来说 **怎么才能在保证业务没有影响的情况下平滑更新 K8s 组件呢?** 头疼的问题就是这么出来的。一旦发生不可知问题,如何排错和高效的解决问题呢。这就是本系列专栏和大家探讨的问题。**场景多:** 在早期的应用编排场景,主要是为了削峰填谷,高效利用浪费的主机资源。一个不公开的企业运维秘密就是生产中主机资源平均利用率不超过 20%。这不是因为运维傻,这是因为如果遇到峰值,主机系统需要能平滑应对,需要给业务容量留有余地。因为容器的引入让原来主机仅可以部署 3-4 个进程的系统,现在可以充分利用容器进程隔离的技术在主机上部署 20-40 个进程系统,并且各自还不受影响。这就是容器应用的最大好处。 社区里面有好学习的技术架构师也曾经说过,在介绍什么是容器时,就拿租房过程中,单身公寓和打隔断的群租房来对比形容,特别形象。随着应用场景的多样性,在应对突发流量的时候,K8s 编排系统就是作为一种弹性伸缩的工具,快速提高进程的数量来承载流量。在解决微服务应用编排上,除了传统的微服务部署需求之外,还有混合部署需要的 Service Mesh 技术也对 K8s 提出了流量编排的新要求。另外还有 Serverless 场景下的 FaaS 框架 Openfaas 也对 K8s 带来了新的机会和应用难点。还有很多有状态中间件服务,如数据库集群等也都在大量的迁入到 K8s 集群中,并获得了很好的实践反馈。 @@ -24,7 +24,7 @@ Kubernetes 是谷歌开源的分布式容器编排和资源管理系统。因为 ### 你将获得什么 -通过本专栏的学习,你将 **全方位的理解 K8s 组件的原理技术** ,并结合云原生开源思想,学习到分布式系统的组合过程。为了解决日常场景中可能的问题,你也可以分章节获得 **独家的实践理解和解决思路过程** ,让你可以推演并学习到一些架构思维模型。并且笔者也给大家精选了 K8s 组件的详细讲解,可以让好奇的使用者,不断可以知道这些组件的原理,还能知道它们内部的实现,让你可以更准确的把握这些组件,相信你也有机会参与 K8s 的开发并写出更多的组件代替它们。 +通过本专栏的学习,你将 **全方位的理解 K8s 组件的原理技术**,并结合云原生开源思想,学习到分布式系统的组合过程。为了解决日常场景中可能的问题,你也可以分章节获得 **独家的实践理解和解决思路过程**,让你可以推演并学习到一些架构思维模型。并且笔者也给大家精选了 K8s 组件的详细讲解,可以让好奇的使用者,不断可以知道这些组件的原理,还能知道它们内部的实现,让你可以更准确的把握这些组件,相信你也有机会参与 K8s 的开发并写出更多的组件代替它们。 ### 专栏结构 diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25401\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25401\350\256\262.md" index c69e0efc4..0e43f55ce 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25401\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25401\350\256\262.md" @@ -191,7 +191,7 @@ accept packages to nodeport service with `externalTrafficPolicy=local` 大家看到标题,基本上就能理解这些组件的用处。我这里还是从实用的角度和大家一起重新认识一下这些组件,为之后的使用提供经验参考。 -**1. 网络和网络策略** 对于网络,我们主要指容器网络。注意在 Kubernetes 集群里面,是有两层虚拟网络的。一说虚拟网络,就会有丢包率,这个是以往虚拟化环境不可想象的问题。为了提高或者说规避这方面的棘手问题,我们会放弃所有官方的方案,采用传统的网络方案来支持。当然传统的网络方案大都不是为 Kubernetes 网络设计的,需要做很多自定义适配工作来完善体验。在不理想的传统方案之外,容器网络方案中最流行的有 Calico、Cilium、Flannel、Contiv 等等。采用这些方案之后,随着业务流量的增加一定会带来网络丢包的情况。网络丢包带来的问题是业务处理能力的降低,为了恢复业务实例的处理能力,我们常规的操作是水平扩展容器实例数。注意,正是因为实例数的增加反而会提高业务处理能力,让运维人员忽略容器网络带来的性能损耗。另外,Kubernetes 在业务实践中还参考了主流网络管理的需求设计,引入了 Network Policies。这些策略定义了 Pod 之间的连通关系,方便对业务容器组的安全网络隔离。当然笔者在实践中发现,这些策略完全依赖容器网络的实现能力,依赖性强,只能作为试验品体验,但是在实际业务中,还没有看到实际的能力优势。 **2. 服务发现** 目前提供的能力就是给 Pod 提供 DNS 服务,并引入了域名的定义规则。官方认可的只有 CoreDNS。注意 ,这个服务发现只能在集群内部使用。不推荐直接暴露给外部服务,集群对外暴露的服务仍然是 IP 和端口。外部 DNS 可以灵活的指定这个固定 IP 来让业务在全局服务发现。 **3. 可视化管理** 官方提供了 Dashboard,这是官方提供的标准管理集群的 web 界面,很多开发集成测试环境,使用它就可以满足业务管理的需求。这个可选安装。 **4. 基础设施** 官方提供了 KubeVirt,是可以让 Kubernetes 运行虚拟机的附加组件,默认运行在裸机群集上。从目前的实践经验来看,这种能力还属于试验性的能力,一般很少人使用。 **5. 遗留组件** +**1. 网络和网络策略** 对于网络,我们主要指容器网络。注意在 Kubernetes 集群里面,是有两层虚拟网络的。一说虚拟网络,就会有丢包率,这个是以往虚拟化环境不可想象的问题。为了提高或者说规避这方面的棘手问题,我们会放弃所有官方的方案,采用传统的网络方案来支持。当然传统的网络方案大都不是为 Kubernetes 网络设计的,需要做很多自定义适配工作来完善体验。在不理想的传统方案之外,容器网络方案中最流行的有 Calico、Cilium、Flannel、Contiv 等等。采用这些方案之后,随着业务流量的增加一定会带来网络丢包的情况。网络丢包带来的问题是业务处理能力的降低,为了恢复业务实例的处理能力,我们常规的操作是水平扩展容器实例数。注意,正是因为实例数的增加反而会提高业务处理能力,让运维人员忽略容器网络带来的性能损耗。另外,Kubernetes 在业务实践中还参考了主流网络管理的需求设计,引入了 Network Policies。这些策略定义了 Pod 之间的连通关系,方便对业务容器组的安全网络隔离。当然笔者在实践中发现,这些策略完全依赖容器网络的实现能力,依赖性强,只能作为试验品体验,但是在实际业务中,还没有看到实际的能力优势。**2. 服务发现** 目前提供的能力就是给 Pod 提供 DNS 服务,并引入了域名的定义规则。官方认可的只有 CoreDNS。注意 ,这个服务发现只能在集群内部使用。不推荐直接暴露给外部服务,集群对外暴露的服务仍然是 IP 和端口。外部 DNS 可以灵活的指定这个固定 IP 来让业务在全局服务发现。**3. 可视化管理** 官方提供了 Dashboard,这是官方提供的标准管理集群的 web 界面,很多开发集成测试环境,使用它就可以满足业务管理的需求。这个可选安装。**4. 基础设施** 官方提供了 KubeVirt,是可以让 Kubernetes 运行虚拟机的附加组件,默认运行在裸机群集上。从目前的实践经验来看,这种能力还属于试验性的能力,一般很少人使用。**5. 遗留组件** 对于很多老版本的 Kubernetes,有很多历史遗留的组件可以选用,所以官方把这些可选的组件都保留了下来,帮助用户在迁移集群版本的过程中可以继续发挥老集群的能力。一般很少人使用。 diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25402\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25402\350\256\262.md" index 3f402e057..9f55d74db 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25402\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25402\350\256\262.md" @@ -34,7 +34,7 @@ spec: #### **策略二:采用节点亲和,Pod 间亲和/反亲和确保 Pod 实现高可用运行** 当运维发布多个副本实例的业务容器的时候,一定需要仔细注意到一个事实。Kubernetes 的调度默认策略是选取最空闲的资源主机来部署容器应用,不考虑业务高可用的实际情况。当你的集群中部署的业务越多,你的业务风险会越大。一旦你的业务容器所在的主机出现宕机之后,带来的容器重启动风暴也会即可到来。为了实现业务容错和高可用的场景,我们需要考虑通过 Node 的亲和性和 Pod 的反亲和性来达到合理的部署。这里需要注意的地方是,Kubernetes 的调度系统接口是开放式的,你可以实现自己的业务调度策略来替换默认的调度策略。我们这里的策略是尽量采用 Kubernetes 原生能力来实现 -为了更好地理解高可用的重要性,我们深入探讨一些实际的业务场景。 **首先,Kubernetes 并不是谷歌内部使用的 Borg 系统** ,大部分中小企业使用的 Kubernetes 部署方案都是人工扩展的私有资源池。当你发布容器到集群中,集群不会因为资源不够能自动扩展主机并自动负载部署容器 Pod。即使是在公有云上的 Kubernetes 服务,只有当你选择 Serverlesss Kubernetes 类型时才能实现资源的弹性伸缩。很多传统企业在落地 Kubernetes 技术时比较关心的弹性伸缩能力,目前只能折中满足于在有限静态资源的限制内动态启停容器组 Pod,实现类似的业务容器的弹性。用一个不太恰当的比喻就是房屋中介中,从独立公寓变成了格子间公寓,空间并没有实质性扩大。在实际有限资源的情况下,Kubernetes 提供了打标签的功能,你可以给主机、容器组 Pod 打上各种标签,这些标签的灵活运用,可以帮你快速实现业务的高可用运行。 +为了更好地理解高可用的重要性,我们深入探讨一些实际的业务场景。**首先,Kubernetes 并不是谷歌内部使用的 Borg 系统**,大部分中小企业使用的 Kubernetes 部署方案都是人工扩展的私有资源池。当你发布容器到集群中,集群不会因为资源不够能自动扩展主机并自动负载部署容器 Pod。即使是在公有云上的 Kubernetes 服务,只有当你选择 Serverlesss Kubernetes 类型时才能实现资源的弹性伸缩。很多传统企业在落地 Kubernetes 技术时比较关心的弹性伸缩能力,目前只能折中满足于在有限静态资源的限制内动态启停容器组 Pod,实现类似的业务容器的弹性。用一个不太恰当的比喻就是房屋中介中,从独立公寓变成了格子间公寓,空间并没有实质性扩大。在实际有限资源的情况下,Kubernetes 提供了打标签的功能,你可以给主机、容器组 Pod 打上各种标签,这些标签的灵活运用,可以帮你快速实现业务的高可用运行。 **其次,实践中你会发现,为了高效有效的控制业务容器,你是需要资源主机的。** 你不能任由 Kubernetes 调度来分配主机启动容器,这个在早期资源充裕的情况下看不到问题。当你的业务复杂之后,你会部署更多的容器到资源池中,这个时间你的业务运行的潜在危机就会出现。因为你没有管理调度资源,导致很多关键业务是运行在同一台服务器上,当主机宕机发生时,让你很难处理这种灾难。所以在实际的业务场景中,业务之间的关系需要梳理清楚,设计单元化主机资源模块,比如 2 台主机为一个单元,部署固定的业务容器组 Pod,并且让容器组 Pod 能足够分散的运行在这两台主机之上,当任何一台主机宕机也不会影响到主体业务,实现真正的高可用。 @@ -82,7 +82,7 @@ containers: image: gcr.azk8s.cn/google-samples/node-hello:1.0 ``` -目前有两种类型的节点亲和,分别为 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution。你可以视它们为“硬”和“软”,意思是,前者指定了将 pod 调度到一个节点上必须满足的规则,后者指定调度器将尝试执行但不能保证的偏好。 **Pod 间亲和与反亲和示例** 使用反亲和性避免单点故障例子: +目前有两种类型的节点亲和,分别为 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution。你可以视它们为“硬”和“软”,意思是,前者指定了将 pod 调度到一个节点上必须满足的规则,后者指定调度器将尝试执行但不能保证的偏好。**Pod 间亲和与反亲和示例** 使用反亲和性避免单点故障例子: ```yaml affinity: diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25406\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25406\350\256\262.md" index 25a88a856..3e5a341ac 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25406\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25406\350\256\262.md" @@ -247,7 +247,7 @@ Kubernetes 大版本基本上 3 个月就会更新一次,如果让我们天天 > [https://landscape.cncf.io/](https://landscape.cncf.io/) -相信你一定可以获得满意的架构建议和方案。 **笔者建议** :遇到概念问题不清楚,请到 [https://kubernetes.io/docs](https://kubernetes.io/docs) 搜索获取最新的资料。遇到技术架构的问题,请到 [https://landscape.cncf.io/](https://landscape.cncf.io/) 参考云原生的技术架构蓝图,相信也可以找到线索。 +相信你一定可以获得满意的架构建议和方案。**笔者建议** :遇到概念问题不清楚,请到 [https://kubernetes.io/docs](https://kubernetes.io/docs) 搜索获取最新的资料。遇到技术架构的问题,请到 [https://landscape.cncf.io/](https://landscape.cncf.io/) 参考云原生的技术架构蓝图,相信也可以找到线索。 ### 总结 diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25408\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25408\350\256\262.md" index d11bf7a5d..a32e753c2 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25408\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25408\350\256\262.md" @@ -137,7 +137,7 @@ NodePort 服务** ### 安装容器运行时引擎 -kubenet 并不能直接启动容器,所以集群节点机器需要统一部署容器运行时引擎。从 v1.6.0 版本起,Kubernetes 开始默认允许使用 CRI(容器运行时接口)。从 v1.14.0 版本起,kubeadm 将通过观察已知的 UNIX 域套接字来自动检测 Linux 节点上的容器运行时。下表中是可检测到的正在运行的 runtime 和 socket 路径。 **运行时** **域套接字** Docker +kubenet 并不能直接启动容器,所以集群节点机器需要统一部署容器运行时引擎。从 v1.6.0 版本起,Kubernetes 开始默认允许使用 CRI(容器运行时接口)。从 v1.14.0 版本起,kubeadm 将通过观察已知的 UNIX 域套接字来自动检测 Linux 节点上的容器运行时。下表中是可检测到的正在运行的 runtime 和 socket 路径。**运行时** **域套接字** Docker /var/run/docker.sock diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25412\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25412\350\256\262.md" index 1b3a8d1b5..8be539a3c 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25412\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25412\350\256\262.md" @@ -12,11 +12,11 @@ 我总结出来的经验如下,方便大家参考: -**1. x86-64 仍然是当前对容器最好的系统架构体系** ,目前主流的系统聚集在 redhat/centos 7.x 系列,Ubuntu 16.04 系列。对于内核红帽系主要在 3.10 以上,Ubuntu 能到 4.4 以上。有些用户会通过开源 kernel 仓库把红帽系的 Kernel 升级到 4.4,也比较常见。升级内核的代价就是引入很多未知的模块,让系统变得不稳定。ARM 系统架构会对整个 Kubernetes 组件的文件格式产生兼容性要求,在选择适配的时候,一定要注意有没有准备好 Kubernetes 相应的组件。总结下来,主流的操作系统主要是红帽的 7.x 系列和 Ubuntu LTS 系列 16.04。升级大版本操作系统对 Kubernetes 来说,需要做很多适配工作,目前开源社区是不太可能帮用户做的。一定注意。 **2. Kubernetes 的版本更新很快** ,整个社区会维护 3 个主线版本,如现在主要为 1.16.x、1.17.x、1.18.x。这个 x 版本号差不多 2 周就一个迭代,主要是修复 Bug。很多团队在使用上总结了一些技巧,比如取奇数版本或者偶数版本作为自己的主力版本,这个做法的目的就是规避最新版本带来的不稳定性。并不是说奇数版本好或者是偶数版本稳定,这是纯属瞎猜。作为开源软件,它的质量是社区在维护,落实到用户这里,就是大家都是小白鼠,需要在自己的环境试验验证组件的可靠性。总结下来,主流的环境还是选择比最新版本低 1 个或者 2 个子版本作为周期来当做自己的软件来维护。维护开源软件不是免费的,它是通过大家的努力才能保证组件的使用可靠性的。 +**1. x86-64 仍然是当前对容器最好的系统架构体系**,目前主流的系统聚集在 redhat/centos 7.x 系列,Ubuntu 16.04 系列。对于内核红帽系主要在 3.10 以上,Ubuntu 能到 4.4 以上。有些用户会通过开源 kernel 仓库把红帽系的 Kernel 升级到 4.4,也比较常见。升级内核的代价就是引入很多未知的模块,让系统变得不稳定。ARM 系统架构会对整个 Kubernetes 组件的文件格式产生兼容性要求,在选择适配的时候,一定要注意有没有准备好 Kubernetes 相应的组件。总结下来,主流的操作系统主要是红帽的 7.x 系列和 Ubuntu LTS 系列 16.04。升级大版本操作系统对 Kubernetes 来说,需要做很多适配工作,目前开源社区是不太可能帮用户做的。一定注意。**2. Kubernetes 的版本更新很快**,整个社区会维护 3 个主线版本,如现在主要为 1.16.x、1.17.x、1.18.x。这个 x 版本号差不多 2 周就一个迭代,主要是修复 Bug。很多团队在使用上总结了一些技巧,比如取奇数版本或者偶数版本作为自己的主力版本,这个做法的目的就是规避最新版本带来的不稳定性。并不是说奇数版本好或者是偶数版本稳定,这是纯属瞎猜。作为开源软件,它的质量是社区在维护,落实到用户这里,就是大家都是小白鼠,需要在自己的环境试验验证组件的可靠性。总结下来,主流的环境还是选择比最新版本低 1 个或者 2 个子版本作为周期来当做自己的软件来维护。维护开源软件不是免费的,它是通过大家的努力才能保证组件的使用可靠性的。 -\\3. 除了 Kubernetes 主线版本的选择我们应该延迟 1 到 2 个版本之外, **对于其它附属组件如 Calico、kube-dns、Containerd 等,应该需要选择最新版本** 。主要原因在于它们是一线运行的组件,被调用的次数是更多的,发现问题的机会更突出。越早发现问题越快得到修复。这又是开源里面的原则,就是越早发现、越早修复,组件越稳定。很多用户在组件选择上,会比较保守,导致很多修复过的 Bug 还存在于你的集群中,让不确定性得到蔓延。总结下来,跑容器的一线组件应该使用最新版本,越早发现,你的程序越稳固。言下之意,当开源小白鼠,咱们也要有对策,通过自动化测试的环境,把这些组件多测测。 +\\3. 除了 Kubernetes 主线版本的选择我们应该延迟 1 到 2 个版本之外,**对于其它附属组件如 Calico、kube-dns、Containerd 等,应该需要选择最新版本** 。主要原因在于它们是一线运行的组件,被调用的次数是更多的,发现问题的机会更突出。越早发现问题越快得到修复。这又是开源里面的原则,就是越早发现、越早修复,组件越稳定。很多用户在组件选择上,会比较保守,导致很多修复过的 Bug 还存在于你的集群中,让不确定性得到蔓延。总结下来,跑容器的一线组件应该使用最新版本,越早发现,你的程序越稳固。言下之意,当开源小白鼠,咱们也要有对策,通过自动化测试的环境,把这些组件多测测。 -\\4. 很多以为 Kubernetes 安装上之后就完事大吉,环境的事情就不用操心了。诚然,通过容器确实可以解决一部分运维的问题。但是应用架构的可靠性并不能依靠 Kubernetes 。为什么在有了 容器之后,在 DevOps 领域开始引入了 SRE 的概念,就是说业务保障一直是业务核心能力,不能依赖 Kubernetes。 **用了 Kubernetes 之后,你更要关注架构的稳定性。** #### **kubeadm 的配置测验** +\\4. 很多以为 Kubernetes 安装上之后就完事大吉,环境的事情就不用操心了。诚然,通过容器确实可以解决一部分运维的问题。但是应用架构的可靠性并不能依靠 Kubernetes 。为什么在有了 容器之后,在 DevOps 领域开始引入了 SRE 的概念,就是说业务保障一直是业务核心能力,不能依赖 Kubernetes。**用了 Kubernetes 之后,你更要关注架构的稳定性。** #### **kubeadm 的配置测验** kubeadm 推出的初衷是为了用更平滑的方式来安装、升级 Kubernetes。在早期我是排斥的,因为二进制的安装方式好像更简洁,排错也更方便。但是随着安装经验的丰富,我发现二进制的安装还是无法标准化,配置起来手工操作的地方很多,无法满足一键安装的目的。 diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25414\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25414\350\256\262.md" index 525fe3701..d9b14c93f 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25414\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25414\350\256\262.md" @@ -4,7 +4,7 @@ ### 为什么需要 OpenResty -原生 Kubernetes Service 提供对外暴露服务的能力,通过唯一的 ClusterIP 接入 Pod 业务负载容器组对外提供服务名(附注:服务发现使用,采用内部 kube-dns 解析服务名称)并提供流量的软负载均衡。缺点是 Service 的 ClusterIP 地址只能在集群内部被访问,如果需要对集群外部用户提供此 Service 的访问能力,Kubernetes 需要通过另外两种方式来实现此类需求,一种是 **NodePort** ,另一种是 **LoadBalancer** 。 +原生 Kubernetes Service 提供对外暴露服务的能力,通过唯一的 ClusterIP 接入 Pod 业务负载容器组对外提供服务名(附注:服务发现使用,采用内部 kube-dns 解析服务名称)并提供流量的软负载均衡。缺点是 Service 的 ClusterIP 地址只能在集群内部被访问,如果需要对集群外部用户提供此 Service 的访问能力,Kubernetes 需要通过另外两种方式来实现此类需求,一种是 **NodePort**,另一种是 **LoadBalancer** 。 ![nodeport](assets/ea127350-f337-11ea-a9e7-47fb41a2df40.jpg) diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25415\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25415\350\256\262.md" index 4d0fda013..401cc5e73 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25415\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25415\350\256\262.md" @@ -310,19 +310,19 @@ nginx.ingress.kubernetes.io/limit-connections:一个 IP 地址允许的并发 nginx.ingress.kubernetes.io/limit-rps:每秒从给定 IP 接受的连接数 ```plaintext -在一个 Ingress 规则中同时指定这两个注解,limit-rps 优先。 **后端支持接入 SSL 案例** 默认情况下,Nginx 使用 HTTP 来到达服务。在 Ingress 规则中添加注解: +在一个 Ingress 规则中同时指定这两个注解,limit-rps 优先。**后端支持接入 SSL 案例** 默认情况下,Nginx 使用 HTTP 来到达服务。在 Ingress 规则中添加注解: ``` nginx.ingress.kubernetes.io/secure-backends: "true" ```plaintext -在 Ingress 规则中把协议改为 HTTPS。 **白名单案例** 你可以通过: +在 Ingress 规则中把协议改为 HTTPS。**白名单案例** 你可以通过: ``` nginx.ingress.kubernetes.io/whitelist-source-range ```plaintext -注解来指定允许的客户端 IP 源范围,该值是一个逗号分隔的 CIDRs 列表,例如 10.0.0.0/24,172.10.0.1。 **Session Affinity 和 Cookie affinity** 注解: +注解来指定允许的客户端 IP 源范围,该值是一个逗号分隔的 CIDRs 列表,例如 10.0.0.0/24,172.10.0.1。**Session Affinity 和 Cookie affinity** 注解: ``` nginx.ingress.kubernetes.io/affinity diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25420\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25420\350\256\262.md" index 8792a7aa4..61cc19f47 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25420\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25420\350\256\262.md" @@ -170,7 +170,7 @@ StatefulSet 增加了 podUpdatePolicy 来允许用户指定重建升级还是原 我们在一套业务环境中更新最多的就是镜像版本,所以这个需求特别适合云原生体系中的日常应用运维操作。 -**更重要的是** ,使用 InPlaceIfPossible 或 InPlaceOnly 策略,必须要增加一个 InPlaceUpdateReady readinessGate,用来在原地升级的时候控制器将 Pod 设置为 NotReady。一个完整的案例参考: +**更重要的是**,使用 InPlaceIfPossible 或 InPlaceOnly 策略,必须要增加一个 InPlaceUpdateReady readinessGate,用来在原地升级的时候控制器将 Pod 设置为 NotReady。一个完整的案例参考: ```yaml apiVersion: apps.kruise.io/v1alpha1 diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25421\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25421\350\256\262.md" index edfc8c33d..755e0699f 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25421\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25421\350\256\262.md" @@ -82,7 +82,7 @@ mysql> show databases; 下图说明了 Vitess 的组件架构,我们需要熟悉这些术语: -![18-1-vitess-arch33](assets/36be7e60-2cc7-11eb-90f6-fbd19bda6e6e) **Topology** 拓扑服务是一个元数据存储对象,包含有关正在运行的服务器、分片方案和复制关系图的信息。拓扑由一致性的数据存储支持,默认支持 etcd2 插件。您可以使用 vtctl(命令行)和 vtctld(web)查看拓扑信息。 **VTGate** VTGate 是一个轻型代理服务器,它将流量路由到正确的 VTTablet,并将合并的结果返回给客户端。应用程序向 VTGate 发起查询。客户端使用起来非常简单,它只需要能够找到 VTGate 实例就能使 Vitess。 **VTTablet** VTTablet 是一个位于 MySQL 数据库前面的代理服务器,执行的任务试图最大化吞吐量,同时保护 MySQL 不受有害查询的影响。它的特性包括连接池、查询重写和重用重复数据。 **Keyspace** +![18-1-vitess-arch33](assets/36be7e60-2cc7-11eb-90f6-fbd19bda6e6e) **Topology** 拓扑服务是一个元数据存储对象,包含有关正在运行的服务器、分片方案和复制关系图的信息。拓扑由一致性的数据存储支持,默认支持 etcd2 插件。您可以使用 vtctl(命令行)和 vtctld(web)查看拓扑信息。**VTGate** VTGate 是一个轻型代理服务器,它将流量路由到正确的 VTTablet,并将合并的结果返回给客户端。应用程序向 VTGate 发起查询。客户端使用起来非常简单,它只需要能够找到 VTGate 实例就能使 Vitess。**VTTablet** VTTablet 是一个位于 MySQL 数据库前面的代理服务器,执行的任务试图最大化吞吐量,同时保护 MySQL 不受有害查询的影响。它的特性包括连接池、查询重写和重用重复数据。**Keyspace** 关键空间是一个逻辑数据库。如果使用 Sharding,一个 keyspace 映射到多个 MySQL 数据库;如果不使用 Sharding,一个 keyspace 直接映射到一个 MySQL 数据库名。无论哪种情况,从应用程序的角度来看,一个关键空间都是作为一个单一的数据库出现的。 diff --git "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25422\350\256\262.md" "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25422\350\256\262.md" index 5e2377b8b..dc2d2e6b4 100644 --- "a/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25422\350\256\262.md" +++ "b/docs/Linux/Kubernetes \345\256\236\350\267\265\345\205\245\351\227\250\346\214\207\345\215\227/\347\254\25422\350\256\262.md" @@ -76,11 +76,11 @@ VolumeSnapshotClass 允许您指定属于 VolumeSnapshot 的不同属性。这 - CSI 驱动程序可能已经实现或没有实现卷快照功能。已提供卷快照支持的 CSI 驱动程序可能会使用 csi-snapshotter。详情请参见 CSI 驱动程序文档。 - CRD 和 快照控制器的安装是 Kubernetes 发行版的责任。 -#### **VolumeSnapshot 和 VolumeSnapshotContent 的生命周期** VolumeSnapshotContents 是集群中的资源。VolumeSnapshot 是对这些资源的请求。VolumeSnapshotContents 和 VolumeSnapshot 之间的交互遵循这个生命周期。 **1. 供应卷快照** 有两种方式可以配置快照:预配置或动态配置。 **2. 预备** 群集管理员会创建一些 VolumeSnapshotContents。它们携带了存储系 统上真实卷照的详细信息,可供集群用户使用。它们存在于 Kubernetes API 中,可供消费。 **3. 动态** 您可以请求从 PersistentVolumeClaim 动态获取快照,而不是使用预先存在的快照。VolumeSnapshotClass 指定了存储提供商的特定参数,以便在获取快照时使用。 **4. 绑定** 快照控制器处理 VolumeSnapshot 对象与适当的 VolumeSnapshotContent 对象的绑定,在预供应和动态供应的情况下都是如此。绑定是一个一对一的映射 +#### **VolumeSnapshot 和 VolumeSnapshotContent 的生命周期** VolumeSnapshotContents 是集群中的资源。VolumeSnapshot 是对这些资源的请求。VolumeSnapshotContents 和 VolumeSnapshot 之间的交互遵循这个生命周期。**1. 供应卷快照** 有两种方式可以配置快照:预配置或动态配置。**2. 预备** 群集管理员会创建一些 VolumeSnapshotContents。它们携带了存储系 统上真实卷照的详细信息,可供集群用户使用。它们存在于 Kubernetes API 中,可供消费。**3. 动态** 您可以请求从 PersistentVolumeClaim 动态获取快照,而不是使用预先存在的快照。VolumeSnapshotClass 指定了存储提供商的特定参数,以便在获取快照时使用。**4. 绑定** 快照控制器处理 VolumeSnapshot 对象与适当的 VolumeSnapshotContent 对象的绑定,在预供应和动态供应的情况下都是如此。绑定是一个一对一的映射 -在预供应绑定的情况下,VolumeSnapshot 将保持未绑定状态,直到请求的 VolumeSnapshotContent 对象被创建。 **5. 作为快照源保护的持久性卷索赔** 这个保护的目的是为了确保在使用中的 PersistentVolumeClaim API 对象在快照时不会被从系统中移除(因为这可能导致数据丢失)。 +在预供应绑定的情况下,VolumeSnapshot 将保持未绑定状态,直到请求的 VolumeSnapshotContent 对象被创建。**5. 作为快照源保护的持久性卷索赔** 这个保护的目的是为了确保在使用中的 PersistentVolumeClaim API 对象在快照时不会被从系统中移除(因为这可能导致数据丢失)。 -当一个 PersistentVolumeClaim 的快照被取走时,该 PersistentVolumeClaim 是在使用中的。如果您删除了一个正在使用的 PersistentVolumeClaim API 对象作为快照源,PersistentVolumeClaim 对象不会被立即删除。相反,PersistentVolumeClaim 对象的删除会被推迟到快照准备好或中止之后。 **6. 删除** 删除是通过删除 VolumeSnapshot 对象来触发的,将遵循 DeletionPolicy。如果 DeletionPolicy 是 Delete,那么底层存储快照将和 VolumeSnapshotContent 对象一起被删除。如果 DeletionPolicy 是 Retain,那么底层快照和 VolumeSnapshotContent 都会保留。 +当一个 PersistentVolumeClaim 的快照被取走时,该 PersistentVolumeClaim 是在使用中的。如果您删除了一个正在使用的 PersistentVolumeClaim API 对象作为快照源,PersistentVolumeClaim 对象不会被立即删除。相反,PersistentVolumeClaim 对象的删除会被推迟到快照准备好或中止之后。**6. 删除** 删除是通过删除 VolumeSnapshot 对象来触发的,将遵循 DeletionPolicy。如果 DeletionPolicy 是 Delete,那么底层存储快照将和 VolumeSnapshotContent 对象一起被删除。如果 DeletionPolicy 是 Retain,那么底层快照和 VolumeSnapshotContent 都会保留。 #### **VolumeSnapshots** 每个 VolumeSnapshot 包含一个规格和一个状态 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25400\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25400\350\256\262.md" index e869983f5..68f415ab7 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25400\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25400\350\256\262.md" @@ -20,26 +20,26 @@ 这些问题或者场景,你肯定或多或少都遇到过。 -实际上, **性能优化一直都是大多数软件工程师头上的“紧箍咒”** ,甚至许多工作多年的资深工程师,也无法准确地分析出线上的很多性能问题。 +实际上,**性能优化一直都是大多数软件工程师头上的“紧箍咒”**,甚至许多工作多年的资深工程师,也无法准确地分析出线上的很多性能问题。 性能问题为什么这么难呢?我觉得主要是因为性能优化是个系统工程,总是牵一发而动全身。它涉及了从程序设计、算法分析、编程语言,再到系统、存储、网络等各种底层基础设施的方方面面。每一个组件都有可能出问题,而且很有可能多个组件同时出问题。 -毫无疑问,性能优化是软件系统中最有挑战的工作之一,但是换个角度看, **它也是最考验体现你综合能力的工作之一** 。如果说你能把性能优化的各个关键点吃透,那我可以肯定地说,你已经是一个非常优秀的软件工程师了。 +毫无疑问,性能优化是软件系统中最有挑战的工作之一,但是换个角度看,**它也是最考验体现你综合能力的工作之一** 。如果说你能把性能优化的各个关键点吃透,那我可以肯定地说,你已经是一个非常优秀的软件工程师了。 那怎样才能掌握这个技能呢?你可以像我前面说的那样,花大量的时间和精力去钻研,从内功到实战一一苦练。当然,那样可行,但也会走很多弯路,而且可能你啃了很多大块头的书,终于拿下了最难的底层体系,却因为缺乏实战经验,在实际开发工作中仍然没有头绪。 -其实,对于我们大多数人来说, **最好的学习方式一定是带着问题学习** ,而不是先去啃那几本厚厚的原理书籍,这样很容易把自己的信心压垮。 +其实,对于我们大多数人来说,**最好的学习方式一定是带着问题学习**,而不是先去啃那几本厚厚的原理书籍,这样很容易把自己的信心压垮。 -我认为, **学习要会抓重点** 。其实只要你了解少数几个系统组件的基本原理和协作方式,掌握基本的性能指标和工具,学会实际工作中性能优化的常用技巧,你就已经可以准确分析和优化大多数的性能问题了。在这个认知的基础上,再反过来去阅读那些经典的操作系统或者其它图书,你才能事半功倍。 +我认为,**学习要会抓重点** 。其实只要你了解少数几个系统组件的基本原理和协作方式,掌握基本的性能指标和工具,学会实际工作中性能优化的常用技巧,你就已经可以准确分析和优化大多数的性能问题了。在这个认知的基础上,再反过来去阅读那些经典的操作系统或者其它图书,你才能事半功倍。 所以,在这个专栏里,我会以 **案例驱动** 的思路,给你讲解 Linux 性能的基本指标、工具,以及相应的观测、分析和调优方法。 具体来看,我会分为 5 个模块。前 4 个模块我会从资源使用的视角出发,带你分析各种 Linux 资源可能会碰到的性能问题,包括 **CPU 性能** 、 **磁盘 I/O 性能** 、 **内存性能** 以及 **网络性能** 。每个模块还由浅入深划分为四个不同的篇章。 -- **基础篇** ,介绍 Linux 必备的基本原理以及对应的性能指标和性能工具。比如怎么理解平均负载,怎么理解上下文切换,Linux 内存的工作原理等等。 -- **案例篇** ,这里我会通过模拟案例,帮你分析高手在遇到资源瓶颈时,是如何观测、定位、分析并优化这些性能问题的。 -- **套路篇** ,在理解了基础,亲身体验了模拟案例之后,我会帮你梳理出排查问题的整体思路,也就是检查性能问题的一般步骤,这样,以后你遇到问题,就可以按照这样的路子来。 -- **答疑篇** ,我相信在学习完每一个模块之后,你都会有很多的问题,在答疑篇里,我会拿出提问频次较高的问题给你系统解答。 +- **基础篇**,介绍 Linux 必备的基本原理以及对应的性能指标和性能工具。比如怎么理解平均负载,怎么理解上下文切换,Linux 内存的工作原理等等。 +- **案例篇**,这里我会通过模拟案例,帮你分析高手在遇到资源瓶颈时,是如何观测、定位、分析并优化这些性能问题的。 +- **套路篇**,在理解了基础,亲身体验了模拟案例之后,我会帮你梳理出排查问题的整体思路,也就是检查性能问题的一般步骤,这样,以后你遇到问题,就可以按照这样的路子来。 +- **答疑篇**,我相信在学习完每一个模块之后,你都会有很多的问题,在答疑篇里,我会拿出提问频次较高的问题给你系统解答。 第 5 个综合实战模块,我将为你还原真实的工作场景,手把手带你在“ **高级战场** ”中演练,这样你能把前面学到的所有知识融会贯通,并且看完专栏,马上就能用在工作中。 @@ -49,4 +49,4 @@ 不为别的,就希望你能和我坚持下去,一直到最后一篇文章。这中间,有想不明白的地方,你要先自己多琢磨几次;还是不懂的,你可以在留言区找我问;有需要总结提炼的知识点,你也要自己多下笔。你还可以写下自己的经历,记录你的分析步骤和思路,我都会及时回复你。 -最后,你可以在留言区给自己立个 Flag, **哪怕只是在留言区打卡你的学习天数,我相信都是会有效果的** 。3 个月后,我们一起再来验收。 +最后,你可以在留言区给自己立个 Flag,**哪怕只是在留言区打卡你的学习天数,我相信都是会有效果的** 。3 个月后,我们一起再来验收。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25401\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25401\350\256\262.md" index 2e2f5b188..0f37444e2 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25401\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25401\350\256\262.md" @@ -14,11 +14,11 @@ 所以,相同的错误重复在犯,相同的状况也是重复出现。 -其实,性能问题并没有你想像得那么难, **只要你理解了应用程序和系统的少数几个基本原理,再进行大量的实战练习,建立起整体性能的全局观** ,大多数性能问题的优化就会水到渠成。 +其实,性能问题并没有你想像得那么难,**只要你理解了应用程序和系统的少数几个基本原理,再进行大量的实战练习,建立起整体性能的全局观**,大多数性能问题的优化就会水到渠成。 我见过很多工程师,在分析应用程序所使用的第三方组件的性能时,并不熟悉这些组件所用的编程语言,却依然可以分析出线上问题的根源,并能通过一些方法进行优化,比如修改应用程序对它们的调用逻辑,或者调整组件的配置选项等。 -还是那句话, **你不需要了解每个组件的所有实现细节** ,只要能理解它们最基本的工作原理和协作方式,你也可以做到。 +还是那句话,**你不需要了解每个组件的所有实现细节**,只要能理解它们最基本的工作原理和协作方式,你也可以做到。 ## 性能指标是什么? @@ -28,9 +28,9 @@ ![img](assets/920601da775da08844d231bc2b4c301d.png) -我们知道,随着应用负载的增加,系统资源的使用也会升高,甚至达到极限。而 **性能问题的本质** ,就是系统资源已经达到瓶颈,但请求的处理却还不够快,无法支撑更多的请求。 +我们知道,随着应用负载的增加,系统资源的使用也会升高,甚至达到极限。而 **性能问题的本质**,就是系统资源已经达到瓶颈,但请求的处理却还不够快,无法支撑更多的请求。 -性能分析,其实就是 **找出应用或系统的瓶颈,并设法去避免或者缓解它们** ,从而更高效地利用系统资源处理更多的请求。这包含了一系列的步骤,比如下面这六个步骤。 +性能分析,其实就是 **找出应用或系统的瓶颈,并设法去避免或者缓解它们**,从而更高效地利用系统资源处理更多的请求。这包含了一系列的步骤,比如下面这六个步骤。 - 选择指标评估应用程序和系统的性能; - 为应用程序和系统设置性能目标; @@ -45,7 +45,7 @@ 首先你要明白,我们这个专栏的核心是性能的分析和优化,而不是最基本的 Linux 操作系统的使用方法。 -因而,我希望你最好用过 Ubuntu 或其他 Linux 操作系统,然后要具备一些 **编程基础** ,比如: +因而,我希望你最好用过 Ubuntu 或其他 Linux 操作系统,然后要具备一些 **编程基础**,比如: - 了解 Linux 常用命令的使用方法; - 知道怎么安装和管理软件包; @@ -57,7 +57,7 @@ ## 学习的重点是什么? -想要学习好性能分析和优化, **建立整体系统性能的全局观** 是最核心的话题。因而, +想要学习好性能分析和优化,**建立整体系统性能的全局观** 是最核心的话题。因而, - 理解最基本的几个系统知识原理; - 掌握必要的性能工具; @@ -77,7 +77,7 @@ 另外,我还要特别强调一点,就是 **性能工具的选用** 。有句话是这么说的,一个正确的选择胜过千百次的努力。虽然夸张了些,但是选用合适的性能工具,确实可以大大简化整个性能优化过程。在什么场景选用什么样的工具、以及怎么学会选择合适工具,都是我想教给你的东西。 -但是切记, **千万不要把性能工具当成学习的全部** 。工具只是解决问题的手段,关键在于你的用法。只有真正理解了它们背后的原理,并且结合具体场景,融会贯通系统的不同组件,你才能真正掌握它们。 +但是切记,**千万不要把性能工具当成学习的全部** 。工具只是解决问题的手段,关键在于你的用法。只有真正理解了它们背后的原理,并且结合具体场景,融会贯通系统的不同组件,你才能真正掌握它们。 最后,为了让你对性能有个全面的认识,我画了一张思维导图,里面涵盖了大部分性能分析和优化都会包含的知识,专栏中也基本都会讲到。你可以保存或者打印下来,每学会一部分就标记出来,记录并把握自己的学习进度。 @@ -85,7 +85,7 @@ ## 怎么学更高效? -前面我给你讲了 Linux 性能优化的学习重点,接下来我再跟你分享一下,我的几个学习技巧。掌握这些技巧,可以让你学得更轻松。 **技巧一:虽然系统的原理很重要,但在刚开始一定不要试图抓住所有的实现细节。** +前面我给你讲了 Linux 性能优化的学习重点,接下来我再跟你分享一下,我的几个学习技巧。掌握这些技巧,可以让你学得更轻松。**技巧一:虽然系统的原理很重要,但在刚开始一定不要试图抓住所有的实现细节。** 深陷到系统实现的内部,可能会让你丢掉学习的重点,而且繁杂的实现逻辑,很可能会打退你学习的积极性。所以,我个人观点是一定要适度。 @@ -97,7 +97,7 @@ **技巧二:边学边实践,通过大量的案例演习掌握 Linux 性能的分析和优化。** 只有通过在机器上练习,把我讲的知识和案例自己过一遍,这些东西才能转化成你的。我精心设计这些案例,正是为了让你有更好的学习理解和操作体验。 -所以我强烈推荐你去实际运行、分析这些案例,或者用学到的知识去分析你自己的系统,这样你会有更直观的感受,获得更好的学习效果。 **技巧三:勤思考,多反思,善总结,多问为什么。** +所以我强烈推荐你去实际运行、分析这些案例,或者用学到的知识去分析你自己的系统,这样你会有更直观的感受,获得更好的学习效果。**技巧三:勤思考,多反思,善总结,多问为什么。** 想真正学懂一门知识,最好的方法就是问问题。当你能提出好的问题时,就说明你已经深入了解了它。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25402\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25402\350\256\262.md" index 6a14c6dce..3817f8712 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25402\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25402\350\256\262.md" @@ -21,7 +21,7 @@ up 2 days, 20:14 // 系统运行时间 而最后三个数字呢,依次则是过去 1 分钟、5 分钟、15 分钟的平均负载(Load Average)。 -**平均负载** ?这个词对很多人来说,可能既熟悉又陌生,我们每天的工作中,也都会提到这个词,但你真正理解它背后的含义吗?如果你们团队来了一个实习生,他揪住你不放,你能给他讲清楚什么是平均负载吗? +**平均负载**?这个词对很多人来说,可能既熟悉又陌生,我们每天的工作中,也都会提到这个词,但你真正理解它背后的含义吗?如果你们团队来了一个实习生,他揪住你不放,你能给他讲清楚什么是平均负载吗? 其实,6 年前,我就遇到过这样的一个场景。公司一个实习生一直追问我,什么是平均负载,我支支吾吾半天,最后也没能解释明白。明明总看到也总会用到,怎么就说不明白呢?后来我静下来想想,其实还是自己的功底不够。 @@ -31,7 +31,7 @@ up 2 days, 20:14 // 系统运行时间 我猜一定有人会说,平均负载不就是单位时间内的 CPU 使用率吗?上面的 0.63,就代表 CPU 使用率是 63%。其实并不是这样,如果你方便的话,可以通过执行 man uptime 命令,来了解平均负载的详细解释。 -简单来说,平均负载是指单位时间内,系统处于 **可运行状态** 和 **不可中断状态** 的平均进程数,也就是 **平均活跃进程数** ,它和 CPU 使用率并没有直接关系。这里我先解释下,可运行状态和不可中断状态这俩词儿。 +简单来说,平均负载是指单位时间内,系统处于 **可运行状态** 和 **不可中断状态** 的平均进程数,也就是 **平均活跃进程数**,它和 CPU 使用率并没有直接关系。这里我先解释下,可运行状态和不可中断状态这俩词儿。 所谓可运行状态的进程,是指正在使用 CPU 或者正在等待 CPU 的进程,也就是我们常用 ps 命令看到的,处于 R 状态(Running 或 Runnable)的进程。 @@ -53,7 +53,7 @@ up 2 days, 20:14 // 系统运行时间 讲完了什么是平均负载,现在我们再回到最开始的例子,不知道你能否判断出,在 uptime 命令的结果里,那三个时间段的平均负载数,多大的时候能说明系统负载高?或是多小的时候就能说明系统负载很低呢? -我们知道,平均负载最理想的情况是等于 CPU 个数。所以在评判平均负载时, **首先你要知道系统有几个 CPU** ,这可以通过 top 命令或者从文件 /proc/cpuinfo 中读取,比如: +我们知道,平均负载最理想的情况是等于 CPU 个数。所以在评判平均负载时,**首先你要知道系统有几个 CPU**,这可以通过 top 命令或者从文件 /proc/cpuinfo 中读取,比如: ```bash # 关于 grep 和 wc 的用法请查询它们的手册或者网络搜索 @@ -79,7 +79,7 @@ up 2 days, 20:14 // 系统运行时间 那么,在实际生产环境中,平均负载多高时,需要我们重点关注呢? -在我看来, **当平均负载高于 CPU 数量 70% 的时候** ,你就应该分析排查负载高的问题了。一旦负载过高,就可能导致进程响应变慢,进而影响服务的正常功能。 +在我看来,**当平均负载高于 CPU 数量 70% 的时候**,你就应该分析排查负载高的问题了。一旦负载过高,就可能导致进程响应变慢,进而影响服务的正常功能。 但 70% 这个数字并不是绝对的,最推荐的方法,还是把系统的平均负载监控起来,然后根据更多的历史数据,判断负载的变化趋势。当发现负载有明显升高趋势时,比如说负载翻倍了,你再去做分析和调查。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25403\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25403\350\256\262.md" index 9465cdfdf..8b082fc24 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25403\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25403\350\256\262.md" @@ -54,7 +54,7 @@ CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着 - 进程上下文切换,是指从一个进程切换到另一个进程运行。 - 而系统调用过程中一直是同一个进程在运行。 -所以, **系统调用过程通常称为特权模式切换,而不是上下文切换** 。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。 +所以,**系统调用过程通常称为特权模式切换,而不是上下文切换** 。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。 那么,进程上下文切换跟系统调用又有什么区别呢? @@ -94,7 +94,7 @@ CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着 说完了进程的上下文切换,我们再来看看线程相关的问题。 -线程与进程最大的区别在于, **线程是调度的基本单位,而进程则是资源拥有的基本单位** 。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。所以,对于线程和进程,我们可以这么理解: +线程与进程最大的区别在于,**线程是调度的基本单位,而进程则是资源拥有的基本单位** 。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。所以,对于线程和进程,我们可以这么理解: - 当进程只有一个线程时,可以认为进程就等于线程。 - 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。 @@ -112,9 +112,9 @@ CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着 除了前面两种上下文切换,还有一个场景也会切换 CPU 上下文,那就是中断。 -为了快速响应硬件的事件, **中断处理会打断进程的正常调度和执行** ,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。 +为了快速响应硬件的事件,**中断处理会打断进程的正常调度和执行**,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。 -跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。 **对同一个 CPU 来说,中断处理比进程拥有更高的优先级** ,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。 +跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。**对同一个 CPU 来说,中断处理比进程拥有更高的优先级**,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。 另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25404\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25404\350\256\262.md" index fbd7e1ce4..4cd60968e 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25404\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25404\350\256\262.md" @@ -189,7 +189,7 @@ RES: 2450431 5279697 Rescheduling interrupts 通过这个案例,你应该也发现了多工具、多方面指标对比观测的好处。如果最开始时,我们只用了 pidstat 观测,这些很严重的上下文切换线程,压根儿就发现不了了。 -现在再回到最初的问题,每秒上下文切换多少次才算正常呢? **这个数值其实取决于系统本身的 CPU 性能** 。在我看来,如果系统的上下文切换次数比较稳定,那么从数百到一万以内,都应该算是正常的。但当上下文切换次数超过一万次,或者切换次数出现数量级的增长时,就很可能已经出现了性能问题。 +现在再回到最初的问题,每秒上下文切换多少次才算正常呢?**这个数值其实取决于系统本身的 CPU 性能** 。在我看来,如果系统的上下文切换次数比较稳定,那么从数百到一万以内,都应该算是正常的。但当上下文切换次数超过一万次,或者切换次数出现数量级的增长时,就很可能已经出现了性能问题。 这时,你还需要根据上下文切换的类型,再做具体分析。比方说: @@ -199,7 +199,7 @@ RES: 2450431 5279697 Rescheduling interrupts ## 小结 -今天,我通过一个 sysbench 的案例,给你讲了上下文切换问题的分析思路。碰到上下文切换次数过多的问题时, **我们可以借助 vmstat 、 pidstat 和 /proc/interrupts 等工具** ,来辅助排查性能问题的根源。 +今天,我通过一个 sysbench 的案例,给你讲了上下文切换问题的分析思路。碰到上下文切换次数过多的问题时,**我们可以借助 vmstat 、 pidstat 和 /proc/interrupts 等工具**,来辅助排查性能问题的根源。 ## 思考 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25405\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25405\350\256\262.md" index 0691d65a4..fe824ddab 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25405\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25405\350\256\262.md" @@ -48,7 +48,7 @@ cpu1 135834 3226 109383 86476907 31525 0 282 0 0 0 - guest(通常缩写为 guest),代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。 - guest_nice(通常缩写为 gnice),代表以低优先级运行虚拟机的时间。 -而我们通常所说的 **CPU 使用率,就是除了空闲时间外的其他时间占总 CPU 时间的百分比** ,用公式来表示就是: +而我们通常所说的 **CPU 使用率,就是除了空闲时间外的其他时间占总 CPU 时间的百分比**,用公式来表示就是: ![img](assets/3edcc7f908c7c1ddba4bbcccc0277c09.png)根据这个公式,我们就可以从 /proc/stat 中的数据,很容易地计算出 CPU 使用率。当然,也可以用每一个场景的 CPU 时间,除以总的 CPU 时间,计算出每个场景的 CPU 使用率。 @@ -68,7 +68,7 @@ cpu1 135834 3226 109383 86476907 31525 0 282 0 0 0 回过头来看,是不是说要查看 CPU 使用率,就必须先读取 /proc/stat 和 /proc/\[pid\]/stat 这两个文件,然后再按照上面的公式计算出来呢? -当然不是,各种各样的性能分析工具已经帮我们计算好了。不过要注意的是, **性能分析工具给出的都是间隔一段时间的平均 CPU 使用率,所以要注意间隔时间的设置** ,特别是用多个工具对比分析时,你一定要保证它们用的是相同的间隔时间。 +当然不是,各种各样的性能分析工具已经帮我们计算好了。不过要注意的是,**性能分析工具给出的都是间隔一段时间的平均 CPU 使用率,所以要注意间隔时间的设置**,特别是用多个工具对比分析时,你一定要保证它们用的是相同的间隔时间。 比如,对比一下 top 和 ps 这两个工具报告的 CPU 使用率,默认的结果很可能不一样,因为 top 默认使用 3 秒时间间隔,而 ps 使用的却是进程的整个生命周期。 @@ -149,7 +149,7 @@ Overhead Shared Object Symbol 输出结果中,第一行包含三个数据,分别是采样数(Samples)、事件类型(event)和事件总数量(Event count)。比如这个例子中,perf 总共采集了 833 个 CPU 时钟事件,而总事件数则为 97742399。 -另外, **采样数需要我们特别注意** 。如果采样数过少(比如只有十几个),那下面的排序和百分比就没什么实际参考价值了。 +另外,**采样数需要我们特别注意** 。如果采样数过少(比如只有十几个),那下面的排序和百分比就没什么实际参考价值了。 再往下看是一个表格式样的数据,每一行包含四列,分别是: diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25406\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25406\350\256\262.md" index e5df1c53a..efc000cec 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25406\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25406\350\256\262.md" @@ -6,7 +6,7 @@ 那是不是所有 CPU 使用率高的问题,都可以这么分析呢?我想,你的答案应该是否定的。 -回顾前面的内容,我们知道,系统的 CPU 使用率,不仅包括进程用户态和内核态的运行,还包括中断处理、等待 I/O 以及内核线程等。所以, **当你发现系统的 CPU 使用率很高的时候,不一定能找到相对应的高 CPU 使用率的进程** 。 +回顾前面的内容,我们知道,系统的 CPU 使用率,不仅包括进程用户态和内核态的运行,还包括中断处理、等待 I/O 以及内核线程等。所以,**当你发现系统的 CPU 使用率很高的时候,不一定能找到相对应的高 CPU 使用率的进程** 。 今天,我就用一个 Nginx + PHP 的 Web 服务的案例,带你来分析这种情况。 @@ -35,7 +35,7 @@ 走到这一步,准备工作就完成了。接下来,我们正式进入操作环节。 -> 温馨提示:案例中 PHP 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,我依旧建议, **操作之前别看源码** ,避免先入为主,而要把它当成一个黑盒来分析。这样,你可以更好把握,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用,以及瓶颈在应用中大概的位置。 +> 温馨提示:案例中 PHP 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,我依旧建议,**操作之前别看源码**,避免先入为主,而要把它当成一个黑盒来分析。这样,你可以更好把握,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用,以及瓶颈在应用中大概的位置。 ### 操作和分析 @@ -175,7 +175,7 @@ $ pidstat -p 24344 16:14:55 UID PID %usr %system %guest %wait %CPU CPU Command ``` -奇怪,居然没有任何输出。难道是 pidstat 命令出问题了吗?之前我说过, **在怀疑性能工具出问题前,最好还是先用其他工具交叉确认一下** 。那用什么工具呢? ps 应该是最简单易用的。我们在终端里运行下面的命令,看看 24344 进程的状态: +奇怪,居然没有任何输出。难道是 pidstat 命令出问题了吗?之前我说过,**在怀疑性能工具出问题前,最好还是先用其他工具交叉确认一下** 。那用什么工具呢? ps 应该是最简单易用的。我们在终端里运行下面的命令,看看 24344 进程的状态: ```bash # 从所有进程中查找 PID 是 24344 的进程 @@ -329,8 +329,8 @@ execsnoop 所用的 ftrace 是一种常用的动态追踪技术,一般用于 碰到常规问题无法解释的 CPU 使用率情况时,首先要想到有可能是短时应用导致的问题,比如有可能是下面这两种情况。 -- 第一, **应用里直接调用了其他二进制程序,这些程序通常运行时间比较短,通过 top 等工具也不容易发现** 。 -- 第二, **应用本身在不停地崩溃重启,而启动过程的资源初始化,很可能会占用相当多的 CPU** 。 +- 第一,**应用里直接调用了其他二进制程序,这些程序通常运行时间比较短,通过 top 等工具也不容易发现** 。 +- 第二,**应用本身在不停地崩溃重启,而启动过程的资源初始化,很可能会占用相当多的 CPU** 。 对于这类进程,我们可以用 pstree 或者 execsnoop 找到它们的父进程,再从父进程所在的应用入手,排查问题的根源。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25407\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25407\350\256\262.md" index 0a9e17629..4468854e4 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25407\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25407\350\256\262.md" @@ -41,13 +41,13 @@ $ top 当然了,上面的示例并没有包括进程的所有状态。除了以上 5 个状态,进程还包括下面这 2 个状态。 -第一个是 **T 或者 t** ,也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。 +第一个是 **T 或者 t**,也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。 向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要你用 fg 命令,恢复到前台运行)。 而当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行。 -另一个是 **X** ,也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。 +另一个是 **X**,也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。 了解了这些,我们再回到今天的主题。先看不可中断状态,这其实是为了保证进程数据与硬件状态一致,并且正常情况下,不可中断状态在很短时间内就会结束。所以,短时的不可中断状态进程,我们一般可以忽略。 @@ -101,7 +101,7 @@ root 4288 0.6 0.4 37280 33668 pts/0 D+ 05:54 0:00 /app 从这个界面,我们可以发现多个 app 进程已经启动,并且它们的状态分别是 Ss+ 和 D+。其中,S 表示可中断睡眠状态,D 表示不可中断睡眠状态,我们在前面刚学过,那后面的 s 和 + 是什么意思呢?不知道也没关系,查一下 man ps 就可以。现在记住,s 表示这个进程是一个会话的领导进程,而 + 表示前台进程组。 -这里又出现了两个新概念, **进程组** 和 **会话** 。它们用来管理一组相互关联的进程,意思其实很好理解。 +这里又出现了两个新概念,**进程组** 和 **会话** 。它们用来管理一组相互关联的进程,意思其实很好理解。 - 进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员; - 而会话是指共享同一个控制终端的一个或多个进程组。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25408\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25408\350\256\262.md" index ad7b434ba..6e0bcb524 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25408\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25408\350\256\262.md" @@ -154,7 +154,7 @@ perf report 这个图里的 swapper 是内核中的调度进程,你可以先忽略掉。 -我们来看其他信息,你可以发现, app 的确在通过系统调用 sys_read() 读取数据。并且从 new_sync_read 和 blkdev_direct_IO 能看出,进程正在对磁盘进行 **直接读** ,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。 +我们来看其他信息,你可以发现, app 的确在通过系统调用 sys_read() 读取数据。并且从 new_sync_read 和 blkdev_direct_IO 能看出,进程正在对磁盘进行 **直接读**,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。 看来,罪魁祸首是 app 内部进行了磁盘的直接 I/O 啊! @@ -197,7 +197,7 @@ Tasks: 137 total, 1 running, 72 sleeping, 0 stopped, 12 zombie ## 僵尸进程 -接下来,我们就来处理僵尸进程的问题。既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,要解决掉它们,就要找到它们的根儿, **也就是找出父进程,然后在父进程里解决。** 父进程的找法我们前面讲过,最简单的就是运行 pstree 命令: +接下来,我们就来处理僵尸进程的问题。既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,要解决掉它们,就要找到它们的根儿,**也就是找出父进程,然后在父进程里解决。** 父进程的找法我们前面讲过,最简单的就是运行 pstree 命令: ```bash # -a 表示输出命令行选项 @@ -264,7 +264,7 @@ Tasks: 125 total, 1 running, 72 sleeping, 0 stopped, 0 zombie 今天我用一个多进程的案例,带你分析系统等待 I/O 的 CPU 使用率(也就是 iowait%)升高的情况。 -虽然这个案例是磁盘 I/O 导致了 iowait 升高,不过, **iowait 高不一定代表 I/O 有性能瓶颈。当系统中只有 I/O 类型的进程在运行时,iowait 也会很高,但实际上,磁盘的读写远没有达到性能瓶颈的程度** 。 +虽然这个案例是磁盘 I/O 导致了 iowait 升高,不过,**iowait 高不一定代表 I/O 有性能瓶颈。当系统中只有 I/O 类型的进程在运行时,iowait 也会很高,但实际上,磁盘的读写远没有达到性能瓶颈的程度** 。 因此,碰到 iowait 升高时,需要先用 dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,然后再找是哪些进程导致了 I/O。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25409\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25409\350\256\262.md" index 843e85ef8..645488bfc 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25409\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25409\350\256\262.md" @@ -20,9 +20,9 @@ 这里的“打电话”,其实就是一个中断。没接到电话的时候,你可以做其他的事情;只有接到了电话(也就是发生中断),你才要进行另一个动作:取外卖。 -这个例子你就可以发现, **中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力** 。 +这个例子你就可以发现,**中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力** 。 -由于中断处理程序会打断其他进程的运行,所以, **为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行** 。如果中断本身要做的事情不多,那么处理起来也不会有太大问题;但如果中断要处理的事情很多,中断服务程序就有可能要运行很长时间。 +由于中断处理程序会打断其他进程的运行,所以,**为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行** 。如果中断本身要做的事情不多,那么处理起来也不会有太大问题;但如果中断要处理的事情很多,中断服务程序就有可能要运行很长时间。 特别是,中断处理程序在响应中断时,还会临时关闭中断。这就会导致上一次中断处理完成之前,其他中断都不能响应,也就是说中断有可能会丢失。 @@ -36,7 +36,7 @@ 如果你弄清楚了“取外卖”的模式,那对系统的中断机制就很容易理解了。事实上,为了解决中断处理程序执行过长和中断丢失的问题,Linux 将中断处理过程分成了两个阶段,也就是 **上半部和下半部** : -- **上半部用来快速处理中断** ,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。 +- **上半部用来快速处理中断**,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。 - **下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行** 。 比如说前面取外卖的例子,上半部就是你接听电话,告诉配送员你已经知道了,其他事儿见面再说,然后电话就可以挂断了;下半部才是取外卖的动作,以及见面后商量发票处理的动作。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25411\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25411\350\256\262.md" index 843a18177..166e97704 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25411\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25411\350\256\262.md" @@ -12,7 +12,7 @@ 我们先来回顾下,描述 CPU 的性能指标都有哪些。你可以自己先找张纸,凭着记忆写一写;或者打开前面的文章,自己总结一下。 -首先, **最容易想到的应该是 CPU 使用率** ,这也是实际环境中最常见的一个性能指标。 +首先,**最容易想到的应该是 CPU 使用率**,这也是实际环境中最常见的一个性能指标。 CPU 使用率描述了非空闲时间占总 CPU 时间的百分比,根据 CPU 上运行任务的不同,又被分为用户 CPU、系统 CPU、等待 I/O CPU、软中断和硬中断等。 @@ -22,16 +22,16 @@ CPU 使用率描述了非空闲时间占总 CPU 时间的百分比,根据 CPU - 软中断和硬中断的 CPU 使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。 - 除了上面这些,还有在虚拟化环境中会用到的窃取 CPU 使用率(steal)和客户 CPU 使用率(guest),分别表示被其他虚拟机占用的 CPU 时间百分比,和运行客户虚拟机的 CPU 时间百分比。 -**第二个比较容易想到的,应该是平均负载(Load Average)** ,也就是系统的平均活跃进程数。它反应了系统的整体负载情况,主要包括三个数值,分别指过去 1 分钟、过去 5 分钟和过去 15 分钟的平均负载。 +**第二个比较容易想到的,应该是平均负载(Load Average)**,也就是系统的平均活跃进程数。它反应了系统的整体负载情况,主要包括三个数值,分别指过去 1 分钟、过去 5 分钟和过去 15 分钟的平均负载。 -理想情况下,平均负载等于逻辑 CPU 个数,这表示每个 CPU 都恰好被充分利用。如果平均负载大于逻辑 CPU 个数,就表示负载比较重了。 **第三个,也是在专栏学习前你估计不太会注意到的,进程上下文切换** ,包括: +理想情况下,平均负载等于逻辑 CPU 个数,这表示每个 CPU 都恰好被充分利用。如果平均负载大于逻辑 CPU 个数,就表示负载比较重了。**第三个,也是在专栏学习前你估计不太会注意到的,进程上下文切换**,包括: - 无法获取资源而导致的自愿上下文切换; - 被系统强制调度导致的非自愿上下文切换。 上下文切换,本身是保证 Linux 正常运行的一项核心功能。但过多的上下文切换,会将原本运行进程的 CPU 时间,消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,缩短进程真正运行的时间,成为性能瓶颈。 -除了上面几种, **还有一个指标,CPU 缓存的命中率** 。由于 CPU 发展的速度远快于内存的发展,CPU 的处理速度就比内存的访问速度快得多。这样,CPU 在访问内存的时候,免不了要等待内存的响应。为了协调这两者巨大的性能差距,CPU 缓存(通常是多级缓存)就出现了。 +除了上面几种,**还有一个指标,CPU 缓存的命中率** 。由于 CPU 发展的速度远快于内存的发展,CPU 的处理速度就比内存的访问速度快得多。这样,CPU 在访问内存的时候,免不了要等待内存的响应。为了协调这两者巨大的性能差距,CPU 缓存(通常是多级缓存)就出现了。 ![img](assets/aa08816b60e453b52b5fae5e63549e33.png) @@ -80,7 +80,7 @@ CPU 使用率描述了非空闲时间占总 CPU 时间的百分比,根据 CPU ![img](assets/596397e1d6335d2990f70427ad4b14ec.png) -下面,我们再来看第二个维度。 **第二个维度,从工具出发。也就是当你已经安装了某个工具后,要知道这个工具能提供哪些指标** 。 +下面,我们再来看第二个维度。**第二个维度,从工具出发。也就是当你已经安装了某个工具后,要知道这个工具能提供哪些指标** 。 这在实际环境特别是生产环境中也是非常重要的,因为很多情况下,你并没有权限安装新的工具包,只能最大化地利用好系统中已经安装好的工具,这就需要你对它们有足够的了解。 @@ -104,7 +104,7 @@ CPU 使用率描述了非空闲时间占总 CPU 时间的百分比,根据 CPU 那有没有什么方法,可以又快又准找出系统瓶颈呢?答案是肯定的。 -虽然 CPU 的性能指标比较多,但要知道,既然都是描述系统的 CPU 性能,它们就不会是完全孤立的,很多指标间都有一定的关联。 **想弄清楚性能指标的关联性,就要通晓每种性能指标的工作原理** 。这也是为什么我在介绍每个性能指标时,都要穿插讲解相关的系统原理,希望你能记住这一点。 +虽然 CPU 的性能指标比较多,但要知道,既然都是描述系统的 CPU 性能,它们就不会是完全孤立的,很多指标间都有一定的关联。**想弄清楚性能指标的关联性,就要通晓每种性能指标的工作原理** 。这也是为什么我在介绍每个性能指标时,都要穿插讲解相关的系统原理,希望你能记住这一点。 举个例子,用户 CPU 使用率高,我们应该去排查进程的用户态而不是内核态。因为用户 CPU 使用率反映的就是用户态的 CPU 使用情况,而内核态的 CPU 使用情况只会反映到系统 CPU 使用率上。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25412\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25412\350\256\262.md" index 0cabdaeb5..aeb50e6f7 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25412\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25412\350\256\262.md" @@ -20,7 +20,7 @@ 如果你可以轻松回答这三个问题,那么二话不说就可以开始优化。 -比如,在前面的不可中断进程案例中,通过性能分析,我们发现是因为一个进程的 **直接 I/O** ,导致了 iowait 高达 90%。那是不是用“ **直接 I/O 换成缓存 I/O** ”的方法,就可以立即优化了呢? +比如,在前面的不可中断进程案例中,通过性能分析,我们发现是因为一个进程的 **直接 I/O**,导致了 iowait 高达 90%。那是不是用“ **直接 I/O 换成缓存 I/O** ”的方法,就可以立即优化了呢? 按照上面讲的,你可以先自己思考下那三点。如果不能确定,我们一起来看看。 @@ -48,7 +48,7 @@ 先看第一步,性能的量化指标有很多,比如 CPU 使用率、应用程序的吞吐量、客户端请求的延迟等,都可以评估性能。那我们应该选择什么指标来评估呢? -我的建议是 **不要局限在单一维度的指标上** ,你至少要从应用程序和系统资源这两个维度,分别选择不同的指标。比如,以 Web 应用为例: +我的建议是 **不要局限在单一维度的指标上**,你至少要从应用程序和系统资源这两个维度,分别选择不同的指标。比如,以 Web 应用为例: - 应用程序的维度,我们可以用 **吞吐量和请求延迟** 来评估应用程序的性能。 - 系统资源的维度,我们可以用 **CPU 使用率** 来评估系统的 CPU 使用情况。 @@ -76,7 +76,7 @@ 再来看第二个问题,开篇词里我们就说过,系统性能总是牵一发而动全身,所以性能问题通常也不是独立存在的。那当多个性能问题同时发生的时候,应该先去优化哪一个呢? -在性能测试的领域,流传很广的一个说法是“二八原则”,也就是说 80% 的问题都是由 20% 的代码导致的。只要找出这 20% 的位置,你就可以优化 80% 的性能。所以,我想表达的是, **并不是所有的性能问题都值得优化** 。 +在性能测试的领域,流传很广的一个说法是“二八原则”,也就是说 80% 的问题都是由 20% 的代码导致的。只要找出这 20% 的位置,你就可以优化 80% 的性能。所以,我想表达的是,**并不是所有的性能问题都值得优化** 。 我的建议是,动手优化之前先动脑,先把所有这些性能问题给分析一遍,找出最重要的、可以最大程度提升性能的问题,从它开始优化。这样的好处是,不仅性能提升的收益最大,而且很可能其他问题都不用优化,就已经满足了性能要求。 @@ -96,7 +96,7 @@ 一般情况下,我们当然想选能最大提升性能的方法,这其实也是性能优化的目标。 -但要注意,现实情况要考虑的因素却没那么简单。最直观来说, **性能优化并非没有成本** 。性能优化通常会带来复杂度的提升,降低程序的可维护性,还可能在优化一个指标时,引发其他指标的异常。也就是说,很可能你优化了一个指标,另一个指标的性能却变差了。 +但要注意,现实情况要考虑的因素却没那么简单。最直观来说,**性能优化并非没有成本** 。性能优化通常会带来复杂度的提升,降低程序的可维护性,还可能在优化一个指标时,引发其他指标的异常。也就是说,很可能你优化了一个指标,另一个指标的性能却变差了。 一个很典型的例子是我将在网络部分讲到的 DPDK(Data Plane Development Kit)。DPDK 是一种优化网络处理速度的方法,它通过绕开内核网络协议栈的方法,提升网络的处理能力。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25413\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25413\350\256\262.md" index d35835ec7..ac5282fb7 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25413\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25413\350\256\262.md" @@ -136,7 +136,7 @@ Reading data from disk /dev/sdb with buffer size 67108864 and count 20 这个问题主要是说,在执行 vmstat 时,第一行数据跟其他行相比较,数值相差特别大。我相信不少同学都注意到了这个现象,这里我简单解释一下。 -首先还是要记住,我总强调的那句话, **在碰到直观上解释不了的现象时,要第一时间去查命令手册** 。 +首先还是要记住,我总强调的那句话,**在碰到直观上解释不了的现象时,要第一时间去查命令手册** 。 比如,运行 man vmstat 命令,你可以在手册中发现下面这句话: @@ -149,7 +149,7 @@ pling period of length delay. The process and memory reports are instantaneous 你看,这并不是什么不得了的事故,但如果我们不清楚这一点,很可能卡住我们的思维,阻止我们进一步的分析。这里我也不得不提一下,文档的重要作用。 -授之以鱼,不如授之以渔。我们专栏的学习核心,一定是教会你 **性能分析的原理和思路** ,性能工具只是我们的路径和手段。所以,在提到各种性能工具时,我并没有详细解释每个工具的各种命令行选项的作用,一方面是因为你很容易通过文档查到这些,另一方面就是不同版本、不同系统中,个别选项的含义可能并不相同。 +授之以鱼,不如授之以渔。我们专栏的学习核心,一定是教会你 **性能分析的原理和思路**,性能工具只是我们的路径和手段。所以,在提到各种性能工具时,我并没有详细解释每个工具的各种命令行选项的作用,一方面是因为你很容易通过文档查到这些,另一方面就是不同版本、不同系统中,个别选项的含义可能并不相同。 所以,不管因为哪个因素,自己 man 一下,一定是最快速并且最准确的方式。特别是,当你发现某些工具的输出不符合常识时,一定记住,第一时间查文档弄明白。实在读不懂文档的话,再上网去搜,或者在专栏里向我提问。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25414\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25414\350\256\262.md" index 3a944b310..91adc7792 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25414\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25414\350\256\262.md" @@ -24,7 +24,7 @@ Failed to open /opt/bitnami/php/lib/php/extensions/opcache.so, continuing withou 这个问题,其实也是在分析 Docker 容器应用时,我们经常碰到的一个问题,因为容器应用依赖的库都在镜像里面。 -针对这种情况,我总结了下面 **四个解决方法** 。 **第一个方法,在容器外面构建相同路径的依赖库** 。这种方法从原理上可行,但是我并不推荐,一方面是因为找出这些依赖库比较麻烦,更重要的是,构建这些路径,会污染容器主机的环境。 **第二个方法,在容器内部运行 perf** 。不过,这需要容器运行在特权模式下,但实际的应用程序往往只以普通容器的方式运行。所以,容器内部一般没有权限执行 perf 分析。 +针对这种情况,我总结了下面 **四个解决方法** 。**第一个方法,在容器外面构建相同路径的依赖库** 。这种方法从原理上可行,但是我并不推荐,一方面是因为找出这些依赖库比较麻烦,更重要的是,构建这些路径,会污染容器主机的环境。**第二个方法,在容器内部运行 perf** 。不过,这需要容器运行在特权模式下,但实际的应用程序往往只以普通容器的方式运行。所以,容器内部一般没有权限执行 perf 分析。 比方说,如果你在普通容器内部运行 perf record ,你将会看到下面这个错误提示: @@ -36,7 +36,7 @@ perf_event_open(..., 0) failed unexpectedly with error 1 (Operation not permitte 当然,其实你还可以通过配置 /proc/sys/kernel/perf_event_paranoid (比如改成 -1),来允许非特权用户执行 perf 事件分析。 -不过还是那句话,为了安全起见,这种方法我也不推荐。 **第三个方法,指定符号路径为容器文件系统的路径** 。比如对于第 05 讲的应用,你可以执行下面这个命令: +不过还是那句话,为了安全起见,这种方法我也不推荐。**第三个方法,指定符号路径为容器文件系统的路径** 。比如对于第 05 讲的应用,你可以执行下面这个命令: ```bash mkdir /tmp/foo @@ -49,7 +49,7 @@ perf_event_open(..., 0) failed unexpectedly with error 1 (Operation not permitte 不过这里要注意,bindfs 这个工具需要你额外安装。bindfs 的基本功能是实现目录绑定(类似于 mount --bind),这里需要你安装的是 1.13.10 版本(这也是它的最新发布版)。 -如果你安装的是旧版本,你可以到 [GitHub](https://github.com/mpartel/bindfs)上面下载源码,然后编译安装。 **第四个方法,在容器外面把分析纪录保存下来,再去容器里查看结果** 。这样,库和符号的路径也就都对了。 +如果你安装的是旧版本,你可以到 [GitHub](https://github.com/mpartel/bindfs)上面下载源码,然后编译安装。**第四个方法,在容器外面把分析纪录保存下来,再去容器里查看结果** 。这样,库和符号的路径也就都对了。 比如,你可以这么做。先运行 perf record -g -p \< pid>,执行一会儿(比如 15 秒)后,按 Ctrl+C 停止。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25415\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25415\350\256\262.md" index c20641ec7..d15d07120 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25415\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25415\350\256\262.md" @@ -32,7 +32,7 @@ Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并 页表实际上存储在 CPU 的内存管理单元 MMU 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。 -而当进程访问的虚拟地址在页表中查不到时,系统会产生一个 **缺页异常** ,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。 +而当进程访问的虚拟地址在页表中查不到时,系统会产生一个 **缺页异常**,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。 另外,我在 \[CPU 上下文切换的文章中\]曾经提到, TLB(Translation Lookaside Buffer,转译后备缓冲器)会影响 CPU 的内存访问性能,在这里其实就可以得到解释。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25416\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25416\350\256\262.md" index 64c986590..9ff6c5b4d 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25416\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25416\350\256\262.md" @@ -77,7 +77,7 @@ SUnreclaim %lu (since Linux 2.6.19) 通过这个文档,我们可以看到: -- Buffers 是对原始磁盘块的临时存储,也就是用来 **缓存磁盘的数据** ,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。 +- Buffers 是对原始磁盘块的临时存储,也就是用来 **缓存磁盘的数据**,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。 - Cached 是从磁盘读取文件的页缓存,也就是用来 **缓存从文件读取的数据** 。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。 - SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。 @@ -259,7 +259,7 @@ procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- 这样,我们就回答了案例开始前的两个问题。 -简单来说, **Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中** 。 +简单来说,**Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中** 。 ## 小结 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25417\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25417\350\256\262.md" index ddc9dd457..7ecaaff06 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25417\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25417\350\256\262.md" @@ -19,7 +19,7 @@ Buffer 和 Cache 分别缓存的是对磁盘和文件系统的读写数据。 在案例开始前,你应该习惯性地先问自己一个问题,你想要做成某件事情,结果应该怎么评估?比如说,我们想利用缓存来提升程序的运行效率,应该怎么评估这个效果呢?换句话说,有没有哪个指标可以衡量缓存使用的好坏呢? -我估计你已经想到了, **缓存的命中率** 。所谓缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。 **命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。** +我估计你已经想到了,**缓存的命中率** 。所谓缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。**命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。** 实际上,缓存是现在所有高并发系统必需的核心模块,主要作用就是把经常访问的数据(也就是热点数据),提前读入到内存中。这样,下次访问时就可以直接从内存读取数据,而不需要经过硬盘,从而加快应用程序的响应速度。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25419\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25419\350\256\262.md" index 8ed64d1d0..de0f6db29 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25419\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25419\350\256\262.md" @@ -66,10 +66,10 @@ kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。 -- 剩余内存小于 **页最小阈值** ,说明进程可用内存都耗尽了,只有内核才可以分配内存。 +- 剩余内存小于 **页最小阈值**,说明进程可用内存都耗尽了,只有内核才可以分配内存。 - 剩余内存落在 **页最小阈值** 和 **页低阈值** 中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。 - 剩余内存落在 **页低阈值** 和 **页高阈值** 中间,说明内存有一定压力,但还可以满足新内存请求。 -- 剩余内存大于 **页高阈值** ,说明剩余内存比较多,没有内存压力。 +- 剩余内存大于 **页高阈值**,说明剩余内存比较多,没有内存压力。 我们可以看到,一旦剩余内存小于页低阈值,就会触发内存的回收。这个页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下 : diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25421\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25421\350\256\262.md" index 1ba937b14..9c020ffa7 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25421\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25421\350\256\262.md" @@ -98,7 +98,7 @@ 举个最简单的例子,当你看到系统的剩余内存很低时,是不是就说明,进程一定不能申请分配新内存了呢?当然不是,因为进程可以使用的内存,除了剩余内存,还包括了可回收的缓存和缓冲区。 -所以, **为了迅速定位内存问题,我通常会先运行几个覆盖面比较大的性能工具,比如 free、top、vmstat、pidstat 等** 。 +所以,**为了迅速定位内存问题,我通常会先运行几个覆盖面比较大的性能工具,比如 free、top、vmstat、pidstat 等** 。 具体的分析思路主要有这几步。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25424\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25424\350\256\262.md" index 2349878d4..ae9b8e23b 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25424\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25424\350\256\262.md" @@ -59,7 +59,7 @@ VFS 内部又通过目录项、索引节点、逻辑块以及超级块等数据 最后一种架构,是把这些磁盘组合成一个网络存储集群,再通过 NFS、SMB、iSCSI 等网络存储协议,暴露给服务器使用。 -其实在 Linux 中, **磁盘实际上是作为一个块设备来管理的** ,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。 +其实在 Linux 中,**磁盘实际上是作为一个块设备来管理的**,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。 ## 通用块层 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25429\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25429\350\256\262.md" index e507d02ed..b0f9ea1e7 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25429\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25429\350\256\262.md" @@ -221,9 +221,9 @@ $ docker exec -it redis redis-cli config get 'append*' Redis 提供了两种数据持久化的方式,分别是快照和追加文件。 -**快照方式** ,会按照指定的时间间隔,生成数据的快照,并且保存到磁盘文件中。为了避免阻塞主进程,Redis 还会 fork 出一个子进程,来负责快照的保存。这种方式的性能好,无论是备份还是恢复,都比追加文件好很多。 +**快照方式**,会按照指定的时间间隔,生成数据的快照,并且保存到磁盘文件中。为了避免阻塞主进程,Redis 还会 fork 出一个子进程,来负责快照的保存。这种方式的性能好,无论是备份还是恢复,都比追加文件好很多。 -不过,它的缺点也很明显。在数据量大时,fork 子进程需要用到比较大的内存,保存数据也很耗时。所以,你需要设置一个比较长的时间间隔来应对,比如至少 5 分钟。这样,如果发生故障,你丢失的就是几分钟的数据。 **追加文件** ,则是用在文件末尾追加记录的方式,对 Redis 写入的数据,依次进行持久化,所以它的持久化也更安全。 +不过,它的缺点也很明显。在数据量大时,fork 子进程需要用到比较大的内存,保存数据也很耗时。所以,你需要设置一个比较长的时间间隔来应对,比如至少 5 分钟。这样,如果发生故障,你丢失的就是几分钟的数据。**追加文件**,则是用在文件末尾追加记录的方式,对 Redis 写入的数据,依次进行持久化,所以它的持久化也更安全。 此外,它还提供了一个用 appendfsync 选项设置 fsync 的策略,确保写入的数据都落到磁盘中,具体选项包括 always、everysec、no 等。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25430\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25430\350\256\262.md" index 40b5ace45..9ba8dbb88 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25430\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25430\350\256\262.md" @@ -26,7 +26,7 @@ 而且,如果你配置了 RAID,从文件系统看到的使用量跟实际磁盘的占用空间,也会因为 RAID 级别的不同而不一样。比方说,配置 RAID10 后,你从文件系统最多也只能看到所有磁盘容量的一半。 -除了数据本身的存储空间,还有一个 **容易忽略的是索引节点的使用情况,它也包括容量、使用量以及剩余量等三个指标** 。如果文件系统中存储过多的小文件,就可能碰到索引节点容量已满的问题。 **其次,你应该想到的是前面多次提到过的缓存使用情况,包括页缓存、目录项缓存、索引节点缓存以及各个具体文件系统(如 ext4、XFS 等)的缓存** 。这些缓存会使用速度更快的内存,用来临时存储文件数据或者文件系统的元数据,从而可以减少访问慢速磁盘的次数。 +除了数据本身的存储空间,还有一个 **容易忽略的是索引节点的使用情况,它也包括容量、使用量以及剩余量等三个指标** 。如果文件系统中存储过多的小文件,就可能碰到索引节点容量已满的问题。**其次,你应该想到的是前面多次提到过的缓存使用情况,包括页缓存、目录项缓存、索引节点缓存以及各个具体文件系统(如 ext4、XFS 等)的缓存** 。这些缓存会使用速度更快的内存,用来临时存储文件数据或者文件系统的元数据,从而可以减少访问慢速磁盘的次数。 除了以上这两点,文件 I/O 也是很重要的性能指标,包括 IOPS(包括 r/s 和 w/s)、响应时间(延迟)以及吞吐量(B/s)等。在考察这类指标时,通常还要考虑实际文件的读写情况。比如,结合文件大小、文件数量、I/O 类型等,综合分析文件 I/O 的性能。 @@ -40,16 +40,16 @@ 在磁盘 I/O 原理的文章中,我曾提到过四个核心的磁盘 I/O 指标。 -- **使用率** ,是指磁盘忙处理 I/O 请求的百分比。过高的使用率(比如超过 60%)通常意味着磁盘 I/O 存在性能瓶颈。 +- **使用率**,是指磁盘忙处理 I/O 请求的百分比。过高的使用率(比如超过 60%)通常意味着磁盘 I/O 存在性能瓶颈。 - **IOPS** (Input/Output Per Second),是指每秒的 I/O 请求数。 -- **吞吐量** ,是指每秒的 I/O 请求大小。 -- **响应时间** ,是指从发出 I/O 请求到收到响应的间隔时间。 +- **吞吐量**,是指每秒的 I/O 请求大小。 +- **响应时间**,是指从发出 I/O 请求到收到响应的间隔时间。 考察这些指标时,一定要注意综合 I/O 的具体场景来分析,比如读写类型(顺序还是随机)、读写比例、读写大小、存储类型(有无 RAID 以及 RAID 级别、本地存储还是网络存储)等。 不过,这里有个大忌,就是把不同场景的 I/O 性能指标,直接进行分析对比。这是很常见的一个误区,你一定要避免。 -除了这些指标外,在前面 Cache 和 Buffer 原理的文章中,我曾多次提到, **缓冲区(Buffer)** 也是要重点掌握的指标,它经常出现在内存和磁盘问题的分析中。 +除了这些指标外,在前面 Cache 和 Buffer 原理的文章中,我曾多次提到,**缓冲区(Buffer)** 也是要重点掌握的指标,它经常出现在内存和磁盘问题的分析中。 文件系统和磁盘 I/O 的这些指标都很有用,需要我们熟练掌握,所以我总结成了一张图,帮你分类和记忆。你可以保存并打印出来,方便随时查看复习,也可以把它当成 I/O 性能分析的“指标筛选”清单使用。 @@ -92,7 +92,7 @@ ![img](assets/6f26fa18a73458764fcda00212006698.png) -下面,我们再来看第二个维度。 **第二个维度,从工具出发。也就是当你已经安装了某个工具后,要知道这个工具能提供哪些指标。** 这在实际环境中,特别是生产环境中也是非常重要的。因为很多情况下,你并没有权限安装新的工具包,只能最大化地利用好系统已有的工具,而这就需要你对它们有足够的了解。 +下面,我们再来看第二个维度。**第二个维度,从工具出发。也就是当你已经安装了某个工具后,要知道这个工具能提供哪些指标。** 这在实际环境中,特别是生产环境中也是非常重要的。因为很多情况下,你并没有权限安装新的工具包,只能最大化地利用好系统已有的工具,而这就需要你对它们有足够的了解。 具体到每个工具的使用方法,一般都支持丰富的配置选项。不过不用担心,这些配置选项并不用背下来。你只要知道有哪些工具,以及这些工具的基本功能是什么就够了。真正要用到的时候, 通过 man 命令,查它们的使用手册就可以了。 @@ -112,14 +112,14 @@ 那有没有什么方法,可以又快又准地找出系统的 I/O 瓶颈呢?答案是肯定的。 -还是那句话,找关联。多种性能指标间都有一定的关联性,不要完全孤立的看待他们。 **想弄清楚性能指标的关联性,就要通晓每种性能指标的工作原理** 。这也是为什么我在介绍每个性能指标时,都要穿插讲解相关的系统原理,再次希望你能记住这一点。 +还是那句话,找关联。多种性能指标间都有一定的关联性,不要完全孤立的看待他们。**想弄清楚性能指标的关联性,就要通晓每种性能指标的工作原理** 。这也是为什么我在介绍每个性能指标时,都要穿插讲解相关的系统原理,再次希望你能记住这一点。 以我们前面几期的案例为例,如果你仔细对比前面的几个案例,从 I/O 延迟的案例到 MySQL 和 Redis 的案例,就会发现,虽然这些问题千差万别,但从 I/O 角度来分析,最开始的分析思路基本上类似,都是: 1. 先用 iostat 发现磁盘 I/O 性能瓶颈; 1. 再借助 pidstat ,定位出导致瓶颈的进程; 1. 随后分析进程的 I/O 行为; -1. 最后,结合应用程序的原理,分析这些 I/O 的来源。 **所以,为了缩小排查范围,我通常会先运行那几个支持指标较多的工具,如 iostat、vmstat、pidstat 等。** 然后再根据观察到的现象,结合系统和应用程序的原理,寻找下一步的分析方向。我把这个过程画成了一张图,你可以保存下来参考使用。 +1. 最后,结合应用程序的原理,分析这些 I/O 的来源。**所以,为了缩小排查范围,我通常会先运行那几个支持指标较多的工具,如 iostat、vmstat、pidstat 等。** 然后再根据观察到的现象,结合系统和应用程序的原理,寻找下一步的分析方向。我把这个过程画成了一张图,你可以保存下来参考使用。 ![img](assets/1802a35475ee2755fb45aec55ed2d98a.png) diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25434\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25434\350\256\262.md" index dd2b1f3d4..b4b9731bf 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25434\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25434\350\256\262.md" @@ -12,14 +12,14 @@ 实际上,我们通常用带宽、吞吐量、延时、PPS(Packet Per Second)等指标衡量网络的性能。 -- **带宽** ,表示链路的最大传输速率,单位通常为 b/s (比特 / 秒)。 -- **吞吐量** ,表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽限制,而吞吐量 / 带宽,也就是该网络的使用率。 -- **延时** ,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。 -- **PPS** ,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。 +- **带宽**,表示链路的最大传输速率,单位通常为 b/s (比特 / 秒)。 +- **吞吐量**,表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽限制,而吞吐量 / 带宽,也就是该网络的使用率。 +- **延时**,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。 +- **PPS**,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。 -除了这些指标, **网络的可用性** (网络能否正常通信)、 **并发连接数** (TCP 连接数量)、 **丢包率** (丢包百分比)、 **重传率** (重新传输的网络包比例)等也是常用的性能指标。 +除了这些指标,**网络的可用性** (网络能否正常通信)、 **并发连接数** (TCP 连接数量)、 **丢包率** (丢包百分比)、 **重传率** (重新传输的网络包比例)等也是常用的性能指标。 -## 接下来,请你打开一个终端,SSH 登录到服务器上,然后跟我一起来探索、观测这些性能指标。 **网络配置** +## 接下来,请你打开一个终端,SSH 登录到服务器上,然后跟我一起来探索、观测这些性能指标。**网络配置** 分析网络问题的第一步,通常是查看网络接口的配置和状态。你可以使用 ifconfig 或者 ip 命令,来查看网络的配置。我个人更推荐使用 ip 工具,因为它提供了更丰富的功能和更易用的接口。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25435\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25435\350\256\262.md" index d4bbf4a5c..6b2878d2b 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25435\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25435\350\256\262.md" @@ -51,7 +51,7 @@ select 使用固定长度的位相量,表示文件描述符的集合,因此 除此之外,应用程序每次调用 select 和 poll 时,还需要把文件描述符的集合,从用户空间传入内核空间,由内核修改后,再传出到用户空间中。这一来一回的内核空间与用户空间切换,也增加了处理成本。 -有没有什么更好的方式来处理呢?答案自然是肯定的。 **第二种,使用非阻塞 I/O 和边缘触发通知,比如 epoll** 。 +有没有什么更好的方式来处理呢?答案自然是肯定的。**第二种,使用非阻塞 I/O 和边缘触发通知,比如 epoll** 。 既然 select 和 poll 有那么多的问题,就需要继续对其进行优化,而 epoll 就很好地解决了这些问题。 @@ -66,7 +66,7 @@ select 使用固定长度的位相量,表示文件描述符的集合,因此 ### 工作模型优化 -了解了 I/O 模型后,请求处理的优化就比较直观了。使用 I/O 多路复用后,就可以在一个进程或线程中处理多个请求,其中,又有下面两种不同的工作模型。 **第一种,主进程 + 多个 worker 子进程,这也是最常用的一种模型** 。这种方法的一个通用工作模式就是: +了解了 I/O 模型后,请求处理的优化就比较直观了。使用 I/O 多路复用后,就可以在一个进程或线程中处理多个请求,其中,又有下面两种不同的工作模型。**第一种,主进程 + 多个 worker 子进程,这也是最常用的一种模型** 。这种方法的一个通用工作模式就是: - 主进程执行 bind() + listen() 后,创建多个子进程; - 然后,在每个子进程中,都通过 accept() 或 epoll_wait() ,来处理相同的套接字。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25436\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25436\350\256\262.md" index 25aa7bb16..8f63d824c 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25436\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25436\350\256\262.md" @@ -27,13 +27,13 @@ I/O 模型的优化,是解决 C10K 问题的最佳良方。Linux 2.6 中引入 在评估网络性能前,我们先来回顾一下,衡量网络性能的指标。在 Linux 网络基础篇中,我们曾经说到,带宽、吞吐量、延时、PPS 等,都是最常用的网络性能指标。还记得它们的具体含义吗?你可以先思考一下,再继续下面的内容。 -首先, **带宽** ,表示链路的最大传输速率,单位是 b/s(比特 / 秒)。在你为服务器选购网卡时,带宽就是最核心的参考指标。常用的带宽有 1000M、10G、40G、100G 等。 +首先,**带宽**,表示链路的最大传输速率,单位是 b/s(比特 / 秒)。在你为服务器选购网卡时,带宽就是最核心的参考指标。常用的带宽有 1000M、10G、40G、100G 等。 -第二, **吞吐量** ,表示没有丢包时的最大数据传输速率,单位通常为 b/s (比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽的限制,吞吐量 / 带宽也就是该网络链路的使用率。 +第二,**吞吐量**,表示没有丢包时的最大数据传输速率,单位通常为 b/s (比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽的限制,吞吐量 / 带宽也就是该网络链路的使用率。 -第三, **延时** ,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。这个指标在不同场景中可能会有不同的含义。它可以表示建立连接需要的时间(比如 TCP 握手延时),或者一个数据包往返所需时间(比如 RTT)。 +第三,**延时**,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。这个指标在不同场景中可能会有不同的含义。它可以表示建立连接需要的时间(比如 TCP 握手延时),或者一个数据包往返所需时间(比如 RTT)。 -最后, **PPS** ,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,而基于 Linux 服务器的转发,很容易受到网络包大小的影响(交换机通常不会受到太大影响,即交换机可以线性转发)。 +最后,**PPS**,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,而基于 Linux 服务器的转发,很容易受到网络包大小的影响(交换机通常不会受到太大影响,即交换机可以线性转发)。 这四个指标中,带宽跟物理网卡配置是直接关联的。一般来说,网卡确定后,带宽也就确定了(当然,实际带宽会受限于整个网络链路中最小的那个模块)。 diff --git "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25438\350\256\262.md" "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25438\350\256\262.md" index bb0cbc685..54ff42399 100644 --- "a/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25438\350\256\262.md" +++ "b/docs/Linux/Linux \346\200\247\350\203\275\344\274\230\345\214\226/\347\254\25438\350\256\262.md" @@ -324,7 +324,7 @@ curl http://example.com 当你发现针对相同的网络服务,使用 IP 地址快而换成域名却慢很多时,就要想到,有可能是 DNS 在捣鬼。DNS 的解析,不仅包括从域名解析出 IP 地址的 A 记录请求,还包括性能工具帮你,“聪明”地从 IP 地址反查域名的 PTR 请求。 -实际上, **根据 IP 地址反查域名、根据端口号反查协议名称,是很多网络工具默认的行为,而这往往会导致性能工具的工作缓慢** 。所以,通常,网络性能工具都会提供一个选项(比如 -n 或者 -nn),来禁止名称解析。 +实际上,**根据 IP 地址反查域名、根据端口号反查协议名称,是很多网络工具默认的行为,而这往往会导致性能工具的工作缓慢** 。所以,通常,网络性能工具都会提供一个选项(比如 -n 或者 -nn),来禁止名称解析。 在工作中,当你碰到网络性能问题时,不要忘记 tcpdump 和 Wireshark 这两个大杀器。你可以用它们抓取实际传输的网络包,再排查是否有潜在的性能问题。 diff --git "a/docs/Linux/\345\256\271\345\231\250\345\256\236\346\210\230\351\253\230\346\211\213\350\257\276/\347\254\25427\350\256\262.md" "b/docs/Linux/\345\256\271\345\231\250\345\256\236\346\210\230\351\253\230\346\211\213\350\257\276/\347\254\25427\350\256\262.md" index 6b19b36fc..30b62c4d5 100644 --- "a/docs/Linux/\345\256\271\345\231\250\345\256\236\346\210\230\351\253\230\346\211\213\350\257\276/\347\254\25427\350\256\262.md" +++ "b/docs/Linux/\345\256\271\345\231\250\345\256\236\346\210\230\351\253\230\346\211\213\350\257\276/\347\254\25427\350\256\262.md" @@ -40,7 +40,7 @@ C 应该不会被回收,waitpid 仅等待直接 children 的状态变化。 为什么先进入僵尸状态而不是直接消失?觉得是留给父进程一次机会,查看子进程的 PID、终止状态(退出码、终止原因,比如是信号终止还是正常退出等)、资源使用信息。如果子进程直接消失,那么父进程没有机会掌握子进程的具体终止情况。 -一般情况下,程序逻辑可能会依据子进程的终止情况做出进一步处理:比如 Nginx Master 进程获知 Worker 进程异常退出,则重新拉起来一个 Worker 进程。 **第 4 讲** +一般情况下,程序逻辑可能会依据子进程的终止情况做出进一步处理:比如 Nginx Master 进程获知 Worker 进程异常退出,则重新拉起来一个 Worker 进程。**第 4 讲** Q:请你回顾一下基本概念中最后的这段代码,你可以想一想,在不做编译运行的情况下,它的输出是什么? @@ -177,7 +177,7 @@ cat $CGROUP_CONTAINER_PATH/memory.limit_in_bytes **第 10 讲** Q:在一个有 Swap 分区的节点上用 Docker 启动一个容器,对它的 Memory Cgroup 控制组设置一个内存上限 N,并且将 memory.swappiness 设置为 0。这时,如果在容器中启动一个不断读写文件的程序,同时这个程序再申请 1/2N 的内存,请你判断一下,Swap 分区中会有数据写入吗? -A:Memory Cgroup 参数 memory.swappiness 起到局部控制的作用,因为已经设置了 memory.swappiness 参数,全局参数 swappiness 参数失效,那么容器里就不能使用 swap 了。 **第 11 讲** Q:在这一讲 OverlayFS 的例子的基础上,建立 2 个 lowerdir 的目录,并且在目录中建立相同文件名的文件,然后一起做一个 overlay mount,看看会发生什么? +A:Memory Cgroup 参数 memory.swappiness 起到局部控制的作用,因为已经设置了 memory.swappiness 参数,全局参数 swappiness 参数失效,那么容器里就不能使用 swap 了。**第 11 讲** Q:在这一讲 OverlayFS 的例子的基础上,建立 2 个 lowerdir 的目录,并且在目录中建立相同文件名的文件,然后一起做一个 overlay mount,看看会发生什么? A:这里引用上邪忘川同学的实验结果。 @@ -235,9 +235,9 @@ Dir: /tmp/xfs_prjquota projectid is 101 在 Cgroup V1 的环境里,我们在 blkio Cgroup V1 的例子基础上,把 fio 中“-direct=1”参数去除之后,再运行 fio,同时运行 iostat 查看实际写入磁盘的速率,确认 Cgroup V1 blkio 无法对 Buffered I/O 限速。 -A: 这是通过 iostat 看到磁盘的写入速率,是可以突破 cgroup V1 blkio 中的限制值的。 **第 17 讲** Q:在这节课的最后,我提到“由于 ipvlan/macvlan 网络接口直接挂载在物理网络接口上,对于需要使用 iptables 规则的容器,比如 Kubernetes 里使用 service 的容器,就不能工作了”,请你思考一下这个判断背后的具体原因。 +A: 这是通过 iostat 看到磁盘的写入速率,是可以突破 cgroup V1 blkio 中的限制值的。**第 17 讲** Q:在这节课的最后,我提到“由于 ipvlan/macvlan 网络接口直接挂载在物理网络接口上,对于需要使用 iptables 规则的容器,比如 Kubernetes 里使用 service 的容器,就不能工作了”,请你思考一下这个判断背后的具体原因。 -A:ipvlan/macvlan 工作在网络 2 层,而 iptables 工作在网络 3 层。所以用 ipvlan/macvlan 为容器提供网络接口,那么基于 iptables 的 service 服务就不工作了。 **第 18 讲** +A:ipvlan/macvlan 工作在网络 2 层,而 iptables 工作在网络 3 层。所以用 ipvlan/macvlan 为容器提供网络接口,那么基于 iptables 的 service 服务就不工作了。**第 18 讲** Q:在这一讲中,我们提到了 Linux 内核中的 tcp\_force\_fast\_retransmit() 函数,那么你可以想想看,这个函数中的 tp->recording 和内核参数 /proc/sys/net/ipv4/tcp\_reordering 是什么关系?它们对数据包的重传会带来什么影响? @@ -264,7 +264,7 @@ A:可以阅读一下这篇文档。 专题加餐 ---- **专题 03** Q:我们讲 ftrace 实现机制时,说过内核中的“inline 函数”不能被 ftrace 到,你知道这是为什么吗?那么内核中的“static 函数”可以被 ftrace 追踪到吗? -A:inline 函数在编译的时候被展开了,所以不能被 ftrace 到。而 static 函数需要看情况,如果加了编译优化参数“-finline-functions-called-once”,对于只被调用到一次的 static 函数也会当成 inline 函数处理,那么也不能被 ftrace 追踪到了。 **专题 04** +A:inline 函数在编译的时候被展开了,所以不能被 ftrace 到。而 static 函数需要看情况,如果加了编译优化参数“-finline-functions-called-once”,对于只被调用到一次的 static 函数也会当成 inline 函数处理,那么也不能被 ftrace 追踪到了。**专题 04** Q:想想看,当我们用 kprobe 为一个内核函数注册了 probe 之后,怎样能看到对应内核函数的第一条指令被替换了呢? diff --git "a/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25401\350\256\262.md" "b/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25401\350\256\262.md" index 0cc1ce54f..c64928c7a 100644 --- "a/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25401\350\256\262.md" +++ "b/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25401\350\256\262.md" @@ -65,7 +65,7 @@ Docker 核心是一个操作系统级虚拟化方法,理解起来可能并不 每个用户实例之间相互隔离,互不影响。一般的硬件虚拟化方法给出的方法是 VM,而 LXC 给出的方法是 container,更细一点讲就是 kernel namespace。其中 pid、net、ipc、mnt、uts、user 等 namespace 将 container 的进程、网络、消息、文件系统、UTS(“UNIX Time-sharing System”)和用户空间隔离开。 -**pid namespace** 不同用户的进程就是通过 pid namespace 隔离开的,且不同 namespace 中可以有相同 pid。所有的 LXC 进程在 Docker中的父进程为 Docker 进程,每个 lxc 进程具有不同的 namespace。同时由于允许嵌套,因此可以很方便地实现 Docker in Docker。 **net namespace** 有了 pid namespace,每个 namespace 中的 pid 能够相互隔离,但是网络端口还是共享 host 的端口。网络隔离是通过 net namespace 实现的,每个 net namespace 有独立的 network devices,IP addresses,IP routing tables,/proc/net 目录。这样每个 container 的网络就能隔离开来。Docker 默认采用 veth 的方式将 container 中的虚拟网卡同 host 上的一个 docker bridge:docker0连接在一起。 **ipc namespace** container 中进程交互还是采用 linux 常见的进程间交互方法(interprocess communication - IPC),包括常见的信号量、消息队列和共享内存。然而同 VM 不同的是,container 的进程间交互实际上还是 host 上具有相同 pid namespace 中的进程间交互,因此需要在 IPC 资源申请时加入 namespace 信息——每个 IPC 资源有一个唯一的32位 ID。 **mnt namespace** 类似 chroot,将一个进程放到一个特定的目录执行。mnt namespace 允许不同 namespace 的进程看到的文件结构不同,这样每个 namespace 中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个 namespace 中的 container 在/proc/mounts 的信息只包含所在 namespace 的 mount point。 **uts namespace** UTS(“UNIX Time-sharing System”)namespace 允许每个 container 拥有独立的 hostname 和 domain name,使其在网络上可以被视作一个独立的节点而非 Host 上的一个进程。 **user namespace** +**pid namespace** 不同用户的进程就是通过 pid namespace 隔离开的,且不同 namespace 中可以有相同 pid。所有的 LXC 进程在 Docker中的父进程为 Docker 进程,每个 lxc 进程具有不同的 namespace。同时由于允许嵌套,因此可以很方便地实现 Docker in Docker。**net namespace** 有了 pid namespace,每个 namespace 中的 pid 能够相互隔离,但是网络端口还是共享 host 的端口。网络隔离是通过 net namespace 实现的,每个 net namespace 有独立的 network devices,IP addresses,IP routing tables,/proc/net 目录。这样每个 container 的网络就能隔离开来。Docker 默认采用 veth 的方式将 container 中的虚拟网卡同 host 上的一个 docker bridge:docker0连接在一起。**ipc namespace** container 中进程交互还是采用 linux 常见的进程间交互方法(interprocess communication - IPC),包括常见的信号量、消息队列和共享内存。然而同 VM 不同的是,container 的进程间交互实际上还是 host 上具有相同 pid namespace 中的进程间交互,因此需要在 IPC 资源申请时加入 namespace 信息——每个 IPC 资源有一个唯一的32位 ID。**mnt namespace** 类似 chroot,将一个进程放到一个特定的目录执行。mnt namespace 允许不同 namespace 的进程看到的文件结构不同,这样每个 namespace 中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个 namespace 中的 container 在/proc/mounts 的信息只包含所在 namespace 的 mount point。**uts namespace** UTS(“UNIX Time-sharing System”)namespace 允许每个 container 拥有独立的 hostname 和 domain name,使其在网络上可以被视作一个独立的节点而非 Host 上的一个进程。**user namespace** 每个 container 可以有不同的 user 和 group id,也就是说可以在 container 内部用 container 内部的用户执行程序而非 Host 上的用户。 diff --git "a/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25402\350\256\262.md" "b/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25402\350\256\262.md" index 62d76d7f5..855009937 100644 --- "a/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25402\350\256\262.md" +++ "b/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25402\350\256\262.md" @@ -450,7 +450,7 @@ Sockets: [fd://] 使用说明: -这个命令在开发者报告 Bug 时会非常有用,结合 docker vesion 一起,可以随时使用这个命令把本地的配置信息提供出来,方便 Docker 的开发者快速定位问题。 **version** 使用方法:docker version +这个命令在开发者报告 Bug 时会非常有用,结合 docker vesion 一起,可以随时使用这个命令把本地的配置信息提供出来,方便 Docker 的开发者快速定位问题。**version** 使用方法:docker version 使用说明: @@ -475,7 +475,7 @@ KiB Swap: 0 total, 0 used, 0 free. 565836 cached Mem 使用说明: -使用这个命令可以挂载正在后台运行的容器,在开发应用的过程中运用这个命令可以随时观察容器內进程的运行状况。开发者在开发应用的场景中,这个命令是一个非常有用的命令。 **build** 使用方法:docker build \[OPTIONS\] PATH | URL | - +使用这个命令可以挂载正在后台运行的容器,在开发应用的过程中运用这个命令可以随时观察容器內进程的运行状况。开发者在开发应用的场景中,这个命令是一个非常有用的命令。**build** 使用方法:docker build \[OPTIONS\] PATH | URL | - 例子: @@ -493,7 +493,7 @@ Successfully built 99cc1ad10469 使用说明: -这个命令是从源码构建新 Image 的命令。因为 Image 是分层的,最关键的 Base Image 是如何构建的是用户比较关心的,Docker 官方文档给出了构建方法,请参考[这里](https://docs.docker.com/engine/userguide/eng-image/baseimages/)。 **commit** 使用方法:docker commit \[OPTIONS\] CONTAINER \[REPOSITORY\[:TAG\]\] +这个命令是从源码构建新 Image 的命令。因为 Image 是分层的,最关键的 Base Image 是如何构建的是用户比较关心的,Docker 官方文档给出了构建方法,请参考[这里](https://docs.docker.com/engine/userguide/eng-image/baseimages/)。**commit** 使用方法:docker commit \[OPTIONS\] CONTAINER \[REPOSITORY\[:TAG\]\] 例子: @@ -511,11 +511,11 @@ SvenDowideit/testimage version3 f5283438590d 16 sec 使用说明: -这个命令的用处在于把有修改的 container 提交成新的 Image,然后导出此 Imange 分发给其他场景中调试使用。Docker 官方的建议是,当你在调试完 Image 的问题后,应该写一个新的 Dockerfile 文件来维护此 Image。commit 命令仅是一个临时创建 Imange 的辅助命令。 **cp** 使用方法: cp CONTAINER:PATH HOSTPATH +这个命令的用处在于把有修改的 container 提交成新的 Image,然后导出此 Imange 分发给其他场景中调试使用。Docker 官方的建议是,当你在调试完 Image 的问题后,应该写一个新的 Dockerfile 文件来维护此 Image。commit 命令仅是一个临时创建 Imange 的辅助命令。**cp** 使用方法: cp CONTAINER:PATH HOSTPATH 使用说明: -使用 cp 可以把容器內的文件复制到 Host 主机上。这个命令在开发者开发应用的场景下,会需要把运行程序产生的结果复制出来的需求,在这个情况下就可以使用这个 cp 命令。 **diff** 使用方法:docker diff CONTAINER +使用 cp 可以把容器內的文件复制到 Host 主机上。这个命令在开发者开发应用的场景下,会需要把运行程序产生的结果复制出来的需求,在这个情况下就可以使用这个 cp 命令。**diff** 使用方法:docker diff CONTAINER 例子: @@ -534,7 +534,7 @@ A /go/src/github.com/dotcloud 使用说明: -diff 会列出3种容器内文件状态变化(A - Add,D - Delete,C - Change)的列表清单。构建Image的过程中需要的调试指令。 **images** 使用方法:docker images \[OPTIONS\] \[NAME\] +diff 会列出3种容器内文件状态变化(A - Add,D - Delete,C - Change)的列表清单。构建Image的过程中需要的调试指令。**images** 使用方法:docker images \[OPTIONS\] \[NAME\] 例子: @@ -554,7 +554,7 @@ tryout latest 2629d1fa0b81 23 hours ago 131.5 MB 使用说明: -Docker Image 是多层结构的,默认只显示最顶层的 Image。不显示的中间层默认是为了增加可复用性、减少磁盘使用空间,加快 build 构建的速度的功能,一般用户不需要关心这个细节。 **export/ import / save / load** 使用方法: +Docker Image 是多层结构的,默认只显示最顶层的 Image。不显示的中间层默认是为了增加可复用性、减少磁盘使用空间,加快 build 构建的速度的功能,一般用户不需要关心这个细节。**export/ import / save / load** 使用方法: ```bash docker export red_panda > latest.tar @@ -565,7 +565,7 @@ docker load 使用说明: -这一组命令是系统运维里非常关键的命令。加载(两种方法:import,load),导出(一种方法:save,export)容器系统文件。 **inspect** 使用方法: +这一组命令是系统运维里非常关键的命令。加载(两种方法:import,load),导出(一种方法:save,export)容器系统文件。**inspect** 使用方法: ```bash docker inspect CONTAINER|IMAGE [CONTAINER|IMAGE...] @@ -579,7 +579,7 @@ docker inspect CONTAINER|IMAGE [CONTAINER|IMAGE...] 使用说明: -查看容器运行时详细信息的命令。了解一个 Image 或者 Container 的完整构建信息就可以通过这个命令实现。 **kill** 使用方法: +查看容器运行时详细信息的命令。了解一个 Image 或者 Container 的完整构建信息就可以通过这个命令实现。**kill** 使用方法: ```bash docker kill [OPTIONS] CONTAINER [CONTAINER...] @@ -587,7 +587,7 @@ docker kill [OPTIONS] CONTAINER [CONTAINER...] 使用说明: -杀掉容器的进程。 **port** 使用方法: +杀掉容器的进程。**port** 使用方法: ```bash docker port CONTAINER PRIVATE_PORT @@ -603,7 +603,7 @@ docker pause CONTAINER 使用说明: -使用 cgroup 的 freezer 顺序暂停、恢复容器里的所有进程。详细 freezer 的特性,请参考[官方文档](https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt)。 **ps** 使用方法: +使用 cgroup 的 freezer 顺序暂停、恢复容器里的所有进程。详细 freezer 的特性,请参考[官方文档](https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt)。**ps** 使用方法: ```bash docker ps [OPTIONS] @@ -620,7 +620,7 @@ d7886598dbe2 crosbymichael/redis:latest /redis-server --dir 33 minut 使用说明: -docker ps 打印出正在运行的容器,docker ps -a 打印出所有运行过的容器。 **rm** 使用方法: +docker ps 打印出正在运行的容器,docker ps -a 打印出所有运行过的容器。**rm** 使用方法: ```bash docker rm [OPTIONS] CONTAINER [CONTAINER...] @@ -635,7 +635,7 @@ docker rm [OPTIONS] CONTAINER [CONTAINER...] 使用说明: -删除指定的容器。 **rmi** 使用方法: +删除指定的容器。**rmi** 使用方法: ```bash docker rmi IMAGE [IMAGE...] @@ -666,7 +666,7 @@ Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8 使用说明: -指定删除 Image 文件。 **run** 使用方法: +指定删除 Image 文件。**run** 使用方法: ```bash docker run [OPTIONS] IMAGE [COMMAND] [ARG...] @@ -680,7 +680,7 @@ docker run [OPTIONS] IMAGE [COMMAND] [ARG...] 使用说明: -这个命令是核心命令,可以配置的子参数详细解释可以通过 docker run --help 列出。 **start / stop / restart** 使用方法: +这个命令是核心命令,可以配置的子参数详细解释可以通过 docker run --help 列出。**start / stop / restart** 使用方法: ```bash docker start CONTAINER [CONTAINER...] @@ -688,7 +688,7 @@ docker start CONTAINER [CONTAINER...] 使用说明: -这组命令可以开启(两个:start,restart),停止(一个:stop)一个容器。 **tag** 使用方法: +这组命令可以开启(两个:start,restart),停止(一个:stop)一个容器。**tag** 使用方法: ```bash docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG] @@ -696,7 +696,7 @@ docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG] 使用说明: -组合使用用户名,Image 名字,标签名来组织管理Image。 **top** 使用方法: +组合使用用户名,Image 名字,标签名来组织管理Image。**top** 使用方法: ```bash docker top CONTAINER [ps OPTIONS] @@ -704,7 +704,7 @@ docker top CONTAINER [ps OPTIONS] 使用说明: -显示容器內运行的进程。 **wait** 使用方法: +显示容器內运行的进程。**wait** 使用方法: ```bash docker wait CONTAINER [CONTAINER...] @@ -712,7 +712,7 @@ docker wait CONTAINER [CONTAINER...] 使用说明: -阻塞对指定容器的其他调用方法,直到容器停止后退出阻塞。 **rename** 使用方法: +阻塞对指定容器的其他调用方法,直到容器停止后退出阻塞。**rename** 使用方法: ```bash docker rename CONTAINER NEW_NAME @@ -720,7 +720,7 @@ docker rename CONTAINER NEW_NAME 使用说明: -重新命名一个容器。 **stats** 使用方法: +重新命名一个容器。**stats** 使用方法: ```bash docker stats [OPTIONS] [CONTAINER...] @@ -728,7 +728,7 @@ docker stats [OPTIONS] [CONTAINER...] 使用说明: -实时显示容器资源使用监控指标。 **update** 使用方法: +实时显示容器资源使用监控指标。**update** 使用方法: ```sql docker update [OPTIONS] CONTAINER [CONTAINER...] @@ -736,7 +736,7 @@ docker update [OPTIONS] CONTAINER [CONTAINER...] 使用说明: -更新一或多个容器实例的 IO、CPU、内存,启动策略参数。 **exec** 使用方法: +更新一或多个容器实例的 IO、CPU、内存,启动策略参数。**exec** 使用方法: ```bash docker exec [OPTIONS] CONTAINER COMMAND [ARG...] @@ -744,7 +744,7 @@ docker exec [OPTIONS] CONTAINER COMMAND [ARG...] 使用说明: -在运行中容器中运行命令。 **deploy** 使用方法: +在运行中容器中运行命令。**deploy** 使用方法: ```bash docker deploy [OPTIONS] STACK @@ -752,7 +752,7 @@ docker deploy [OPTIONS] STACK 使用说明: -部署新的 stack 文件,两种格式 DAB 格式和 Compose 格式,当前使用趋势来看,Compose 成为默认标准。 **create** 使用方法: +部署新的 stack 文件,两种格式 DAB 格式和 Compose 格式,当前使用趋势来看,Compose 成为默认标准。**create** 使用方法: ```bash docker create [OPTIONS] IMAGE [COMMAND] [ARG...] @@ -770,7 +770,7 @@ docker events [OPTIONS] 使用说明: -打印容器实时的系统事件。 **history** 使用方法: +打印容器实时的系统事件。**history** 使用方法: ```bash docker history [OPTIONS] IMAGE @@ -799,7 +799,7 @@ make gcc python-dev locales python-pip 338.3 MB 使用说明: -打印指定 Image 中每一层 Image 命令行的历史记录。 **logs** 使用方法: +打印指定 Image 中每一层 Image 命令行的历史记录。**logs** 使用方法: ```bash docker logs CONTAINER @@ -818,7 +818,7 @@ docker logout [SERVER] 使用说明: -登录登出 Docker Hub 服务。 **pull / push** 使用方法: +登录登出 Docker Hub 服务。**pull / push** 使用方法: ```bash docker push NAME[:TAG] @@ -826,7 +826,7 @@ docker push NAME[:TAG] 使用说明: -通过此命令分享 Image 到 Hub 服务或者自服务的 Registry 服务。 **search** +通过此命令分享 Image 到 Hub 服务或者自服务的 Registry 服务。**search** 使用方法: diff --git "a/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25403\350\256\262.md" "b/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25403\350\256\262.md" index e1d6fc442..f32f7dc81 100644 --- "a/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25403\350\256\262.md" +++ "b/docs/Linux/\346\267\261\345\205\245\346\265\205\345\207\272 Docker \346\212\200\346\234\257\346\240\210\345\256\236\350\267\265\350\257\276/\347\254\25403\350\256\262.md" @@ -20,7 +20,7 @@ **Base Image** 包括了操作系统命令行和类库的最小集合,一旦启用,所有应用都需要以它为基础创建应用镜像。Ubuntu 作为官方使用的默认版本,是目前最易用的版本,但系统没有经过优化,可以考虑使用第三方有划过的版本,比如 phusion-baseimage。 -对于选择 RHEL、CentOS 分支的 Base Image,提供安全框架 SELinux 的使用、块级存储文件系统 devicemapper 等技术,这些特性是不能和 Ubuntu 分支通用的。另外需要注意的是,使用的操作系统分支不同,其裁剪系统的方法也完全不同,所以大家在选择操作系统时一定要慎重。 **管理 Docker 应用配置工具的选择** 主要用于基于 Dockerfile 创建 Image 的配置管理。我们需要结合开发团队的现状,选择一款团队熟悉的工具作为通用工具。配置工具有很多种选择,其中 [Ansible](https://www.ansible.com/home) 作为后起之秀,在配置管理的使用中体验非常简单易用,推荐大家参考使用。 **Host 主机系统** +对于选择 RHEL、CentOS 分支的 Base Image,提供安全框架 SELinux 的使用、块级存储文件系统 devicemapper 等技术,这些特性是不能和 Ubuntu 分支通用的。另外需要注意的是,使用的操作系统分支不同,其裁剪系统的方法也完全不同,所以大家在选择操作系统时一定要慎重。**管理 Docker 应用配置工具的选择** 主要用于基于 Dockerfile 创建 Image 的配置管理。我们需要结合开发团队的现状,选择一款团队熟悉的工具作为通用工具。配置工具有很多种选择,其中 [Ansible](https://www.ansible.com/home) 作为后起之秀,在配置管理的使用中体验非常简单易用,推荐大家参考使用。**Host 主机系统** 是 Docker 后台进程的运行环境。从开发角度来看,它就是一台普通的单机 OS 系统,我们仅部署Docker 后台进程以及集群工具,所以希望 Host 主机系统的开销越小越好。这里推荐给大家的 Host 主机系统是 [CoreOS](https://coreos.com/),它是目前开销最小的主机系统。另外,还有红帽的开源 [Atomic](https://www.projectatomic.io/download/) 主机系统,有基于[Fedora](https://www.projectatomic.io/download/)、[CentOS](https://www.projectatomic.io/blog/2014/06/centos-atomic-host-sig-propposed/)、[RHEL](http://rhelblog.redhat.com/2014/07/10/going-atomic-with-the-red-hat-enterprise-linux-7-high-touch-beta/)多个版本的分支选择,也是不错的候选对象。 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25400\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25400\350\256\262.md" index 5c90b24d6..27b98a5b4 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25400\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25400\350\256\262.md" @@ -45,7 +45,7 @@ spec: 1.0.0-rc2-dev 我首先打开 Docker 的调试模式,查看详细日志,我根据调试日志去查找对应的 Docker 代码,发现是 dockerd 请求 containerd 无响应(这里你需要知道 Docker 组件构成和调用关系),然后发送 Linux`SIGUSR1`信号量(这里你需要知道 Linux 的一些信号量),打印 Golang 堆栈信息(这里你需要了解 Golang 语言)。最后结合内核 Cgroups 相关日志(这里你需要了解 Cgroups 的工作机制),才最终定位和解决问题。 -可以看到,排查一个看起来很简单的问题就需要用到非常多的知识, **首先需要理解 Docker 架构,需要阅读 Docker 源码,还得懂一些 Linux 内核问题才能完全定位并解决问题。** 相信大多数了解 Docker 的人都知道,Docker 是基于 Linux Kernel 的 Namespace 和 Cgroups 技术实现的,但究竟什么是 Namespace?什么是 Cgroups?容器是如何一步步创建的?很多人可能都难以回答。你可能在想,我不用理会这些,照样可以正常使用容器呀,但如果你要真正在生产环境中使用容器,你就会发现如果不了解容器的技术原理,生产环境中遇到的问题你很难轻松解决。所以, **仅仅掌握容器的一些皮毛是远远不够的,需要我们了解容器的底层技术实现,结合生产实践经验,才能让我们更好地向上攀登** 。 +可以看到,排查一个看起来很简单的问题就需要用到非常多的知识,**首先需要理解 Docker 架构,需要阅读 Docker 源码,还得懂一些 Linux 内核问题才能完全定位并解决问题。** 相信大多数了解 Docker 的人都知道,Docker 是基于 Linux Kernel 的 Namespace 和 Cgroups 技术实现的,但究竟什么是 Namespace?什么是 Cgroups?容器是如何一步步创建的?很多人可能都难以回答。你可能在想,我不用理会这些,照样可以正常使用容器呀,但如果你要真正在生产环境中使用容器,你就会发现如果不了解容器的技术原理,生产环境中遇到的问题你很难轻松解决。所以,**仅仅掌握容器的一些皮毛是远远不够的,需要我们了解容器的底层技术实现,结合生产实践经验,才能让我们更好地向上攀登** 。 当然,我知道每个人的基础都不一样,所以在一开始规划这个课程的时候,我就和拉勾教育的团队一起定义好了我们的核心目标,就是“由浅入深带你吃透 Docker”,希望让不同基础的人都能在这个课程中收获满满。 @@ -55,7 +55,7 @@ spec: 1.0.0-rc2-dev ![11.png](assets/Ciqc1F9YoBKAP5TpAAHqwwYYWWc486.png) -用一句话总结,我希望这个课程从 Docker **基础知识点** 到 **底层原理** ,再到 **编排实践** ,层层递进地展开介绍,最大程度帮你吸收和掌握 Docker 知识。 +用一句话总结,我希望这个课程从 Docker **基础知识点** 到 **底层原理**,再到 **编排实践**,层层递进地展开介绍,最大程度帮你吸收和掌握 Docker 知识。 - **模块一:基础概念与操作** diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25402\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25402\350\256\262.md" index 503b55bf7..df315c372 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25402\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25402\350\256\262.md" @@ -6,7 +6,7 @@ Docker 的操作围绕镜像、容器、仓库三大核心概念。在学架构 #### 镜像 -镜像是什么呢?通俗地讲,它是一个只读的文件和文件夹组合。它包含了容器运行时所需要的所有基础文件和配置信息,是容器启动的基础。所以你想启动一个容器,那首先必须要有一个镜像。 **镜像是 Docker 容器启动的先决条件。** 如果你想要使用一个镜像,你可以用这两种方式: +镜像是什么呢?通俗地讲,它是一个只读的文件和文件夹组合。它包含了容器运行时所需要的所有基础文件和配置信息,是容器启动的基础。所以你想启动一个容器,那首先必须要有一个镜像。**镜像是 Docker 容器启动的先决条件。** 如果你想要使用一个镜像,你可以用这两种方式: 1. 自己创建镜像。通常情况下,一个镜像是基于一个基础镜像构建的,你可以在基础镜像上添加一些用户自定义的内容。例如你可以基于`centos`镜像制作你自己的业务镜像,首先安装`nginx`服务,然后部署你的应用程序,最后做一些自定义配置,这样一个业务镜像就做好了。 1. 从功能镜像仓库拉取别人制作好的镜像。一些常用的软件或者系统都会有官方已经制作好的镜像,例如`nginx`、`ubuntu`、`centos`、`mysql`等,你可以到 [Docker Hub](https://hub.docker.com/) 搜索并下载它们。 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25404\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25404\350\256\262.md" index f6e8fc663..70c4f23de 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25404\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25404\350\256\262.md" @@ -89,7 +89,7 @@ PID USER TIME COMMAND 6 root 0:00 ps aux ``` -我们可以看到容器的 1 号进程为 sh 命令,在容器内部并不能看到主机上的进程信息,因为容器内部和主机是完全隔离的。同时由于 sh 是 1 号进程,意味着如果通过 exit 退出 sh,那么容器也会退出。所以对于容器来说, **杀死容器中的主进程,则容器也会被杀死。** +我们可以看到容器的 1 号进程为 sh 命令,在容器内部并不能看到主机上的进程信息,因为容器内部和主机是完全隔离的。同时由于 sh 是 1 号进程,意味着如果通过 exit 退出 sh,那么容器也会退出。所以对于容器来说,**杀死容器中的主进程,则容器也会被杀死。** #### (2)终止容器 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25406\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25406\350\256\262.md" index c4a4e7850..2f35ebdcf 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25406\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25406\350\256\262.md" @@ -1,6 +1,6 @@ # 06 最佳实践:如何在生产中编写最优 Dockerfile? -在介绍 Dockerfile 最佳实践前,这里再强调一下, **生产实践中一定优先使用 Dockerfile 的方式构建镜像。** 因为使用 Dockerfile 构建镜像可以带来很多好处: +在介绍 Dockerfile 最佳实践前,这里再强调一下,**生产实践中一定优先使用 Dockerfile 的方式构建镜像。** 因为使用 Dockerfile 构建镜像可以带来很多好处: - 易于版本化管理,Dockerfile 本身是一个文本文件,方便存放在代码仓库做版本管理,可以很方便地找到各个版本之间的变更历史; - 过程可追溯,Dockerfile 的每一行指令代表一个镜像层,根据 Dockerfile 的内容即可很明确地查看镜像的完整构建过程; diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25407\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25407\350\256\262.md" index 461cd7351..d40920bc2 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25407\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25407\350\256\262.md" @@ -62,7 +62,7 @@ Docker 自身是基于 Linux 的多种 Namespace 实现的,其中有一个很 由于仅仅依赖内核的隔离可能会引发安全问题,因此我们对于内核的安全应该更加重视。可以从以下几个方面进行加强。 -**宿主机及时升级内核漏洞** 宿主机内核应该尽量安装最新补丁,因为更新的内核补丁往往有着更好的安全性和稳定性。 **使用 Capabilities 划分权限** +**宿主机及时升级内核漏洞** 宿主机内核应该尽量安装最新补丁,因为更新的内核补丁往往有着更好的安全性和稳定性。**使用 Capabilities 划分权限** Capabilities 是 Linux 内核的概念,Linux 将系统权限分为了多个 Capabilities,它们都可以单独地开启或关闭,Capabilities 实现了系统更细粒度的访问控制。 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25408\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25408\350\256\262.md" index dfe2b5665..7bd34a4bc 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25408\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25408\350\256\262.md" @@ -271,7 +271,7 @@ Inter-| Receive | Transmit /proc/27348/net/dev 文件记录了该容器里每一个网卡的流量接收和发送情况,以及错误数、丢包数等信息。可见容器的网络监控数据都是定时从这里读取并展示的。 -总结一下, **容器的监控原理其实就是定时读取 Linux 主机上相关的文件并展示给用户。** +总结一下,**容器的监控原理其实就是定时读取 Linux 主机上相关的文件并展示给用户。** ### 结语 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25409\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25409\350\256\262.md" index 4ac924534..1981a7e26 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25409\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25409\350\256\262.md" @@ -78,7 +78,7 @@ Time Namespace Mount Namespace 是 Linux 内核实现的第一个 Namespace,从内核的 2.4.19 版本开始加入。它可以用来隔离不同的进程或进程组看到的挂载点。通俗地说,就是可以实现在不同的进程中看到不同的挂载目录。使用 Mount Namespace 可以实现容器内只能看到自己的挂载信息,在容器内的挂载操作不会影响主机的挂载目录。 -下面我们通过一个实例来演示下 Mount Namespace。在演示之前,我们先来认识一个命令行工具 unshare。unshare 是 util-linux 工具包中的一个工具,CentOS 7 系统默认已经集成了该工具, **使用 unshare 命令可以实现创建并访问不同类型的 Namespace** 。 +下面我们通过一个实例来演示下 Mount Namespace。在演示之前,我们先来认识一个命令行工具 unshare。unshare 是 util-linux 工具包中的一个工具,CentOS 7 系统默认已经集成了该工具,**使用 unshare 命令可以实现创建并访问不同类型的 Namespace** 。 首先我们使用以下命令创建一个 bash 进程并且新建一个 Mount Namespace: @@ -158,7 +158,7 @@ lrwxrwxrwx. 1 centos centos 0 Sep 4 08:20 uts -> uts:[4026531838] 通过对比两次命令的输出结果,我们可以看到,除了 Mount Namespace 的 ID 值不一样外,其他Namespace 的 ID 值均一致。 -通过以上结果我们可以得出结论, **使用 unshare 命令可以新建 Mount Namespace,并且在新建的 Mount Namespace 内 mount 是和外部完全隔离的。** +通过以上结果我们可以得出结论,**使用 unshare 命令可以新建 Mount Namespace,并且在新建的 Mount Namespace 内 mount 是和外部完全隔离的。** #### (2)PID Namespace diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25410\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25410\350\256\262.md" index fd9e7e419..69df0cc53 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25410\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25410\350\256\262.md" @@ -89,7 +89,7 @@ total 0 由上可以看到我们新建的目录下被自动创建了很多文件,其中 cpu.cfs_quota_us 文件代表在某一个阶段限制的 CPU 时间总量,单位为微秒。例如,我们想限制某个进程最多使用 1 核 CPU,就在这个文件里写入 100000(100000 代表限制 1 个核) ,tasks 文件中写入进程的 ID 即可(如果要限制多个进程 ID,在 tasks 文件中用换行符分隔即可)。 -此时,我们所需要的 cgroup 就创建好了。对,就是这么简单。 **第二步:创建进程,加入 cgroup** 这里为了方便演示,我先把当前运行的 shell 进程加入 cgroup,然后在当前 shell 运行 cpu 耗时任务(这里利用到了继承,子进程会继承父进程的 cgroup)。 +此时,我们所需要的 cgroup 就创建好了。对,就是这么简单。**第二步:创建进程,加入 cgroup** 这里为了方便演示,我先把当前运行的 shell 进程加入 cgroup,然后在当前 shell 运行 cpu 耗时任务(这里利用到了继承,子进程会继承父进程的 cgroup)。 使用以下命令将 shell 进程加入 cgroup 中: @@ -106,7 +106,7 @@ total 0 3543 ``` -其中第一个进程 ID 为当前 shell 的主进程,也就是说,当前 shell 主进程为 3485。 **第三步:执行 CPU 耗时任务,验证 cgroup 是否可以限制 cpu 使用时间** 下面,我们使用以下命令制造一个死循环,来提升 cpu 使用率: +其中第一个进程 ID 为当前 shell 的主进程,也就是说,当前 shell 主进程为 3485。**第三步:执行 CPU 耗时任务,验证 cgroup 是否可以限制 cpu 使用时间** 下面,我们使用以下命令制造一个死循环,来提升 cpu 使用率: ```plaintext # while true;do echo;done; diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25411\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25411\350\256\262.md" index 825a09d04..23821d8ec 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25411\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25411\350\256\262.md" @@ -39,7 +39,7 @@ Docker 到底有哪些组件呢?我们可以在 Docker 安装路径下执行 l **(1)docker** docker 是 Docker 客户端的一个完整实现,它是一个二进制文件,对用户可见的操作形式为 docker 命令,通过 docker 命令可以完成所有的 Docker 客户端与服务端的通信(还可以通过 REST API、SDK 等多种形式与 Docker 服务端通信)。 -Docker 客户端与服务端的交互过程是:docker 组件向服务端发送请求后,服务端根据请求执行具体的动作并将结果返回给 docker,docker 解析服务端的返回结果,并将结果通过命令行标准输出展示给用户。这样一次完整的客户端服务端请求就完成了。 **(2)dockerd** +Docker 客户端与服务端的交互过程是:docker 组件向服务端发送请求后,服务端根据请求执行具体的动作并将结果返回给 docker,docker 解析服务端的返回结果,并将结果通过命令行标准输出展示给用户。这样一次完整的客户端服务端请求就完成了。**(2)dockerd** dockerd 是 Docker 服务端的后台常驻进程,用来接收客户端发送的请求,执行具体的处理任务,处理完成后将结果返回给客户端。 @@ -77,7 +77,7 @@ PID USER TIME COMMAND 7 root 0:00 ps aux ``` -可以看到此时容器内的 1 号进程已经变为 /sbin/docker-init,而不再是 sh 了。 **(4)docker-proxy** +可以看到此时容器内的 1 号进程已经变为 /sbin/docker-init,而不再是 sh 了。**(4)docker-proxy** docker-proxy 主要是用来做端口映射的。当我们使用 docker run 命令启动容器时,如果使用了 -p 参数,docker-proxy 组件就会把容器内相应的端口映射到主机上来,底层是依赖于 iptables 实现的。 @@ -184,7 +184,7 @@ containerd 包含一个后台常驻进程,默认的 socket 路径为 /run/cont 如果你不想使用 dockerd,也可以直接使用 containerd 来管理容器,由于 containerd 更加简单和轻量,生产环境中越来越多的人开始直接使用 containerd 来管理容器。 -**(2)containerd-shim** containerd-shim 的意思是垫片,类似于拧螺丝时夹在螺丝和螺母之间的垫片。containerd-shim 的主要作用是将 containerd 和真正的容器进程解耦,使用 containerd-shim 作为容器进程的父进程,从而实现重启 containerd 不影响已经启动的容器进程。 **(3)ctr** +**(2)containerd-shim** containerd-shim 的意思是垫片,类似于拧螺丝时夹在螺丝和螺母之间的垫片。containerd-shim 的主要作用是将 containerd 和真正的容器进程解耦,使用 containerd-shim 作为容器进程的父进程,从而实现重启 containerd 不影响已经启动的容器进程。**(3)ctr** ctr 实际上是 containerd-ctr,它是 containerd 的客户端,主要用来开发和调试,在没有 dockerd 的环境中,ctr 可以充当 docker 客户端的部分角色,直接向 containerd 守护进程发送操作容器的请求。 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25413\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25413\350\256\262.md" index 1db89ffe6..e91131832 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25413\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25413\350\256\262.md" @@ -277,7 +277,7 @@ total 0 可以看到主机上的 \_data 目录下也出现了 data.log 文件。这说明,在容器内操作卷挂载的目录就是直接操作主机上的 \_data 目录,符合我上面的说法。 -综上, **Docker 卷的实现原理是在主机的 /var/lib/docker/volumes 目录下,根据卷的名称创建相应的目录,然后在每个卷的目录下创建 _data 目录,在容器启动时如果使用 --mount 参数,Docker 会把主机上的目录直接映射到容器的指定目录下,实现数据持久化。** +综上,**Docker 卷的实现原理是在主机的 /var/lib/docker/volumes 目录下,根据卷的名称创建相应的目录,然后在每个卷的目录下创建 _data 目录,在容器启动时如果使用 --mount 参数,Docker 会把主机上的目录直接映射到容器的指定目录下,实现数据持久化。** ### 结语 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25414\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25414\350\256\262.md" index b4f80fcfe..1194a243f 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25414\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25414\350\256\262.md" @@ -69,7 +69,7 @@ Server: #### AUFS 是如何存储文件的? -AUFS 是联合文件系统,意味着它在主机上使用多层目录存储, **每一个目录在 AUFS 中都叫作分支,而在 Docker 中则称之为层(layer),但最终呈现给用户的则是一个普通单层的文件系统,我们把多层以单一层的方式呈现出来的过程叫作联合挂载。** +AUFS 是联合文件系统,意味着它在主机上使用多层目录存储,**每一个目录在 AUFS 中都叫作分支,而在 Docker 中则称之为层(layer),但最终呈现给用户的则是一个普通单层的文件系统,我们把多层以单一层的方式呈现出来的过程叫作联合挂载。** ![Lark20201014-171313.png](assets/CgqCHl-GwcCAOu4aAABzKSlpRlI180.png) @@ -179,7 +179,7 @@ $ cd /tmp /tmp/aufs$ sudo mount -t aufs -o dirs=./container1:./image2:./image1 none ./mnt ``` -mount 命令创建 AUFS 类型文件系统时,这里要注意, **dirs 参数第一个冒号默认为读写权限,后面的目录均为只读权限,与 Docker 容器使用 AUFS 的模式一致。** +mount 命令创建 AUFS 类型文件系统时,这里要注意,**dirs 参数第一个冒号默认为读写权限,后面的目录均为只读权限,与 Docker 容器使用 AUFS 的模式一致。** 执行完上述命令后,mnt 变成了 AUFS 的联合挂载目录,我们可以使用 mount 命令查看一下已经创建的 AUFS 文件系统: diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25415\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25415\350\256\262.md" index caef9c87f..cd95ce2ef 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25415\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25415\350\256\262.md" @@ -31,7 +31,7 @@ Devicemapper 的工作机制主要围绕三个核心概念。 图 1 Devicemapper 核心概念关系图 -Devicemapper 三个核心概念之间的关系如图 1, **映射设备通过映射表关联到具体的物理目标设备。事实上,映射设备不仅可以通过映射表关联到物理目标设备,也可以关联到虚拟目标设备,然后虚拟目标设备再通过映射表关联到物理目标设备。** Devicemapper 在内核中通过很多模块化的映射驱动(target driver)插件实现了对真正 IO 请求的拦截、过滤和转发工作,比如 Raid、软件加密、瘦供给(Thin Provisioning)等。其中瘦供给模块是 Docker 使用 Devicemapper 技术框架中非常重要的模块,下面我们来详细了解下瘦供给(Thin Provisioning)。 +Devicemapper 三个核心概念之间的关系如图 1,**映射设备通过映射表关联到具体的物理目标设备。事实上,映射设备不仅可以通过映射表关联到物理目标设备,也可以关联到虚拟目标设备,然后虚拟目标设备再通过映射表关联到物理目标设备。** Devicemapper 在内核中通过很多模块化的映射驱动(target driver)插件实现了对真正 IO 请求的拦截、过滤和转发工作,比如 Raid、软件加密、瘦供给(Thin Provisioning)等。其中瘦供给模块是 Docker 使用 Devicemapper 技术框架中非常重要的模块,下面我们来详细了解下瘦供给(Thin Provisioning)。 #### 瘦供给(Thin Provisioning) @@ -45,11 +45,11 @@ Devicemapper 三个核心概念之间的关系如图 1, **映射设备通过 > 关于指定数据集合的一个完全可用拷贝,该拷贝包括相应数据在某个时间点(拷贝开始的时间点)的映像。快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。 -简单来说, **快照是数据在某一个时间点的存储状态。快照的主要作用是对数据进行备份,当存储设备发生故障时,可以使用已经备份的快照将数据恢复到某一个时间点,而 Docker 中的数据分层存储也是基于快照实现的。** 以上便是实现 Devicemapper 的关键技术,那 Docker 究竟是如何使用 Devicemapper 实现存储数据和镜像分层共享的呢? +简单来说,**快照是数据在某一个时间点的存储状态。快照的主要作用是对数据进行备份,当存储设备发生故障时,可以使用已经备份的快照将数据恢复到某一个时间点,而 Docker 中的数据分层存储也是基于快照实现的。** 以上便是实现 Devicemapper 的关键技术,那 Docker 究竟是如何使用 Devicemapper 实现存储数据和镜像分层共享的呢? ### Devicemapper 是如何数据存储的? -当 Docker 使用 Devicemapper 作为文件存储驱动时, **Docker 将镜像和容器的文件存储在瘦供给池(thinpool)中,并将这些内容挂载在 /var/lib/docker/devicemapper/ 目录下。** +当 Docker 使用 Devicemapper 作为文件存储驱动时,**Docker 将镜像和容器的文件存储在瘦供给池(thinpool)中,并将这些内容挂载在 /var/lib/docker/devicemapper/ 目录下。** 这些目录储存 Docker 的容器和镜像相关数据,目录的数据内容和功能说明如下。 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25416\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25416\350\256\262.md" index 1f3259700..b4c7da423 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25416\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25416\350\256\262.md" @@ -282,11 +282,11 @@ drwxr-xr-x. 1 root root 6 Sep 13 08:47 merged drwx------. 3 root root 18 Sep 13 08:47 work ``` -link 和 lower 文件与镜像层的功能一致, **** link 文件内容为该容器层的短 ID,lower 文件为该层的所有父层镜像的短 ID 。 **diff 目录为容器的读写层,容器内修改的文件都会在 diff 中出现,merged 目录为分层文件联合挂载后的结果,也是容器内的工作目录。** 总体来说,overlay2 是这样储存文件的:`overlay2`将镜像层和容器层都放在单独的目录,并且有唯一 ID,每一层仅存储发生变化的文件,最终使用联合挂载技术将容器层和镜像层的所有文件统一挂载到容器中,使得容器中看到完整的系统文件。 +link 和 lower 文件与镜像层的功能一致,**** link 文件内容为该容器层的短 ID,lower 文件为该层的所有父层镜像的短 ID 。**diff 目录为容器的读写层,容器内修改的文件都会在 diff 中出现,merged 目录为分层文件联合挂载后的结果,也是容器内的工作目录。** 总体来说,overlay2 是这样储存文件的:`overlay2`将镜像层和容器层都放在单独的目录,并且有唯一 ID,每一层仅存储发生变化的文件,最终使用联合挂载技术将容器层和镜像层的所有文件统一挂载到容器中,使得容器中看到完整的系统文件。 #### overlay2 如何读取、修改文件? -overlay2 的工作过程中对文件的操作分为读取文件和修改文件。 **读取文件** +overlay2 的工作过程中对文件的操作分为读取文件和修改文件。**读取文件** 容器内进程读取文件分为以下三种情况。 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25417\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25417\350\256\262.md" index 0fe0a1adb..f5a0adbdc 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25417\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25417\350\256\262.md" @@ -123,7 +123,7 @@ GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-bu #### Linux Proc 文件系统 -Linux 系统中,/proc 目录是一种“文件系统”,这里我用了引号,其实 /proc 目录并不是一个真正的文件系统。 **/proc 目录存放于内存中,是一个虚拟的文件系统,该目录存放了当前内核运行状态的一系列特殊的文件,你可以通过这些文件查看当前的进程信息。** +Linux 系统中,/proc 目录是一种“文件系统”,这里我用了引号,其实 /proc 目录并不是一个真正的文件系统。**/proc 目录存放于内存中,是一个虚拟的文件系统,该目录存放了当前内核运行状态的一系列特殊的文件,你可以通过这些文件查看当前的进程信息。** 下面,我们通过 ls 命令查看一下 /proc 目录下的内容: diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25420\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25420\350\256\262.md" index 4601ab17e..7e1311198 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25420\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25420\350\256\262.md" @@ -26,7 +26,7 @@ Swarm 的架构整体分为 **管理节点** (Manager Nodes)和 **工作节 ![image.png](assets/CgqCHl-iZxSAbYhzAABiA3_fQM8971.png) -图 1 Swarm 架构图 **管理节点:** 管理节点负责接受用户的请求,用户的请求中包含用户定义的容器运行状态描述,然后 Swarm 负责调度和管理容器,并且努力达到用户所期望的状态。 **工作节点:** 工作节点运行执行器(Executor)负责执行具体的容器管理任务(Task),例如容器的启动、停止、删除等操作。 +图 1 Swarm 架构图 **管理节点:** 管理节点负责接受用户的请求,用户的请求中包含用户定义的容器运行状态描述,然后 Swarm 负责调度和管理容器,并且努力达到用户所期望的状态。**工作节点:** 工作节点运行执行器(Executor)负责执行具体的容器管理任务(Task),例如容器的启动、停止、删除等操作。 > 管理节点和工作节点的角色并不是一成不变的,你可以手动将工作节点转换为管理节点,也可以将管理节点转换为工作节点。 @@ -40,7 +40,7 @@ Swarm 集群是一组被 Swarm 统一管理和调度的节点,被 Swarm纳管 #### 节点 -Swarm 集群中的每一台物理机或者虚拟机称为节点。节点按照工作职责分为 **管理节点** 和 **工作节点** ,管理节点由于需要使用 Raft 协议来协商节点状态,生产环境中通常建议将管理节点的数量设置为奇数个,一般为 3 个、5 个或 7 个。 +Swarm 集群中的每一台物理机或者虚拟机称为节点。节点按照工作职责分为 **管理节点** 和 **工作节点**,管理节点由于需要使用 Raft 协议来协商节点状态,生产环境中通常建议将管理节点的数量设置为奇数个,一般为 3 个、5 个或 7 个。 #### 服务 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25423\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25423\350\256\262.md" index 860a746b0..8458d142d 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25423\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25423\350\256\262.md" @@ -40,7 +40,7 @@ DevOps 的整体目标是 **促进开发和运维人员之间的配合,并且 在 Docker 技术出现之前,人们通常更加关注如何做好 CI(Continuous Integration,持续集成)/CD(Continuous Delivery持续交付)以及 IAAS(基础设施即服务),这时我们称之为 DevOps 1.0 时代。 -随着 Docker 技术的诞生,我们开始迎来了 DevOps 2.0 时代,DevOps 所有的这些需求都与 Docker 所提供的能力极其匹配。首先 Docker 足够轻量,可以帮助我们的微服务实现快速迭代。其次 Docker 可以很方便地帮助我们构建任何语言的运行环境,帮助我们顺利地使用多种语言来开发的我们的服务,最后 Docker 可以帮助我们更好地隔离开发环境和生产环境。 **可以说 Docker 几乎满足了微服务的所有需求,Docker 为 DevOps 提供了很好的基础支撑。** 这时的研发和运维都开始关注软件统一交付的格式和软件生命周期的管理, **而不像之前一样研发只关注“打包前”,而运维只关注“打包后”的模式** ,DevOps 无论是研发环境还是生产环境都开始围绕 Docker 进行构建。 +随着 Docker 技术的诞生,我们开始迎来了 DevOps 2.0 时代,DevOps 所有的这些需求都与 Docker 所提供的能力极其匹配。首先 Docker 足够轻量,可以帮助我们的微服务实现快速迭代。其次 Docker 可以很方便地帮助我们构建任何语言的运行环境,帮助我们顺利地使用多种语言来开发的我们的服务,最后 Docker 可以帮助我们更好地隔离开发环境和生产环境。**可以说 Docker 几乎满足了微服务的所有需求,Docker 为 DevOps 提供了很好的基础支撑。** 这时的研发和运维都开始关注软件统一交付的格式和软件生命周期的管理,**而不像之前一样研发只关注“打包前”,而运维只关注“打包后”的模式**,DevOps 无论是研发环境还是生产环境都开始围绕 Docker 进行构建。 ![Drawing 9.png](assets/Ciqc1F-uVmOASObQAAA7V7ib-l8145.png) @@ -80,7 +80,7 @@ Docker 可以在 DevOps 各个阶段发挥重要作用,例如 Docker 可以帮 ![Drawing 10.png](assets/Ciqc1F-uVomAACq6AAGSnXiZ7Xg745.png) -[Git](https://git-scm.com/) 是一种分布式的版本控制工具, 是目前使用最广泛的 DevOps 工具之一。Git 相比于其他版本控制工具,它可以 **实现离线代码提交** ,它允许我们提交代码时未连接到 Git 服务器,等到网络恢复再将我们的代码提交到远程服务器。 +[Git](https://git-scm.com/) 是一种分布式的版本控制工具, 是目前使用最广泛的 DevOps 工具之一。Git 相比于其他版本控制工具,它可以 **实现离线代码提交**,它允许我们提交代码时未连接到 Git 服务器,等到网络恢复再将我们的代码提交到远程服务器。 Git 非常容易上手,并且占用空间很小,相比于传统的版本控制工具(例如:Subversion、CVS 等)性能非常优秀,它可以帮助我们快速地创建分支,使得团队多人协作开发更加方便。 diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25425\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25425\350\256\262.md" index a8209f316..dd000dea6 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25425\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25425\350\256\262.md" @@ -214,7 +214,7 @@ mkdir -p ./shell && echo \ "docker run --rm --name=hello -p 8090:8090 -d REPOSITORY" >> ./shell/release ``` -我们在 docker push 命令后,增加一个输出 shell 脚本到 release 文件的命令,这个脚本会发送到远端的服务器上并执行,通过执行这个脚本文件可以在远端服务器上,拉取最新镜像并且重新启动容器。 **第四步,配置远程执行。在 Jenkins 的 hello 项目中,点击配置,在执行步骤中点击添加Send files or execute commands over SSH** 的步骤,选择之前添加的服务器,并且按照以下内容填写相关信息。 +我们在 docker push 命令后,增加一个输出 shell 脚本到 release 文件的命令,这个脚本会发送到远端的服务器上并执行,通过执行这个脚本文件可以在远端服务器上,拉取最新镜像并且重新启动容器。**第四步,配置远程执行。在 Jenkins 的 hello 项目中,点击配置,在执行步骤中点击添加Send files or execute commands over SSH** 的步骤,选择之前添加的服务器,并且按照以下内容填写相关信息。 ![Drawing 14.png](assets/Ciqc1F-2QhKAPblBAAC4Bp33K2Y632.png) diff --git "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25426\350\256\262.md" "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25426\350\256\262.md" index 5db7c8ebc..1d7c3d926 100644 --- "a/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25426\350\256\262.md" +++ "b/docs/Linux/\347\224\261\346\265\205\345\205\245\346\267\261\345\220\203\351\200\217 Docker/\347\254\25426\350\256\262.md" @@ -12,7 +12,7 @@ 在容器技术早期,由于容器技术本身的稳定性和可用性不是很理想,容器的编排技术相对也不够成熟。因此很多企业在做容器化改造的过程一直都是小心翼翼,业务容器化改造的程度也不够理想。 -但随着容器技术的逐渐成熟和稳定,越来越多的企业开始将业务迁移到容器中来(例如我们经常访问的 GitHub 的核心服务已经全部运行在了容器中),虽然目前有些公司还没有使用容器来部署业务,但是已经有很多公司在尝试和探索使用容器来改变现有的业务部署模式。 **在未来,容器业务一定会占据越来越多的份额。** +但随着容器技术的逐渐成熟和稳定,越来越多的企业开始将业务迁移到容器中来(例如我们经常访问的 GitHub 的核心服务已经全部运行在了容器中),虽然目前有些公司还没有使用容器来部署业务,但是已经有很多公司在尝试和探索使用容器来改变现有的业务部署模式。**在未来,容器业务一定会占据越来越多的份额。** ### 混合云和多云将成为趋势 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" index 72ac25ec3..bcd3d8cd4 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" @@ -36,7 +36,7 @@ - 可靠、智能的容错和负载均衡; - 服务自动注册和发现能力。 -简单地说, **Dubbo 是一个分布式服务框架,致力于提供高性能、透明化的 RPC 远程服务调用方案以及服务治理方案,以帮助我们解决微服务架构落地时的问题。** Dubbo 是由阿里开源,后来加入了 Apache 基金会,目前已经从孵化器毕业,成为 Apache 的顶级项目。Apache Dubbo 目前已经有接近 32.8 K 的 Star、21.4 K 的 Fork,其热度可见一斑, **很多互联网大厂(如阿里、滴滴、去哪儿网等)都是直接使用 Dubbo 作为其 RPC 框架,也有些大厂会基于 Dubbo 进行二次开发实现自己的 RPC 框架** ,如当当网的 DubboX。 +简单地说,**Dubbo 是一个分布式服务框架,致力于提供高性能、透明化的 RPC 远程服务调用方案以及服务治理方案,以帮助我们解决微服务架构落地时的问题。** Dubbo 是由阿里开源,后来加入了 Apache 基金会,目前已经从孵化器毕业,成为 Apache 的顶级项目。Apache Dubbo 目前已经有接近 32.8 K 的 Star、21.4 K 的 Fork,其热度可见一斑,**很多互联网大厂(如阿里、滴滴、去哪儿网等)都是直接使用 Dubbo 作为其 RPC 框架,也有些大厂会基于 Dubbo 进行二次开发实现自己的 RPC 框架**,如当当网的 DubboX。 作为一名 Java 工程师,深入掌握 Dubbo 的原理和实现已经是大势所趋,并且成为你职场竞争力的关键项。拉勾网显示,研发工程师、架构师等高薪岗位,都要求你熟悉并曾经深入使用某种 RPC 框架,一线大厂更是要求你至少深入了解一款 RPC 框架的原理和核心实现。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" index 66a8b4520..73104fa86 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" @@ -94,7 +94,7 @@ mvn idea:idea // 要是执行报错,就执行这个 mvn idea:workspace #### 启动 ZooKeeper -在前面 Dubbo 的架构图中,你可以看到 Provider 的地址以及配置信息是通过注册中心传递给 Consumer 的。 Dubbo 支持的注册中心尽管有很多, 但在生产环境中, **基本都是用 ZooKeeper 作为注册中心** 。因此,在调试 Dubbo 源码时,自然需要在本地启动 ZooKeeper。 +在前面 Dubbo 的架构图中,你可以看到 Provider 的地址以及配置信息是通过注册中心传递给 Consumer 的。 Dubbo 支持的注册中心尽管有很多, 但在生产环境中,**基本都是用 ZooKeeper 作为注册中心** 。因此,在调试 Dubbo 源码时,自然需要在本地启动 ZooKeeper。 那怎么去启动 ZooKeeper 呢? diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" index 7afd5193f..207c7865f 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" @@ -2,7 +2,7 @@ Dubbo 为了更好地达到 OCP 原则(即“对扩展开放,对修改封闭”的原则),采用了“ **微内核+插件** ”的架构。那什么是微内核架构呢?微内核架构也被称为插件化架构(Plug-in Architecture),这是一种面向功能进行拆分的可扩展性架构。内核功能是比较稳定的,只负责管理插件的生命周期,不会因为系统功能的扩展而不断进行修改。功能上的扩展全部封装到插件之中,插件模块是独立存在的模块,包含特定的功能,能拓展内核系统的功能。 -微内核架构中,内核通常采用 Factory、IoC、OSGi 等方式管理插件生命周期, **Dubbo 最终决定采用 SPI 机制来加载插件** ,Dubbo SPI 参考 JDK 原生的 SPI 机制,进行了性能优化以及功能增强。因此,在讲解 Dubbo SPI 之前,我们有必要先来介绍一下 JDK SPI 的工作原理。 +微内核架构中,内核通常采用 Factory、IoC、OSGi 等方式管理插件生命周期,**Dubbo 最终决定采用 SPI 机制来加载插件**,Dubbo SPI 参考 JDK 原生的 SPI 机制,进行了性能优化以及功能增强。因此,在讲解 Dubbo SPI 之前,我们有必要先来介绍一下 JDK SPI 的工作原理。 ### JDK SPI @@ -92,7 +92,7 @@ public void reload() { ![image](assets/Ciqc1F8o_WmAZSkmAABmcc0uM54214.png) -首先来看 LazyIterator.hasNextService() 方法,该方法主要 **负责查找 META-INF/services 目录下的 SPI 配置文件** ,并进行遍历,大致实现如下所示: +首先来看 LazyIterator.hasNextService() 方法,该方法主要 **负责查找 META-INF/services 目录下的 SPI 配置文件**,并进行遍历,大致实现如下所示: ```java private static final String PREFIX = "META-INF/services/"; @@ -126,7 +126,7 @@ private boolean hasNextService() { } ``` -在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看 LazyIterator.nextService() 方法,该方法 **负责实例化 hasNextService() 方法读取到的实现类** ,其中会将实例化的对象放到 providers 集合中缓存起来,核心实现如下所示: +在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看 LazyIterator.nextService() 方法,该方法 **负责实例化 hasNextService() 方法读取到的实现类**,其中会将实例化的对象放到 providers 集合中缓存起来,核心实现如下所示: ```plaintext private S nextService() { @@ -187,7 +187,7 @@ String url = "jdbc:xxx://xxx:xxx/xxx"; Connection conn = DriverManager.getConnection(url, username, pwd); ``` -**DriverManager 是 JDK 提供的数据库驱动管理器** ,其中的代码片段,如下所示: +**DriverManager 是 JDK 提供的数据库驱动管理器**,其中的代码片段,如下所示: ```java static { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" index 741bd40e2..6ca73adb4 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" @@ -9,7 +9,7 @@ - **扩展点** :通过 SPI 机制查找并加载实现的接口(又称“扩展接口”)。前文示例中介绍的 Log 接口、com.mysql.cj.jdbc.Driver 接口,都是扩展点。 - **扩展点实现** :实现了扩展接口的实现类。 -通过前面的分析可以发现,JDK SPI 在查找扩展实现类的过程中,需要遍历 SPI 配置文件中定义的所有实现类,该过程中会将这些实现类全部实例化。如果 SPI 配置文件中定义了多个实现类,而我们只需要使用其中一个实现类时,就会生成不必要的对象。例如,org.apache.dubbo.rpc.Protocol 接口有 InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol、ThriftProtocol 等多个实现,如果使用 JDK SPI,就会加载全部实现类,导致资源的浪费。 **Dubbo SPI 不仅解决了上述资源浪费的问题,还对 SPI 配置文件扩展和修改。** +通过前面的分析可以发现,JDK SPI 在查找扩展实现类的过程中,需要遍历 SPI 配置文件中定义的所有实现类,该过程中会将这些实现类全部实例化。如果 SPI 配置文件中定义了多个实现类,而我们只需要使用其中一个实现类时,就会生成不必要的对象。例如,org.apache.dubbo.rpc.Protocol 接口有 InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol、ThriftProtocol 等多个实现,如果使用 JDK SPI,就会加载全部实现类,导致资源的浪费。**Dubbo SPI 不仅解决了上述资源浪费的问题,还对 SPI 配置文件扩展和修改。** 首先,Dubbo 按照 SPI 配置文件的用途,将其分成了三类目录。 @@ -17,7 +17,7 @@ - META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件。 - META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件。 -然后,Dubbo 将 SPI 配置文件改成了 **KV 格式** ,例如: +然后,Dubbo 将 SPI 配置文件改成了 **KV 格式**,例如: ```plaintext dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol @@ -31,7 +31,7 @@ dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol #### 1. @SPI 注解 -Dubbo 中某个接口被 @SPI注解修饰时,就表示该接口是 **扩展接口** ,前文示例中的 org.apache.dubbo.rpc.Protocol 接口就是一个扩展接口: +Dubbo 中某个接口被 @SPI注解修饰时,就表示该接口是 **扩展接口**,前文示例中的 org.apache.dubbo.rpc.Protocol 接口就是一个扩展接口: ![Drawing 0.png](assets/CgqCHl8s936AYuePAABLd6cRz6w646.png) @@ -280,7 +280,7 @@ private T injectExtension(T instance) { } ``` -injectExtension() 方法实现的自动装配依赖了 ExtensionFactory(即 objectFactory 字段),前面我们提到过 ExtensionFactory 有 SpringExtensionFactory 和 SpiExtensionFactory 两个真正的实现(还有一个实现是 AdaptiveExtensionFactory 是适配器)。下面我们分别介绍下这两个真正的实现。 **第一个,SpiExtensionFactory。** 根据扩展接口获取相应的适配器,没有到属性名称: +injectExtension() 方法实现的自动装配依赖了 ExtensionFactory(即 objectFactory 字段),前面我们提到过 ExtensionFactory 有 SpringExtensionFactory 和 SpiExtensionFactory 两个真正的实现(还有一个实现是 AdaptiveExtensionFactory 是适配器)。下面我们分别介绍下这两个真正的实现。**第一个,SpiExtensionFactory。** 根据扩展接口获取相应的适配器,没有到属性名称: ```java @Override diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25405\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25405\350\256\262.md" index 6f000addb..23d21e963 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25405\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25405\350\256\262.md" @@ -28,16 +28,16 @@ Timer 接口定义了定时器的基本行为,如下图所示,其核心是 n HashedWheelTimeout 是 Timeout 接口的唯一实现,是 HashedWheelTimer 的内部类。HashedWheelTimeout 扮演了两个角色: -- 第一个,时间轮中双向链表的 **节点** ,即定时任务 TimerTask 在 HashedWheelTimer 中的容器。 +- 第一个,时间轮中双向链表的 **节点**,即定时任务 TimerTask 在 HashedWheelTimer 中的容器。 - 第二个,定时任务 TimerTask 提交到 HashedWheelTimer 之后返回的 **句柄** (Handle),用于在时间轮外部查看和控制定时任务。 HashedWheelTimeout 中的核心字段如下: -- **prev、next(HashedWheelTimeout类型)** ,分别对应当前定时任务在链表中的前驱节点和后继节点。 -- **task(TimerTask类型)** ,指实际被调度的任务。 -- **deadline(long类型)** ,指定时任务执行的时间。这个时间是在创建 HashedWheelTimeout 时指定的,计算公式是:currentTime(创建 HashedWheelTimeout 的时间) + delay(任务延迟时间) - startTime(HashedWheelTimer 的启动时间),时间单位为纳秒。 -- **state(volatile int类型)** ,指定时任务当前所处状态,可选的有三个,分别是 INIT(0)、CANCELLED(1)和 EXPIRED(2)。另外,还有一个 STATE_UPDATER 字段(AtomicIntegerFieldUpdater类型)实现 state 状态变更的原子性。 -- **remainingRounds(long类型)** ,指当前任务剩余的时钟周期数。时间轮所能表示的时间长度是有限的,在任务到期时间与当前时刻的时间差,超过时间轮单圈能表示的时长,就出现了套圈的情况,需要该字段值表示剩余的时钟周期。 +- **prev、next(HashedWheelTimeout类型)**,分别对应当前定时任务在链表中的前驱节点和后继节点。 +- **task(TimerTask类型)**,指实际被调度的任务。 +- **deadline(long类型)**,指定时任务执行的时间。这个时间是在创建 HashedWheelTimeout 时指定的,计算公式是:currentTime(创建 HashedWheelTimeout 的时间) + delay(任务延迟时间) - startTime(HashedWheelTimer 的启动时间),时间单位为纳秒。 +- **state(volatile int类型)**,指定时任务当前所处状态,可选的有三个,分别是 INIT(0)、CANCELLED(1)和 EXPIRED(2)。另外,还有一个 STATE_UPDATER 字段(AtomicIntegerFieldUpdater类型)实现 state 状态变更的原子性。 +- **remainingRounds(long类型)**,指当前任务剩余的时钟周期数。时间轮所能表示的时间长度是有限的,在任务到期时间与当前时刻的时间差,超过时间轮单圈能表示的时长,就出现了套圈的情况,需要该字段值表示剩余的时钟周期。 HashedWheelTimeout 中的核心方法有: diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" index b66ec6a6d..bb5637800 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" @@ -1,14 +1,14 @@ # 06 ZooKeeper 与 Curator,求你别用 ZkClient 了(上) -在前面我们介绍 Dubbo 简化架构的时候提到过,Dubbo Provider 在启动时会将自身的服务信息整理成 URL 注册到注册中心,Dubbo Consumer 在启动时会向注册中心订阅感兴趣的 Provider 信息,之后 Provider 和 Consumer 才能建立连接,进行后续的交互。可见, **一个稳定、高效的注册中心对基于 Dubbo 的微服务来说是至关重要的** 。 +在前面我们介绍 Dubbo 简化架构的时候提到过,Dubbo Provider 在启动时会将自身的服务信息整理成 URL 注册到注册中心,Dubbo Consumer 在启动时会向注册中心订阅感兴趣的 Provider 信息,之后 Provider 和 Consumer 才能建立连接,进行后续的交互。可见,**一个稳定、高效的注册中心对基于 Dubbo 的微服务来说是至关重要的** 。 Dubbo 目前支持 Consul、etcd、Nacos、ZooKeeper、Redis 等多种开源组件作为注册中心,并且在 Dubbo 源码也有相应的接入模块,如下图所示: ![Drawing 0.png](assets/Ciqc1F81FQ2ANt6EAADZ01G_QYM489.png) -**Dubbo 官方推荐使用 ZooKeeper 作为注册中心** ,它是在实际生产中最常用的注册中心实现,这也是我们本课时要介绍 ZooKeeper 核心原理的原因。 +**Dubbo 官方推荐使用 ZooKeeper 作为注册中心**,它是在实际生产中最常用的注册中心实现,这也是我们本课时要介绍 ZooKeeper 核心原理的原因。 -要与 ZooKeeper 集群进行交互,我们可以使用 ZooKeeper 原生客户端或是 ZkClient、Apache Curator 等第三方开源客户端。在后面介绍 dubbo-registry-zookeeper 模块的具体实现时你会看到,Dubbo 底层使用的是 Apache Curator。 **Apache Curator 是实践中最常用的 ZooKeeper 客户端。** ### ZooKeeper 核心概念 **Apache ZooKeeper 是一个针对分布式系统的、可靠的、可扩展的协调服务** ,它通常作为统一命名服务、统一配置管理、注册中心(分布式集群管理)、分布式锁服务、Leader 选举服务等角色出现。很多分布式系统都依赖与 ZooKeeper 集群实现分布式系统间的协调调度,例如:Dubbo、HDFS 2.x、HBase、Kafka 等。 **ZooKeeper 已经成为现代分布式系统的标配** 。 +要与 ZooKeeper 集群进行交互,我们可以使用 ZooKeeper 原生客户端或是 ZkClient、Apache Curator 等第三方开源客户端。在后面介绍 dubbo-registry-zookeeper 模块的具体实现时你会看到,Dubbo 底层使用的是 Apache Curator。**Apache Curator 是实践中最常用的 ZooKeeper 客户端。** ### ZooKeeper 核心概念 **Apache ZooKeeper 是一个针对分布式系统的、可靠的、可扩展的协调服务**,它通常作为统一命名服务、统一配置管理、注册中心(分布式集群管理)、分布式锁服务、Leader 选举服务等角色出现。很多分布式系统都依赖与 ZooKeeper 集群实现分布式系统间的协调调度,例如:Dubbo、HDFS 2.x、HBase、Kafka 等。**ZooKeeper 已经成为现代分布式系统的标配** 。 ZooKeeper 本身也是一个分布式应用程序,下图展示了 ZooKeeper 集群的核心架构。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" index c054849eb..8e3da5025 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" @@ -173,11 +173,11 @@ public class Main2 { #### 3. 连接状态监听 -除了基础的数据操作,Curator 还提供了 **监听连接状态的监听器——ConnectionStateListener** ,它主要是处理 Curator 客户端和 ZooKeeper 服务器间连接的异常情况,例如, 短暂或者长时间断开连接。 +除了基础的数据操作,Curator 还提供了 **监听连接状态的监听器——ConnectionStateListener**,它主要是处理 Curator 客户端和 ZooKeeper 服务器间连接的异常情况,例如, 短暂或者长时间断开连接。 短暂断开连接时,ZooKeeper 客户端会检测到与服务端的连接已经断开,但是服务端维护的客户端 Session 尚未过期,之后客户端和服务端重新建立了连接;当客户端重新连接后,由于 Session 没有过期,ZooKeeper 能够保证连接恢复后保持正常服务。 -而长时间断开连接时,Session 已过期,与先前 Session 相关的 Watcher 和临时节点都会丢失。当 Curator 重新创建了与 ZooKeeper 的连接时,会获取到 Session 过期的相关异常,Curator 会销毁老 Session,并且创建一个新的 Session。由于老 Session 关联的数据不存在了,在 ConnectionStateListener 监听到 LOST 事件时,就可以依靠本地存储的数据恢复 Session 了。 **这里 Session 指的是 ZooKeeper 服务器与客户端的会话** 。客户端启动的时候会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watch 事件通知。 +而长时间断开连接时,Session 已过期,与先前 Session 相关的 Watcher 和临时节点都会丢失。当 Curator 重新创建了与 ZooKeeper 的连接时,会获取到 Session 过期的相关异常,Curator 会销毁老 Session,并且创建一个新的 Session。由于老 Session 关联的数据不存在了,在 ConnectionStateListener 监听到 LOST 事件时,就可以依靠本地存储的数据恢复 Session 了。**这里 Session 指的是 ZooKeeper 服务器与客户端的会话** 。客户端启动的时候会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watch 事件通知。 我们可以设置客户端会话的超时时间(sessionTimeout),当服务器压力太大、网络故障或是客户端主动断开连接等原因导致连接断开时,只要客户端在 sessionTimeout 规定的时间内能够重新连接到 ZooKeeper 集群中任意一个实例,那么之前创建的会话仍然有效。ZooKeeper 通过 sessionID 唯一标识 Session,所以在 ZooKeeper 集群中,sessionID 需要保证全局唯一。 由于 ZooKeeper 会将 Session 信息存放到硬盘中,即使节点重启,之前未过期的 Session 仍然会存在。 @@ -267,7 +267,7 @@ NodeChildrenChanged,/user 之所以这样,是因为通过 usingWatcher() 方法添加的 CuratorWatcher 只会触发一次,触发完毕后就会销毁。checkExists() 方法、getData() 方法通过 usingWatcher() 方法添加的 Watcher 也是一样的原理,只不过监听的事件不同,你若感兴趣的话,可以自行尝试一下。 -相信你已经感受到,直接通过注册 Watcher 进行事件监听不是特别方便,需要我们自己反复注册 Watcher。 **Apache Curator 引入了 Cache 来实现对 ZooKeeper 服务端事件的监听** 。Cache 是 Curator 中对事件监听的包装,其对事件的监听其实可以近似看作是一个本地缓存视图和远程ZooKeeper 视图的对比过程。同时,Curator 能够自动为开发人员处理反复注册监听,从而大大简化了代码的复杂程度。 +相信你已经感受到,直接通过注册 Watcher 进行事件监听不是特别方便,需要我们自己反复注册 Watcher。**Apache Curator 引入了 Cache 来实现对 ZooKeeper 服务端事件的监听** 。Cache 是 Curator 中对事件监听的包装,其对事件的监听其实可以近似看作是一个本地缓存视图和远程ZooKeeper 视图的对比过程。同时,Curator 能够自动为开发人员处理反复注册监听,从而大大简化了代码的复杂程度。 实践中常用的 Cache 有三大类: @@ -418,7 +418,7 @@ PathChildrenCache删除子节点:/user/test3 为了避免 curator-framework 包过于膨胀,Curator 将很多其他解决方案都拆出来了,作为单独的一个包,例如:curator-recipes、curator-x-discovery、curator-x-rpc 等。 -在后面我们会使用到 curator-x-discovery 来完成一个简易 RPC 框架的注册中心模块。 **curator-x-discovery 扩展包是一个服务发现的解决方案** 。在 ZooKeeper 中,我们可以使用临时节点实现一个服务注册机制。当服务启动后在 ZooKeeper 的指定 Path 下创建临时节点,服务断掉与 ZooKeeper 的会话之后,其相应的临时节点就会被删除。这个 curator-x-discovery 扩展包抽象了这种功能,并提供了一套简单的 API 来实现服务发现机制。curator-x-discovery 扩展包的核心概念如下: +在后面我们会使用到 curator-x-discovery 来完成一个简易 RPC 框架的注册中心模块。**curator-x-discovery 扩展包是一个服务发现的解决方案** 。在 ZooKeeper 中,我们可以使用临时节点实现一个服务注册机制。当服务启动后在 ZooKeeper 的指定 Path 下创建临时节点,服务断掉与 ZooKeeper 的会话之后,其相应的临时节点就会被删除。这个 curator-x-discovery 扩展包抽象了这种功能,并提供了一套简单的 API 来实现服务发现机制。curator-x-discovery 扩展包的核心概念如下: * **ServiceInstance。** 这是 curator-x-discovery 扩展包对服务实例的抽象,由 name、id、address、port 以及一个可选的 payload 属性构成。其存储在 ZooKeeper 中的方式如下图展示的这样。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25408\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25408\350\256\262.md" index ba2e500e6..d5d013ea3 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25408\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25408\350\256\262.md" @@ -10,7 +10,7 @@ ![image](assets/CgqCHl8_hxqAY6vaAAGcUb0A8A4971.png) -图中的 Subject 是程序中的 **业务逻辑接口** ,RealSubject 是实现了 Subject 接口的 **真正业务类** ,Proxy 是实现了 Subject 接口的 **代理类** ,封装了一个 RealSubject 引用。 **在程序中不会直接调用 RealSubject 对象的方法,而是使用 Proxy 对象实现相关功能。** Proxy.operation() 方法的实现会调用其中封装的 RealSubject 对象的 operation() 方法,执行真正的业务逻辑。代理的作用不仅仅是正常地完成业务逻辑,还会在业务逻辑前后添加一些代理逻辑,也就是说,Proxy.operation() 方法会在 RealSubject.operation() 方法调用前后进行一些预处理以及一些后置处理。这就是我们常说的“ **代理模式** ”。 **使用代理模式可以控制程序对 RealSubject 对象的访问** ,如果发现异常的访问,可以直接限流或是返回,也可以在执行业务处理的前后进行相关的预处理和后置处理,帮助上层调用方屏蔽底层的细节。例如,在 RPC 框架中,代理可以完成序列化、网络 I/O 操作、负载均衡、故障恢复以及服务发现等一系列操作,而上层调用方只感知到了一次本地调用。 **代理模式还可以用于实现延迟加载的功能** 。我们知道查询数据库是一个耗时的操作,而有些时候查询到的数据也并没有真正被程序使用。延迟加载功能就可以有效地避免这种浪费,系统访问数据库时,首先可以得到一个代理对象,此时并没有执行任何数据库查询操作,代理对象中自然也没有真正的数据;当系统真正需要使用数据时,再调用代理对象完成数据库查询并返回数据。常见 ORM 框架(例如,MyBatis、 Hibernate)中的延迟加载的原理大致也是如此。 +图中的 Subject 是程序中的 **业务逻辑接口**,RealSubject 是实现了 Subject 接口的 **真正业务类**,Proxy 是实现了 Subject 接口的 **代理类**,封装了一个 RealSubject 引用。**在程序中不会直接调用 RealSubject 对象的方法,而是使用 Proxy 对象实现相关功能。** Proxy.operation() 方法的实现会调用其中封装的 RealSubject 对象的 operation() 方法,执行真正的业务逻辑。代理的作用不仅仅是正常地完成业务逻辑,还会在业务逻辑前后添加一些代理逻辑,也就是说,Proxy.operation() 方法会在 RealSubject.operation() 方法调用前后进行一些预处理以及一些后置处理。这就是我们常说的“ **代理模式** ”。**使用代理模式可以控制程序对 RealSubject 对象的访问**,如果发现异常的访问,可以直接限流或是返回,也可以在执行业务处理的前后进行相关的预处理和后置处理,帮助上层调用方屏蔽底层的细节。例如,在 RPC 框架中,代理可以完成序列化、网络 I/O 操作、负载均衡、故障恢复以及服务发现等一系列操作,而上层调用方只感知到了一次本地调用。**代理模式还可以用于实现延迟加载的功能** 。我们知道查询数据库是一个耗时的操作,而有些时候查询到的数据也并没有真正被程序使用。延迟加载功能就可以有效地避免这种浪费,系统访问数据库时,首先可以得到一个代理对象,此时并没有执行任何数据库查询操作,代理对象中自然也没有真正的数据;当系统真正需要使用数据时,再调用代理对象完成数据库查询并返回数据。常见 ORM 框架(例如,MyBatis、 Hibernate)中的延迟加载的原理大致也是如此。 另外,代理对象可以协调真正RealSubject 对象与调用者之间的关系,在一定程度上实现了解耦的效果。 @@ -18,7 +18,7 @@ 上面介绍的这种代理模式实现,也被称为“静态代理模式”,这是因为在编译阶段就要为每个RealSubject 类创建一个 Proxy 类,当需要代理的类很多时,就会出现大量的 Proxy 类。 -这种场景下,我们可以使用 JDK 动态代理解决这个问题。 **JDK 动态代理的核心是InvocationHandler 接口** 。这里提供一个 InvocationHandler 的Demo 实现,代码如下: +这种场景下,我们可以使用 JDK 动态代理解决这个问题。**JDK 动态代理的核心是InvocationHandler 接口** 。这里提供一个 InvocationHandler 的Demo 实现,代码如下: ```java public class DemoInvokerHandler implements InvocationHandler { @@ -178,9 +178,9 @@ public final class Proxy37 JDK 动态代理是 Java 原生支持的,不需要任何外部依赖,但是正如上面分析的那样,它只能基于接口进行代理,对于没有继承任何接口的类,JDK 动态代理就没有用武之地了。 -如果想对没有实现任何接口的类进行代理,可以考虑使用 CGLib。 **CGLib(Code Generation Library)是一个基于 ASM 的字节码生成库** ,它允许我们在运行时对字节码进行修改和动态生成。CGLib 采用字节码技术实现动态代理功能,其底层原理是通过字节码技术为目标类生成一个子类,并在该子类中采用方法拦截的方式拦截所有父类方法的调用,从而实现代理的功能。 +如果想对没有实现任何接口的类进行代理,可以考虑使用 CGLib。**CGLib(Code Generation Library)是一个基于 ASM 的字节码生成库**,它允许我们在运行时对字节码进行修改和动态生成。CGLib 采用字节码技术实现动态代理功能,其底层原理是通过字节码技术为目标类生成一个子类,并在该子类中采用方法拦截的方式拦截所有父类方法的调用,从而实现代理的功能。 -因为 CGLib 使用生成子类的方式实现动态代理,所以无法代理 final 关键字修饰的方法(因为final 方法是不能够被重写的)。这样的话, **CGLib 与 JDK 动态代理之间可以相互补充:在目标类实现接口时,使用 JDK 动态代理创建代理对象;当目标类没有实现接口时,使用 CGLib 实现动态代理的功能** 。在 Spring、MyBatis 等多种开源框架中,都可以看到JDK动态代理与 CGLib 结合使用的场景。 +因为 CGLib 使用生成子类的方式实现动态代理,所以无法代理 final 关键字修饰的方法(因为final 方法是不能够被重写的)。这样的话,**CGLib 与 JDK 动态代理之间可以相互补充:在目标类实现接口时,使用 JDK 动态代理创建代理对象;当目标类没有实现接口时,使用 CGLib 实现动态代理的功能** 。在 Spring、MyBatis 等多种开源框架中,都可以看到JDK动态代理与 CGLib 结合使用的场景。 CGLib 的实现有两个重要的成员组成。 @@ -254,7 +254,7 @@ public class CGLibTest { // 目标类 到此,CGLib 基础使用的内容就介绍完了,在后面介绍 Dubbo 源码时我们还会继续介绍涉及的 CGLib 内容。 Javassist -========= **Javassist 是一个开源的生成 Java 字节码的类库** ,其主要优点在于简单、快速,直接使用Javassist 提供的 Java API 就能动态修改类的结构,或是动态生成类。 +========= **Javassist 是一个开源的生成 Java 字节码的类库**,其主要优点在于简单、快速,直接使用Javassist 提供的 Java API 就能动态修改类的结构,或是动态生成类。 Javassist 的使用比较简单,首先来看如何使用 Javassist 提供的 Java API 动态创建类。示例代码如下: @@ -367,7 +367,7 @@ public class JavassitMain2 { } ``` -Javassist 的基础知识就介绍到这里。Javassist可以直接使用 Java 语言的字符串生成类,还是比较好用的。 **Javassist 的性能也比较好,是 Dubbo 默认的代理生成方式** 。 +Javassist 的基础知识就介绍到这里。Javassist可以直接使用 Java 语言的字符串生成类,还是比较好用的。**Javassist 的性能也比较好,是 Dubbo 默认的代理生成方式** 。 ### 总结 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25409\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25409\350\256\262.md" index ea9be0c97..587966d59 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25409\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25409\350\256\262.md" @@ -47,7 +47,7 @@ Netty 就是采用了上述 I/O 复用的模型。由于多路复用器 Selector 服务器程序在读取到二进制数据之后,首先需要通过编解码,得到程序逻辑可以理解的消息,然后将消息传入业务逻辑进行处理,并产生相应的结果,返回给客户端。编解码逻辑、消息派发逻辑、业务处理逻辑以及返回响应的逻辑,是放到一个线程里面串行执行,还是分配到不同的线程中执行,会对程序的性能产生很大的影响。所以,优秀的线程模型对一个高性能网络库来说是至关重要的。 -**Netty 采用了 Reactor 线程模型的设计。** Reactor 模式,也被称为 Dispatcher 模式, **核心原理是 Selector 负责监听 I/O 事件,在监听到 I/O 事件之后,分发(Dispatch)给相关线程进行处理** 。 +**Netty 采用了 Reactor 线程模型的设计。** Reactor 模式,也被称为 Dispatcher 模式,**核心原理是 Selector 负责监听 I/O 事件,在监听到 I/O 事件之后,分发(Dispatch)给相关线程进行处理** 。 为了帮助你更好地了解 Netty 线程模型的设计理念,我们将从最基础的单 Reactor 单线程模型开始介绍,然后逐步增加模型的复杂度,最终到 Netty 目前使用的非常成熟的线程模型设计。 @@ -59,11 +59,11 @@ Reactor 对象监听客户端请求事件,收到事件后通过 Dispatch 进 单 Reactor 单线程的优点就是:线程模型简单,没有引入多线程,自然也就没有多线程并发和竞争的问题。 -但其缺点也非常明显,那就是 **性能瓶颈问题** ,一个线程只能跑在一个 CPU 上,能处理的连接数是有限的,无法完全发挥多核 CPU 的优势。一旦某个业务逻辑耗时较长,这唯一的线程就会卡在上面,无法处理其他连接的请求,程序进入假死的状态,可用性也就降低了。正是由于这种限制,一般只会在 **客户端** 使用这种线程模型。 +但其缺点也非常明显,那就是 **性能瓶颈问题**,一个线程只能跑在一个 CPU 上,能处理的连接数是有限的,无法完全发挥多核 CPU 的优势。一旦某个业务逻辑耗时较长,这唯一的线程就会卡在上面,无法处理其他连接的请求,程序进入假死的状态,可用性也就降低了。正是由于这种限制,一般只会在 **客户端** 使用这种线程模型。 #### 2. 单 Reactor 多线程 -在单 Reactor 多线程的架构中,Reactor 监控到客户端请求之后,如果连接建立的请求,则由Acceptor 通过 accept 处理,然后创建一个 Handler 对象处理连接建立之后的业务请求。如果不是连接建立请求,则 Reactor 会将事件分发给调用连接对应的 Handler 来处理。到此为止,该流程与单 Reactor 单线程的模型基本一致, **唯一的区别就是执行 Handler 逻辑的线程隶属于一个线程池** 。 +在单 Reactor 多线程的架构中,Reactor 监控到客户端请求之后,如果连接建立的请求,则由Acceptor 通过 accept 处理,然后创建一个 Handler 对象处理连接建立之后的业务请求。如果不是连接建立请求,则 Reactor 会将事件分发给调用连接对应的 Handler 来处理。到此为止,该流程与单 Reactor 单线程的模型基本一致,**唯一的区别就是执行 Handler 逻辑的线程隶属于一个线程池** 。 ![8.png](assets/CgqCHl9EvWqAJ5jpAAFbymUVJ8o272.png) @@ -81,7 +81,7 @@ Reactor 对象监听客户端请求事件,收到事件后通过 Dispatch 进 主从 Reactor 多线程模型 -主从 Reactor 多线程的设计模式解决了单一 Reactor 的瓶颈。 **主从 Reactor 职责明确,主 Reactor 只负责监听连接建立事件,SubReactor只负责监听读写事件** 。整个主从 Reactor 多线程架构充分利用了多核 CPU 的优势,可以支持扩展,而且与具体的业务逻辑充分解耦,复用性高。但不足的地方是,在交互上略显复杂,需要一定的编程门槛。 +主从 Reactor 多线程的设计模式解决了单一 Reactor 的瓶颈。**主从 Reactor 职责明确,主 Reactor 只负责监听连接建立事件,SubReactor只负责监听读写事件** 。整个主从 Reactor 多线程架构充分利用了多核 CPU 的优势,可以支持扩展,而且与具体的业务逻辑充分解耦,复用性高。但不足的地方是,在交互上略显复杂,需要一定的编程门槛。 #### 4. Netty 线程模型 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" index 4218db4c3..f6fbf78e5 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" @@ -18,7 +18,7 @@ Netty 也支持同步 I/O 操作,但在实践中几乎不使用。绝大多数 另外,Channel 还提供了检测当前网络连接状态等功能,这些可以帮助我们实现网络异常断开后自动重连的功能。 -### Selector **Selector 是对多路复用器的抽象** ,也是 Java NIO 的核心基础组件之一。Netty 就是基于 Selector 对象实现 I/O 多路复用的,在 Selector 内部,会通过系统调用不断地查询这些注册在其上的 Channel 是否有已就绪的 I/O 事件,例如,可读事件(OP_READ)、可写事件(OP_WRITE)或是网络连接事件(OP_ACCEPT)等,而无须使用用户线程进行轮询。这样,我们就可以用一个线程监听多个 Channel 上发生的事件 +### Selector **Selector 是对多路复用器的抽象**,也是 Java NIO 的核心基础组件之一。Netty 就是基于 Selector 对象实现 I/O 多路复用的,在 Selector 内部,会通过系统调用不断地查询这些注册在其上的 Channel 是否有已就绪的 I/O 事件,例如,可读事件(OP_READ)、可写事件(OP_WRITE)或是网络连接事件(OP_ACCEPT)等,而无须使用用户线程进行轮询。这样,我们就可以用一个线程监听多个 Channel 上发生的事件 ### ChannelPipeline&ChannelHandler @@ -46,17 +46,17 @@ p.addLast("5", new InboundOutboundHandlerX()); **可见,入站(Inbound)与出站(Outbound)事件处理顺序正好相反。** **入站(Inbound)事件一般由 I/O 线程触发** 。举个例子,我们自定义了一种消息协议,一条完整的消息是由消息头和消息体两部分组成,其中消息头会含有消息类型、控制位、数据长度等元数据,消息体则包含了真正传输的数据。在面对一块较大的数据时,客户端一般会将数据切分成多条消息发送,服务端接收到数据后,一般会先进行解码和缓存,待收集到长度足够的字节数据,组装成有固定含义的消息之后,才会传递给下一个 ChannelInboudHandler 进行后续处理。 -在 Netty 中就提供了很多 Encoder 的实现用来解码读取到的数据,Encoder 会处理多次 channelRead() 事件,等拿到有意义的数据之后,才会触发一次下一个 ChannelInboundHandler 的 channelRead() 方法。 **出站(Outbound)事件与入站(Inbound)事件相反,一般是由用户触发的。** ChannelHandler 接口中并没有定义方法来处理事件,而是由其子类进行处理的,如下图所示,ChannelInboundHandler 拦截并处理入站事件,ChannelOutboundHandler 拦截并处理出站事件。 +在 Netty 中就提供了很多 Encoder 的实现用来解码读取到的数据,Encoder 会处理多次 channelRead() 事件,等拿到有意义的数据之后,才会触发一次下一个 ChannelInboundHandler 的 channelRead() 方法。**出站(Outbound)事件与入站(Inbound)事件相反,一般是由用户触发的。** ChannelHandler 接口中并没有定义方法来处理事件,而是由其子类进行处理的,如下图所示,ChannelInboundHandler 拦截并处理入站事件,ChannelOutboundHandler 拦截并处理出站事件。 ![Drawing 1.png](assets/Ciqc1F9IlmmABbbRAADcN9APiZs099.png) Netty 提供的 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 主要是帮助完成事件流转功能的,即自动调用传递事件的相应方法。这样,我们在自定义 ChannelHandler 实现类的时候,就可以直接继承相应的 Adapter 类,并覆盖需要的事件处理方法,其他不关心的事件方法直接使用默认实现即可,从而提高开发效率。 -ChannelHandler 中的很多方法都需要一个 ChannelHandlerContext 类型的参数,ChannelHandlerContext 抽象的是 ChannleHandler 之间的关系以及 ChannelHandler 与ChannelPipeline 之间的关系。 **ChannelPipeline 中的事件传播主要依赖于ChannelHandlerContext 实现** ,在 ChannelHandlerContext 中维护了 ChannelHandler 之间的关系,所以我们可以从 ChannelHandlerContext 中得到当前 ChannelHandler 的后继节点,从而将事件传播到后续的 ChannelHandler。 +ChannelHandler 中的很多方法都需要一个 ChannelHandlerContext 类型的参数,ChannelHandlerContext 抽象的是 ChannleHandler 之间的关系以及 ChannelHandler 与ChannelPipeline 之间的关系。**ChannelPipeline 中的事件传播主要依赖于ChannelHandlerContext 实现**,在 ChannelHandlerContext 中维护了 ChannelHandler 之间的关系,所以我们可以从 ChannelHandlerContext 中得到当前 ChannelHandler 的后继节点,从而将事件传播到后续的 ChannelHandler。 ChannelHandlerContext 继承了 AttributeMap,所以提供了 attr() 方法设置和删除一些状态属性信息,我们可将业务逻辑中所需使用的状态属性值存入到 ChannelHandlerContext 中,然后这些属性就可以随它传播了。Channel 中也维护了一个 AttributeMap,与 ChannelHandlerContext 中的 AttributeMap,从 Netty 4.1 开始,都是作用于整个 ChannelPipeline。 -通过上述分析,我们可以了解到, **一个 Channel 对应一个 ChannelPipeline,一个 ChannelHandlerContext 对应一个ChannelHandler。** 如下图所示: +通过上述分析,我们可以了解到,**一个 Channel 对应一个 ChannelPipeline,一个 ChannelHandlerContext 对应一个ChannelHandler。** 如下图所示: ![1.png](assets/CgqCHl9Ixi-APR5UAADY4pM97IU060.png) @@ -78,7 +78,7 @@ ChannelHandlerContext 继承了 AttributeMap,所以提供了 attr() 方法设 - **定时任务队列** 。当用户在非 I/O 线程产生定时操作时,Netty 将用户的定时操作封装成定时任务,并将其放入该定时任务队列中等待相应 NioEventLoop 串行执行。 -到这里我们可以看出, **NioEventLoop 主要做三件事:监听 I/O 事件、执行普通任务以及执行定时任务** 。NioEventLoop 到底分配多少时间在不同类型的任务上,是可以配置的。另外,为了防止 NioEventLoop 长时间阻塞在一个任务上,一般会将耗时的操作提交到其他业务线程池处理。 +到这里我们可以看出,**NioEventLoop 主要做三件事:监听 I/O 事件、执行普通任务以及执行定时任务** 。NioEventLoop 到底分配多少时间在不同类型的任务上,是可以配置的。另外,为了防止 NioEventLoop 长时间阻塞在一个任务上,一般会将耗时的操作提交到其他业务线程池处理。 ### NioEventLoopGroup **NioEventLoopGroup 表示的是一组 NioEventLoop** 。Netty 为了能更充分地利用多核 CPU 资源,一般会有多个 NioEventLoop 同时工作,至于多少线程可由用户决定,Netty 会根据实际上的处理器核数计算一个默认值,具体计算公式是:CPU 的核心数 * 2,当然我们也可以根据实际情况手动调整 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" index 3d56bb0bf..e8323ee70 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" @@ -8,7 +8,7 @@ RPC 是“远程过程调用(Remote Procedure Call)”的缩写形式,比 简易 RPC 框架的架构图 -本课时我们主要实现 **RPC 框架的基石部分——远程调用** ,简易版 RPC 框架一次远程调用的核心流程是这样的: +本课时我们主要实现 **RPC 框架的基石部分——远程调用**,简易版 RPC 框架一次远程调用的核心流程是这样的: 1. Client 首先会调用本地的代理,也就是图中的 Proxy。 1. Client 端 Proxy 会按照协议(Protocol),将调用中传入的数据序列化成字节流。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" index 7e93b7cf3..fca2ac4cc 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" @@ -118,7 +118,7 @@ public class Connection implements Closeable { 我们可以看到,Connection 中没有定时清理 IN_FLIGHT_REQUEST_MAP 集合的操作,在无法正常获取响应的时候,就会导致 IN_FLIGHT_REQUEST_MAP 不断膨胀,最终 OOM。你也可以添加一个时间轮定时器,定时清理过期的请求消息,这里我们就不再展开讲述了。 -完成自定义 ChannelHandler 的编写之后,我们需要再定义两个类—— DemoRpcClient 和 DemoRpcServer,分别作为 Client 和 Server 的启动入口。 **DemoRpcClient 的实现如下:** +完成自定义 ChannelHandler 的编写之后,我们需要再定义两个类—— DemoRpcClient 和 DemoRpcServer,分别作为 Client 和 Server 的启动入口。**DemoRpcClient 的实现如下:** ```java public class DemoRpcClient implements Closeable { @@ -429,7 +429,7 @@ System.out.println(result); ``` ### 总结 -本课时我们首先介绍了简易 RPC 框架中的 **transport 包** ,它在上一课时介绍的编解码器基础之上, **实现了服务端和客户端的通信能力** 。之后讲解了 **registry 包** 如何实现与 ZooKeeper 的交互, **完善了简易 RPC 框架的服务注册与服务发现的能力** 。接下来又分析了 **proxy 包** 的实现,其中通过 JDK 动态代理的方式, **帮接入方屏蔽了底层网络通信的复杂性** 。最后,我们编写了一个简单的 DemoService 业务接口,以及相应的 Provider 和 Consumer 接入简易 RPC 框架。 +本课时我们首先介绍了简易 RPC 框架中的 **transport 包**,它在上一课时介绍的编解码器基础之上,**实现了服务端和客户端的通信能力** 。之后讲解了 **registry 包** 如何实现与 ZooKeeper 的交互,**完善了简易 RPC 框架的服务注册与服务发现的能力** 。接下来又分析了 **proxy 包** 的实现,其中通过 JDK 动态代理的方式,**帮接入方屏蔽了底层网络通信的复杂性** 。最后,我们编写了一个简单的 DemoService 业务接口,以及相应的 Provider 和 Consumer 接入简易 RPC 框架。 在本课时最后,留给你一个小问题:在 transport 中创建 EventLoopGroup 的时候,为什么针对 Linux 系统使用的 EventLoopGroup会有所不同呢?期待你的留言。 简易版 RPC 框架 Demo 的链接:[https://github.com/xxxlxy2008/demo-prc](https://github.com/xxxlxy2008/demo-prc) 。 ``` diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25413\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25413\350\256\262.md" index 69259fc86..0583cea76 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25413\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25413\350\256\262.md" @@ -1,6 +1,6 @@ # 13 本地缓存:降低 ZooKeeper 压力的一个常用手段 -从这一课时开始,我们就进入了第二部分:注册中心。注册中心(Registry)在微服务架构中的作用举足轻重,有了它, **服务提供者(Provider)** 和 **消费者(Consumer)** 就能感知彼此。从下面的 Dubbo 架构图中可知: +从这一课时开始,我们就进入了第二部分:注册中心。注册中心(Registry)在微服务架构中的作用举足轻重,有了它,**服务提供者(Provider)** 和 **消费者(Consumer)** 就能感知彼此。从下面的 Dubbo 架构图中可知: ![Drawing 0.png](assets/CgqCHl9W91KABCfoAAB7_C-aKWA893.png) @@ -12,7 +12,7 @@ Dubbo 架构图 Registry 只是 Consumer 和 Provider 感知彼此状态变化的一种便捷途径而已,它们彼此的实际通讯交互过程是直接进行的,对于 Registry 来说是透明无感的。Provider 状态发生变化了,会由 Registry 主动推送订阅了该 Provider 的所有 Consumer,这保证了 Consumer 感知 Provider 状态变化的及时性,也将和具体业务需求逻辑交互解耦,提升了系统的稳定性。 -Dubbo 中存在很多概念,但有些理解起来就特别费劲,如本文的 Registry,翻译过来的意思是“注册中心”,但它其实是应用本地的注册中心客户端, **真正的“注册中心”服务是其他独立部署的进程,或进程组成的集群,比如 ZooKeeper 集群** 。本地的 Registry 通过和 ZooKeeper 等进行实时的信息同步,维持这些内容的一致性,从而实现了注册中心这个特性。另外,就 Registry 而言,Consumer 和 Provider 只是个用户视角的概念,它们被抽象为了一条 URL 。 +Dubbo 中存在很多概念,但有些理解起来就特别费劲,如本文的 Registry,翻译过来的意思是“注册中心”,但它其实是应用本地的注册中心客户端,**真正的“注册中心”服务是其他独立部署的进程,或进程组成的集群,比如 ZooKeeper 集群** 。本地的 Registry 通过和 ZooKeeper 等进行实时的信息同步,维持这些内容的一致性,从而实现了注册中心这个特性。另外,就 Registry 而言,Consumer 和 Provider 只是个用户视角的概念,它们被抽象为了一条 URL 。 从本课时开始,我们就真正开始分析 Dubbo 源码了。首先看一下本课程第二部分内容在 Dubbo 架构中所处的位置(如下图红框所示),可以看到这部分内容在整个 Dubbo 体系中还是相对独立的,没有涉及 Protocol、Invoker 等 Dubbo 内部的概念。等介绍完这些概念之后,我们还会回看图中 Registry 红框之外的内容。 @@ -26,7 +26,7 @@ Dubbo 中存在很多概念,但有些理解起来就特别费劲,如本文 ![Drawing 2.png](assets/Ciqc1F9W94aAIB3iAAE7RxqxFDw401.png) -在 Dubbo 中,一般使用 Node 这个接口来抽象节点的概念。 **Node** 不仅可以表示 Provider 和 Consumer 节点,还可以表示注册中心节点。Node 接口中定义了三个非常基础的方法(如下图所示): +在 Dubbo 中,一般使用 Node 这个接口来抽象节点的概念。**Node** 不仅可以表示 Provider 和 Consumer 节点,还可以表示注册中心节点。Node 接口中定义了三个非常基础的方法(如下图所示): ![Drawing 3.png](assets/Ciqc1F9W942AJdaYAAAlxcqD4vE542.png) @@ -70,7 +70,7 @@ AbstractRegistryFactory 是一个实现了 RegistryFactory 接口的抽象类, ### AbstractRegistry -AbstractRegistry 实现了 Registry 接口,虽然 AbstractRegistry 本身在内存中实现了注册数据的读写功能,也没有什么抽象方法,但它依然被标记成了抽象类,从前面的Registry 继承关系图中可以看出, **Registry 接口的所有实现类都继承了 AbstractRegistry** 。 +AbstractRegistry 实现了 Registry 接口,虽然 AbstractRegistry 本身在内存中实现了注册数据的读写功能,也没有什么抽象方法,但它依然被标记成了抽象类,从前面的Registry 继承关系图中可以看出,**Registry 接口的所有实现类都继承了 AbstractRegistry** 。 为了减轻注册中心组件的压力,AbstractRegistry 会把当前节点订阅的 URL 信息缓存到本地的 Properties 文件中,其核心字段如下: @@ -146,7 +146,7 @@ protected void notify(URL url, NotifyListener listener, **AbstractRegistry 的核心是本地文件缓存的功能。** 在 AbstractRegistry 的构造方法中,会调用 loadProperties() 方法将上面写入的本地缓存文件,加载到 properties 对象中。 -在网络抖动等原因而导致订阅失败时,Consumer 端的 Registry 就可以调用 getCacheUrls() 方法获取本地缓存,从而得到最近注册的 Provider URL。可见, **AbstractRegistry 通过本地缓存提供了一种容错机制,保证了服务的可靠性** 。 +在网络抖动等原因而导致订阅失败时,Consumer 端的 Registry 就可以调用 getCacheUrls() 方法获取本地缓存,从而得到最近注册的 Provider URL。可见,**AbstractRegistry 通过本地缓存提供了一种容错机制,保证了服务的可靠性** 。 #### 2\. 注册/订阅 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25414\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25414\350\256\262.md" index 772ef611c..20982b932 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25414\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25414\350\256\262.md" @@ -1,12 +1,12 @@ # 14 重试机制是网络操作的基本保证 -在真实的微服务系统中, ZooKeeper、etcd 等服务发现组件一般会独立部署成一个集群,业务服务通过网络连接这些服务发现节点,完成注册和订阅操作。但即使是机房内部的稳定网络,也无法保证两个节点之间的请求一定成功,因此 Dubbo 这类 RPC 框架在稳定性和容错性方面,就受到了比较大的挑战。 **为了保证服务的可靠性,重试机制就变得必不可少了** 。 +在真实的微服务系统中, ZooKeeper、etcd 等服务发现组件一般会独立部署成一个集群,业务服务通过网络连接这些服务发现节点,完成注册和订阅操作。但即使是机房内部的稳定网络,也无法保证两个节点之间的请求一定成功,因此 Dubbo 这类 RPC 框架在稳定性和容错性方面,就受到了比较大的挑战。**为了保证服务的可靠性,重试机制就变得必不可少了** 。 所谓的 **“重试机制”就是在请求失败时,客户端重新发起一个一模一样的请求,尝试调用相同或不同的服务端,完成相应的业务操作** 。能够使用重试机制的业务接口得是“幂等”的,也就是无论请求发送多少次,得到的结果都是一样的,例如查询操作。 ### 核心设计 -在上一课时中,我们介绍了 AbstractRegistry 中的 register()/unregister()、subscribe()/unsubscribe() 以及 notify() 等核心操作,详细分析了通过 **本地缓存** 实现的容错功能。其实,这几个核心方法同样也是 **重试机制** 的关注点。 **dubbo-registry 将重试机制的相关实现放到了 AbstractRegistry 的子类—— FailbackRegistry 中** 。如下图所示,接入 ZooKeeper、etcd 等开源服务发现组件的 Registry 实现,都继承了 FailbackRegistry,也就都拥有了失败重试的能力。 +在上一课时中,我们介绍了 AbstractRegistry 中的 register()/unregister()、subscribe()/unsubscribe() 以及 notify() 等核心操作,详细分析了通过 **本地缓存** 实现的容错功能。其实,这几个核心方法同样也是 **重试机制** 的关注点。**dubbo-registry 将重试机制的相关实现放到了 AbstractRegistry 的子类—— FailbackRegistry 中** 。如下图所示,接入 ZooKeeper、etcd 等开源服务发现组件的 Registry 实现,都继承了 FailbackRegistry,也就都拥有了失败重试的能力。 ![Registry继承关系.png](assets/Ciqc1F9bIqGAH2BVAAHKapYWDoE565.png) **FailbackRegistry 设计核心是** :覆盖了 AbstractRegistry 中 register()/unregister()、subscribe()/unsubscribe() 以及 notify() 这五个核心方法,结合前面介绍的时间轮,实现失败重试的能力;真正与服务发现组件的交互能力则是放到了 doRegister()/doUnregister()、doSubscribe()/doUnsubscribe() 以及 doNotify() 这五个抽象方法中,由具体子类实现。这是典型的模板方法模式的应用。 @@ -70,7 +70,7 @@ public void register(URL url) { 从以上代码可以看出,当 Provider 向 Registry 注册 URL 的时候,如果注册失败,且未设置 check 属性,则创建一个定时任务,添加到时间轮中。 -下面我们再来看看创建并添加这个重试任务的相关方法—— **addFailedRegistered() 方法** ,具体实现如下: +下面我们再来看看创建并添加这个重试任务的相关方法—— **addFailedRegistered() 方法**,具体实现如下: ```java private void addFailedRegistered(URL url) { @@ -91,7 +91,7 @@ private void addFailedRegistered(URL url) { ### 重试任务 -FailbackRegistry.addFailedRegistered() 方法中创建的 FailedRegisteredTask 任务以及其他的重试任务, **都继承了 AbstractRetryTask 抽象类** ,如下图所示: +FailbackRegistry.addFailedRegistered() 方法中创建的 FailedRegisteredTask 任务以及其他的重试任务,**都继承了 AbstractRetryTask 抽象类**,如下图所示: ![重试任务.png](assets/CgqCHl9bIseASX_6AAEchEJzpew190.png) diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25415\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25415\350\256\262.md" index 8db38e2a9..30a4949fb 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25415\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25415\350\256\262.md" @@ -38,7 +38,7 @@ ZookeeperRegistryFactory 实现了 AbstractRegistryFactory,其中的 createReg dubbo-remoting-zookeeper 模块是 dubbo-remoting 模块的子模块,但它并不依赖 dubbo-remoting 中的其他模块,是相对独立的,所以这里我们可以直接介绍该模块。 -简单来说,dubbo-remoting-zookeeper 模块是在 Apache Curator 的基础上封装了一套 Zookeeper 客户端,将与 Zookeeper 的交互融合到 Dubbo 的体系之中。 **dubbo-remoting-zookeeper 模块中有两个核心接口:ZookeeperTransporter 接口和 ZookeeperClient 接口。** +简单来说,dubbo-remoting-zookeeper 模块是在 Apache Curator 的基础上封装了一套 Zookeeper 客户端,将与 Zookeeper 的交互融合到 Dubbo 的体系之中。**dubbo-remoting-zookeeper 模块中有两个核心接口:ZookeeperTransporter 接口和 ZookeeperClient 接口。** ZookeeperTransporter 只负责一件事情,那就是创建 ZookeeperClient 对象。 @@ -92,7 +92,7 @@ ZookeeperClient 实例连接到 Zookeeper 集群之后,就可以了解整个 Z - add_Listener() / remove_Listener() 方法:添加/删除监听器。 - close() 方法:关闭当前 ZookeeperClient 实例。 -**AbstractZookeeperClient 作为 ZookeeperClient 接口的抽象实现** ,主要提供了如下几项能力: +**AbstractZookeeperClient 作为 ZookeeperClient 接口的抽象实现**,主要提供了如下几项能力: - 缓存当前 ZookeeperClient 实例创建的持久 ZNode 节点; - 管理当前 ZookeeperClient 实例添加的各类监听器; @@ -137,7 +137,7 @@ public void addDataListener(String path, > 虽然在 Dubbo 2.7.7 版本中只支持 Curator,但是在 Dubbo 2.6.5 版本的源码中可以看到,ZookeeperClient 还有使用 ZkClient 的实现。 -**在最新的 Dubbo 版本中,CuratorZookeeperClient 是 AbstractZookeeperClient 的唯一实现类** ,在其构造方法中会初始化 Curator 客户端并阻塞等待连接成功: +**在最新的 Dubbo 版本中,CuratorZookeeperClient 是 AbstractZookeeperClient 的唯一实现类**,在其构造方法中会初始化 Curator 客户端并阻塞等待连接成功: ```java public CuratorZookeeperClient(URL url) { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25416\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25416\350\256\262.md" index 60ff974b3..d6087c306 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25416\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25416\350\256\262.md" @@ -17,7 +17,7 @@ public class Student implements Serializable { } ``` -在这个示例中我们可以看到 **transient 关键字** ,它的作用就是: **在对象序列化过程中忽略被其修饰的成员属性变量** 。一般情况下,它可以用来修饰一些非数据型的字段以及一些可以通过其他字段计算得到的值。通过合理地使用 transient 关键字,可以降低序列化后的数据量,提高网络传输效率。 +在这个示例中我们可以看到 **transient 关键字**,它的作用就是: **在对象序列化过程中忽略被其修饰的成员属性变量** 。一般情况下,它可以用来修饰一些非数据型的字段以及一些可以通过其他字段计算得到的值。通过合理地使用 transient 关键字,可以降低序列化后的数据量,提高网络传输效率。 第二步,生成一个序列号 serialVersionUID,这个序列号不是必需的,但还是建议你生成。serialVersionUID 的字面含义是序列化的版本号,只有序列化和反序列化的 serialVersionUID 都相同的情况下,才能够成功地反序列化。如果类中没有定义 serialVersionUID,那么 JDK 也会随机生成一个 serialVersionUID。如果在某些场景中,你希望不同版本的类序列化和反序列化相互兼容,那就需要定义相同的 serialVersionUID。 @@ -31,7 +31,7 @@ public class Student implements Serializable { 为了帮助你快速了解 Dubbo 支持的序列化算法,我们这里就对其中常见的序列化算法进行简单介绍。 -**Apache Avro 是一种与编程语言无关的序列化格式** 。Avro 依赖于用户自定义的 Schema,在进行序列化数据的时候,无须多余的开销,就可以快速完成序列化,并且生成的序列化数据也较小。当进行反序列化的时候,需要获取到写入数据时用到的 Schema。在 Kafka、Hadoop 以及 Dubbo 中都可以使用 Avro 作为序列化方案。 **FastJson 是阿里开源的 JSON 解析库,可以解析 JSON 格式的字符串** 。它支持将 Java 对象序列化为 JSON 字符串,反过来从 JSON 字符串也可以反序列化为 Java 对象。FastJson 是 Java 程序员常用到的类库之一,正如其名,“快”是其主要卖点。从官方的测试结果来看,FastJson 确实是最快的,比 Jackson 快 20% 左右,但是近几年 FastJson 的安全漏洞比较多,所以你在选择版本的时候,还是需要谨慎一些。 **Fst(全称是 fast-serialization)是一款高性能 Java 对象序列化工具包** ,100% 兼容 JDK 原生环境,序列化速度大概是JDK 原生序列化的 4~10 倍,序列化后的数据大小是 JDK 原生序列化大小的 1/3 左右。目前,Fst 已经更新到 3.x 版本,支持 JDK 14。 **Kryo 是一个高效的 Java 序列化/反序列化库** ,目前 Twitter、Yahoo、Apache 等都在使用该序列化技术,特别是 Spark、Hive 等大数据领域用得较多。Kryo 提供了一套快速、高效和易用的序列化 API。无论是数据库存储,还是网络传输,都可以使用 Kryo 完成 Java 对象的序列化。Kryo 还可以执行自动深拷贝和浅拷贝,支持环形引用。Kryo 的特点是 API 代码简单,序列化速度快,并且序列化之后得到的数据比较小。另外,Kryo 还提供了 NIO 的网络通信库——KryoNet,你若感兴趣的话可以自行查询和了解一下。 **Hessian2 序列化是一种支持动态类型、跨语言的序列化协议** ,Java 对象序列化的二进制流可以被其他语言使用。Hessian2 序列化之后的数据可以进行自描述,不会像 Avro 那样依赖外部的 Schema 描述文件或者接口定义。Hessian2 可以用一个字节表示常用的基础类型,这极大缩短了序列化之后的二进制流。需要注意的是,在 Dubbo 中使用的 Hessian2 序列化并不是原生的 Hessian2 序列化,而是阿里修改过的 Hessian Lite,它是 Dubbo 默认使用的序列化方式。其序列化之后的二进制流大小大约是 Java 序列化的 50%,序列化耗时大约是 Java 序列化的 30%,反序列化耗时大约是 Java 序列化的 20%。 **Protobuf(Google Protocol Buffers)是 Google 公司开发的一套灵活、高效、自动化的、用于对结构化数据进行序列化的协议** 。但相比于常用的 JSON 格式,Protobuf 有更高的转化效率,时间效率和空间效率都是 JSON 的 5 倍左右。Protobuf 可用于通信协议、数据存储等领域,它本身是语言无关、平台无关、可扩展的序列化结构数据格式。目前 Protobuf提供了 C++、Java、Python、Go 等多种语言的 API,gRPC 底层就是使用 Protobuf 实现的序列化。 +**Apache Avro 是一种与编程语言无关的序列化格式** 。Avro 依赖于用户自定义的 Schema,在进行序列化数据的时候,无须多余的开销,就可以快速完成序列化,并且生成的序列化数据也较小。当进行反序列化的时候,需要获取到写入数据时用到的 Schema。在 Kafka、Hadoop 以及 Dubbo 中都可以使用 Avro 作为序列化方案。**FastJson 是阿里开源的 JSON 解析库,可以解析 JSON 格式的字符串** 。它支持将 Java 对象序列化为 JSON 字符串,反过来从 JSON 字符串也可以反序列化为 Java 对象。FastJson 是 Java 程序员常用到的类库之一,正如其名,“快”是其主要卖点。从官方的测试结果来看,FastJson 确实是最快的,比 Jackson 快 20% 左右,但是近几年 FastJson 的安全漏洞比较多,所以你在选择版本的时候,还是需要谨慎一些。**Fst(全称是 fast-serialization)是一款高性能 Java 对象序列化工具包**,100% 兼容 JDK 原生环境,序列化速度大概是JDK 原生序列化的 4~10 倍,序列化后的数据大小是 JDK 原生序列化大小的 1/3 左右。目前,Fst 已经更新到 3.x 版本,支持 JDK 14。**Kryo 是一个高效的 Java 序列化/反序列化库**,目前 Twitter、Yahoo、Apache 等都在使用该序列化技术,特别是 Spark、Hive 等大数据领域用得较多。Kryo 提供了一套快速、高效和易用的序列化 API。无论是数据库存储,还是网络传输,都可以使用 Kryo 完成 Java 对象的序列化。Kryo 还可以执行自动深拷贝和浅拷贝,支持环形引用。Kryo 的特点是 API 代码简单,序列化速度快,并且序列化之后得到的数据比较小。另外,Kryo 还提供了 NIO 的网络通信库——KryoNet,你若感兴趣的话可以自行查询和了解一下。**Hessian2 序列化是一种支持动态类型、跨语言的序列化协议**,Java 对象序列化的二进制流可以被其他语言使用。Hessian2 序列化之后的数据可以进行自描述,不会像 Avro 那样依赖外部的 Schema 描述文件或者接口定义。Hessian2 可以用一个字节表示常用的基础类型,这极大缩短了序列化之后的二进制流。需要注意的是,在 Dubbo 中使用的 Hessian2 序列化并不是原生的 Hessian2 序列化,而是阿里修改过的 Hessian Lite,它是 Dubbo 默认使用的序列化方式。其序列化之后的二进制流大小大约是 Java 序列化的 50%,序列化耗时大约是 Java 序列化的 30%,反序列化耗时大约是 Java 序列化的 20%。**Protobuf(Google Protocol Buffers)是 Google 公司开发的一套灵活、高效、自动化的、用于对结构化数据进行序列化的协议** 。但相比于常用的 JSON 格式,Protobuf 有更高的转化效率,时间效率和空间效率都是 JSON 的 5 倍左右。Protobuf 可用于通信协议、数据存储等领域,它本身是语言无关、平台无关、可扩展的序列化结构数据格式。目前 Protobuf提供了 C++、Java、Python、Go 等多种语言的 API,gRPC 底层就是使用 Protobuf 实现的序列化。 ### dubbo-serialization @@ -63,7 +63,7 @@ Dubbo 提供了多个 Serialization 接口实现,用于接入各种各样的 ![Drawing 1.png](assets/CgqCHl9gbJKAFOslAAFjEeB7nf0890.png) -这里我们 **以默认的 hessian2 序列化方式为例** ,介绍 Serialization 接口的实现以及其他相关实现。 Hessian2Serialization 实现如下所示: +这里我们 **以默认的 hessian2 序列化方式为例**,介绍 Serialization 接口的实现以及其他相关实现。 Hessian2Serialization 实现如下所示: ```java public class Hessian2Serialization implements Serialization { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" index 9b4ebc53a..b4cb24979 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" @@ -16,7 +16,7 @@ Dubbo 并没有自己实现一套完整的网络库,而是使用现有的、 ### dubbo-remoting-api 模块 -需要注意的是, **Dubbo 的 dubbo-remoting-api 是其他 dubbo-remoting-* 模块的顶层抽象,其他 dubbo-remoting 子模块都是依赖第三方 NIO 库实现 dubbo-remoting-api 模块的** ,依赖关系如下图所示: +需要注意的是,**Dubbo 的 dubbo-remoting-api 是其他 dubbo-remoting-* 模块的顶层抽象,其他 dubbo-remoting 子模块都是依赖第三方 NIO 库实现 dubbo-remoting-api 模块的**,依赖关系如下图所示: ![Drawing 2.png](assets/CgqCHl9ptY2ADzl8AAEVDPN3HVo908.png) @@ -35,7 +35,7 @@ Dubbo 并没有自己实现一套完整的网络库,而是使用现有的、 ### 传输层核心接口 -在 Dubbo 中会抽象出一个“ **端点(Endpoint)** ”的概念,我们可以通过一个 ip 和 port 唯一确定一个端点,两个端点之间会创建 TCP 连接,可以双向传输数据。Dubbo 将 Endpoint 之间的 TCP 连接抽象为 **通道(Channel)** ,将发起请求的 Endpoint 抽象为 **客户端(Client)** ,将接收请求的 Endpoint 抽象为 **服务端(Server)** 。这些抽象出来的概念,也是整个 dubbo-remoting-api 模块的基础,下面我们会逐个进行介绍。 +在 Dubbo 中会抽象出一个“ **端点(Endpoint)** ”的概念,我们可以通过一个 ip 和 port 唯一确定一个端点,两个端点之间会创建 TCP 连接,可以双向传输数据。Dubbo 将 Endpoint 之间的 TCP 连接抽象为 **通道(Channel)**,将发起请求的 Endpoint 抽象为 **客户端(Client)**,将接收请求的 Endpoint 抽象为 **服务端(Server)** 。这些抽象出来的概念,也是整个 dubbo-remoting-api 模块的基础,下面我们会逐个进行介绍。 Dubbo 中 **Endpoint 接口** 的定义如下: @@ -51,7 +51,7 @@ Channel 是对两个 Endpoint 连接的抽象,好比连接两个位置的传 ![Drawing 5.png](assets/Ciqc1F9ptfKAeNrwAADvN7mxisw072.png) -**ChannelHandler 是注册在 Channel 上的消息处理器** ,在 Netty 中也有类似的抽象,相信你对此应该不会陌生。下图展示了 ChannelHandler 接口的定义,在 ChannelHandler 中可以处理 Channel 的连接建立以及连接断开事件,还可以处理读取到的数据、发送的数据以及捕获到的异常。从这些方法的命名可以看到,它们都是动词的过去式,说明相应事件已经发生过了。 +**ChannelHandler 是注册在 Channel 上的消息处理器**,在 Netty 中也有类似的抽象,相信你对此应该不会陌生。下图展示了 ChannelHandler 接口的定义,在 ChannelHandler 中可以处理 Channel 的连接建立以及连接断开事件,还可以处理读取到的数据、发送的数据以及捕获到的异常。从这些方法的命名可以看到,它们都是动词的过去式,说明相应事件已经发生过了。 ![Drawing 6.png](assets/CgqCHl9ptf-AM7HwAABIy1ahqFw153.png) @@ -78,7 +78,7 @@ public interface Codec2 { DecodeResult 这个枚举是在处理 TCP 传输时粘包和拆包使用的,之前简易版本 RPC 也处理过这种问题,例如,当前能读取到的数据不足以构成一个消息时,就会使用 NEED_MORE_INPUT 这个枚举。 -接下来看 **Client 和 RemotingServer 两个接口** ,分别抽象了客户端和服务端,两者都继承了 Channel、Resetable 等接口,也就是说两者都具备了读写数据能力。 +接下来看 **Client 和 RemotingServer 两个接口**,分别抽象了客户端和服务端,两者都继承了 Channel、Resetable 等接口,也就是说两者都具备了读写数据能力。 ![Drawing 7.png](assets/CgqCHl9ptgaAPRDbAAA7kgy1X5k082.png) @@ -86,7 +86,7 @@ Client 和 Server 本身都是 Endpoint,只不过在语义上区分了请求 ![Drawing 8.png](assets/Ciqc1F9pthSAPWv0AAA0yX1lW-Y033.png) -Dubbo 在 Client 和 Server 之上又封装了一层 **Transporter 接口** ,其具体定义如下: +Dubbo 在 Client 和 Server 之上又封装了一层 **Transporter 接口**,其具体定义如下: ```plaintext @SPI("netty") @@ -116,7 +116,7 @@ Transporter 接口的实现有哪些呢?如下图所示,针对每个支持 有了 Transporter 层之后,我们可以通过 Dubbo SPI 修改使用的具体 Transporter 扩展实现,从而切换到不同的 Client 和 RemotingServer 实现,达到底层 NIO 库切换的目的,而且无须修改任何代码。即使有更先进的 NIO 库出现,我们也只需要开发相应的 dubbo-remoting-\* 实现模块提供 Transporter、Client、RemotingServer 等核心接口的实现,即可接入,完全符合开放-封闭原则。 -在最后,我们还要看一个类—— **Transporters** ,它不是一个接口,而是门面类,其中 **封装了 Transporter 对象的创建(通过 Dubbo SPI)以及 ChannelHandler 的处理** ,如下所示: +在最后,我们还要看一个类—— **Transporters**,它不是一个接口,而是门面类,其中 **封装了 Transporter 对象的创建(通过 Dubbo SPI)以及 ChannelHandler 的处理**,如下所示: ```java public class Transporters { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" index b6da1e153..df3cf066c 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" @@ -52,7 +52,7 @@ public void writeBytes(byte[] src, int srcIndex, int length) { 了解了 ChannelBuffer 接口的核心方法以及 AbstractChannelBuffer 的公共实现之后,我们再来看 ChannelBuffer 的具体实现。 -**HeapChannelBuffer 是基于字节数组的 ChannelBuffer 实现** ,我们可以看到其中有一个 array(byte\[\]数组)字段,它就是 HeapChannelBuffer 存储数据的地方。HeapChannelBuffer 的 setBytes() 以及 getBytes() 方法实现是调用 System.arraycopy() 方法完成数组操作的,具体实现如下: +**HeapChannelBuffer 是基于字节数组的 ChannelBuffer 实现**,我们可以看到其中有一个 array(byte\[\]数组)字段,它就是 HeapChannelBuffer 存储数据的地方。HeapChannelBuffer 的 setBytes() 以及 getBytes() 方法实现是调用 System.arraycopy() 方法完成数组操作的,具体实现如下: ```java public void setBytes(int index, byte[] src, int srcIndex, int length) { @@ -79,7 +79,7 @@ public ChannelBuffer getBuffer(byte[] array, int offset, int length) { } ``` -其他 getBuffer() 方法重载这里就不再展示,你若感兴趣的话可以参考源码进行学习。 **DynamicChannelBuffer 可以认为是其他 ChannelBuffer 的装饰器,它可以为其他 ChannelBuffer 添加动态扩展容量的功能** 。DynamicChannelBuffer 中有两个核心字段: +其他 getBuffer() 方法重载这里就不再展示,你若感兴趣的话可以参考源码进行学习。**DynamicChannelBuffer 可以认为是其他 ChannelBuffer 的装饰器,它可以为其他 ChannelBuffer 添加动态扩展容量的功能** 。DynamicChannelBuffer 中有两个核心字段: - buffer(ChannelBuffer 类型),是被修饰的 ChannelBuffer,默认为 HeapChannelBuffer。 - factory(ChannelBufferFactory 类型),用于创建被修饰的 HeapChannelBuffer 对象的 ChannelBufferFactory 工厂,默认为 HeapChannelBufferFactory。 @@ -111,7 +111,7 @@ public void ensureWritableBytes(int minWritableBytes) { } ``` -**ByteBufferBackedChannelBuffer 是基于 Java NIO 中 ByteBuffer 的 ChannelBuffer 实现** ,其中的方法基本都是通过组合 ByteBuffer 的 API 实现的。下面以 getBytes() 方法和 setBytes() 方法的一个重载为例,进行分析: +**ByteBufferBackedChannelBuffer 是基于 Java NIO 中 ByteBuffer 的 ChannelBuffer 实现**,其中的方法基本都是通过组合 ByteBuffer 的 API 实现的。下面以 getBytes() 方法和 setBytes() 方法的一个重载为例,进行分析: ```java public void getBytes(int index, byte[] dst, int dstIndex, int length) { @@ -134,7 +134,7 @@ public void setBytes(int index, byte[] src, int srcIndex, int length) { } ``` -ByteBufferBackedChannelBuffer 的其他方法实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。 **NettyBackedChannelBuffer 是基于 Netty 中 ByteBuf 的 ChannelBuffer 实现** ,Netty 中的 ByteBuf 内部维护了 readerIndex 和 writerIndex 以及 markedReaderIndex、markedWriterIndex 这四个索引,所以 NettyBackedChannelBuffer 没有再继承 AbstractChannelBuffer 抽象类,而是直接实现了 ChannelBuffer 接口。 +ByteBufferBackedChannelBuffer 的其他方法实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。**NettyBackedChannelBuffer 是基于 Netty 中 ByteBuf 的 ChannelBuffer 实现**,Netty 中的 ByteBuf 内部维护了 readerIndex 和 writerIndex 以及 markedReaderIndex、markedWriterIndex 这四个索引,所以 NettyBackedChannelBuffer 没有再继承 AbstractChannelBuffer 抽象类,而是直接实现了 ChannelBuffer 接口。 NettyBackedChannelBuffer 对 ChannelBuffer 接口的实现都是调用底层封装的 Netty ByteBuf 实现的,这里就不再展开介绍,你若感兴趣的话也可以参考相关代码进行学习。 @@ -146,7 +146,7 @@ NettyBackedChannelBuffer 对 ChannelBuffer 接口的实现都是调用底层封 ChannelBufferInputStream 底层封装了一个 ChannelBuffer,其实现 InputStream 接口的 read\*() 方法全部都是从 ChannelBuffer 中读取数据。ChannelBufferInputStream 中还维护了一个 startIndex 和一个endIndex 索引,作为读取数据的起止位置。ChannelBufferOutputStream 与 ChannelBufferInputStream 类似,会向底层的 ChannelBuffer 写入数据,这里就不再展开,你若感兴趣的话可以参考源码进行分析。 -最后要介绍 ChannelBuffers 这个 **门面类** ,下图展示了 ChannelBuffers 这个门面类的所有方法: +最后要介绍 ChannelBuffers 这个 **门面类**,下图展示了 ChannelBuffers 这个门面类的所有方法: ![Drawing 4.png](assets/CgqCHl9pukOAT_8kAACo0xRQ2po574.png) diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" index 6fa006d57..b73ad9718 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" @@ -280,7 +280,7 @@ NettyServer 模型 下面我们来逐个看看这四个 ChannelHandler 的核心功能。 -首先是 **decoder 和 encoder** ,它们都是 NettyCodecAdapter 的内部类,如下图所示,分别继承了 Netty 中的 ByteToMessageDecoder 和 MessageToByteEncoder: +首先是 **decoder 和 encoder**,它们都是 NettyCodecAdapter 的内部类,如下图所示,分别继承了 Netty 中的 ByteToMessageDecoder 和 MessageToByteEncoder: ![Drawing 4.png](assets/CgqCHl9wcESANfPCAABDUdzhtNU066.png) @@ -317,9 +317,9 @@ private class InternalDecoder extends ByteToMessageDecoder { InternalEncoder 的具体实现就不再展开讲解了,你若感兴趣可以翻看源码进行研究和分析。 -接下来是 **IdleStateHandler** ,它是 Netty 提供的一个工具型 ChannelHandler,用于定时心跳请求的功能或是自动关闭长时间空闲连接的功能。它的原理到底是怎样的呢?在 IdleStateHandler 中通过 lastReadTime、lastWriteTime 等几个字段,记录了最近一次读/写事件的时间,IdleStateHandler 初始化的时候,会创建一个定时任务,定时检测当前时间与最后一次读/写时间的差值。如果超过我们设置的阈值(也就是上面 NettyServer 中设置的 idleTimeout),就会触发 IdleStateEvent 事件,并传递给后续的 ChannelHandler 进行处理。后续 ChannelHandler 的 userEventTriggered() 方法会根据接收到的 IdleStateEvent 事件,决定是关闭长时间空闲的连接,还是发送心跳探活。 +接下来是 **IdleStateHandler**,它是 Netty 提供的一个工具型 ChannelHandler,用于定时心跳请求的功能或是自动关闭长时间空闲连接的功能。它的原理到底是怎样的呢?在 IdleStateHandler 中通过 lastReadTime、lastWriteTime 等几个字段,记录了最近一次读/写事件的时间,IdleStateHandler 初始化的时候,会创建一个定时任务,定时检测当前时间与最后一次读/写时间的差值。如果超过我们设置的阈值(也就是上面 NettyServer 中设置的 idleTimeout),就会触发 IdleStateEvent 事件,并传递给后续的 ChannelHandler 进行处理。后续 ChannelHandler 的 userEventTriggered() 方法会根据接收到的 IdleStateEvent 事件,决定是关闭长时间空闲的连接,还是发送心跳探活。 -最后来看 **NettyServerHandler** ,它继承了 ChannelDuplexHandler,这是 Netty 提供的一个同时处理 Inbound 数据和 Outbound 数据的 ChannelHandler,从下面的继承图就能看出来。 +最后来看 **NettyServerHandler**,它继承了 ChannelDuplexHandler,这是 Netty 提供的一个同时处理 Inbound 数据和 Outbound 数据的 ChannelHandler,从下面的继承图就能看出来。 ![Drawing 5.png](assets/Ciqc1F9wcFKAQQZ3AAB282frbWw282.png) diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" index da5d4dc46..a8edacc3b 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" @@ -131,9 +131,9 @@ ChannelHandler 继承关系图 **ChannelHandlerAdapter** 是 ChannelHandler 的一个空实现,TelnetHandlerAdapter 继承了它并实现了 TelnetHandler 接口。至于Dubbo 对 Telnet 的支持,我们会在后面的课时中单独介绍,这里就先不展开分析了。 -从名字上看, **ChannelHandlerDelegate** 接口是对另一个 ChannelHandler 对象的封装,它的两个实现类 AbstractChannelHandlerDelegate 和 WrappedChannelHandler 中也仅仅是封装了另一个 ChannelHandler 对象。 +从名字上看,**ChannelHandlerDelegate** 接口是对另一个 ChannelHandler 对象的封装,它的两个实现类 AbstractChannelHandlerDelegate 和 WrappedChannelHandler 中也仅仅是封装了另一个 ChannelHandler 对象。 -其中, **AbstractChannelHandlerDelegate** 有三个实现类,都比较简单,我们来逐个讲解。 +其中,**AbstractChannelHandlerDelegate** 有三个实现类,都比较简单,我们来逐个讲解。 - MultiMessageHandler:专门处理 MultiMessage 的 ChannelHandler 实现。MultiMessage 是 Exchange 层的一种消息类型,它其中封装了多个消息。在 MultiMessageHandler 收到 MultiMessage 消息的时候,received() 方法会遍历其中的所有消息,并交给底层的 ChannelHandler 对象进行处理。 - DecodeHandler:专门处理 Decodeable 的 ChannelHandler 实现。实现了 Decodeable 接口的类都会提供了一个 decode() 方法实现对自身的解码,DecodeHandler.received() 方法就是通过该方法得到解码后的消息,然后传递给底层的 ChannelHandler 对象继续处理。 @@ -164,7 +164,7 @@ public void received(Channel channel, Object message) throws RemotingException { #### Dispatcher 与 ChannelHandler -接下来,我们介绍 ChannelHandlerDelegate 接口的另一条继承线—— **WrappedChannelHandler** ,其子类主要是决定了 Dubbo 以何种线程模型处理收到的事件和消息,就是所谓的“消息派发机制”,与前面介绍的 ThreadPool 有紧密的联系。 +接下来,我们介绍 ChannelHandlerDelegate 接口的另一条继承线—— **WrappedChannelHandler**,其子类主要是决定了 Dubbo 以何种线程模型处理收到的事件和消息,就是所谓的“消息派发机制”,与前面介绍的 ThreadPool 有紧密的联系。 ![Drawing 3.png](assets/CgqCHl9wcTGAdInYAAJOSSxusf4539.png) @@ -181,7 +181,7 @@ public interface Dispatcher { } ``` -**AllDispatcher 创建的是 AllChannelHandler 对象** ,它会将所有网络事件以及消息交给关联的线程池进行处理。AllChannelHandler覆盖了 WrappedChannelHandler 中除了 sent() 方法之外的其他网络事件处理方法,将调用其底层的 ChannelHandler 的逻辑放到关联的线程池中执行。 +**AllDispatcher 创建的是 AllChannelHandler 对象**,它会将所有网络事件以及消息交给关联的线程池进行处理。AllChannelHandler覆盖了 WrappedChannelHandler 中除了 sent() 方法之外的其他网络事件处理方法,将调用其底层的 ChannelHandler 的逻辑放到关联的线程池中执行。 我们先来看 connect() 方法,其中会将CONNECTED 事件的处理封装成ChannelEventRunnable提交到线程池中执行,具体实现如下: @@ -312,7 +312,7 @@ Dubbo Consumer 同步请求线程模型 在这个设计里面,Consumer 端会维护一个线程池,而且线程池是按照连接隔离的,即每个连接独享一个线程池。这样,当面临需要消费大量服务且并发数比较大的场景时,例如,典型网关类场景,可能会导致 Consumer 端线程个数不断增加,导致线程调度消耗过多 CPU ,也可能因为线程创建过多而导致 OOM。 -为了解决上述问题,Dubbo 在 2.7.5 版本之后, **引入了 ThreadlessExecutor** ,将线程模型修改成了下图的样子: +为了解决上述问题,Dubbo 在 2.7.5 版本之后,**引入了 ThreadlessExecutor**,将线程模型修改成了下图的样子: ![Drawing 5.png](assets/CgqCHl9wcVCAQdJjAAFE8eFivcY750.png) diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" index 7997548e5..a0b8f8173 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" @@ -2,7 +2,7 @@ 在前面的课程中,我们深入介绍了 Dubbo Remoting 中的 Transport 层,了解了 Dubbo 抽象出来的端到端的统一传输层接口,并分析了以 Netty 为基础的相关实现。当然,其他 NIO 框架的接入也是类似的,本课程就不再展开赘述了。 -在本课时中,我们将介绍 Transport 层的上一层,也是 Dubbo Remoting 层中的最顶层—— Exchange 层。 **Dubbo 将信息交换行为抽象成 Exchange 层,官方文档对这一层的说明是:封装了请求-响应的语义,即关注一问一答的交互模式,实现了同步转异步** 。在 Exchange 这一层,以 Request 和 Response 为中心,针对 Channel、ChannelHandler、Client、RemotingServer 等接口进行实现。 +在本课时中,我们将介绍 Transport 层的上一层,也是 Dubbo Remoting 层中的最顶层—— Exchange 层。**Dubbo 将信息交换行为抽象成 Exchange 层,官方文档对这一层的说明是:封装了请求-响应的语义,即关注一问一答的交互模式,实现了同步转异步** 。在 Exchange 这一层,以 Request 和 Response 为中心,针对 Channel、ChannelHandler、Client、RemotingServer 等接口进行实现。 下面我们从 Request 和 Response 这一对基础类开始,依次介绍 Exchange 层中 ExchangeChannel、HeaderExchangeHandler 的核心实现。 @@ -60,7 +60,7 @@ ExchangeChannel 接口 HeaderExchangeChannel 继承关系图 -**从上图中可以看出,HeaderExchangeChannel 是 ExchangeChannel 的实现** ,它本身是 Channel 的装饰器,封装了一个 Channel 对象,其 send() 方法和 request() 方法的实现都是依赖底层修饰的这个 Channel 对象实现的。 +**从上图中可以看出,HeaderExchangeChannel 是 ExchangeChannel 的实现**,它本身是 Channel 的装饰器,封装了一个 Channel 对象,其 send() 方法和 request() 方法的实现都是依赖底层修饰的这个 Channel 对象实现的。 ```java public void send(Object message, boolean sent) throws RemotingException { @@ -202,7 +202,7 @@ private void notifyTimeout(DefaultFuture future) { 在前面介绍 DefaultFuture 时,我们简单说明了请求-响应的流程,其实无论是发送请求还是处理响应,都会涉及 HeaderExchangeHandler,所以这里我们就来介绍一下 HeaderExchangeHandler 的内容。 -**HeaderExchangeHandler 是 ExchangeHandler 的装饰器** ,其中维护了一个 ExchangeHandler 对象,ExchangeHandler 接口是 Exchange 层与上层交互的接口之一,上层调用方可以实现该接口完成自身的功能;然后再由 HeaderExchangeHandler 修饰,具备 Exchange 层处理 Request-Response 的能力;最后再由 Transport ChannelHandler 修饰,具备 Transport 层的能力。如下图所示: +**HeaderExchangeHandler 是 ExchangeHandler 的装饰器**,其中维护了一个 ExchangeHandler 对象,ExchangeHandler 接口是 Exchange 层与上层交互的接口之一,上层调用方可以实现该接口完成自身的功能;然后再由 HeaderExchangeHandler 修饰,具备 Exchange 层处理 Request-Response 的能力;最后再由 Transport ChannelHandler 修饰,具备 Transport 层的能力。如下图所示: ![Lark20201013-153600.png](assets/Ciqc1F-FWUqAVkr0AADiEwO4wK4124.png) @@ -259,7 +259,7 @@ void handleRequest(final ExchangeChannel channel, Request req) throws RemotingEx - 对于 Response 的处理,前文已提到了,HeaderExchangeHandler 会通过 **handleResponse() 方法** 将关联的 DefaultFuture 设置为完成状态(或是异常完成状态),具体内容这里不再展开讲述。 - 对于 String 类型的消息,HeaderExchangeHandler 会根据当前服务的角色进行分类,具体与 Dubbo 对 telnet 的支持相关,后面的课时会详细介绍,这里就不展开分析了。 -接下来我们再来看 **sent() 方法** ,该方法会通知上层 ExchangeHandler 实现的 sent() 方法,同时还会针对 Request 请求调用 DefaultFuture.sent() 方法记录请求的具体发送时间,该逻辑在前文也已经介绍过了,这里不再重复。 +接下来我们再来看 **sent() 方法**,该方法会通知上层 ExchangeHandler 实现的 sent() 方法,同时还会针对 Request 请求调用 DefaultFuture.sent() 方法记录请求的具体发送时间,该逻辑在前文也已经介绍过了,这里不再重复。 在 **connected() 方法** 中,会为 Dubbo Channel 创建相应的 HeaderExchangeChannel,并将两者绑定,然后通知上层 ExchangeHandler 处理 connect 事件。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" index 1fecded4c..e894adad0 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" @@ -202,7 +202,7 @@ public class HeaderExchanger implements Exchanger { ### 再谈 Codec2 -在前面第 17 课时介绍 Dubbo Remoting 核心接口的时候提到,Codec2 接口提供了 encode() 和 decode() 两个方法来实现消息与字节流之间的相互转换。需要注意与 DecodeHandler 区分开来, **DecodeHandler 是对请求体和响应结果的解码,Codec2 是对整个请求和响应的编解码** 。 +在前面第 17 课时介绍 Dubbo Remoting 核心接口的时候提到,Codec2 接口提供了 encode() 和 decode() 两个方法来实现消息与字节流之间的相互转换。需要注意与 DecodeHandler 区分开来,**DecodeHandler 是对请求体和响应结果的解码,Codec2 是对整个请求和响应的编解码** 。 这里重点介绍 Transport 层和 Exchange 层对 Codec2 接口的实现,涉及的类如下图所示: @@ -214,11 +214,11 @@ public class HeaderExchanger implements Exchanger { - checkPayload() 方法:检查编解码数据的长度,如果数据超长,会抛出异常。 - isClientSide()、isServerSide() 方法:判断当前是 Client 端还是 Server 端。 -接下来看 **TransportCodec** ,我们可以看到这类上被标记了 @Deprecated 注解,表示已经废弃。TransportCodec 的实现非常简单,其中根据 getSerialization() 方法选择的序列化方法对传入消息或 ChannelBuffer 进行序列化或反序列化,这里就不再介绍 TransportCodec 实现了。 +接下来看 **TransportCodec**,我们可以看到这类上被标记了 @Deprecated 注解,表示已经废弃。TransportCodec 的实现非常简单,其中根据 getSerialization() 方法选择的序列化方法对传入消息或 ChannelBuffer 进行序列化或反序列化,这里就不再介绍 TransportCodec 实现了。 **TelnetCodec** 继承了 TransportCodec 序列化和反序列化的基本能力,同时还提供了对 Telnet 命令处理的能力。 -最后来看 **ExchangeCodec** ,它在 TelnetCodec 的基础之上,添加了处理协议头的能力。下面是 Dubbo 协议的格式,能够清晰地看出协议中各个数据所占的位数: +最后来看 **ExchangeCodec**,它在 TelnetCodec 的基础之上,添加了处理协议头的能力。下面是 Dubbo 协议的格式,能够清晰地看出协议中各个数据所占的位数: ![Drawing 5.png](assets/CgqCHl-AF-eAdTmiAADznCJnMrw389.png) diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" index 2d0aebde7..e0a7c3c46 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" @@ -1,12 +1,12 @@ # 23 核心接口介绍,RPC 层骨架梳理 -在前面的课程中,我们深入介绍了 Dubbo 架构中的 Dubbo Remoting 层的相关内容,了解了 Dubbo 底层的网络模型以及线程模型。从本课时开始,我们就开始介绍 Dubbo Remoting 上面的一层—— Protocol 层(如下图所示), **Protocol 层是 Remoting 层的使用者** ,会通过 Exchangers 门面类创建 ExchangeClient 以及 ExchangeServer,还会创建相应的 ChannelHandler 实现以及 Codec2 实现并交给 Exchange 层进行装饰。 +在前面的课程中,我们深入介绍了 Dubbo 架构中的 Dubbo Remoting 层的相关内容,了解了 Dubbo 底层的网络模型以及线程模型。从本课时开始,我们就开始介绍 Dubbo Remoting 上面的一层—— Protocol 层(如下图所示),**Protocol 层是 Remoting 层的使用者**,会通过 Exchangers 门面类创建 ExchangeClient 以及 ExchangeServer,还会创建相应的 ChannelHandler 实现以及 Codec2 实现并交给 Exchange 层进行装饰。 ![Drawing 0.png](assets/Ciqc1F-FS2eAdVorABDINpNLpXY061.png) Dubbo 架构中 Protocol 层的位置图 -**Protocol 层在 Dubbo 源码中对应的是 dubbo-rpc 模块** ,该模块的结构如下图所示: +**Protocol 层在 Dubbo 源码中对应的是 dubbo-rpc 模块**,该模块的结构如下图所示: ![Drawing 1.png](assets/Ciqc1F-FS4aAMyvkAABpKhWTC9Q132.png) @@ -176,7 +176,7 @@ void unexport(); } ``` -为了监听服务发布事件以及取消暴露事件,Dubbo 定义了一个 SPI 扩展接口—— **ExporterListener 接口** ,其定义如下: +为了监听服务发布事件以及取消暴露事件,Dubbo 定义了一个 SPI 扩展接口—— **ExporterListener 接口**,其定义如下: ```plaintext @SPI @@ -190,7 +190,7 @@ void unexported(Exporter exporter); 虽然 ExporterListener 是个扩展接口,但是 Dubbo 本身并没有提供什么有用的扩展实现,我们需要自己提供具体实现监听感兴趣的事情。 -相应地,我们可以添加 InvokerListener 监听器,监听 Consumer 引用服务时触发的事件, **InvokerListener 接口** 的定义如下: +相应地,我们可以添加 InvokerListener 监听器,监听 Consumer 引用服务时触发的事件,**InvokerListener 接口** 的定义如下: ```plaintext @SPI @@ -229,7 +229,7 @@ return Collections.emptyList(); 在 Protocol 接口的实现中,export() 方法并不是简单地将 Invoker 对象包装成 Exporter 对象返回,其中还涉及代理对象的创建、底层 Server 的启动等操作;refer() 方法除了根据传入的 type 类型以及 URL 参数查询 Invoker 之外,还涉及相关 Client 的创建等操作。 -Dubbo 在 Protocol 层专门定义了一个 **ProxyFactory 接口** ,作为创建代理对象的工厂。ProxyFactory 接口是一个扩展接口,其中定义了 getProxy() 方法为 Invoker 创建代理对象,还定义了 getInvoker() 方法将代理对象反向封装成 Invoker 对象。 +Dubbo 在 Protocol 层专门定义了一个 **ProxyFactory 接口**,作为创建代理对象的工厂。ProxyFactory 接口是一个扩展接口,其中定义了 getProxy() 方法为 Invoker 创建代理对象,还定义了 getInvoker() 方法将代理对象反向封装成 Invoker 对象。 ```java @SPI("javassist") @@ -245,7 +245,7 @@ public interface ProxyFactory { } ``` -看到 ProxyFactory 上的 @SPI 注解,我们知道其默认实现使用 javassist 来创建代码对象,当然,Dubbo 还提供了其他方式来创建代码,例如 JDK 动态代理。 **ProtocolServer 接口** 是对前文介绍的 RemotingServer 的一层简单封装,其实现也都非常简单,这里就不再展开。 +看到 ProxyFactory 上的 @SPI 注解,我们知道其默认实现使用 javassist 来创建代码对象,当然,Dubbo 还提供了其他方式来创建代码,例如 JDK 动态代理。**ProtocolServer 接口** 是对前文介绍的 RemotingServer 的一层简单封装,其实现也都非常简单,这里就不再展开。 最后一个要介绍的核心接口是 **Filter 接口** 。关于 Filter,相信做过 Java Web 编程的同学们会非常熟悉这个基础概念,Java Web 开发中的 Filter 是用来拦截 HTTP 请求的,Dubbo 中的 Filter 接口功能与之类似,是用来拦截 Dubbo 请求的。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" index 092a62c6b..00a9e50bd 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" @@ -6,7 +6,7 @@ Protocol 接口继承关系图 -其中, **AbstractProtocol** 提供了一些 Protocol 实现需要的公共能力以及公共字段,它的核心字段有如下三个。 +其中,**AbstractProtocol** 提供了一些 Protocol 实现需要的公共能力以及公共字段,它的核心字段有如下三个。 - exporterMap(Map\>类型):用于存储出去的服务集合,其中的 Key 通过 ProtocolUtils.serviceKey() 方法创建的服务标识,在 ProtocolUtils 中维护了多层的 Map 结构(如下图所示)。首先按照 group 分组,在实践中我们可以根据需求设置 group,例如,按照机房、地域等进行 group 划分,做到就近调用;在 GroupServiceKeyCache 中,依次按照 serviceName、serviceVersion、port 进行分类,最终缓存的 serviceKey 是前面三者拼接而成的。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" index f3f2e28d0..804999b56 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" @@ -17,7 +17,7 @@ public Invoker protocolBindingRefer(Class serviceType, URL url) throws } ``` -关于 DubboInvoker 的具体实现,我们先暂时不做深入分析。这里我们需要先关注的是 **getClients() 方法** ,它创建了底层发送请求和接收响应的 Client 集合,其核心分为了两个部分,一个是针对 **共享连接** 的处理,另一个是针对 **独享连接** 的处理,具体实现如下: +关于 DubboInvoker 的具体实现,我们先暂时不做深入分析。这里我们需要先关注的是 **getClients() 方法**,它创建了底层发送请求和接收响应的 Client 集合,其核心分为了两个部分,一个是针对 **共享连接** 的处理,另一个是针对 **独享连接** 的处理,具体实现如下: ```java private ExchangeClient[] getClients(URL url) { @@ -61,7 +61,7 @@ Service 独享连接示意图 Service 共享连接示意图 -那怎么去创建共享连接呢? **创建共享连接的实现细节是在 getSharedClient() 方法中** ,它首先从 referenceClientMap 缓存(Map\`> 类型)中查询 Key(host 和 port 拼接成的字符串)对应的共享 Client 集合,如果查找到的 Client 集合全部可用,则直接使用这些缓存的 Client,否则要创建新的 Client 来补充替换缓存中不可用的 Client。示例代码如下: +那怎么去创建共享连接呢?**创建共享连接的实现细节是在 getSharedClient() 方法中**,它首先从 referenceClientMap 缓存(Map\`> 类型)中查询 Key(host 和 port 拼接成的字符串)对应的共享 Client 集合,如果查找到的 Client 集合全部可用,则直接使用这些缓存的 Client,否则要创建新的 Client 来补充替换缓存中不可用的 Client。示例代码如下: ```java private List getSharedClient(URL url, int connectNum) { @@ -176,7 +176,7 @@ private void initClient() throws RemotingException { 在这些发送请求的方法中,除了通过 initClient() 方法初始化底层 ExchangeClient 外,还会调用warning() 方法,其会根据当前 URL 携带的参数决定是否打印 WARN 级别日志。为了防止瞬间打印大量日志的情况发生,这里有打印的频率限制,默认每发送 5000 次请求打印 1 条日志。你可以看到在前面展示的兜底场景中,我们就开启了打印日志的选项。 -**分析完 getSharedClient() 方法创建共享 Client 的核心流程之后,我们回到 DubboProtocol 中,继续介绍创建独享 Client 的流程。** 创建独享 Client 的入口在 **DubboProtocol.initClient() 方法** ,它首先会在 URL 中设置一些默认的参数,然后根据 LAZY_CONNECT_KEY 参数决定是否使用 LazyConnectExchangeClient 进行封装,实现懒加载功能,如下代码所示: +**分析完 getSharedClient() 方法创建共享 Client 的核心流程之后,我们回到 DubboProtocol 中,继续介绍创建独享 Client 的流程。** 创建独享 Client 的入口在 **DubboProtocol.initClient() 方法**,它首先会在 URL 中设置一些默认的参数,然后根据 LAZY_CONNECT_KEY 参数决定是否使用 LazyConnectExchangeClient 进行封装,实现懒加载功能,如下代码所示: ```java private ExchangeClient initClient(URL url) { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25426\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25426\350\256\262.md" index 22d266cfa..1ac46d9bc 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25426\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25426\350\256\262.md" @@ -58,7 +58,7 @@ public Result invoke(Invocation inv) throws RpcException { ### RpcContext -**RpcContext 是线程级别的上下文信息** ,每个线程绑定一个 RpcContext 对象,底层依赖 ThreadLocal 实现。RpcContext 主要用于存储一个线程中一次请求的临时状态,当线程处理新的请求(Provider 端)或是线程发起新的请求(Consumer 端)时,RpcContext 中存储的内容就会更新。 +**RpcContext 是线程级别的上下文信息**,每个线程绑定一个 RpcContext 对象,底层依赖 ThreadLocal 实现。RpcContext 主要用于存储一个线程中一次请求的临时状态,当线程处理新的请求(Provider 端)或是线程发起新的请求(Consumer 端)时,RpcContext 中存储的内容就会更新。 下面来看 RpcContext 中两个 **InternalThreadLocal** 的核心字段,这两个字段的定义如下所示: @@ -74,7 +74,7 @@ private static final InternalThreadLocal LOCAL = new InternalThreadL private static final InternalThreadLocal SERVER_LOCAL = ... ``` -JDK 提供的 ThreadLocal 底层实现大致如下:对于不同线程创建对应的 ThreadLocalMap,用于存放线程绑定信息,当用户调用 **ThreadLocal.get() 方法** 获取变量时,底层会先获取当前线程 Thread,然后获取绑定到当前线程 Thread 的 ThreadLocalMap,最后将当前 ThreadLocal 对象作为 Key 去 ThreadLocalMap 表中获取线程绑定的数据。 **ThreadLocal.set() 方法** 的逻辑与之类似,首先会获取绑定到当前线程的 ThreadLocalMap,然后将 ThreadLocal 实例作为 Key、待存储的数据作为 Value 存储到 ThreadLocalMap 中。 +JDK 提供的 ThreadLocal 底层实现大致如下:对于不同线程创建对应的 ThreadLocalMap,用于存放线程绑定信息,当用户调用 **ThreadLocal.get() 方法** 获取变量时,底层会先获取当前线程 Thread,然后获取绑定到当前线程 Thread 的 ThreadLocalMap,最后将当前 ThreadLocal 对象作为 Key 去 ThreadLocalMap 表中获取线程绑定的数据。**ThreadLocal.set() 方法** 的逻辑与之类似,首先会获取绑定到当前线程的 ThreadLocalMap,然后将 ThreadLocal 实例作为 Key、待存储的数据作为 Value 存储到 ThreadLocalMap 中。 Dubbo 的 InternalThreadLocal 与 JDK 提供的 ThreadLocal 功能类似,只是底层实现略有不同,其底层的 InternalThreadLocalMap 采用数组结构存储数据,直接通过 index 获取变量,相较于 Map 方式计算 hash 值的性能更好。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25427\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25427\350\256\262.md" index 9d11f40ab..d162c8dea 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25427\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25427\350\256\262.md" @@ -21,7 +21,7 @@ protected ExecutorService getCallbackExecutor(URL url, Invocation inv) { InvokeMode 有三个可选值,分别是 SYNC、ASYNC 和 FUTURE。这里对于 SYNC 模式返回的线程池是 ThreadlessExecutor,至于其他两种异步模式,会根据 URL 选择对应的共享线程池。 -**SYNC 表示同步模式** ,是 Dubbo 的默认调用模式,具体含义如下图所示,客户端发送请求之后,客户端线程会阻塞等待服务端返回响应。 +**SYNC 表示同步模式**,是 Dubbo 的默认调用模式,具体含义如下图所示,客户端发送请求之后,客户端线程会阻塞等待服务端返回响应。 ![Lark20201027-180625.png](assets/CgqCHl-X8UOAOTRbAACy-uBf52M689.png) @@ -138,7 +138,7 @@ private BiConsumer afterContext = (appResponse, t) -> { 在前面的分析中我们看到,RpcInvocation.InvokeMode 字段中可以指定调用为 SYNC 模式,也就是同步调用模式,那 AsyncRpcResult 这种异步设计是如何支持同步调用的呢? 在 AbstractProtocol.refer() 方法中,Dubbo 会将 DubboProtocol.protocolBindingRefer() 方法返回的 Invoker 对象(即 DubboInvoker 对象)用 AsyncToSyncInvoker 封装一层。 -**AsyncToSyncInvoker 是 Invoker 的装饰器,负责将异步调用转换成同步调用** ,其 invoke() 方法的核心实现如下: +**AsyncToSyncInvoker 是 Invoker 的装饰器,负责将异步调用转换成同步调用**,其 invoke() 方法的核心实现如下: ```plaintext public Result invoke(Invocation invocation) throws RpcException { @@ -167,7 +167,7 @@ public Result get() throws InterruptedException, ExecutionException { ThreadlessExecutor 针对同步请求的优化,我们在前面的第 20 课时已经详细介绍过了,这里不再重复。 -最后要说明的是, **AsyncRpcResult 实现了 Result 接口** ,如下图所示: +最后要说明的是,**AsyncRpcResult 实现了 Result 接口**,如下图所示: ![Drawing 1.png](assets/Ciqc1F-WqdmAbppOAABOGWzVljY775.png) @@ -205,14 +205,14 @@ public Object recreate() throws Throwable { } ``` -这里我们注意到,在 recreate() 方法中,AsyncRpcResult 会对 FUTURE 特殊处理。如果服务接口定义的返回参数是 CompletableFuture,则属于 FUTURE 模式, **FUTURE 模式也属于 Dubbo 提供的一种异步调用方式,只不过是服务端异步** 。FUTURE 模式下拿到的 CompletableFuture 对象其实是在 AbstractInvoker 中塞到 RpcContext 中的,在 AbstractInvoker.invoke() 方法中有这么一段代码: +这里我们注意到,在 recreate() 方法中,AsyncRpcResult 会对 FUTURE 特殊处理。如果服务接口定义的返回参数是 CompletableFuture,则属于 FUTURE 模式,**FUTURE 模式也属于 Dubbo 提供的一种异步调用方式,只不过是服务端异步** 。FUTURE 模式下拿到的 CompletableFuture 对象其实是在 AbstractInvoker 中塞到 RpcContext 中的,在 AbstractInvoker.invoke() 方法中有这么一段代码: ```java RpcContext.getContext().setFuture( new FutureAdapter(asyncResult.getResponseFuture())); ``` -这里拿到的其实就是 AsyncRpcResult 中 responseFuture,即前面介绍的 DefaultFuture。可见, **无论是 SYNC 模式、ASYNC 模式还是 FUTURE 模式,都是围绕 DefaultFuture 展开的。** +这里拿到的其实就是 AsyncRpcResult 中 responseFuture,即前面介绍的 DefaultFuture。可见,**无论是 SYNC 模式、ASYNC 模式还是 FUTURE 模式,都是围绕 DefaultFuture 展开的。** 其实,在 Dubbo 2.6.x 及之前的版本提供了一定的异步编程能力,但其异步方式存在如下一些问题: @@ -220,7 +220,7 @@ RpcContext.getContext().setFuture( - Future 接口无法实现自动回调,而自定义 ResponseFuture(这是 Dubbo 2.6.x 中类)虽支持回调,但支持的异步场景有限,并且还不支持 Future 间的相互协调或组合等。 - 不支持 Provider 端异步。 -Dubbo 2.6.x 及之前版本中使用的 Future 是在 Java 5 中引入的,所以存在以上一些功能设计上的问题;而在 Java 8 中引入的 CompletableFuture 进一步丰富了 Future 接口,很好地解决了这些问题。 **Dubbo 在 2.7.0 版本已经升级了对 Java 8 的支持,同时基于 CompletableFuture 对当前的异步功能进行了增强,弥补了上述不足。** 因为 CompletableFuture 实现了 CompletionStage 和 Future 接口,所以它还是可以像以前一样通过 get() 阻塞或者 isDone() 方法轮询的方式获得结果,这就保证了同步调用依旧可用。当然,在实际工作中,不是很建议用 get() 这样阻塞的方式来获取结果,因为这样就丢失了异步操作带来的性能提升。 +Dubbo 2.6.x 及之前版本中使用的 Future 是在 Java 5 中引入的,所以存在以上一些功能设计上的问题;而在 Java 8 中引入的 CompletableFuture 进一步丰富了 Future 接口,很好地解决了这些问题。**Dubbo 在 2.7.0 版本已经升级了对 Java 8 的支持,同时基于 CompletableFuture 对当前的异步功能进行了增强,弥补了上述不足。** 因为 CompletableFuture 实现了 CompletionStage 和 Future 接口,所以它还是可以像以前一样通过 get() 阻塞或者 isDone() 方法轮询的方式获得结果,这就保证了同步调用依旧可用。当然,在实际工作中,不是很建议用 get() 这样阻塞的方式来获取结果,因为这样就丢失了异步操作带来的性能提升。 另外,CompletableFuture 提供了良好的回调方法,例如,whenComplete()、whenCompleteAsync() 等方法都可以在逻辑完成后,执行该方法中添加的 action 逻辑,实现回调的逻辑。同时,CompletableFuture 很好地支持了 Future 间的相互协调或组合,例如,thenApply()、thenApplyAsync() 等方法。 @@ -250,7 +250,7 @@ ProtocolListenerWrapper 是 Protocol 接口的实现之一,如下图所示: ProtocolListenerWrapper 继承关系图 -ProtocolListenerWrapper 本身是 Protocol 接口的装饰器,在其 export() 方法和 refer() 方法中,会分别在原有 Invoker 基础上封装一层 ListenerExporterWrapper 和 ListenerInvokerWrapper。 **ListenerInvokerWrapper 是 Invoker 的装饰器** ,其构造方法参数列表中除了被修饰的 Invoker 外,还有 InvokerListener 列表,在构造方法内部会遍历整个 InvokerListener 列表,并调用每个 InvokerListener 的 referred() 方法,通知它们 Invoker 被引用的事件。核心逻辑如下: +ProtocolListenerWrapper 本身是 Protocol 接口的装饰器,在其 export() 方法和 refer() 方法中,会分别在原有 Invoker 基础上封装一层 ListenerExporterWrapper 和 ListenerInvokerWrapper。**ListenerInvokerWrapper 是 Invoker 的装饰器**,其构造方法参数列表中除了被修饰的 Invoker 外,还有 InvokerListener 列表,在构造方法内部会遍历整个 InvokerListener 列表,并调用每个 InvokerListener 的 referred() 方法,通知它们 Invoker 被引用的事件。核心逻辑如下: ```plaintext public ListenerInvokerWrapper(Invoker invoker, List listeners) { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" index b8509242b..5c7bd8af2 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" @@ -38,7 +38,7 @@ public interface ProxyFactory { } ``` -看到 ProxyFactory 上的 @SPI 注解我们知道,其默认实现使用 Javassist 来创建代码对象。 **AbstractProxyFactory 是代理工厂的抽象类** ,继承关系如下图所示: +看到 ProxyFactory 上的 @SPI 注解我们知道,其默认实现使用 Javassist 来创建代码对象。**AbstractProxyFactory 是代理工厂的抽象类**,继承关系如下图所示: ![Drawing 2.png](assets/Ciqc1F-WrMiAXWheAACKwcyiNxw669.png) AbstractProxyFactory 继承关系图 ### AbstractProxyFactory diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" index 928412459..7daed3dc9 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" @@ -4,7 +4,7 @@ Filter 链的组装逻辑设计得非常灵活,其中可以通过“-”配置手动剔除 Dubbo 原生提供的、默认加载的 Filter,通过“default”来代替 Dubbo 原生提供的 Filter,这样就可以很好地控制哪些 Filter 要加载,以及 Filter 的真正执行顺序。 -**Filter 是扩展 Dubbo 功能的首选方案** ,并且 Dubbo 自身也提供了非常多的 Filter 实现来扩展自身功能。在回顾了 ProtocolFilterWrapper 加载 Filter 的大致逻辑之后,我们本课时就来深入介绍 Dubbo 内置的多种 Filter 实现类,以及自定义 Filter 扩展 Dubbo 的方式。 +**Filter 是扩展 Dubbo 功能的首选方案**,并且 Dubbo 自身也提供了非常多的 Filter 实现来扩展自身功能。在回顾了 ProtocolFilterWrapper 加载 Filter 的大致逻辑之后,我们本课时就来深入介绍 Dubbo 内置的多种 Filter 实现类,以及自定义 Filter 扩展 Dubbo 的方式。 在开始介绍 Filter 接口实现之前,我们需要了解一下 Filter 在 Dubbo 架构中的位置,这样才能明确 Filter 链处理请求/响应的位置,如下图红框所示: @@ -414,7 +414,7 @@ public Result invoke(Invoker invoker, Invocation invocation) throws RpcExcept ### ExecuteLimitFilter -**ExecuteLimitFilter 是 Dubbo 在 Provider 端限流的实现** ,与 Consumer 端的限流实现 ActiveLimitFilter 相对应。ExecuteLimitFilter 的核心实现与 ActiveLimitFilter类似,也是依赖 RpcStatus 的 beginCount() 方法和 endCount() 方法来实现 RpcStatus.active 字段的增减,具体实现如下: +**ExecuteLimitFilter 是 Dubbo 在 Provider 端限流的实现**,与 Consumer 端的限流实现 ActiveLimitFilter 相对应。ExecuteLimitFilter 的核心实现与 ActiveLimitFilter类似,也是依赖 RpcStatus 的 beginCount() 方法和 endCount() 方法来实现 RpcStatus.active 字段的增减,具体实现如下: ```java public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" index 124a7ccc3..ff457b898 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" @@ -13,7 +13,7 @@ - 服务端集群如何做到负载均衡,负载均衡的标准是什么呢? - …… -为了解决上述问题, **Dubbo 独立出了一个实现集群功能的模块—— dubbo-cluster** 。 +为了解决上述问题,**Dubbo 独立出了一个实现集群功能的模块—— dubbo-cluster** 。 ![Drawing 0.png](assets/Ciqc1F-qN92ADHx8AACiY_cvusQ921.png) @@ -31,10 +31,10 @@ Cluster 核心接口图 由图我们可以看出,dubbo-cluster 主要包括以下四个核心接口: -- **Cluster 接口** ,是集群容错的接口,主要是在某些 Provider 节点发生故障时,让 Consumer 的调用请求能够发送到正常的 Provider 节点,从而保证整个系统的可用性。 -- **Directory 接口** ,表示多个 Invoker 的集合,是后续路由规则、负载均衡策略以及集群容错的基础。 -- **Router 接口** ,抽象的是路由器,请求经过 Router 的时候,会按照用户指定的规则匹配出符合条件的 Provider。 -- **LoadBalance 接口** ,是负载均衡接口,Consumer 会按照指定的负载均衡策略,从 Provider 集合中选出一个最合适的 Provider 节点来处理请求。 +- **Cluster 接口**,是集群容错的接口,主要是在某些 Provider 节点发生故障时,让 Consumer 的调用请求能够发送到正常的 Provider 节点,从而保证整个系统的可用性。 +- **Directory 接口**,表示多个 Invoker 的集合,是后续路由规则、负载均衡策略以及集群容错的基础。 +- **Router 接口**,抽象的是路由器,请求经过 Router 的时候,会按照用户指定的规则匹配出符合条件的 Provider。 +- **LoadBalance 接口**,是负载均衡接口,Consumer 会按照指定的负载均衡策略,从 Provider 集合中选出一个最合适的 Provider 节点来处理请求。 Cluster 层的核心流程是这样的:当调用进入 Cluster 的时候,Cluster 会创建一个 AbstractClusterInvoker 对象,在这个 AbstractClusterInvoker 中,首先会从 Directory 中获取当前 Invoker 集合;然后按照 Router 集合进行路由,得到符合条件的 Invoker 集合;接下来按照 LoadBalance 指定的负载均衡策略得到最终要调用的 Invoker 对象。 @@ -59,15 +59,15 @@ public interface Directory extends Node { } ``` -**AbstractDirectory 是 Directory 接口的抽象实现** ,其中除了维护 Consumer 端的 URL 信息,还维护了一个 RouterChain 对象,用于记录当前使用的 Router 对象集合,也就是后面课时要介绍的路由规则。 +**AbstractDirectory 是 Directory 接口的抽象实现**,其中除了维护 Consumer 端的 URL 信息,还维护了一个 RouterChain 对象,用于记录当前使用的 Router 对象集合,也就是后面课时要介绍的路由规则。 -AbstractDirectory 对 list() 方法的实现也比较简单,就是直接委托给了 doList() 方法,doList() 是个抽象方法,由 AbstractDirectory 的子类具体实现。 **Directory 接口有 RegistryDirectory 和 StaticDirectory 两个具体实现** ,如下图所示: +AbstractDirectory 对 list() 方法的实现也比较简单,就是直接委托给了 doList() 方法,doList() 是个抽象方法,由 AbstractDirectory 的子类具体实现。**Directory 接口有 RegistryDirectory 和 StaticDirectory 两个具体实现**,如下图所示: ![Drawing 2.png](assets/Ciqc1F-qN_-AMVHmAAA3C6TAxsA315.png) Directory 接口继承关系图 -其中, **RegistryDirectory 实现** 中维护的 Invoker 集合会随着注册中心中维护的注册信息 **动态** 发生变化,这就依赖了 ZooKeeper 等注册中心的推送能力; **StaticDirectory 实现** 中维护的 Invoker 集合则是 **静态** 的,在 StaticDirectory 对象创建完成之后,不会再发生变化。 +其中,**RegistryDirectory 实现** 中维护的 Invoker 集合会随着注册中心中维护的注册信息 **动态** 发生变化,这就依赖了 ZooKeeper 等注册中心的推送能力; **StaticDirectory 实现** 中维护的 Invoker 集合则是 **静态** 的,在 StaticDirectory 对象创建完成之后,不会再发生变化。 下面我们就来分别介绍 Directory 接口的这两个具体实现。 @@ -98,7 +98,7 @@ this.setRouterChain(routerChain); // 设置routerChain字段 #### 2\. RegistryDirectory -RegistryDirectory 是一个动态的 Directory 实现, **实现了 NotifyListener 接口** ,当注册中心的服务配置发生变化时,RegistryDirectory 会收到变更通知,然后RegistryDirectory 会根据注册中心推送的通知,动态增删底层 Invoker 集合。 +RegistryDirectory 是一个动态的 Directory 实现,**实现了 NotifyListener 接口**,当注册中心的服务配置发生变化时,RegistryDirectory 会收到变更通知,然后RegistryDirectory 会根据注册中心推送的通知,动态增删底层 Invoker 集合。 下面我们先来看一下 RegistryDirectory 中的核心字段。 @@ -116,7 +116,7 @@ RegistryDirectory 是一个动态的 Directory 实现, **实现了 NotifyListe * cachedInvokerUrls(volatile Set类型):当前缓存的所有 Provider 的 URL,该集合会与 invokers 字段同时动态更新。 * configurators(volatile List< Configurator>类型):动态更新的配置信息,配置的具体内容在后面的分析中会介绍到。 -在 RegistryDirectory 的构造方法中,会 **根据传入的注册中心 URL 初始化上述核心字段** ,具体实现如下: +在 RegistryDirectory 的构造方法中,会 **根据传入的注册中心 URL 初始化上述核心字段**,具体实现如下: ```plaintext public RegistryDirectory(Class serviceType, URL url) { @@ -185,7 +185,7 @@ refreshOverrideAndInvoker(providerURLs); } ``` -我们这里首先来专注 **providers 类型 URL 的处理** ,具体实现位置在 refreshInvoker() 方法中,具体实现如下: +我们这里首先来专注 **providers 类型 URL 的处理**,具体实现位置在 refreshInvoker() 方法中,具体实现如下: ```java private void refreshInvoker(List invokerUrls) { @@ -355,7 +355,7 @@ return mergedInvokers; 到此为止,RegistryDirectory 处理一次完整的动态 Provider 发现流程就介绍完了。 -最后,我们再分析下 **RegistryDirectory 中另外一个核心方法—— doList() 方法** ,该方法是 AbstractDirectory 留给其子类实现的一个方法,也是通过 Directory 接口获取 Invoker 集合的核心所在,具体实现如下: +最后,我们再分析下 **RegistryDirectory 中另外一个核心方法—— doList() 方法**,该方法是 AbstractDirectory 留给其子类实现的一个方法,也是通过 Directory 接口获取 Invoker 集合的核心所在,具体实现如下: ```java public List\> doList(Invocation invocation) { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" index 4cd3a3620..b87c0fa90 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" @@ -56,7 +56,7 @@ public List> route(URL url, Invocation invocation) { } ``` -了解了 RouterChain 的大致逻辑之后,我们知道 **真正进行路由的是 routers 集合中的 Router 对象** 。接下来我们再来看 RouterFactory 这个工厂接口, **RouterFactory 接口是一个扩展接口** ,具体定义如下: +了解了 RouterChain 的大致逻辑之后,我们知道 **真正进行路由的是 routers 集合中的 Router 对象** 。接下来我们再来看 RouterFactory 这个工厂接口,**RouterFactory 接口是一个扩展接口**,具体定义如下: ```plaintext @SPI @@ -72,7 +72,7 @@ RouterFactory 接口有很多实现类,如下图所示: RouterFactory 继承关系图 -下面我们就来深入介绍下每个 RouterFactory 实现类以及对应的 Router 实现对象。 **Router 决定了一次 Dubbo 调用的目标服务,Router 接口的每个实现类代表了一个路由规则** ,当 Consumer 访问 Provider 时,Dubbo 根据路由规则筛选出合适的 Provider 列表,之后通过负载均衡算法再次进行筛选。Router 接口的继承关系如下图所示: +下面我们就来深入介绍下每个 RouterFactory 实现类以及对应的 Router 实现对象。**Router 决定了一次 Dubbo 调用的目标服务,Router 接口的每个实现类代表了一个路由规则**,当 Consumer 访问 Provider 时,Dubbo 根据路由规则筛选出合适的 Provider 列表,之后通过负载均衡算法再次进行筛选。Router 接口的继承关系如下图所示: ![Drawing 1.png](assets/Ciqc1F-qOL2AAXYIAACMVPC1qW0732.png) @@ -90,7 +90,7 @@ public Router getRouter(URL url) { } ``` -**ConditionRouter 是基于条件表达式的路由实现类** ,下面就是一条基于条件表达式的路由规则: +**ConditionRouter 是基于条件表达式的路由实现类**,下面就是一条基于条件表达式的路由规则: ```javascript host = 192.168.0.100 => host = 192.168.0.150 @@ -98,7 +98,7 @@ host = 192.168.0.100 => host = 192.168.0.150 在上述规则中,`=>`之前的为 Consumer 匹配的条件,该条件中的所有参数会与 Consumer 的 URL 进行对比,当 Consumer 满足匹配条件时,会对该 Consumer 的此次调用执行 `=>` 后面的过滤规则。 -`=>` 之后为 Provider 地址列表的过滤条件,该条件中的所有参数会和 Provider 的 URL 进行对比,Consumer 最终只拿到过滤后的地址列表。 **如果 Consumer 匹配条件为空,表示 => 之后的过滤条件对所有 Consumer 生效** ,例如:=> host != 192.168.0.150,含义是所有 Consumer 都不能请求 192.168.0.150 这个 Provider 节点。 **如果 Provider 过滤条件为空,表示禁止访问所有 Provider** ,例如:host = 192.168.0.100 =>,含义是 192.168.0.100 这个 Consumer 不能访问任何 Provider 节点。 +`=>` 之后为 Provider 地址列表的过滤条件,该条件中的所有参数会和 Provider 的 URL 进行对比,Consumer 最终只拿到过滤后的地址列表。**如果 Consumer 匹配条件为空,表示 => 之后的过滤条件对所有 Consumer 生效**,例如:=> host != 192.168.0.150,含义是所有 Consumer 都不能请求 192.168.0.150 这个 Provider 节点。**如果 Provider 过滤条件为空,表示禁止访问所有 Provider**,例如:host = 192.168.0.100 =>,含义是 192.168.0.100 这个 Consumer 不能访问任何 Provider 节点。 ConditionRouter 的核心字段有如下几个。 @@ -142,7 +142,7 @@ Value 是 MatchPair 对象,包含两个 Set 类型的集合—— matches 和 上述流程具体实现在 MatchPair 的 isMatch() 方法中,比较简单,这里就不再展示。 -了解了每个 MatchPair 的匹配流程之后,我们来看 **parseRule() 方法是如何解析一条完整的条件表达式,生成对应 MatchPair 的** ,具体实现如下: +了解了每个 MatchPair 的匹配流程之后,我们来看 **parseRule() 方法是如何解析一条完整的条件表达式,生成对应 MatchPair 的**,具体实现如下: ```java private static Map parseRule(String rule) throws ParseException { @@ -261,7 +261,7 @@ public List> route(List> invokers, URL url, Invocation ScriptRouterFactory 的扩展名为 script,其 getRouter() 方法中会创建一个 ScriptRouter 对象并返回。 -**ScriptRouter 支持 JDK 脚本引擎的所有脚本** ,例如,JavaScript、JRuby、Groovy 等,通过 `type=javascript` 参数设置脚本类型,缺省为 javascript。下面我们就定义一个 route() 函数进行 host 过滤: +**ScriptRouter 支持 JDK 脚本引擎的所有脚本**,例如,JavaScript、JRuby、Groovy 等,通过 `type=javascript` 参数设置脚本类型,缺省为 javascript。下面我们就定义一个 route() 函数进行 host 过滤: ```java function route(invokers, invocation, context){ @@ -330,4 +330,4 @@ private Bindings createBindings(List> invokers, Invocation invoca ### 总结 -本课时重点介绍了 Router 接口的相关内容。首先我们介绍了 RouterChain 的核心实现以及构建过程,然后讲解了 RouterFactory 接口和 Router 接口中核心方法的功能。接下来,我们还深入分析了 **ConditionRouter 对条件路由功能的实现** ,以及 **ScriptRouter 对脚本路由功能的实现** 。 +本课时重点介绍了 Router 接口的相关内容。首先我们介绍了 RouterChain 的核心实现以及构建过程,然后讲解了 RouterFactory 接口和 Router 接口中核心方法的功能。接下来,我们还深入分析了 **ConditionRouter 对条件路由功能的实现**,以及 **ScriptRouter 对脚本路由功能的实现** 。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" index 4c20aad3c..97c0ce501 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" @@ -4,7 +4,7 @@ ### FileRouterFactory -**FileRouterFactory 是 ScriptRouterFactory 的装饰器** ,其扩展名为 file,FileRouterFactory 在 ScriptRouterFactory 基础上 **增加了读取文件的能力** 。我们可以将 ScriptRouter 使用的路由规则保存到文件中,然后在 URL 中指定文件路径,FileRouterFactory 从中解析到该脚本文件的路径并进行读取,调用 ScriptRouterFactory 去创建相应的 ScriptRouter 对象。 +**FileRouterFactory 是 ScriptRouterFactory 的装饰器**,其扩展名为 file,FileRouterFactory 在 ScriptRouterFactory 基础上 **增加了读取文件的能力** 。我们可以将 ScriptRouter 使用的路由规则保存到文件中,然后在 URL 中指定文件路径,FileRouterFactory 从中解析到该脚本文件的路径并进行读取,调用 ScriptRouterFactory 去创建相应的 ScriptRouter 对象。 下面我们来看 FileRouterFactory 对 getRouter() 方法的具体实现,其中完成了 file 协议的 URL 到 script 协议 URL 的转换,如下是一个转换示例,首先会将 file:// 协议转换成 script:// 协议,然后会添加 type 参数和 rule 参数,其中 type 参数值根据文件后缀名确定,该示例为 js,rule 参数值为文件内容。 @@ -39,11 +39,11 @@ public Router getRouter(URL url) { } ``` -### TagRouterFactory & TagRouter **TagRouterFactory 作为 RouterFactory 接口的扩展实现** ,其扩展名为 tag。但是需要注意的是,TagRouterFactory 与上一课时介绍的 ConditionRouterFactory、ScriptRouterFactory 的不同之处在于,它是 **通过继承 CacheableRouterFactory 这个抽象类,间接实现了 RouterFactory 接口** +### TagRouterFactory & TagRouter **TagRouterFactory 作为 RouterFactory 接口的扩展实现**,其扩展名为 tag。但是需要注意的是,TagRouterFactory 与上一课时介绍的 ConditionRouterFactory、ScriptRouterFactory 的不同之处在于,它是 **通过继承 CacheableRouterFactory 这个抽象类,间接实现了 RouterFactory 接口** CacheableRouterFactory 抽象类中维护了一个 ConcurrentMap 集合(routerMap 字段)用来缓存 Router,其中的 Key 是 ServiceKey。在 CacheableRouterFactory 的 getRouter() 方法中,会优先根据 URL 的 ServiceKey 查询 routerMap 集合,查询失败之后会调用 createRouter() 抽象方法来创建相应的 Router 对象。在 TagRouterFactory.createRouter() 方法中,创建的自然就是 TagRouter 对象了。 -#### 基于 Tag 的测试环境隔离方案 **通过 TagRouter,我们可以将某一个或多个 Provider 划分到同一分组,约束流量只在指定分组中流转,这样就可以轻松达到流量隔离的目的,从而支持灰度发布等场景。** 目前,Dubbo 提供了动态和静态两种方式给 Provider 打标签,其中动态方式就是通过服务治理平台动态下发标签,静态方式就是在 XML 等静态配置中打标签。Consumer 端可以在 RpcContext 的 attachment 中添加 request.tag 附加属性,注意 **保存在 attachment 中的值将会在一次完整的远程调用中持续传递** ,我们只需要在起始调用时进行设置,就可以达到标签的持续传递 +#### 基于 Tag 的测试环境隔离方案 **通过 TagRouter,我们可以将某一个或多个 Provider 划分到同一分组,约束流量只在指定分组中流转,这样就可以轻松达到流量隔离的目的,从而支持灰度发布等场景。** 目前,Dubbo 提供了动态和静态两种方式给 Provider 打标签,其中动态方式就是通过服务治理平台动态下发标签,静态方式就是在 XML 等静态配置中打标签。Consumer 端可以在 RpcContext 的 attachment 中添加 request.tag 附加属性,注意 **保存在 attachment 中的值将会在一次完整的远程调用中持续传递**,我们只需要在起始调用时进行设置,就可以达到标签的持续传递 了解了 Tag 的基本概念和功能之后,我们再简单介绍一个 Tag 的使用示例。 @@ -63,7 +63,7 @@ CacheableRouterFactory 抽象类中维护了一个 ConcurrentMap 集合(router 在一些特殊场景中,会有 Tag 降级的场景,比如找不到对应 Tag 的 Provider,会按照一定的规则进行降级。如果在 Provider 集群中不存在与请求 Tag 对应的 Provider 节点,则默认将降级请求 Tag 为空的 Provider;如果希望在找不到匹配 Tag 的 Provider 节点时抛出异常的话,我们需设置 request.tag.force = true。 -如果请求中的 request.tag 未设置,只会匹配 Tag 为空的 Provider,也就是说即使集群中存在可用的服务,若 Tag 不匹配也就无法调用。一句话总结, **携带 Tag 的请求可以降级访问到无 Tag 的 Provider,但不携带 Tag 的请求永远无法访问到带有 Tag 的 Provider** 。 +如果请求中的 request.tag 未设置,只会匹配 Tag 为空的 Provider,也就是说即使集群中存在可用的服务,若 Tag 不匹配也就无法调用。一句话总结,**携带 Tag 的请求可以降级访问到无 Tag 的 Provider,但不携带 Tag 的请求永远无法访问到带有 Tag 的 Provider** 。 #### TagRouter @@ -139,11 +139,11 @@ AbstractRouterRule 中核心字段的具体含义大致可总结为如下。 1. 如果 invokers 为空,直接返回空集合。 1. 检查关联的 tagRouterRule 对象是否可用,如果不可用,则会直接调用 filterUsingStaticTag() 方法进行过滤,并返回过滤结果。在 filterUsingStaticTag() 方法中,会比较请求携带的 tag 值与 Provider URL 中的 tag 参数值。 1. 获取此次调用的 tag 信息,这里会尝试从 Invocation 以及 URL 的参数中获取。 -1. 如果 **此次请求指定了 tag 信息** ,则首先会获取 tag 关联的 address 集合。 +1. 如果 **此次请求指定了 tag 信息**,则首先会获取 tag 关联的 address 集合。 1. 如果 address 集合不为空,则根据该 address 集合中的地址,匹配出符合条件的 Invoker 集合。如果存在符合条件的 Invoker,则直接将过滤得到的 Invoker 集合返回;如果不存在,就会根据 force 配置决定是否返回空 Invoker 集合。 1. 如果 address 集合为空,则会将请求携带的 tag 值与 Provider URL 中的 tag 参数值进行比较,匹配出符合条件的 Invoker 集合。如果存在符合条件的 Invoker,则直接将过滤得到的 Invoker 集合返回;如果不存在,就会根据 force 配置决定是否返回空 Invoker 集合。 1. 如果 force 配置为 false,且符合条件的 Invoker 集合为空,则返回所有不包含任何 tag 的 Provider 列表。 -1. 如果 **此次请求未携带 tag 信息** ,则会先获取 TagRouterRule 规则中全部 tag 关联的 address 集合。如果 address 集合不为空,则过滤出不在 address 集合中的 Invoker 并添加到结果集合中,最后,将 Provider URL 中的 tag 值与 TagRouterRule 中的 tag 名称进行比较,得到最终的 Invoker 集合。 +1. 如果 **此次请求未携带 tag 信息**,则会先获取 TagRouterRule 规则中全部 tag 关联的 address 集合。如果 address 集合不为空,则过滤出不在 address 集合中的 Invoker 并添加到结果集合中,最后,将 Provider URL 中的 tag 值与 TagRouterRule 中的 tag 名称进行比较,得到最终的 Invoker 集合。 上述流程的具体实现是在 TagRouter.route() 方法中,如下所示: @@ -202,19 +202,19 @@ public List> route(List> invokers, URL url, Invocation ### ServiceRouter & AppRouter -除了前文介绍的 TagRouterFactory 继承了 CacheableRouterFactory 之外, **ServiceRouterFactory 也继承 CachabelRouterFactory,具有了缓存的能力** ,具体继承关系如下图所示: +除了前文介绍的 TagRouterFactory 继承了 CacheableRouterFactory 之外,**ServiceRouterFactory 也继承 CachabelRouterFactory,具有了缓存的能力**,具体继承关系如下图所示: ![8.png](assets/Ciqc1F-zkHqAH3diAAGWl6aQJy8860.png) CacheableRouterFactory 继承关系图 -ServiceRouterFactory 创建的 Router 实现是 ServiceRouter,与 ServiceRouter 类似的是 AppRouter, **两者都继承了 ListenableRouter 抽象类** (虽然 ListenableRouter 是个抽象类,但是没有抽象方法留给子类实现),继承关系如下图所示: +ServiceRouterFactory 创建的 Router 实现是 ServiceRouter,与 ServiceRouter 类似的是 AppRouter,**两者都继承了 ListenableRouter 抽象类** (虽然 ListenableRouter 是个抽象类,但是没有抽象方法留给子类实现),继承关系如下图所示: ![7.png](assets/Ciqc1F-zkISAPopjAAH9Njd3pOE049.png) ListenableRouter 继承关系图 -**ListenableRouter 在 ConditionRouter 基础上添加了动态配置的能力** ,ListenableRouter 的 process() 方法与 TagRouter 中的 process() 方法类似,对于 ConfigChangedEvent.DELETE 事件,直接清空 ListenableRouter 中维护的 ConditionRouterRule 和 ConditionRouter 集合的引用;对于 ADDED、UPDATED 事件,则通过 ConditionRuleParser 解析事件内容,得到相应的 ConditionRouterRule 对象和 ConditionRouter 集合。这里的 ConditionRuleParser 同样是以 yaml 文件的格式解析 ConditionRouterRule 的相关配置。ConditionRouterRule 中维护了一个 conditions 集合(List`` 类型),记录了多个 Condition 路由规则,对应生成多个 ConditionRouter 对象。 +**ListenableRouter 在 ConditionRouter 基础上添加了动态配置的能力**,ListenableRouter 的 process() 方法与 TagRouter 中的 process() 方法类似,对于 ConfigChangedEvent.DELETE 事件,直接清空 ListenableRouter 中维护的 ConditionRouterRule 和 ConditionRouter 集合的引用;对于 ADDED、UPDATED 事件,则通过 ConditionRuleParser 解析事件内容,得到相应的 ConditionRouterRule 对象和 ConditionRouter 集合。这里的 ConditionRuleParser 同样是以 yaml 文件的格式解析 ConditionRouterRule 的相关配置。ConditionRouterRule 中维护了一个 conditions 集合(List`` 类型),记录了多个 Condition 路由规则,对应生成多个 ConditionRouter 对象。 整个解析 ConditionRouterRule 的过程,与前文介绍的解析 TagRouterRule 的流程类似,这里不再赘述。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25434\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25434\350\256\262.md" index 21f676668..8302c78bc 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25434\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25434\350\256\262.md" @@ -115,7 +115,7 @@ Configurator 接口的继承关系如下图所示: Configurator 继承关系图 -其中,AbstractConfigurator 中维护了一个 configuratorUrl 字段,记录了完整的配置 URL。 **AbstractConfigurator 是一个模板类,其核心实现是 configure() 方法** ,具体实现如下: +其中,AbstractConfigurator 中维护了一个 configuratorUrl 字段,记录了完整的配置 URL。**AbstractConfigurator 是一个模板类,其核心实现是 configure() 方法**,具体实现如下: ```plaintext public URL configure(URL url) { @@ -141,7 +141,7 @@ public URL configure(URL url) { } ``` -这里我们需要关注下 **configureDeprecated() 方法对历史版本的兼容** ,其实这也是对注册中心 configurators 目录下配置 URL 的处理,具体实现如下: +这里我们需要关注下 **configureDeprecated() 方法对历史版本的兼容**,其实这也是对注册中心 configurators 目录下配置 URL 的处理,具体实现如下: ```plaintext private URL configureDeprecated(URL url) { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" index de357555a..a9431fe6a 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" @@ -6,7 +6,7 @@ LoadBalance 核心接口图 -**LoadBalance(负载均衡)的职责是将网络请求或者其他形式的负载“均摊”到不同的服务节点上,从而避免服务集群中部分节点压力过大、资源紧张,而另一部分节点比较空闲的情况。** 通过合理的负载均衡算法,我们希望可以让每个服务节点获取到适合自己处理能力的负载, **实现处理能力和流量的合理分配** 。常用的负载均衡可分为 **软件负载均衡** (比如,日常工作中使用的 Nginx)和 **硬件负载均衡** (主要有 F5、Array、NetScaler 等,不过开发工程师在实践中很少直接接触到)。 +**LoadBalance(负载均衡)的职责是将网络请求或者其他形式的负载“均摊”到不同的服务节点上,从而避免服务集群中部分节点压力过大、资源紧张,而另一部分节点比较空闲的情况。** 通过合理的负载均衡算法,我们希望可以让每个服务节点获取到适合自己处理能力的负载,**实现处理能力和流量的合理分配** 。常用的负载均衡可分为 **软件负载均衡** (比如,日常工作中使用的 Nginx)和 **硬件负载均衡** (主要有 F5、Array、NetScaler 等,不过开发工程师在实践中很少直接接触到)。 常见的 RPC 框架中都有负载均衡的概念和相应的实现,Dubbo 也不例外。Dubbo 需要对 Consumer 的调用请求进行分配,避免少数 Provider 节点负载过大,而剩余的其他 Provider 节点处于空闲的状态。因为当 Provider 负载过大时,就会导致一部分请求超时、丢失等一系列问题发生,造成线上故障。 @@ -26,7 +26,7 @@ Dubbo 提供了 5 种负载均衡实现,分别是: LoadBalance 继承关系图 -**LoadBalance 是一个扩展接口,默认使用的扩展实现是 RandomLoadBalance** ,其定义如下所示,其中的 @Adaptive 注解参数为 loadbalance,即动态生成的适配器会按照 URL 中的 loadbalance 参数值选择扩展实现类。 +**LoadBalance 是一个扩展接口,默认使用的扩展实现是 RandomLoadBalance**,其定义如下所示,其中的 @Adaptive 注解参数为 loadbalance,即动态生成的适配器会按照 URL 中的 loadbalance 参数值选择扩展实现类。 ```plaintext @SPI(RandomLoadBalance.NAME) @@ -102,13 +102,13 @@ static int calculateWarmupWeight(int uptime, int warmup, int weight) { ConsistentHashLoadBalance 底层使用一致性 Hash 算法实现负载均衡。为了让你更好地理解这部分内容,我们先来简单介绍一下一致性 Hash 算法相关的知识点。 -#### 1. 一致性 Hash 简析 **一致性 Hash 负载均衡可以让参数相同的请求每次都路由到相同的服务节点上** ,这种负载均衡策略可以在某些 Provider 节点下线的时候,让这些节点上的流量平摊到其他 Provider 上,不会引起流量的剧烈波动 +#### 1. 一致性 Hash 简析 **一致性 Hash 负载均衡可以让参数相同的请求每次都路由到相同的服务节点上**,这种负载均衡策略可以在某些 Provider 节点下线的时候,让这些节点上的流量平摊到其他 Provider 上,不会引起流量的剧烈波动 下面我们通过一个示例,简单介绍一致性 Hash 算法的原理。 假设现在有 1、2、3 三个 Provider 节点对外提供服务,有 100 个请求同时到达,如果想让请求尽可能均匀地分布到这三个 Provider 节点上,我们可能想到的最简单的方法就是 Hash 取模,即 hash(请求参数) % 3。如果参与 Hash 计算的是请求的全部参数,那么参数相同的请求将会落到同一个 Provider 节点上。不过此时如果突然有一个 Provider 节点出现宕机的情况,那我们就需要对 2 取模,即请求会重新分配到相应的 Provider 之上。在极端情况下,甚至会出现所有请求的处理节点都发生了变化,这就会造成比较大的波动。 -为了避免因一个 Provider 节点宕机,而导致大量请求的处理节点发生变化的情况,我们可以考虑使用一致性 Hash 算法。 **一致性 Hash 算法的原理也是取模算法,与 Hash 取模的不同之处在于:Hash 取模是对 Provider 节点数量取模,而一致性 Hash 算法是对 2^32 取模。** 一致性 Hash 算法需要同时对 Provider 地址以及请求参数进行取模: +为了避免因一个 Provider 节点宕机,而导致大量请求的处理节点发生变化的情况,我们可以考虑使用一致性 Hash 算法。**一致性 Hash 算法的原理也是取模算法,与 Hash 取模的不同之处在于:Hash 取模是对 Provider 节点数量取模,而一致性 Hash 算法是对 2^32 取模。** 一致性 Hash 算法需要同时对 Provider 地址以及请求参数进行取模: ```plaintext hash(Provider地址) % 232 @@ -129,7 +129,7 @@ Provider 地址和请求经过对 232 取模得到的结果值,都会落到一 一致性 Hash 节点非均匀分布图 -这就出现了数据倾斜的问题。 **所谓数据倾斜是指由于节点不够分散,导致大量请求落到了同一个节点上,而其他节点只会接收到少量请求的情况** 。 +这就出现了数据倾斜的问题。**所谓数据倾斜是指由于节点不够分散,导致大量请求落到了同一个节点上,而其他节点只会接收到少量请求的情况** 。 为了解决一致性 Hash 算法中出现的数据倾斜问题,又演化出了 Hash 槽的概念。 @@ -141,7 +141,7 @@ Hash 槽解决数据倾斜的思路是:既然问题是由 Provider 节点在 H #### 2. ConsistentHashSelector 实现分析 -了解了一致性 Hash 算法的基本原理之后,我们再来看一下 ConsistentHashLoadBalance 一致性 Hash 负载均衡的具体实现。首先来看 doSelect() 方法的实现,其中会根据 ServiceKey 和 methodName 选择一个 ConsistentHashSelector 对象, **核心算法都委托给 ConsistentHashSelector 对象完成。** +了解了一致性 Hash 算法的基本原理之后,我们再来看一下 ConsistentHashLoadBalance 一致性 Hash 负载均衡的具体实现。首先来看 doSelect() 方法的实现,其中会根据 ServiceKey 和 methodName 选择一个 ConsistentHashSelector 对象,**核心算法都委托给 ConsistentHashSelector 对象完成。** ```java protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" index 08a17e51d..7b248b38a 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" @@ -82,7 +82,7 @@ RoundRobinLoadBalance 实现的是 **加权轮询负载均衡算法** 。 **轮询是一种无状态负载均衡算法,实现简单,适用于集群中所有 Provider 节点性能相近的场景。** 但现实情况中就很难保证这一点了,因为很容易出现集群中性能最好和最差的 Provider 节点处理同样流量的情况,这就可能导致性能差的 Provider 节点各方面资源非常紧张,甚至无法及时响应了,但是性能好的 Provider 节点的各方面资源使用还较为空闲。这时我们可以通过加权轮询的方式,降低分配到性能较差的 Provider 节点的流量。 -加权之后,分配给每个 Provider 节点的流量比会接近或等于它们的权重比。例如,Provider 节点 A、B、C 权重比为 5:1:1,那么在 7 次请求中,节点 A 将收到 5 次请求,节点 B 会收到 1 次请求,节点 C 则会收到 1 次请求。 **在 Dubbo 2.6.4 版本及之前,RoundRobinLoadBalance 的实现存在一些问题,例如,选择 Invoker 的性能问题、负载均衡时不够平滑等。在 Dubbo 2.6.5 版本之后,这些问题都得到了修复** ,所以这里我们就来介绍最新的 RoundRobinLoadBalance 实现。 +加权之后,分配给每个 Provider 节点的流量比会接近或等于它们的权重比。例如,Provider 节点 A、B、C 权重比为 5:1:1,那么在 7 次请求中,节点 A 将收到 5 次请求,节点 B 会收到 1 次请求,节点 C 则会收到 1 次请求。**在 Dubbo 2.6.4 版本及之前,RoundRobinLoadBalance 的实现存在一些问题,例如,选择 Invoker 的性能问题、负载均衡时不够平滑等。在 Dubbo 2.6.5 版本之后,这些问题都得到了修复**,所以这里我们就来介绍最新的 RoundRobinLoadBalance 实现。 每个 Provider 节点有两个权重:一个权重是配置的 weight,该值在负载均衡的过程中不会变化;另一个权重是 currentWeight,该值会在负载均衡的过程中动态调整,初始值为 0。 @@ -104,7 +104,7 @@ RoundRobinLoadBalance 实现的是 **加权轮询负载均衡算法** 。 而在 Dubbo 2.6.4 版本中,上面示例的一次轮询结果是 \[A, A, A, A, A, B, C\],也就是说前 5 个请求会全部都落到 A 这个节点上。这将会使节点 A 在短时间内接收大量的请求,压力陡增,而节点 B 和节点 C 此时没有收到任何请求,处于完全空闲的状态,这种“瞬间分配不平衡”的情况也就是前面提到的“不平滑问题”。 -在 RoundRobinLoadBalance 中,我们 **为每个 Invoker 对象创建了一个对应的 WeightedRoundRobin 对象** ,用来记录配置的权重(weight 字段)以及随每次负载均衡算法执行变化的 current 权重(current 字段)。 +在 RoundRobinLoadBalance 中,我们 **为每个 Invoker 对象创建了一个对应的 WeightedRoundRobin 对象**,用来记录配置的权重(weight 字段)以及随每次负载均衡算法执行变化的 current 权重(current 字段)。 了解了 WeightedRoundRobin 这个内部类后,我们再来看 RoundRobinLoadBalance.doSelect() 方法的具体实现: @@ -158,7 +158,7 @@ protected Invoker doSelect(List> invokers, URL url, Invocation ### ShortestResponseLoadBalance -ShortestResponseLoadBalance 是 **Dubbo 2.7 版本之后新增加的一个 LoadBalance 实现类** 。它实现了 **最短响应时间的负载均衡算法** ,也就是从多个 Provider 节点中选出调用成功的且响应时间最短的 Provider 节点,不过满足该条件的 Provider 节点可能有多个,所以还要再使用随机算法进行一次选择,得到最终要调用的 Provider 节点。 +ShortestResponseLoadBalance 是 **Dubbo 2.7 版本之后新增加的一个 LoadBalance 实现类** 。它实现了 **最短响应时间的负载均衡算法**,也就是从多个 Provider 节点中选出调用成功的且响应时间最短的 Provider 节点,不过满足该条件的 Provider 节点可能有多个,所以还要再使用随机算法进行一次选择,得到最终要调用的 Provider 节点。 了解了 ShortestResponseLoadBalance 的核心原理之后,我们一起来看 ShortestResponseLoadBalance.doSelect() 方法的核心实现,如下所示: diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25437\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25437\350\256\262.md" index 415e44924..f3ea2cd33 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25437\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25437\350\256\262.md" @@ -6,7 +6,7 @@ **Cluster 接口提供了我们常说的集群容错功能。** 集群中的单个节点有一定概率出现一些问题,例如,磁盘损坏、系统崩溃等,导致节点无法对外提供服务,因此在分布式 RPC 框架中,必须要重视这种情况。为了避免单点故障,我们的 Provider 通常至少会部署在两台服务器上,以集群的形式对外提供服务,对于一些负载比较高的服务,则需要部署更多 Provider 来抗住流量。 -在 Dubbo 中,通过 Cluster 这个接口把一组可供调用的 Provider 信息组合成为一个统一的 Invoker 供调用方进行调用。经过 Router 过滤、LoadBalance 选址之后,选中一个具体 Provider 进行调用, **如果调用失败,则会按照集群的容错策略进行容错处理** 。 +在 Dubbo 中,通过 Cluster 这个接口把一组可供调用的 Provider 信息组合成为一个统一的 Invoker 供调用方进行调用。经过 Router 过滤、LoadBalance 选址之后,选中一个具体 Provider 进行调用,**如果调用失败,则会按照集群的容错策略进行容错处理** 。 Dubbo 默认内置了若干容错策略,并且每种容错策略都有自己独特的应用场景,我们可以 **通过配置选择不同的容错策略** 。如果这些内置容错策略不能满足需求,我们还可以 **通过自定义容错策略进行配置** 。 @@ -29,7 +29,7 @@ Cluster Invoker 获取 Invoker 的流程大致可描述为如下: 这个过程是一个正常流程,没有涉及容错处理。Dubbo 中常见的容错方式有如下几个。 -- Failover Cluster:失败自动切换。 **它是 Dubbo 的默认容错机制** ,在请求一个 Provider 节点失败的时候,自动切换其他 Provider 节点,默认执行 3 次,适合幂等操作。当然,重试次数越多,在故障容错的时候带给 Provider 的压力就越大,在极端情况下甚至可能造成雪崩式的问题。 +- Failover Cluster:失败自动切换。**它是 Dubbo 的默认容错机制**,在请求一个 Provider 节点失败的时候,自动切换其他 Provider 节点,默认执行 3 次,适合幂等操作。当然,重试次数越多,在故障容错的时候带给 Provider 的压力就越大,在极端情况下甚至可能造成雪崩式的问题。 - Failback Cluster:失败自动恢复。失败后记录到队列中,通过定时器重试。 - Failfast Cluster:快速失败。请求失败后返回异常,不进行任何重试。 - Failsafe Cluster:失败安全。请求失败后忽略异常,不进行任何重试。 @@ -54,7 +54,7 @@ Cluster 接口的实现类如下图所示,分别对应前面提到的多种容 Cluster 接口继承关系 -**在每个 Cluster 接口实现中,都会创建对应的 Invoker 对象,这些都继承自 AbstractClusterInvoker 抽象类** ,如下图所示: +**在每个 Cluster 接口实现中,都会创建对应的 Invoker 对象,这些都继承自 AbstractClusterInvoker 抽象类**,如下图所示: ![Lark20201201-164728.png](assets/CgqCHl_GA0-AcVvrAAGLJ3YaO2Q177.png) @@ -175,7 +175,7 @@ private Invoker doSelect(LoadBalance loadbalance, Invocation invocation, } ``` -**reselect() 方法会重新进行一次负载均衡** ,首先对未尝试过的可用 Invokers 进行负载均衡,如果已经全部重试过了,则将尝试过的 Provider 节点过滤掉,然后在可用的 Provider 节点中重新进行负载均衡。 +**reselect() 方法会重新进行一次负载均衡**,首先对未尝试过的可用 Invokers 进行负载均衡,如果已经全部重试过了,则将尝试过的 Provider 节点过滤掉,然后在可用的 Provider 节点中重新进行负载均衡。 ```java private Invoker reselect(LoadBalance loadbalance, Invocation invocation, @@ -214,7 +214,7 @@ private Invoker reselect(LoadBalance loadbalance, Invocation invocation, ### AbstractCluster -常用的 ClusterInvoker 实现都继承了 AbstractClusterInvoker 类型,对应的 Cluster 扩展实现都继承了 AbstractCluster 抽象类。 **AbstractCluster 抽象类的核心逻辑是在 ClusterInvoker 外层包装一层 ClusterInterceptor,从而实现类似切面的效果** 。 +常用的 ClusterInvoker 实现都继承了 AbstractClusterInvoker 类型,对应的 Cluster 扩展实现都继承了 AbstractCluster 抽象类。**AbstractCluster 抽象类的核心逻辑是在 ClusterInvoker 外层包装一层 ClusterInterceptor,从而实现类似切面的效果** 。 下面是 ClusterInterceptor 接口的定义: diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25438\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25438\350\256\262.md" index f2e60f0aa..ed15fafd7 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25438\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25438\350\256\262.md" @@ -169,7 +169,7 @@ return new FailfastClusterInvoker\<>(directory); } ``` -**FailfastClusterInvoker 只会进行一次请求,请求失败之后会立即抛出异常,这种策略适合非幂等的操作** ,具体实现如下: +**FailfastClusterInvoker 只会进行一次请求,请求失败之后会立即抛出异常,这种策略适合非幂等的操作**,具体实现如下: ```java public Result doInvoke(Invocation invocation, List\> invokers, LoadBalance loadbalance) throws RpcException { @@ -198,7 +198,7 @@ return new FailsafeClusterInvoker\<>(directory); } ``` -**FailsafeClusterInvoker 只会进行一次请求,请求失败之后会返回一个空结果** ,具体实现如下: +**FailsafeClusterInvoker 只会进行一次请求,请求失败之后会返回一个空结果**,具体实现如下: ```plaintext public Result doInvoke(Invocation invocation, List\> invokers, LoadBalance loadbalance) throws RpcException { @@ -227,7 +227,7 @@ return new ForkingClusterInvoker\<>(directory); } ``` -ForkingClusterInvoker 中会 **维护一个线程池** (executor 字段,通过 Executors.newCachedThreadPool() 方法创建的线程池), **并发调用多个 Provider 节点,只要有一个 Provider 节点成功返回了结果,ForkingClusterInvoker 的 doInvoke() 方法就会立即结束运行** 。 +ForkingClusterInvoker 中会 **维护一个线程池** (executor 字段,通过 Executors.newCachedThreadPool() 方法创建的线程池),**并发调用多个 Provider 节点,只要有一个 Provider 节点成功返回了结果,ForkingClusterInvoker 的 doInvoke() 方法就会立即结束运行** 。 ForkingClusterInvoker 主要是为了应对一些实时性要求较高的读操作,因为没有并发控制的多线程写入,可能会导致数据不一致。 @@ -304,7 +304,7 @@ return new BroadcastClusterInvoker\<>(directory); } ``` -**在 BroadcastClusterInvoker 中,会逐个调用每个 Provider 节点,其中任意一个 Provider 节点报错,都会在全部调用结束之后抛出异常** 。BroadcastClusterInvoker **通常用于通知类的操作** ,例如通知所有 Provider 节点更新本地缓存。 +**在 BroadcastClusterInvoker 中,会逐个调用每个 Provider 节点,其中任意一个 Provider 节点报错,都会在全部调用结束之后抛出异常** 。BroadcastClusterInvoker **通常用于通知类的操作**,例如通知所有 Provider 节点更新本地缓存。 下面来看 BroadcastClusterInvoker 的具体实现: @@ -345,7 +345,7 @@ return new AvailableClusterInvoker\<>(directory); } ``` -在 AvailableClusterInvoker 的 doInvoke() 方法中,会遍历整个 Invoker 集合, **逐个调用对应的 Provider 节点,当遇到第一个可用的 Provider 节点时,就尝试访问该 Provider 节点,成功则返回结果;如果访问失败,则抛出异常终止遍历** 。 +在 AvailableClusterInvoker 的 doInvoke() 方法中,会遍历整个 Invoker 集合,**逐个调用对应的 Provider 节点,当遇到第一个可用的 Provider 节点时,就尝试访问该 Provider 节点,成功则返回结果;如果访问失败,则抛出异常终止遍历** 。 下面是 AvailableClusterInvoker 的具体实现: @@ -516,8 +516,8 @@ Consumer 可以使用 ZoneAwareClusterInvoker 先在多个注册中心之间进 ZoneAwareClusterInvoker 在多注册中心之间进行选择的策略有以下四种。 -1. 找到 **preferred 属性为 true 的注册中心,它是优先级最高的注册中心** ,只有该中心无可用 Provider 节点时,才会回落到其他注册中心。 -2. 根据 **请求中的 zone key 做匹配** ,优先派发到相同 zone 的注册中心。 +1. 找到 **preferred 属性为 true 的注册中心,它是优先级最高的注册中心**,只有该中心无可用 Provider 节点时,才会回落到其他注册中心。 +2. 根据 **请求中的 zone key 做匹配**,优先派发到相同 zone 的注册中心。 3. 根据 **权重** (也就是注册中心配置的 weight 属性)进行轮询。 4. 如果上面的策略都未命中,则选择 **第一个可用的 Provider 节点** 。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" index 1d58f45f7..bbccbb312 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" @@ -133,7 +133,7 @@ public int[] merge(int[]... items) { ### MapMerger -SetMerger、ListMerger 和 MapMerger 是针对 Set 、List 和 Map 返回值的 Merger 实现,它们会 **将多个 Set(或 List、Map)集合合并成一个 Set(或 List、Map)集合** ,核心原理与 ArrayMerger 的实现类似。这里我们先来看 MapMerger 的核心实现: +SetMerger、ListMerger 和 MapMerger 是针对 Set 、List 和 Map 返回值的 Merger 实现,它们会 **将多个 Set(或 List、Map)集合合并成一个 Set(或 List、Map)集合**,核心原理与 ArrayMerger 的实现类似。这里我们先来看 MapMerger 的核心实现: ```cpp public Map merge(Map... items) { @@ -177,7 +177,7 @@ public List merge(List... items) { 介绍完 Dubbo 自带的 Merger 实现之后,下面我们还可以尝试动手写一个自己的 Merger 实现,这里我们以 dubbo-demo-xml 中的 Provider 和 Consumer 为例进行修改。 -首先我们在 dubbo-demo-xml-provider 示例模块中 **发布两个服务** ,分别属于 groupA 和 groupB,相应的 dubbo-provider.xml 配置如下: +首先我们在 dubbo-demo-xml-provider 示例模块中 **发布两个服务**,分别属于 groupA 和 groupB,相应的 dubbo-provider.xml 配置如下: ```plaintext merge(List... items) { ``` -接下来,在 dubbo-demo-xml-consumer 示例模块中 **进行服务引用** ,dubbo-consumer.xml 配置文件的具体内容如下: +接下来,在 dubbo-demo-xml-consumer 示例模块中 **进行服务引用**,dubbo-consumer.xml 配置文件的具体内容如下: ```plaintext merge(List... items) { String=org.apache.dubbo.demo.consumer.StringMerger ``` -StringMerger 实现了前面介绍的 Merger 接口,它 **会将多个 Provider 节点返回的 String 结果值拼接起来** ,具体实现如下: +StringMerger 实现了前面介绍的 Merger 接口,它 **会将多个 Provider 节点返回的 String 结果值拼接起来**,具体实现如下: ```java public class StringMerger implements Merger { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25440\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25440\350\256\262.md" index f3c451987..39d88f0ce 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25440\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25440\350\256\262.md" @@ -2,7 +2,7 @@ 你好,我是杨四正,今天我和你分享的主题是:Dubbo 中的 Mock 机制。 -Mock 机制是 RPC 框架中非常常见、也非常有用的功能, **不仅可以用来实现服务降级,还可以用来在测试中模拟调用的各种异常情况** 。Dubbo 中的 Mock 机制是在 Consumer 这一端实现的,具体来说就是在 Cluster 这一层实现的。 +Mock 机制是 RPC 框架中非常常见、也非常有用的功能,**不仅可以用来实现服务降级,还可以用来在测试中模拟调用的各种异常情况** 。Dubbo 中的 Mock 机制是在 Consumer 这一端实现的,具体来说就是在 Cluster 这一层实现的。 在前面第 38 课时中,我们深入介绍了 Dubbo 提供的多种 Cluster 实现以及相关的 Cluster Invoker 实现,其中的 ZoneAwareClusterInvoker 就涉及了 MockClusterInvoker 的相关内容。本课时我们就来介绍 Dubbo 中 Mock 机制的全链路流程,不仅包括与 Cluster 接口相关的 MockClusterWrapper 和 MockClusterInvoker,我们还会回顾前面课程的 Router 和 Protocol 接口,分析它们与 Mock 机制相关的实现。 @@ -14,7 +14,7 @@ Cluster 接口有两条继承线(如下图所示):一条线是 AbstractClu Cluster 继承关系图 -**MockClusterWrapper 是 Cluster 对象的包装类** ,我们在之前\[第 4 课时\]介绍 Dubbo SPI 机制时已经分析过 Wrapper 的功能,MockClusterWrapper 类会对 Cluster 进行包装。下面是 MockClusterWrapper 的具体实现,其中会在 Cluster Invoker 对象的基础上使用 MockClusterInvoker 进行包装: +**MockClusterWrapper 是 Cluster 对象的包装类**,我们在之前\[第 4 课时\]介绍 Dubbo SPI 机制时已经分析过 Wrapper 的功能,MockClusterWrapper 类会对 Cluster 进行包装。下面是 MockClusterWrapper 的具体实现,其中会在 Cluster Invoker 对象的基础上使用 MockClusterInvoker 进行包装: ```java public class MockClusterWrapper implements Cluster { @@ -32,7 +32,7 @@ public class MockClusterWrapper implements Cluster { } ``` -### MockClusterInvoker **MockClusterInvoker 是 Dubbo Mock 机制的核心** ,它主要是通过 invoke()、doMockInvoke() 和 selectMockInvoker() 这三个核心方法来实现 Mock 机制的 +### MockClusterInvoker **MockClusterInvoker 是 Dubbo Mock 机制的核心**,它主要是通过 invoke()、doMockInvoke() 和 selectMockInvoker() 这三个核心方法来实现 Mock 机制的 下面我们就来逐个介绍这三个方法的具体实现。 @@ -116,7 +116,7 @@ private List> selectMockInvoker(Invocation invocation) { ![Drawing 1.png](assets/CgqCHl_PEyqAeilHAAAnrF4cOr8848.png) -MockInvokersSelector 继承关系图 **MockInvokersSelector 是 Dubbo Mock 机制相关的 Router 实现** ,在未开启 Mock 机制的时候,会返回正常的 Invoker 对象集合;在开启 Mock 机制之后,会返回 MockInvoker 对象集合。MockInvokersSelector 的具体实现如下: +MockInvokersSelector 继承关系图 **MockInvokersSelector 是 Dubbo Mock 机制相关的 Router 实现**,在未开启 Mock 机制的时候,会返回正常的 Invoker 对象集合;在开启 Mock 机制之后,会返回 MockInvoker 对象集合。MockInvokersSelector 的具体实现如下: ```plaintext public List> route(final List> invokers, @@ -148,7 +148,7 @@ public List> route(final List> invokers, 介绍完 Mock 功能在 Cluster 层的相关实现之后,我们还要来看一下 Dubbo 在 RPC 层对 Mock 机制的支持,这里涉及 MockProtocol 和 MockInvoker 两个类。 -首先来看 MockProtocol,它是 Protocol 接口的扩展实现,扩展名称为 mock。 **MockProtocol 只能通过 refer() 方法创建 MockInvoker,不能通过 export() 方法暴露服务** ,具体实现如下: +首先来看 MockProtocol,它是 Protocol 接口的扩展实现,扩展名称为 mock。**MockProtocol 只能通过 refer() 方法创建 MockInvoker,不能通过 export() 方法暴露服务**,具体实现如下: ```java final public class MockProtocol extends AbstractProtocol { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25441\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25441\350\256\262.md" index 96613fd63..5d35946b8 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25441\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25441\350\256\262.md" @@ -38,7 +38,7 @@ public DubboBootstrap start() { } ``` -**不仅是直接通过 API 启动 Provider 的方式会使用到 DubboBootstrap,在 Spring 与 Dubbo 集成的时候也是使用 DubboBootstrap 作为服务发布入口的** ,具体逻辑在 DubboBootstrapApplicationListener 这个 Spring Context 监听器中,如下所示: +**不仅是直接通过 API 启动 Provider 的方式会使用到 DubboBootstrap,在 Spring 与 Dubbo 集成的时候也是使用 DubboBootstrap 作为服务发布入口的**,具体逻辑在 DubboBootstrapApplicationListener 这个 Spring Context 监听器中,如下所示: ```java public class DubboBootstrapApplicationListener extends OneTimeExecutionApplicationContextEventListener @@ -70,7 +70,7 @@ return LOWEST_PRECEDENCE; } ``` -这里我们重点关注的是 **exportServices() 方法,它是服务发布核心逻辑的入口** ,其中每一个服务接口都会转换为对应的 ServiceConfig 实例,然后通过代理的方式转换成 Invoker,最终转换成 Exporter 进行发布。服务发布流程中涉及的核心对象转换,如下图所示: +这里我们重点关注的是 **exportServices() 方法,它是服务发布核心逻辑的入口**,其中每一个服务接口都会转换为对应的 ServiceConfig 实例,然后通过代理的方式转换成 Invoker,最终转换成 Exporter 进行发布。服务发布流程中涉及的核心对象转换,如下图所示: ![Lark20201215-163844.png](assets/Ciqc1F_YdkGABhTFAACpT-2oDtw867.png) @@ -151,7 +151,7 @@ application=dubbo-demo-api-provider ### 组装服务 URL -doExportUrlsFor1Protocol() 方法的代码非常长,这里我们分成两个部分进行介绍:一部分是组装服务的 URL,另一部分就是后面紧接着介绍的服务发布。 **组装服务的 URL** 核心步骤有如下 7 步。 +doExportUrlsFor1Protocol() 方法的代码非常长,这里我们分成两个部分进行介绍:一部分是组装服务的 URL,另一部分就是后面紧接着介绍的服务发布。**组装服务的 URL** 核心步骤有如下 7 步。 1. 获取此次发布使用的协议,默认使用 dubbo 协议。 2. 设置服务 URL 中的参数,这里会从 MetricsConfig、ApplicationConfig、ModuleConfig、ProviderConfig、ProtocolConfig 中获取配置信息,并作为参数添加到 URL 中。这里调用的 appendParameters() 方法会将 AbstractConfig 中的配置信息存储到 Map 集合中,后续在构造 URL 的时候,会将该集合中的 KV 作为 URL 的参数。 @@ -269,14 +269,14 @@ anyhost=true ### 服务发布入口 -完成了服务 URL 的组装之后,doExportUrlsFor1Protocol() 方法开始执行服务发布。服务发布可以分为 **远程发布** 和 **本地发布** ,具体发布方式与服务 URL 中的 scope 参数有关。 +完成了服务 URL 的组装之后,doExportUrlsFor1Protocol() 方法开始执行服务发布。服务发布可以分为 **远程发布** 和 **本地发布**,具体发布方式与服务 URL 中的 scope 参数有关。 scope 参数有三个可选值,分别是 none、remote 和 local,分别代表不发布、发布到本地和发布到远端注册中心,从下面介绍的 doExportUrlsFor1Protocol() 方法代码中可以看到: * 发布到本地的条件是 scope != remote; * 发布到注册中心的条件是 scope != local。 -**scope 参数的默认值为 null** ,也就是说,默认会同时在本地和注册中心发布该服务。下面来看 doExportUrlsFor1Protocol() 方法中发布服务的具体实现: +**scope 参数的默认值为 null**,也就是说,默认会同时在本地和注册中心发布该服务。下面来看 doExportUrlsFor1Protocol() 方法中发布服务的具体实现: ```java private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) { @@ -442,5 +442,5 @@ return new DestroyableExporter\<>(exporter); 首先我们介绍了 DubboBootstrap 这个入口门面类中与服务发布相关的方法,重点是 start() 和 exportServices() 两个方法;然后详细介绍了 ServiceConfig 类的三个核心步骤:检查参数、立即(或延迟)执行 doExport() 方法进行发布、回调服务发布的相关监听器。 -接下来,我们分析了 **doExportUrlsFor1Protocol() 方法,它是发布一个服务的入口,也是规定服务发布流程的地方** ,其中涉及 Provider URL 的组装、本地服务发布流程以及远程服务发布流程,对于这些步骤,我们都进行了详细的分析。 +接下来,我们分析了 **doExportUrlsFor1Protocol() 方法,它是发布一个服务的入口,也是规定服务发布流程的地方**,其中涉及 Provider URL 的组装、本地服务发布流程以及远程服务发布流程,对于这些步骤,我们都进行了详细的分析。 ``` diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25442\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25442\350\256\262.md" index 6df0ecdcf..7795259cc 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25442\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25442\350\256\262.md" @@ -4,12 +4,12 @@ Dubbo 作为一个 RPC 框架,暴露给用户最基本的功能就是服务发 Dubbo 支持两种方式引用远程的服务: -- **服务直连的方式** ,仅适合在 **调试服务** 的时候使用; -- **基于注册中心引用服务** ,这是 **生产环境** 中使用的服务引用方式。 +- **服务直连的方式**,仅适合在 **调试服务** 的时候使用; +- **基于注册中心引用服务**,这是 **生产环境** 中使用的服务引用方式。 ### DubboBootstrap 入口 -在上一课时介绍服务发布的时候,我们介绍了 DubboBootstrap.start() 方法的核心流程,其中除了会 **调用 exportServices() 方法完成服务发布** 之外,还会 **调用 referServices() 方法完成服务引用** ,这里就不再贴出 DubboBootstrap.start() 方法的具体代码,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/dubbo)进行学习。 +在上一课时介绍服务发布的时候,我们介绍了 DubboBootstrap.start() 方法的核心流程,其中除了会 **调用 exportServices() 方法完成服务发布** 之外,还会 **调用 referServices() 方法完成服务引用**,这里就不再贴出 DubboBootstrap.start() 方法的具体代码,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/dubbo)进行学习。 在 DubboBootstrap.referServices() 方法中,会从 ConfigManager 中获取所有 ReferenceConfig 列表,并根据 ReferenceConfig 获取对应的代理对象,入口逻辑如下: @@ -46,7 +46,7 @@ public DubboBootstrap reference(ReferenceConfig referenceConfig) { } ``` -### ReferenceConfigCache **服务引用的核心实现在 ReferenceConfig 之中** ,一个 ReferenceConfig 对象对应一个服务接口,每个 ReferenceConfig 对象中都封装了与注册中心的网络连接,以及与 Provider 的网络连接,这是一个非常重要的对象。 **为了避免底层连接泄漏造成性能问题,从 Dubbo 2.4.0 版本开始,Dubbo 提供了 ReferenceConfigCache 用于缓存 ReferenceConfig 实例。** +### ReferenceConfigCache **服务引用的核心实现在 ReferenceConfig 之中**,一个 ReferenceConfig 对象对应一个服务接口,每个 ReferenceConfig 对象中都封装了与注册中心的网络连接,以及与 Provider 的网络连接,这是一个非常重要的对象。**为了避免底层连接泄漏造成性能问题,从 Dubbo 2.4.0 版本开始,Dubbo 提供了 ReferenceConfigCache 用于缓存 ReferenceConfig 实例。** 在 dubbo-demo-api-consumer 示例中,我们可以看到 ReferenceConfigCache 的基本使用方式: @@ -127,7 +127,7 @@ public T get(ReferenceConfigBase referenceConfig) { ### ReferenceConfig -通过前面的介绍我们知道, **ReferenceConfig 是服务引用的真正入口** ,其中会创建相关的代理对象。下面先来看 ReferenceConfig.get() 方法: +通过前面的介绍我们知道,**ReferenceConfig 是服务引用的真正入口**,其中会创建相关的代理对象。下面先来看 ReferenceConfig.get() 方法: ```java public synchronized T get() { @@ -143,7 +143,7 @@ public synchronized T get() { 在 ReferenceConfig.init() 方法中,首先会对服务引用的配置进行处理,以保证配置的正确性。这里的具体实现其实本身并不复杂,但由于涉及很多的配置解析和处理逻辑,代码就显得非常长,我们就不再一一展示,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/dubbo)进行学习。 -**ReferenceConfig.init() 方法的核心逻辑是调用 createProxy() 方法** ,调用之前会从配置中获取 createProxy() 方法需要的参数: +**ReferenceConfig.init() 方法的核心逻辑是调用 createProxy() 方法**,调用之前会从配置中获取 createProxy() 方法需要的参数: ```java public synchronized void init() { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" index 600f796d9..ffbe44f8a 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" @@ -2,7 +2,7 @@ 随着微服务架构的不断发展和普及,RPC 框架成为微服务架构中不可或缺的重要角色,Dubbo 作为 Java 生态中一款成熟的 RPC 框架也在随着技术的更新换代不断发展壮大。当然,传统的 Dubbo 架构也面临着新思想、新生态和新技术带来的挑战。 -在微服务架构中,服务是基本单位,而 Dubbo 架构中服务的基本单位是 Java 接口,这种架构上的差别就会带来一系列挑战。 **从 2.7.5 版本开始,Dubbo 引入了服务自省架构,来应对微服务架构带来的挑战** 。具体都有哪些挑战呢?下面我们就来详细说明一下。 +在微服务架构中,服务是基本单位,而 Dubbo 架构中服务的基本单位是 Java 接口,这种架构上的差别就会带来一系列挑战。**从 2.7.5 版本开始,Dubbo 引入了服务自省架构,来应对微服务架构带来的挑战** 。具体都有哪些挑战呢?下面我们就来详细说明一下。 ### 注册中心面临的挑战 @@ -41,7 +41,7 @@ dubbo://192.168.0.100:20880/org.apache.dubbo.demo.DemoService?anyhost=true&appli Dubbo 从 2.7.0 版本开始增加了 **简化 URL** 的特性,从 URL 中抽出的数据会被存放至元数据中心。但是这次优化只是缩短了 URL 的长度,从内存使用量以及降低通知频繁度的角度降低了注册中心的压力,并没有减少注册中心 URL 的数量,所以注册中心所承受的压力还是比较明显的。 -Dubbo 2.7.5 版本引入了 **服务自省架构** ,进一步降低了注册中心的压力。在此次优化中,Dubbo 修改成应用为粒度的服务注册与发现模型,最大化地减少了 Dubbo 服务元信息注册数量,其核心流程如下图所示: +Dubbo 2.7.5 版本引入了 **服务自省架构**,进一步降低了注册中心的压力。在此次优化中,Dubbo 修改成应用为粒度的服务注册与发现模型,最大化地减少了 Dubbo 服务元信息注册数量,其核心流程如下图所示: ![Lark20201222-120323.png](assets/CgqCHl_hcJqACV_gAAEpu4IHuz4068.png) @@ -74,14 +74,14 @@ Dubbo 2.7.5 版本引入了 **服务自省架构** ,进一步降低了注册 至于上图中涉及的一些新概念,为方便你理解,这里我们对它们的具体实现进行一个简单的介绍。 - Service Name:服务名称,例如,在一个电商系统中,有用户服务、商品服务、库存服务等。 -- Service Instance:服务实例,表示单个 Dubbo 应用进程, **多个 Service Instance 构成一个服务集群,拥有相同的 Service Name** 。 +- Service Instance:服务实例,表示单个 Dubbo 应用进程,**多个 Service Instance 构成一个服务集群,拥有相同的 Service Name** 。 - Service ID:唯一标识一个 Dubbo 服务,由 `${protocol}:${interface}:${version}:${group}` 四部分构成。 在有的场景中,我们会在线上部署两组不同配置的服务节点,来验证某些配置是否生效。例如,共有 100 个服务节点,平均分成 A、B 两组,A 组服务节点超时时间(即 timeout)设置为 3000 ms,B 组的超时时间(即 timeout)设置为 2000 ms,这样的话该服务就有了两组不同的元数据。 按照前面介绍的优化方案,在订阅服务的时候,会得到 100 个 ServiceInstance,因为每个 ServiceInstance 发布的服务元数据都有可能不一样,所以我们需要调用每个 ServiceInstance 的 MetadataService 服务获取元数据。 -为了减少 MetadataService 服务的调用次数,Dubbo 提出了 **服务修订版本的优化方案** ,其核心思想是:将每个 ServiceInstance 发布的服务 URL 计算一个 hash 值(也就是 revision 值),并随 ServiceInstance 一起发布到注册中心;在 Consumer 端进行订阅的时候,对于 revision 值相同的 ServiceInstance,不再调用 MetadataService 服务,直接共用一份 URL 即可。下图展示了 Dubbo 服务修订的核心逻辑: +为了减少 MetadataService 服务的调用次数,Dubbo 提出了 **服务修订版本的优化方案**,其核心思想是:将每个 ServiceInstance 发布的服务 URL 计算一个 hash 值(也就是 revision 值),并随 ServiceInstance 一起发布到注册中心;在 Consumer 端进行订阅的时候,对于 revision 值相同的 ServiceInstance,不再调用 MetadataService 服务,直接共用一份 URL 即可。下图展示了 Dubbo 服务修订的核心逻辑: ![Lark20201222-120318.png](assets/Cip5yF_hcMyALC7UAAEPa7NIifA395.png) @@ -97,7 +97,7 @@ Dubbo 2.7.5 版本引入了 **服务自省架构** ,进一步降低了注册 由于此时的本地缓存已经覆盖了当前场景中全部的 revision 值,后续再次随机选择的 ServiceInstance 的 revision 不是 1 就是 2,都会落到本地缓存中,不会再次发起 MetadataService 服务调用。后续其他 ServiceInstance 的处理都会复用本地缓存的这两个 URL 列表,并根据 ServiceInstance 替换相应的参数(例如,host、port 等),这样即可得到 ServiceInstance 发布的完整的服务 URL 列表。 -一般情况下,revision 的数量不会很多,那么 Consumer 端发起的 MetadataService 服务调用次数也是有限的,不会随着 ServiceInstance 的扩容而增长。 **这样就避免了同一服务的不同版本导致的元数据膨胀** 。 +一般情况下,revision 的数量不会很多,那么 Consumer 端发起的 MetadataService 服务调用次数也是有限的,不会随着 ServiceInstance 的扩容而增长。**这样就避免了同一服务的不同版本导致的元数据膨胀** 。 ### 总结 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25444\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25444\350\256\262.md" index ddf58669e..354f08668 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25444\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25444\350\256\262.md" @@ -6,7 +6,7 @@ ### ServiceInstance -**Service Instance 唯一标识一个服务实例** ,在 Dubbo 的源码中对应 ServiceInstance 接口,该接口的具体定义如下: +**Service Instance 唯一标识一个服务实例**,在 Dubbo 的源码中对应 ServiceInstance 接口,该接口的具体定义如下: ```java public interface ServiceInstance extends Serializable { @@ -35,7 +35,7 @@ public interface ServiceInstance extends Serializable { } ``` -**DefaultServiceInstance 是 ServiceInstance 的唯一实现** ,DefaultServiceInstance 是一个普通的 POJO 类,其中的核心字段如下。 +**DefaultServiceInstance 是 ServiceInstance 的唯一实现**,DefaultServiceInstance 是一个普通的 POJO 类,其中的核心字段如下。 * id(String 类型):ServiceInstance 唯一标识。 * serviceName(String 类型):ServiceInstance 关联的 Service Name。 @@ -47,7 +47,7 @@ public interface ServiceInstance extends Serializable { ### ServiceDefinition -Dubbo 元数据服务与我们业务中发布的 Dubbo 服务无异, **Consumer 端可以调用一个 ServiceInstance 的元数据服务获取其发布的全部服务的元数据** 。 +Dubbo 元数据服务与我们业务中发布的 Dubbo 服务无异,**Consumer 端可以调用一个 ServiceInstance 的元数据服务获取其发布的全部服务的元数据** 。 说到元数据,就不得不提到 ServiceDefinition 这个类,它可以来描述一个服务接口的定义,其核心字段如下。 @@ -64,7 +64,7 @@ TypeBuilder 接口实现关系图 ### MetadataService -接下来看 MetadataService 接口,在上一讲我们提到 **Dubbo 中的每个 ServiceInstance 都会发布 MetadataService 接口供 Consumer 端查询元数据** ,下图展示了 MetadataService 接口的继承关系: +接下来看 MetadataService 接口,在上一讲我们提到 **Dubbo 中的每个 ServiceInstance 都会发布 MetadataService 接口供 Consumer 端查询元数据**,下图展示了 MetadataService 接口的继承关系: ![1.png](assets/CgpVE1_lrZGANC4vAAGdcllZU9o940.png) @@ -106,7 +106,7 @@ String getServiceDefinition(String serviceKey); } ``` -**在 MetadataService 接口中定义的都是查询元数据的方法,在其子接口 WritableMetadataService 中添加了一些发布元数据的写方法** ,具体定义如下: +**在 MetadataService 接口中定义的都是查询元数据的方法,在其子接口 WritableMetadataService 中添加了一些发布元数据的写方法**,具体定义如下: ```java @SPI(DEFAULT_METADATA_STORAGE_TYPE) @@ -140,7 +140,7 @@ return getExtensionLoader(WritableMetadataService.class).getOrDefaultExtension(n } ``` -WritableMetadataService 接口被 @SPI 注解修饰,是一个扩展接口,在前面的继承关系图中也可以看出, **它有两个比较基础的扩展实现,分别是 InMemoryWritableMetadataService(默认扩展实现) 和 RemoteWritableMetadataServiceDelegate,对应扩展名分别是 local 和 remote** 。 +WritableMetadataService 接口被 @SPI 注解修饰,是一个扩展接口,在前面的继承关系图中也可以看出,**它有两个比较基础的扩展实现,分别是 InMemoryWritableMetadataService(默认扩展实现) 和 RemoteWritableMetadataServiceDelegate,对应扩展名分别是 local 和 remote** 。 下面我们先来看 InMemoryWritableMetadataService 的实现,其中维护了三个核心集合。 @@ -171,7 +171,7 @@ return; } ``` -**在 RemoteWritableMetadataService 实现中封装了一个 InMemoryWritableMetadataService 对象,并对 publishServiceDefinition() 方法进行了覆盖** ,具体实现如下: +**在 RemoteWritableMetadataService 实现中封装了一个 InMemoryWritableMetadataService 对象,并对 publishServiceDefinition() 方法进行了覆盖**,具体实现如下: ```java public void publishServiceDefinition(URL url) { @@ -210,7 +210,7 @@ return; publishConsumer() 方法则相对比较简单:首先会清理 Consumer URL 中 pid、timestamp 等参数,然后将 Consumer URL 中的参数集合进行上报。 -不过,在 RemoteWritableMetadataService 中的 exportURL()、subscribeURL()、getExportedURLs()、getServiceDefinition() 等一系列方法都是空实现,这是为什么呢?其实我们从 RemoteWritableMetadataServiceDelegate 中就可以找到答案,注意, **RemoteWritableMetadataServiceDelegate 才是 MetadataService 接口的 remote 扩展实现** 。 **在 RemoteWritableMetadataServiceDelegate 中同时维护了一个 InMemoryWritableMetadataService 对象和 RemoteWritableMetadataService 对象** ,exportURL()、subscribeURL() 等发布订阅相关的方法会同时委托给这两个 MetadataService 对象,getExportedURLs()、getServiceDefinition() 等查询方法则只会调用 InMemoryWritableMetadataService 对象进行查询。这里我们以 exportURL() 方法为例进行说明: +不过,在 RemoteWritableMetadataService 中的 exportURL()、subscribeURL()、getExportedURLs()、getServiceDefinition() 等一系列方法都是空实现,这是为什么呢?其实我们从 RemoteWritableMetadataServiceDelegate 中就可以找到答案,注意,**RemoteWritableMetadataServiceDelegate 才是 MetadataService 接口的 remote 扩展实现** 。**在 RemoteWritableMetadataServiceDelegate 中同时维护了一个 InMemoryWritableMetadataService 对象和 RemoteWritableMetadataService 对象**,exportURL()、subscribeURL() 等发布订阅相关的方法会同时委托给这两个 MetadataService 对象,getExportedURLs()、getServiceDefinition() 等查询方法则只会调用 InMemoryWritableMetadataService 对象进行查询。这里我们以 exportURL() 方法为例进行说明: ```cpp public boolean exportURL(URL url) { @@ -224,7 +224,7 @@ return func.apply(defaultWritableMetadataService, url) && func.apply(remoteWrita ### MetadataReport -元数据中心是 Dubbo 2.7.0 版本之后新增的一项优化,其主要目的是将 URL 中的一部分内容存储到元数据中心,从而减少注册中心的压力。 **元数据中心的数据只是给本端自己使用的,改动不需要告知对端** ,例如,Provider 修改了元数据,不需要实时通知 Consumer。这样,在注册中心存储的数据量减少的同时,还减少了因为配置修改导致的注册中心频繁通知监听者情况的发生,很好地减轻了注册中心的压力。 **MetadataReport 接口是 Dubbo 节点与元数据中心交互的桥梁** ,其继承关系如下图所示: +元数据中心是 Dubbo 2.7.0 版本之后新增的一项优化,其主要目的是将 URL 中的一部分内容存储到元数据中心,从而减少注册中心的压力。**元数据中心的数据只是给本端自己使用的,改动不需要告知对端**,例如,Provider 修改了元数据,不需要实时通知 Consumer。这样,在注册中心存储的数据量减少的同时,还减少了因为配置修改导致的注册中心频繁通知监听者情况的发生,很好地减轻了注册中心的压力。**MetadataReport 接口是 Dubbo 节点与元数据中心交互的桥梁**,其继承关系如下图所示: ![2.png](assets/Cip5yF_lramAYf82AAFkkbA0N2g785.png) @@ -253,7 +253,7 @@ String getServiceDefinition(MetadataIdentifier metadataIdentifier); 了解了 MetadataReport 接口定义的核心行为之后,接下来我们就按照其实现的顺序来介绍:先来分析 AbstractMetadataReport 抽象类提供的公共实现,然后以 ZookeeperMetadataReport 这个具体实现为例,介绍 MetadataReport 如何与 ZooKeeper 配合实现元数据上报。 -#### 1\. AbstractMetadataReport **AbstractMetadataReport 中提供了所有 MetadataReport 的公共实现** ,其核心字段如下: +#### 1\. AbstractMetadataReport **AbstractMetadataReport 中提供了所有 MetadataReport 的公共实现**,其核心字段如下: ```java private URL reportURL; // 元数据中心的URL,其中包含元数据中心的地址 @@ -525,7 +525,7 @@ zookeeper=org.apache.dubbo.metadata.store.zookeeper.ZookeeperMetadataReportFacto **在 ZookeeperMetadataReport 中维护了一个 ZookeeperClient 实例用来和 ZooKeeper 进行交互** 。ZookeeperMetadataReport 读写元数据的根目录是 metadataReportURL 的 group 参数值,默认值为 dubbo。 -下面再来看 ZookeeperMetadataReport 对 AbstractMetadataReport 中各个 do\*() 方法的实现,这些方法核心都是通过 ZookeeperClient 创建、查询、删除对应的 ZNode 节点,没有什么复杂的逻辑, **关键是明确一下操作的 ZNode 节点的 path 是什么** 。 +下面再来看 ZookeeperMetadataReport 对 AbstractMetadataReport 中各个 do\*() 方法的实现,这些方法核心都是通过 ZookeeperClient 创建、查询、删除对应的 ZNode 节点,没有什么复杂的逻辑,**关键是明确一下操作的 ZNode 节点的 path 是什么** 。 doStoreProviderMetadata() 方法和 doStoreConsumerMetadata() 方法会调用 storeMetadata() 创建相应的 ZNode 节点: @@ -577,7 +577,7 @@ boolean isExported(); } ``` -**MetadataServiceExporter 只有 ConfigurableMetadataServiceExporter 这一个实现** ,如下图所示: +**MetadataServiceExporter 只有 ConfigurableMetadataServiceExporter 这一个实现**,如下图所示: ![Drawing 6.png](assets/Cip5yF_hcfmAMtHdAABVR_mzQyg047.png) @@ -623,7 +623,7 @@ return getExtensionLoader(ServiceNameMapping.class).getDefaultExtension(); } ``` -**DynamicConfigurationServiceNameMapping 是 ServiceNameMapping 的默认实现** ,也是唯一实现,其中会依赖 DynamicConfiguration 读写配置中心,完成 Service ID 和 Service Name 的映射。首先来看 DynamicConfigurationServiceNameMapping 的 map() 方法: +**DynamicConfigurationServiceNameMapping 是 ServiceNameMapping 的默认实现**,也是唯一实现,其中会依赖 DynamicConfiguration 读写配置中心,完成 Service ID 和 Service Name 的映射。首先来看 DynamicConfigurationServiceNameMapping 的 map() 方法: ```java public void map(String serviceInterface, String group, String version, String protocol) { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25445\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25445\350\256\262.md" index 6b364597d..f7653d764 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25445\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25445\350\256\262.md" @@ -6,7 +6,7 @@ ### ServiceDiscovery 接口 -**ServiceDiscovery 主要封装了针对 ServiceInstance 的发布和订阅操作** ,你可以暂时将其理解成一个 ServiceInstance 的注册中心。ServiceDiscovery 接口的定义如下所示: +**ServiceDiscovery 主要封装了针对 ServiceInstance 的发布和订阅操作**,你可以暂时将其理解成一个 ServiceInstance 的注册中心。ServiceDiscovery 接口的定义如下所示: ```cpp @SPI("zookeeper") @@ -116,7 +116,7 @@ protected ServiceDiscovery createDiscovery(URL registryURL) { ### ZookeeperServiceDiscovery 实现分析 -Dubbo 提供了多个 ServiceDiscovery 用来接入多种注册中心,下面我们以 ZookeeperServiceDiscovery 为例介绍 Dubbo 是如何接入 ZooKeeper 作为注册中心,实现服务实例发布和订阅的。 **在 ZookeeperServiceDiscovery 中封装了一个 Apache Curator 中的 ServiceDiscovery 对象来实现与 ZooKeeper 的交互** 。在 initialize() 方法中会初始化 CuratorFramework 以及 Curator ServiceDiscovery 对象,如下所示: +Dubbo 提供了多个 ServiceDiscovery 用来接入多种注册中心,下面我们以 ZookeeperServiceDiscovery 为例介绍 Dubbo 是如何接入 ZooKeeper 作为注册中心,实现服务实例发布和订阅的。**在 ZookeeperServiceDiscovery 中封装了一个 Apache Curator 中的 ServiceDiscovery 对象来实现与 ZooKeeper 的交互** 。在 initialize() 方法中会初始化 CuratorFramework 以及 Curator ServiceDiscovery 对象,如下所示: ```java public void initialize(URL registryURL) throws Exception { @@ -168,7 +168,7 @@ ZookeeperServiceDiscovery 除了实现了 ServiceDiscovery 接口之外,还实 ZookeeperServiceDiscovery 继承关系图 -也就是说, **ZookeeperServiceDiscovery 本身也是 EventListener 实现,可以作为 EventListener 监听某些事件** 。下面我们先来看 Dubbo 中 EventListener 接口的定义,其中关注三个方法:onEvent() 方法、getPriority() 方法和 findEventType() 工具方法。 +也就是说,**ZookeeperServiceDiscovery 本身也是 EventListener 实现,可以作为 EventListener 监听某些事件** 。下面我们先来看 Dubbo 中 EventListener 接口的定义,其中关注三个方法:onEvent() 方法、getPriority() 方法和 findEventType() 工具方法。 ```cpp @SPI @@ -229,7 +229,7 @@ protected void registerServiceWatcher(String serviceName) { } ``` -**ZookeeperServiceDiscoveryChangeWatcher 是 ZookeeperServiceDiscovery 配套的 CuratorWatcher 实现** ,其中 process() 方法实现会关注 NodeChildrenChanged 事件和 NodeDataChanged 事件,并调用关联的 ZookeeperServiceDiscovery 对象的 dispatchServiceInstancesChangedEvent() 方法,具体实现如下: +**ZookeeperServiceDiscoveryChangeWatcher 是 ZookeeperServiceDiscovery 配套的 CuratorWatcher 实现**,其中 process() 方法实现会关注 NodeChildrenChanged 事件和 NodeDataChanged 事件,并调用关联的 ZookeeperServiceDiscovery 对象的 dispatchServiceInstancesChangedEvent() 方法,具体实现如下: ```java public void process(WatchedEvent event) throws Exception { @@ -317,7 +317,7 @@ EventDispatcher 继承关系图 AbstractEventDispatcher 中的 addEventListener()、removeEventListener()、getAllEventListeners() 方法都是通过操作 listenersCache 集合实现的,具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考[源码](https://github.com/xxxlxy2008/dubbo)进行学习。 -AbstractEventDispatcher 中另一个要关注的方法是 dispatch() 方法,该方法 **会从 listenersCache 集合中过滤出符合条件的 EventListener 对象,并按照串行或是并行模式进行通知** ,具体实现如下: +AbstractEventDispatcher 中另一个要关注的方法是 dispatch() 方法,该方法 **会从 listenersCache 集合中过滤出符合条件的 EventListener 对象,并按照串行或是并行模式进行通知**,具体实现如下: ```java public void dispatch(Event event) { diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25446\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25446\350\256\262.md" index 594276576..b8ee0cf3a 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25446\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25446\350\256\262.md" @@ -1,6 +1,6 @@ # 46 加餐:深入服务自省方案中的服务发布订阅(下) -在课程第二部分(13~15 课时)中介绍 Dubbo 传统框架中的注册中心部分实现时,我们提到了 Registry、RegistryFactory 等与注册中心交互的接口。 **为了将 ServiceDiscovery 接口的功能与 Registry 融合,Dubbo 提供了一个 ServiceDiscoveryRegistry 实现** ,继承关系如下所示: +在课程第二部分(13~15 课时)中介绍 Dubbo 传统框架中的注册中心部分实现时,我们提到了 Registry、RegistryFactory 等与注册中心交互的接口。**为了将 ServiceDiscovery 接口的功能与 Registry 融合,Dubbo 提供了一个 ServiceDiscoveryRegistry 实现**,继承关系如下所示: ![Drawing 0.png](assets/Ciqc1F_pe3KAQs8SAAPkHLoWbUM655.png) @@ -8,7 +8,7 @@ ServiceDiscoveryRegistry 、ServiceDiscoveryRegistryFactory 继承关系图 由图我们可以看到:ServiceDiscoveryRegistryFactory(扩展名称是 service-discovery-registry)是 ServiceDiscoveryRegistry 对应的工厂类,继承了 AbstractRegistryFactory 提供的公共能力。 -**ServiceDiscoveryRegistry 是一个面向服务实例(ServiceInstance)的注册中心实现** ,其底层依赖前面两个课时介绍的 ServiceDiscovery、WritableMetadataService 等组件。 +**ServiceDiscoveryRegistry 是一个面向服务实例(ServiceInstance)的注册中心实现**,其底层依赖前面两个课时介绍的 ServiceDiscovery、WritableMetadataService 等组件。 ServiceDiscoveryRegistry 中的核心字段有如下几个。 @@ -178,9 +178,9 @@ protected void subscribeURLs(URL subscribedURL, NotifyListener listener, String 这里构造完整 subscribedURL 可以分为两个分支。 -- 第一个分支:结合传入的 subscribedURL 以及从元数据中获取每个 ServiceInstance 的对应参数,组装成每个 ServiceInstance 对应的完整 subscribeURL。 **该部分实现在 getExportedURLs() 方法中,也是订阅操作的核心** 。 +- 第一个分支:结合传入的 subscribedURL 以及从元数据中获取每个 ServiceInstance 的对应参数,组装成每个 ServiceInstance 对应的完整 subscribeURL。**该部分实现在 getExportedURLs() 方法中,也是订阅操作的核心** 。 -- 第二个分支:当上述操作无法获得完整的 subscribeURL 集合时,会使用 SubscribedURLsSynthesizer,基于 subscribedURL 拼凑出每个 ServiceInstance 对应的完整的 subscribedURL。 **该部分实现在 synthesizeSubscribedURLs() 方法中,目前主要针对 rest 协议** 。 +- 第二个分支:当上述操作无法获得完整的 subscribeURL 集合时,会使用 SubscribedURLsSynthesizer,基于 subscribedURL 拼凑出每个 ServiceInstance 对应的完整的 subscribedURL。**该部分实现在 synthesizeSubscribedURLs() 方法中,目前主要针对 rest 协议** 。 #### 3. getExportedURLs() 方法核心实现 @@ -225,7 +225,7 @@ public interface ServiceInstanceCustomizer extends Prioritized { 关于 ServiceInstanceCustomizer 接口,这里需要关注三个点:①该接口被 @SPI 注解修饰,是一个扩展点;②该接口继承了 Prioritized 接口;③该接口中定义的 customize() 方法可以用来自定义 ServiceInstance 信息,其中就包括控制 metadata 集合中的数据。 -也就说, **ServiceInstanceCustomizer 的多个实现可以按序调用,实现 ServiceInstance 的自定义** 。下图展示了 ServiceInstanceCustomizer 接口的所有实现类: +也就说,**ServiceInstanceCustomizer 的多个实现可以按序调用,实现 ServiceInstance 的自定义** 。下图展示了 ServiceInstanceCustomizer 接口的所有实现类: ![Drawing 1.png](assets/CgpVE1_pe6SAT90SAAC2xP9_c7c171.png) @@ -358,7 +358,7 @@ public void onEvent(ServiceInstancePreRegisteredEvent event) { 介绍完 ServiceInstanceMetadataCustomizer 的内容之后,下面我们回到 ServiceDiscoveryRegistry 继续分析。 -在清理完过期的修订版本 URL 之后,接下来会 **检测所有 ServiceInstance 的 revision 值是否已经存在于 serviceRevisionExportedURLsCache 缓存中** ,如果某个 ServiceInstance 的 revision 值没有在该缓存中,则会调用该 ServiceInstance 发布的 MetadataService 接口进行查询,这部分逻辑在 initializeRevisionExportedURLs() 方法中实现。具体实现如下: +在清理完过期的修订版本 URL 之后,接下来会 **检测所有 ServiceInstance 的 revision 值是否已经存在于 serviceRevisionExportedURLsCache 缓存中**,如果某个 ServiceInstance 的 revision 值没有在该缓存中,则会调用该 ServiceInstance 发布的 MetadataService 接口进行查询,这部分逻辑在 initializeRevisionExportedURLs() 方法中实现。具体实现如下: ```plaintext private List initializeRevisionExportedURLs(ServiceInstance serviceInstance) { @@ -410,7 +410,7 @@ private List getExportedURLs(ServiceInstance providerServiceInstance) { } ``` -这里涉及一个新的接口—— **MetadataServiceProxyFactory,它是用来创建 MetadataService 本地代理的工厂类** ,继承关系如下所示: +这里涉及一个新的接口—— **MetadataServiceProxyFactory,它是用来创建 MetadataService 本地代理的工厂类**,继承关系如下所示: ![Drawing 2.png](assets/CgpVE1_pe72AFUTPAADh6TOy_Ak061.png) diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25447\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25447\350\256\262.md" index be7e66fdf..f3cb0060b 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25447\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25447\350\256\262.md" @@ -1,6 +1,6 @@ # 47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上) -**从 2.7.0 版本开始,Dubbo 正式支持配置中心** ,在服务自省架构中也依赖配置中心完成 Service ID 与 Service Name 的映射。配置中心在 Dubbo 中主要承担两个职责: +**从 2.7.0 版本开始,Dubbo 正式支持配置中心**,在服务自省架构中也依赖配置中心完成 Service ID 与 Service Name 的映射。配置中心在 Dubbo 中主要承担两个职责: - 外部化配置; - 服务治理,负责服务治理规则的存储与通知。 @@ -16,13 +16,13 @@ Dubbo 目前支持下面四种配置来源,优先级由 1 到 4 逐级降低 1. API 接口、注解、XML 配置等编程方式收到的配置,最终得到 ServiceConfig、ReferenceConfig 等对象; 1. 本地 dubbo.properties 配置文件。 -### Configuration **Configuration 接口是 Dubbo 中所有配置的基础接口** ,其中定义了根据指定 Key 获取对应配置值的相关方法,如下图所示 +### Configuration **Configuration 接口是 Dubbo 中所有配置的基础接口**,其中定义了根据指定 Key 获取对应配置值的相关方法,如下图所示 ![Drawing 0.png](assets/Cip5yF_zz3yABBYdAACqAETTGm0778.png) Configuration 接口核心方法 -从上图中我们可以看到,Configuration 针对不同的 boolean、int、String 返回值都有对应的 get\*() 方法,同时还提供了带有默认值的 get\*() 方法。 **这些 get** ***() 方法底层首先调用 getInternalProperty() 方法获取配置值** ,然后调用 convert() 方法将获取到的配置值转换成返回值的类型之后返回。getInternalProperty() 是一个抽象方法,由 Configuration 接口的子类具体实现。 +从上图中我们可以看到,Configuration 针对不同的 boolean、int、String 返回值都有对应的 get\*() 方法,同时还提供了带有默认值的 get\*() 方法。**这些 get** ***() 方法底层首先调用 getInternalProperty() 方法获取配置值**,然后调用 convert() 方法将获取到的配置值转换成返回值的类型之后返回。getInternalProperty() 是一个抽象方法,由 Configuration 接口的子类具体实现。 下图展示了 Dubbo 中提供的 Configuration 接口实现,包括:SystemConfiguration、EnvironmentConfiguration、InmemoryConfiguration、PropertiesConfiguration、CompositeConfiguration、ConfigConfigurationAdapter 和 DynamicConfiguration。下面我们将结合具体代码逐个介绍其实现。 @@ -122,7 +122,7 @@ public Object getInternalProperty(String key) { #### CompositeConfiguration -CompositeConfiguration 是一个复合的 Configuration 对象, **其核心就是将多个 Configuration 对象组合起来,对外表现为一个 Configuration 对象** 。 +CompositeConfiguration 是一个复合的 Configuration 对象,**其核心就是将多个 Configuration 对象组合起来,对外表现为一个 Configuration 对象** 。 CompositeConfiguration 组合的 Configuration 对象都保存在 configList 字段中(LinkedList`` 集合),CompositeConfiguration 提供了 addConfiguration() 方法用于向 configList 集合中添加 Configuration 对象,如下所示: @@ -188,7 +188,7 @@ Dubbo 通过 AbstractConfig 类来抽象实例对应的配置,如下图所示 AbstractConfig 继承关系图 -这些 AbstractConfig 实现基本都对应一个固定的配置,也定义了配置对应的字段以及 getter/setter() 方法。例如,RegistryConfig 这个实现类就对应了注册中心的相关配置,其中包含了 address、protocol、port、timeout 等一系列与注册中心相关的字段以及对应的 getter/setter() 方法,来接收用户通过 XML、Annotation 或是 API 方式传入的注册中心配置。 **ConfigConfigurationAdapter 是 AbstractConfig 与 Configuration 之间的适配器** ,它会将 AbstractConfig 对象转换成 Configuration 对象。在 ConfigConfigurationAdapter 的构造方法中会获取 AbstractConfig 对象的全部字段,并转换成一个 Map\ 集合返回,该 Map\ 集合将会被 ConfigConfigurationAdapter 的 metaData 字段引用。相关示例代码如下: +这些 AbstractConfig 实现基本都对应一个固定的配置,也定义了配置对应的字段以及 getter/setter() 方法。例如,RegistryConfig 这个实现类就对应了注册中心的相关配置,其中包含了 address、protocol、port、timeout 等一系列与注册中心相关的字段以及对应的 getter/setter() 方法,来接收用户通过 XML、Annotation 或是 API 方式传入的注册中心配置。**ConfigConfigurationAdapter 是 AbstractConfig 与 Configuration 之间的适配器**,它会将 AbstractConfig 对象转换成 Configuration 对象。在 ConfigConfigurationAdapter 的构造方法中会获取 AbstractConfig 对象的全部字段,并转换成一个 Map\ 集合返回,该 Map\ 集合将会被 ConfigConfigurationAdapter 的 metaData 字段引用。相关示例代码如下: ```java public ConfigConfigurationAdapter(AbstractConfig config) { @@ -222,7 +222,7 @@ DynamicConfiguration 是对 Dubbo 中动态配置的抽象,其核心方法有 在上述三类方法中,每个方法都用多个重载,其中,都会包含一个带有 group 参数的重载,也就是说 **配置中心的配置可以按照 group 进行分组** 。 -与 Dubbo 中很多接口类似,DynamicConfiguration 接口本身不被 @SPI 注解修饰(即不是一个扩展接口), **而是在 DynamicConfigurationFactory 上添加了 @SPI 注解,使其成为一个扩展接口** 。 +与 Dubbo 中很多接口类似,DynamicConfiguration 接口本身不被 @SPI 注解修饰(即不是一个扩展接口),**而是在 DynamicConfigurationFactory 上添加了 @SPI 注解,使其成为一个扩展接口** 。 在 DynamicConfiguration 中提供了 getDynamicConfiguration() 静态方法,该方法会从传入的配置中心 URL 参数中,解析出协议类型并获取对应的 DynamicConfigurationFactory 实现,如下所示: @@ -281,7 +281,7 @@ protected DynamicConfiguration createDynamicConfiguration(URL url) { - cacheListener(CacheListener 类型):用于监听配置变化的监听器。 - url(URL 类型):配置中心对应的 URL 对象。 -在 ZookeeperDynamicConfiguration 的构造函数中,会 **初始化上述核心字段** ,具体实现如下: +在 ZookeeperDynamicConfiguration 的构造函数中,会 **初始化上述核心字段**,具体实现如下: ```java ZookeeperDynamicConfiguration(URL url, ZookeeperTransporter zookeeperTransporter) { @@ -312,7 +312,7 @@ ZookeeperDynamicConfiguration(URL url, ZookeeperTransporter zookeeperTransporter } ``` -在上述初始化过程中,ZookeeperDynamicConfiguration 会创建 CacheListener 监听器。在前面\[第 15 课时\]中,我们介绍了 dubbo-remoting-zookeeper 对外提供了 StateListener、DataListener 和 ChildListener 三种类型的监听器。 **这里的 CacheListener 就是 DataListener 监听器的具体实现** 。 +在上述初始化过程中,ZookeeperDynamicConfiguration 会创建 CacheListener 监听器。在前面\[第 15 课时\]中,我们介绍了 dubbo-remoting-zookeeper 对外提供了 StateListener、DataListener 和 ChildListener 三种类型的监听器。**这里的 CacheListener 就是 DataListener 监听器的具体实现** 。 在 CacheListener 中维护了一个 Map\ 集合(keyListeners 字段)用于记录所有添加的 ConfigurationListener 监听器,其中 Key 是配置信息在 Zookeeper 中存储的 path,Value 为该 path 上的监听器集合。当某个配置项发生变化的时候,CacheListener 会从 keyListeners 中获取该配置对应的 ConfigurationListener 监听器集合,并逐个进行通知。该逻辑是在 CacheListener 的 dataChanged() 方法中实现的: diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25448\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25448\350\256\262.md" index b14e95162..61911dba2 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25448\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25448\350\256\262.md" @@ -1,6 +1,6 @@ # 48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下) -在上一课时,我们详细分析了 Configuration 接口以及 DynamicConfiguration 接口的实现, **其中 DynamicConfiguration 接口实现是动态配置中心的基础** 。那 Dubbo 中的动态配置中心是如何启动的呢?我们将在本课时详细介绍。 +在上一课时,我们详细分析了 Configuration 接口以及 DynamicConfiguration 接口的实现,**其中 DynamicConfiguration 接口实现是动态配置中心的基础** 。那 Dubbo 中的动态配置中心是如何启动的呢?我们将在本课时详细介绍。 ### 基础配置类 @@ -21,7 +21,7 @@ public static void initFrameworkExts() { } ``` -**ConfigManager 用于管理当前 Dubbo 节点中全部 AbstractConfig 对象** ,其中就包括 ConfigCenterConfig 这个实现的对象,我们通过 XML、Annotation 或是 API 方式添加的配置中心的相关信息(例如,配置中心的地址、端口、协议等),会转换成 ConfigCenterConfig 对象。 **在 Environment 中维护了上一课时介绍的多个 Configuration 对象** ,具体含义如下。 +**ConfigManager 用于管理当前 Dubbo 节点中全部 AbstractConfig 对象**,其中就包括 ConfigCenterConfig 这个实现的对象,我们通过 XML、Annotation 或是 API 方式添加的配置中心的相关信息(例如,配置中心的地址、端口、协议等),会转换成 ConfigCenterConfig 对象。**在 Environment 中维护了上一课时介绍的多个 Configuration 对象**,具体含义如下。 - propertiesConfiguration(PropertiesConfiguration 类型):全部 OrderedPropertiesProvider 实现提供的配置以及环境变量或是 -D 参数中指定配置文件的相关配置信息。 - systemConfiguration(SystemConfiguration 类型):-D 参数配置直接添加的配置信息。 diff --git "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25449\350\256\262.md" "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25449\350\256\262.md" index 1b71d9b59..07b5095ae 100644 --- "a/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25449\350\256\262.md" +++ "b/docs/Middleware/Dubbo \346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230/\347\254\25449\350\256\262.md" @@ -8,7 +8,7 @@ 另外,还有一个非常重要的“点”就是:面对失败的态度。工作了这么多年,面试失败过,晋级失败过,也看过很多人不同的人生轨迹:有人离开奋斗多年的一线城市;有人埋头在西二旗的写字楼里,接收福报的洗礼,已经很久没见过夕阳是什么样子;有人创业失败,负债千万……这些都算是失败吗?可能不同的人有不同的答案,毕竟每个人对失败的定义不同,答案自然也会不同。 -不管怎样,人生旅途中难免沟沟坎坎,挫折或失败似乎是人生的主旋律(注意是“似乎”,人生还是很美好的),不用纠结,每个人都会遇到,但如何面对挫折或失败会把我们分成不同的“队伍”:有的人会被击垮,从此一蹶不振;而有的人会站起来继续向前,越挫越勇,直至实现自己的人生目标和价值。所以说, **真正的成长从来不是追求,而是正视自己的缺憾** 。 +不管怎样,人生旅途中难免沟沟坎坎,挫折或失败似乎是人生的主旋律(注意是“似乎”,人生还是很美好的),不用纠结,每个人都会遇到,但如何面对挫折或失败会把我们分成不同的“队伍”:有的人会被击垮,从此一蹶不振;而有的人会站起来继续向前,越挫越勇,直至实现自己的人生目标和价值。所以说,**真正的成长从来不是追求,而是正视自己的缺憾** 。 感谢 2020 年不断学习的你,感谢你的一路陪伴,也期待你继续“认真学习,缩小差距”。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" index 0bce182e6..a2f88d812 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" @@ -4,7 +4,7 @@ 在过去 5 年中,我经历了 Kafka 从最初的 0.8 版本逐步演进到现在的 2.3 版本的完整过程,踩了很多坑也交了很多学费,慢慢地我梳理出了一个相对系统、完整的 Kafka 应用实战指南,最终以“Kafka 核心技术与实战”专栏的形式呈现给你,希望分享我对 Apache Kafka 的理解和实战方面的经验,帮你透彻理解 Kafka、更好地应用 Kafka。 -你可能会有这样的疑问, **我为什么要学习 Kafka 呢** ?要回答这个问题,我们不妨从更大的视角来审视它,先聊聊我对这几年互联网技术发展的理解吧。 +你可能会有这样的疑问,**我为什么要学习 Kafka 呢**?要回答这个问题,我们不妨从更大的视角来审视它,先聊聊我对这几年互联网技术发展的理解吧。 互联网蓬勃发展的这些年涌现出了很多令人眼花缭乱的新技术。以我个人的浅见,截止到 2019 年,当下互联网行业最火的技术当属 ABC 了,即所谓的 AI 人工智能、BigData 大数据和 Cloud 云计算云平台。我个人对区块链技术发展前景存疑,毕竟目前没有看到特别好的落地应用场景,也许在未来几年它会更令人刮目相看吧。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" index 2f3d8f42e..4b4c893dc 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" @@ -8,7 +8,7 @@ 后来我修改了学习的方法,转而从自上而下的角度去理解 Kafka,竟然发现了很多之前学习过程中忽略掉的东西。更特别地是,我发现这种学习方法能够帮助我维持较长时间的学习兴趣,不会阶段性地产生厌烦情绪。特别是在了解 Apache Kafka 整个发展历史的过程中我愉快地学到了很多运营大型开源软件社区的知识和经验,可谓是技术之外的一大收获。 -纵观 Kafka 的发展脉络,它的确是从消息引擎起家的,但正如文章标题所问, **Apache Kafka 真的只是消息引擎吗** ?通常,在回答这个问题之前很多文章可能就要这样展开了:那我们先来讨论下什么是消息引擎以及消息引擎能做什么事情。算了,我还是直给吧,就不从“唐尧虞舜”说起了。这个问题的答案是, **Apache Kafka 是消息引擎系统,也是一个分布式流处理平台** (Distributed Streaming Platform)。如果你通读全篇文字但只能记住一句话,我希望你记住的就是这句。再强调一遍,Kafka 是消息引擎系统,也是分布式流处理平台。 +纵观 Kafka 的发展脉络,它的确是从消息引擎起家的,但正如文章标题所问,**Apache Kafka 真的只是消息引擎吗**?通常,在回答这个问题之前很多文章可能就要这样展开了:那我们先来讨论下什么是消息引擎以及消息引擎能做什么事情。算了,我还是直给吧,就不从“唐尧虞舜”说起了。这个问题的答案是,**Apache Kafka 是消息引擎系统,也是一个分布式流处理平台** (Distributed Streaming Platform)。如果你通读全篇文字但只能记住一句话,我希望你记住的就是这句。再强调一遍,Kafka 是消息引擎系统,也是分布式流处理平台。 众所周知,Kafka 是 LinkedIn 公司内部孵化的项目。根据我和 Kafka 创始团队成员的交流以及查阅到的公开信息显示,LinkedIn 最开始有强烈的数据强实时处理方面的需求,其内部的诸多子系统要执行多种类型的数据处理与分析,主要包括业务系统和应用程序性能监控,以及用户行为数据处理等。 @@ -41,7 +41,7 @@ Kafka 自诞生伊始是以消息引擎系统的面目出现在大众视野中 **第一点是更容易实现端到端的正确性(Correctness)** 。Google 大神 Tyler 曾经说过,流处理要最终替代它的“兄弟”批处理需要具备两点核心优势: **要实现正确性和提供能够推导时间的工具。实现正确性是流处理能够匹敌批处理的基石** 。正确性一直是批处理的强项,而实现正确性的基石则是要求框架能提供精确一次处理语义,即处理一条消息有且只有一次机会能够影响系统状态。目前主流的大数据流处理框架都宣称实现了精确一次处理语义,但这是有限定条件的,即它们只能实现框架内的精确一次处理语义,无法实现端到端的。 -这是为什么呢?因为当这些框架与外部消息引擎系统结合使用时,它们无法影响到外部系统的处理语义,所以如果你搭建了一套环境使得 Spark 或 Flink 从 Kafka 读取消息之后进行有状态的数据计算,最后再写回 Kafka,那么你只能保证在 Spark 或 Flink 内部,这条消息对于状态的影响只有一次。但是计算结果有可能多次写入到 Kafka,因为它们不能控制 Kafka 的语义处理。相反地,Kafka 则不是这样,因为所有的数据流转和计算都在 Kafka 内部完成,故 Kafka 可以实现端到端的精确一次处理语义。 **可能助力 Kafka 胜出的第二点是它自己对于流式计算的定位** 。官网上明确标识 Kafka Streams 是一个用于搭建实时流处理的客户端库而非是一个完整的功能系统。这就是说,你不能期望着 Kafka 提供类似于集群调度、弹性部署等开箱即用的运维特性,你需要自己选择适合的工具或系统来帮助 Kafka 流处理应用实现这些功能。 +这是为什么呢?因为当这些框架与外部消息引擎系统结合使用时,它们无法影响到外部系统的处理语义,所以如果你搭建了一套环境使得 Spark 或 Flink 从 Kafka 读取消息之后进行有状态的数据计算,最后再写回 Kafka,那么你只能保证在 Spark 或 Flink 内部,这条消息对于状态的影响只有一次。但是计算结果有可能多次写入到 Kafka,因为它们不能控制 Kafka 的语义处理。相反地,Kafka 则不是这样,因为所有的数据流转和计算都在 Kafka 内部完成,故 Kafka 可以实现端到端的精确一次处理语义。**可能助力 Kafka 胜出的第二点是它自己对于流式计算的定位** 。官网上明确标识 Kafka Streams 是一个用于搭建实时流处理的客户端库而非是一个完整的功能系统。这就是说,你不能期望着 Kafka 提供类似于集群调度、弹性部署等开箱即用的运维特性,你需要自己选择适合的工具或系统来帮助 Kafka 流处理应用实现这些功能。 读到这你可能会说这怎么算是优点呢?坦率来说,这的确是一个“双刃剑”的设计,也是 Kafka 社区“剑走偏锋”不正面 PK 其他流计算框架的特意考量。大型公司的流处理平台一定是大规模部署的,因此具备集群调度功能以及灵活的部署方案是不可或缺的要素。但毕竟这世界上还存在着很多中小企业,它们的流处理数据量并不巨大,逻辑也并不复杂,部署几台或十几台机器足以应付。在这样的需求之下,搭建重量级的完整性平台实在是“杀鸡焉用牛刀”,而这正是 Kafka 流处理组件的用武之地。因此从这个角度来说,未来在流处理框架中,Kafka 应该是有一席之地的。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" index 4e63d7149..d224207c3 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" @@ -20,25 +20,25 @@ 下面我就来梳理一下这些所谓的“发行版”以及你应该如何选择它们。当然了,“发行版”这个词用在 Kafka 框架上并不严谨,但为了便于我们区分这些不同的 Kafka,我还是勉强套用一下吧。不过切记,当你以后和别人聊到这个话题的时候最好不要提及“发行版”这个词 ,因为这种提法在 Kafka 生态圈非常陌生,说出来难免贻笑大方。 -**1. Apache Kafka** Apache Kafka 是最“正宗”的 Kafka,也应该是你最熟悉的发行版了。自 Kafka 开源伊始,它便在 Apache 基金会孵化并最终毕业成为顶级项目,它也被称为社区版 Kafka。咱们专栏就是以这个版本的 Kafka 作为模板来学习的。更重要的是,它是后面其他所有发行版的基础。也就是说,后面提到的发行版要么是原封不动地继承了 Apache Kafka,要么是在此之上扩展了新功能,总之 Apache Kafka 是我们学习和使用 Kafka 的基础。 **2. Confluent Kafka** 我先说说 Confluent 公司吧。2014 年,Kafka 的 3 个创始人 Jay Kreps、Naha Narkhede 和饶军离开 LinkedIn 创办了 Confluent 公司,专注于提供基于 Kafka 的企业级流处理解决方案。2019 年 1 月,Confluent 公司成功融资 D 轮 1.25 亿美元,估值也到了 25 亿美元,足见资本市场的青睐。 +**1. Apache Kafka** Apache Kafka 是最“正宗”的 Kafka,也应该是你最熟悉的发行版了。自 Kafka 开源伊始,它便在 Apache 基金会孵化并最终毕业成为顶级项目,它也被称为社区版 Kafka。咱们专栏就是以这个版本的 Kafka 作为模板来学习的。更重要的是,它是后面其他所有发行版的基础。也就是说,后面提到的发行版要么是原封不动地继承了 Apache Kafka,要么是在此之上扩展了新功能,总之 Apache Kafka 是我们学习和使用 Kafka 的基础。**2. Confluent Kafka** 我先说说 Confluent 公司吧。2014 年,Kafka 的 3 个创始人 Jay Kreps、Naha Narkhede 和饶军离开 LinkedIn 创办了 Confluent 公司,专注于提供基于 Kafka 的企业级流处理解决方案。2019 年 1 月,Confluent 公司成功融资 D 轮 1.25 亿美元,估值也到了 25 亿美元,足见资本市场的青睐。 这里说点题外话, 饶军是我们中国人,清华大学毕业的大神级人物。我们已经看到越来越多的 Apache 顶级项目创始人中出现了中国人的身影,另一个例子就是 Apache Pulsar,它是一个以打败 Kafka 为目标的新一代消息引擎系统。至于在开源社区中活跃的国人更是数不胜数,这种现象实在令人振奋。 -还说回 Confluent 公司,它主要从事商业化 Kafka 工具开发,并在此基础上发布了 Confluent Kafka。Confluent Kafka 提供了一些 Apache Kafka 没有的高级特性,比如跨数据中心备份、Schema 注册中心以及集群监控工具等。 **3. Cloudera/Hortonworks Kafka** Cloudera 提供的 CDH 和 Hortonworks 提供的 HDP 是非常著名的大数据平台,里面集成了目前主流的大数据框架,能够帮助用户实现从分布式存储、集群调度、流处理到机器学习、实时数据库等全方位的数据处理。我知道很多创业公司在搭建数据平台时首选就是这两个产品。不管是 CDH 还是 HDP 里面都集成了 Apache Kafka,因此我把这两款产品中的 Kafka 称为 CDH Kafka 和 HDP Kafka。 +还说回 Confluent 公司,它主要从事商业化 Kafka 工具开发,并在此基础上发布了 Confluent Kafka。Confluent Kafka 提供了一些 Apache Kafka 没有的高级特性,比如跨数据中心备份、Schema 注册中心以及集群监控工具等。**3. Cloudera/Hortonworks Kafka** Cloudera 提供的 CDH 和 Hortonworks 提供的 HDP 是非常著名的大数据平台,里面集成了目前主流的大数据框架,能够帮助用户实现从分布式存储、集群调度、流处理到机器学习、实时数据库等全方位的数据处理。我知道很多创业公司在搭建数据平台时首选就是这两个产品。不管是 CDH 还是 HDP 里面都集成了 Apache Kafka,因此我把这两款产品中的 Kafka 称为 CDH Kafka 和 HDP Kafka。 当然在 2018 年 10 月两家公司宣布合并,共同打造世界领先的数据平台,也许以后 CDH 和 HDP 也会合并成一款产品,但能肯定的是 Apache Kafka 依然会包含其中,并作为新数据平台的一部分对外提供服务。 ## 特点比较 -Okay,说完了目前市面上的这些 Kafka,我来对比一下它们的优势和劣势。 **1. Apache Kafka** 对 Apache Kafka 而言,它现在依然是开发人数最多、版本迭代速度最快的 Kafka。在 2018 年度 Apache 基金会邮件列表开发者数量最多的 Top 5 排行榜中,Kafka 社区邮件组排名第二位。如果你使用 Apache Kafka 碰到任何问题并提交问题到社区,社区都会比较及时地响应你。这对于我们 Kafka 普通使用者来说无疑是非常友好的。 +Okay,说完了目前市面上的这些 Kafka,我来对比一下它们的优势和劣势。**1. Apache Kafka** 对 Apache Kafka 而言,它现在依然是开发人数最多、版本迭代速度最快的 Kafka。在 2018 年度 Apache 基金会邮件列表开发者数量最多的 Top 5 排行榜中,Kafka 社区邮件组排名第二位。如果你使用 Apache Kafka 碰到任何问题并提交问题到社区,社区都会比较及时地响应你。这对于我们 Kafka 普通使用者来说无疑是非常友好的。 -但是 Apache Kafka 的劣势在于它仅仅提供最最基础的组件,特别是对于前面提到的 Kafka Connect 而言,社区版 Kafka 只提供一种连接器,即读写磁盘文件的连接器,而没有与其他外部系统交互的连接器,在实际使用过程中需要自行编写代码实现,这是它的一个劣势。另外 Apache Kafka 没有提供任何监控框架或工具。显然在线上环境不加监控肯定是不可行的,你必然需要借助第三方的监控框架实现对 Kafka 的监控。好消息是目前有一些开源的监控框架可以帮助用于监控 Kafka(比如 Kafka manager)。 **总而言之,如果你仅仅需要一个消息引擎系统亦或是简单的流处理应用场景,同时需要对系统有较大把控度,那么我推荐你使用 Apache Kafka。** **2. Confluent Kafka** 下面来看 Confluent Kafka。Confluent Kafka 目前分为免费版和企业版两种。前者和 Apache Kafka 非常相像,除了常规的组件之外,免费版还包含 Schema 注册中心和 REST proxy 两大功能。前者是帮助你集中管理 Kafka 消息格式以实现数据前向 / 后向兼容;后者用开放 HTTP 接口的方式允许你通过网络访问 Kafka 的各种功能,这两个都是 Apache Kafka 所没有的。 +但是 Apache Kafka 的劣势在于它仅仅提供最最基础的组件,特别是对于前面提到的 Kafka Connect 而言,社区版 Kafka 只提供一种连接器,即读写磁盘文件的连接器,而没有与其他外部系统交互的连接器,在实际使用过程中需要自行编写代码实现,这是它的一个劣势。另外 Apache Kafka 没有提供任何监控框架或工具。显然在线上环境不加监控肯定是不可行的,你必然需要借助第三方的监控框架实现对 Kafka 的监控。好消息是目前有一些开源的监控框架可以帮助用于监控 Kafka(比如 Kafka manager)。**总而言之,如果你仅仅需要一个消息引擎系统亦或是简单的流处理应用场景,同时需要对系统有较大把控度,那么我推荐你使用 Apache Kafka。** **2. Confluent Kafka** 下面来看 Confluent Kafka。Confluent Kafka 目前分为免费版和企业版两种。前者和 Apache Kafka 非常相像,除了常规的组件之外,免费版还包含 Schema 注册中心和 REST proxy 两大功能。前者是帮助你集中管理 Kafka 消息格式以实现数据前向 / 后向兼容;后者用开放 HTTP 接口的方式允许你通过网络访问 Kafka 的各种功能,这两个都是 Apache Kafka 所没有的。 除此之外,免费版包含了更多的连接器,它们都是 Confluent 公司开发并认证过的,你可以免费使用它们。至于企业版,它提供的功能就更多了。在我看来,最有用的当属跨数据中心备份和集群监控两大功能了。多个数据中心之间数据的同步以及对集群的监控历来是 Kafka 的痛点,Confluent Kafka 企业版提供了强大的解决方案帮助你“干掉”它们。 -不过 Confluent Kafka 的一大缺陷在于,Confluent 公司暂时没有发展国内业务的计划,相关的资料以及技术支持都很欠缺,很多国内 Confluent Kafka 使用者甚至无法找到对应的中文文档,因此目前 Confluent Kafka 在国内的普及率是比较低的。 **一言以蔽之,如果你需要用到 Kafka 的一些高级特性,那么推荐你使用 Confluent Kafka。** **3. CDH/HDP Kafka** 最后说说大数据云公司发布的 Kafka(CDH/HDP Kafka)。这些大数据平台天然集成了 Apache Kafka,通过便捷化的界面操作将 Kafka 的安装、运维、管理、监控全部统一在控制台中。如果你是这些平台的用户一定觉得非常方便,因为所有的操作都可以在前端 UI 界面上完成,而不必去执行复杂的 Kafka 命令。另外这些平台提供的监控界面也非常友好,你通常不需要进行任何配置就能有效地监控 Kafka。 +不过 Confluent Kafka 的一大缺陷在于,Confluent 公司暂时没有发展国内业务的计划,相关的资料以及技术支持都很欠缺,很多国内 Confluent Kafka 使用者甚至无法找到对应的中文文档,因此目前 Confluent Kafka 在国内的普及率是比较低的。**一言以蔽之,如果你需要用到 Kafka 的一些高级特性,那么推荐你使用 Confluent Kafka。** **3. CDH/HDP Kafka** 最后说说大数据云公司发布的 Kafka(CDH/HDP Kafka)。这些大数据平台天然集成了 Apache Kafka,通过便捷化的界面操作将 Kafka 的安装、运维、管理、监控全部统一在控制台中。如果你是这些平台的用户一定觉得非常方便,因为所有的操作都可以在前端 UI 界面上完成,而不必去执行复杂的 Kafka 命令。另外这些平台提供的监控界面也非常友好,你通常不需要进行任何配置就能有效地监控 Kafka。 -但是凡事有利就有弊,这样做的结果是直接降低了你对 Kafka 集群的掌控程度。毕竟你对下层的 Kafka 集群一无所知,你怎么能做到心中有数呢?这种 Kafka 的另一个弊端在于它的滞后性。由于它有自己的发布周期,因此是否能及时地包含最新版本的 Kafka 就成为了一个问题。比如 CDH 6.1.0 版本发布时 Apache Kafka 已经演进到了 2.1.0 版本,但 CDH 中的 Kafka 依然是 2.0.0 版本,显然那些在 Kafka 2.1.0 中修复的 Bug 只能等到 CDH 下次版本更新时才有可能被真正修复。 **简单来说,如果你需要快速地搭建消息引擎系统,或者你需要搭建的是多框架构成的数据平台且 Kafka 只是其中一个组件,那么我推荐你使用这些大数据云公司提供的 Kafka。** +但是凡事有利就有弊,这样做的结果是直接降低了你对 Kafka 集群的掌控程度。毕竟你对下层的 Kafka 集群一无所知,你怎么能做到心中有数呢?这种 Kafka 的另一个弊端在于它的滞后性。由于它有自己的发布周期,因此是否能及时地包含最新版本的 Kafka 就成为了一个问题。比如 CDH 6.1.0 版本发布时 Apache Kafka 已经演进到了 2.1.0 版本,但 CDH 中的 Kafka 依然是 2.0.0 版本,显然那些在 Kafka 2.1.0 中修复的 Bug 只能等到 CDH 下次版本更新时才有可能被真正修复。**简单来说,如果你需要快速地搭建消息引擎系统,或者你需要搭建的是多框架构成的数据平台且 Kafka 只是其中一个组件,那么我推荐你使用这些大数据云公司提供的 Kafka。** ## 小结 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25405\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25405\350\256\262.md" index 2cb049c25..2da3c3011 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25405\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25405\350\256\262.md" @@ -26,13 +26,13 @@ Kafka 目前总共演进了 7 个大版本,分别是 0.7、0.8、0.9、0.10、 我们先从 0.7 版本说起,实际上也没什么可说的,这是最早开源时的“上古”版本了,以至于我也从来都没有接触过。这个版本只提供了最基础的消息队列功能,甚至连副本机制都没有,我实在想不出有什么理由你要使用这个版本,因此一旦有人向你推荐这个版本,果断走开就好了。 -Kafka 从 0.7 时代演进到 0.8 之后正式引入了 **副本机制** ,至此 Kafka 成为了一个真正意义上完备的分布式高可靠消息队列解决方案。有了副本备份机制,Kafka 就能够比较好地做到消息无丢失。那时候生产和消费消息使用的还是老版本的客户端 API,所谓的老版本是指当你用它们的 API 开发生产者和消费者应用时,你需要指定 ZooKeeper 的地址而非 Broker 的地址。 +Kafka 从 0.7 时代演进到 0.8 之后正式引入了 **副本机制**,至此 Kafka 成为了一个真正意义上完备的分布式高可靠消息队列解决方案。有了副本备份机制,Kafka 就能够比较好地做到消息无丢失。那时候生产和消费消息使用的还是老版本的客户端 API,所谓的老版本是指当你用它们的 API 开发生产者和消费者应用时,你需要指定 ZooKeeper 的地址而非 Broker 的地址。 -如果你现在尚不能理解这两者的区别也没关系,我会在专栏的后续文章中详细介绍它们。老版本客户端有很多的问题,特别是生产者 API,它默认使用同步方式发送消息,可以想见其吞吐量一定不会太高。虽然它也支持异步的方式,但实际场景中可能会造成消息的丢失,因此 0.8.2.0 版本社区引入了 **新版本 Producer API** ,即需要指定 Broker 地址的 Producer。 +如果你现在尚不能理解这两者的区别也没关系,我会在专栏的后续文章中详细介绍它们。老版本客户端有很多的问题,特别是生产者 API,它默认使用同步方式发送消息,可以想见其吞吐量一定不会太高。虽然它也支持异步的方式,但实际场景中可能会造成消息的丢失,因此 0.8.2.0 版本社区引入了 **新版本 Producer API**,即需要指定 Broker 地址的 Producer。 -据我所知,国内依然有少部分用户在使用 0.8.1.1、0.8.2 版本。 **我的建议是尽量使用比较新的版本。如果你不能升级大版本,我也建议你至少要升级到 0.8.2.2 这个版本,因为该版本中老版本消费者 API 是比较稳定的。另外即使你升到了 0.8.2.2,也不要使用新版本 Producer API,此时它的 Bug 还非常多。** 时间来到了 2015 年 11 月,社区正式发布了 0.9.0.0 版本。在我看来这是一个重量级的大版本更迭,0.9 大版本增加了基础的安全认证 / 权限功能,同时使用 Java 重写了新版本消费者 API,另外还引入了 Kafka Connect 组件用于实现高性能的数据抽取。如果这么多眼花缭乱的功能你一时无暇顾及,那么我希望你记住这个版本的另一个好处,那就是 **新版本 Producer API 在这个版本中算比较稳定了** 。如果你使用 0.9 作为线上环境不妨切换到新版本 Producer,这是此版本一个不太为人所知的优势。但和 0.8.2 引入新 API 问题类似,不要使用新版本 Consumer API,因为 Bug 超多的,绝对用到你崩溃。即使你反馈问题到社区,社区也不会管的,它会无脑地推荐你升级到新版本再试试,因此千万别用 0.9 的新版本 Consumer API。对于国内一些使用比较老的 CDH 的创业公司,鉴于其内嵌的就是 0.9 版本,所以要格外注意这些问题。 +据我所知,国内依然有少部分用户在使用 0.8.1.1、0.8.2 版本。**我的建议是尽量使用比较新的版本。如果你不能升级大版本,我也建议你至少要升级到 0.8.2.2 这个版本,因为该版本中老版本消费者 API 是比较稳定的。另外即使你升到了 0.8.2.2,也不要使用新版本 Producer API,此时它的 Bug 还非常多。** 时间来到了 2015 年 11 月,社区正式发布了 0.9.0.0 版本。在我看来这是一个重量级的大版本更迭,0.9 大版本增加了基础的安全认证 / 权限功能,同时使用 Java 重写了新版本消费者 API,另外还引入了 Kafka Connect 组件用于实现高性能的数据抽取。如果这么多眼花缭乱的功能你一时无暇顾及,那么我希望你记住这个版本的另一个好处,那就是 **新版本 Producer API 在这个版本中算比较稳定了** 。如果你使用 0.9 作为线上环境不妨切换到新版本 Producer,这是此版本一个不太为人所知的优势。但和 0.8.2 引入新 API 问题类似,不要使用新版本 Consumer API,因为 Bug 超多的,绝对用到你崩溃。即使你反馈问题到社区,社区也不会管的,它会无脑地推荐你升级到新版本再试试,因此千万别用 0.9 的新版本 Consumer API。对于国内一些使用比较老的 CDH 的创业公司,鉴于其内嵌的就是 0.9 版本,所以要格外注意这些问题。 -0.10.0.0 是里程碑式的大版本,因为该版本 **引入了 Kafka Streams** 。从这个版本起,Kafka 正式升级成分布式流处理平台,虽然此时的 Kafka Streams 还基本不能线上部署使用。0.10 大版本包含两个小版本:0.10.1 和 0.10.2,它们的主要功能变更都是在 Kafka Streams 组件上。如果你把 Kafka 用作消息引擎,实际上该版本并没有太多的功能提升。不过在我的印象中自 0.10.2.2 版本起,新版本 Consumer API 算是比较稳定了。 **如果你依然在使用 0.10 大版本,我强烈建议你至少升级到 0.10.2.2 然后使用新版本 Consumer API。还有个事情不得不提,0.10.2.2 修复了一个可能导致 Producer 性能降低的 Bug。基于性能的缘故你也应该升级到 0.10.2.2。** +0.10.0.0 是里程碑式的大版本,因为该版本 **引入了 Kafka Streams** 。从这个版本起,Kafka 正式升级成分布式流处理平台,虽然此时的 Kafka Streams 还基本不能线上部署使用。0.10 大版本包含两个小版本:0.10.1 和 0.10.2,它们的主要功能变更都是在 Kafka Streams 组件上。如果你把 Kafka 用作消息引擎,实际上该版本并没有太多的功能提升。不过在我的印象中自 0.10.2.2 版本起,新版本 Consumer API 算是比较稳定了。**如果你依然在使用 0.10 大版本,我强烈建议你至少升级到 0.10.2.2 然后使用新版本 Consumer API。还有个事情不得不提,0.10.2.2 修复了一个可能导致 Producer 性能降低的 Bug。基于性能的缘故你也应该升级到 0.10.2.2。** 在 2017 年 6 月,社区发布了 0.11.0.0 版本,引入了两个重量级的功能变更:一个是提供幂等性 Producer API 以及事务(Transaction) API;另一个是对 Kafka 消息格式做了重构。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" index 22d844b81..4ccce5296 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" @@ -22,7 +22,7 @@ 你不必详细了解每一种模型的实现细节,通常情况下我们认为后一种模型会比前一种模型要高级,比如 epoll 就比 select 要好,了解到这一程度应该足以应付我们下面的内容了。 -说了这么多,I/O 模型与 Kafka 的关系又是什么呢?实际上 Kafka 客户端底层使用了 Java 的 selector,selector 在 Linux 上的实现机制是 epoll,而在 Windows 平台上的实现机制是 select。 **因此在这一点上将 Kafka 部署在 Linux 上是有优势的,因为能够获得更高效的 I/O 性能。** 其次是网络传输效率的差别。你知道的,Kafka 生产和消费的消息都是通过网络传输的,而消息保存在哪里呢?肯定是磁盘。故 Kafka 需要在磁盘和网络间进行大量数据传输。如果你熟悉 Linux,你肯定听过零拷贝(Zero Copy)技术,就是当数据在磁盘和网络进行传输时避免昂贵的内核态数据拷贝从而实现快速地数据传输。Linux 平台实现了这样的零拷贝机制,但有些令人遗憾的是在 Windows 平台上必须要等到 Java 8 的 60 更新版本才能“享受”到这个福利。 **一句话总结一下,在 Linux 部署 Kafka 能够享受到零拷贝技术所带来的快速数据传输特性。** 最后是社区的支持度。这一点虽然不是什么明显的差别,但如果不了解的话可能比前两个因素对你的影响更大。简单来说就是,社区目前对 Windows 平台上发现的 Kafka Bug 不做任何承诺。虽然口头上依然保证尽力去解决,但根据我的经验,Windows 上的 Bug 一般是不会修复的。 **因此,Windows 平台上部署 Kafka 只适合于个人测试或用于功能验证,千万不要应用于生产环境。** +说了这么多,I/O 模型与 Kafka 的关系又是什么呢?实际上 Kafka 客户端底层使用了 Java 的 selector,selector 在 Linux 上的实现机制是 epoll,而在 Windows 平台上的实现机制是 select。**因此在这一点上将 Kafka 部署在 Linux 上是有优势的,因为能够获得更高效的 I/O 性能。** 其次是网络传输效率的差别。你知道的,Kafka 生产和消费的消息都是通过网络传输的,而消息保存在哪里呢?肯定是磁盘。故 Kafka 需要在磁盘和网络间进行大量数据传输。如果你熟悉 Linux,你肯定听过零拷贝(Zero Copy)技术,就是当数据在磁盘和网络进行传输时避免昂贵的内核态数据拷贝从而实现快速地数据传输。Linux 平台实现了这样的零拷贝机制,但有些令人遗憾的是在 Windows 平台上必须要等到 Java 8 的 60 更新版本才能“享受”到这个福利。**一句话总结一下,在 Linux 部署 Kafka 能够享受到零拷贝技术所带来的快速数据传输特性。** 最后是社区的支持度。这一点虽然不是什么明显的差别,但如果不了解的话可能比前两个因素对你的影响更大。简单来说就是,社区目前对 Windows 平台上发现的 Kafka Bug 不做任何承诺。虽然口头上依然保证尽力去解决,但根据我的经验,Windows 上的 Bug 一般是不会修复的。**因此,Windows 平台上部署 Kafka 只适合于个人测试或用于功能验证,千万不要应用于生产环境。** ## 磁盘 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" index 93b18f492..51373ba8d 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" @@ -36,7 +36,7 @@ Kafka 与 ZooKeeper 相关的最重要的参数当属`zookeeper.connect`。这 一旦你自己定义了协议名称,你必须还要指定`listener.security.protocol.map`参数告诉这个协议底层使用了哪种安全协议,比如指定`listener.security.protocol.map=CONTROLLER:PLAINTEXT表示CONTROLLER`这个自定义协议底层使用明文不加密传输数据。 -至于三元组中的主机名和端口号则比较直观,不需要做过多解释。不过有个事情你还是要注意一下,经常有人会问主机名这个设置中我到底使用 IP 地址还是主机名。 **这里我给出统一的建议:最好全部使用主机名,即 Broker 端和 Client 端应用配置中全部填写主机名。** Broker 源代码中也使用的是主机名,如果你在某些地方使用了 IP 地址进行连接,可能会发生无法连接的问题。 +至于三元组中的主机名和端口号则比较直观,不需要做过多解释。不过有个事情你还是要注意一下,经常有人会问主机名这个设置中我到底使用 IP 地址还是主机名。**这里我给出统一的建议:最好全部使用主机名,即 Broker 端和 Client 端应用配置中全部填写主机名。** Broker 源代码中也使用的是主机名,如果你在某些地方使用了 IP 地址进行连接,可能会发生无法连接的问题。 第四组参数是关于 Topic 管理的。我来讲讲下面这三个参数: diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25409\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25409\350\256\262.md" index 90419aa1d..fe84861d5 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25409\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25409\350\256\262.md" @@ -22,7 +22,7 @@ ## 都有哪些分区策略? -下面我们说说 Kafka 生产者的分区策略。 **所谓分区策略是决定生产者将消息发送到哪个分区的算法。** Kafka 为我们提供了默认的分区策略,同时它也支持你自定义分区策略。 +下面我们说说 Kafka 生产者的分区策略。**所谓分区策略是决定生产者将消息发送到哪个分区的算法。** Kafka 为我们提供了默认的分区策略,同时它也支持你自定义分区策略。 如果要自定义分区策略,你需要显式地配置生产者端的参数`partitioner.class`。这个参数该怎么设定呢?方法很简单,在编写生产者程序时,你可以编写一个具体的类实现`org.apache.kafka.clients.producer.Partitioner`接口。这个接口也很简单,只定义了两个方法:`partition()`和`close()`,通常你只需要实现最重要的 partition 方法。我们来看看这个方法的方法签名: @@ -36,7 +36,7 @@ int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] va ![img](assets/6c630aaf0b365115897231a4e0a7e1af.png) -这就是所谓的轮询策略。轮询策略是 Kafka Java 生产者 API 默认提供的分区策略。如果你未指定`partitioner.class`参数,那么你的生产者程序会按照轮询的方式在主题的所有分区间均匀地“码放”消息。 **轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。** **随机策略** 也称 Randomness 策略。所谓随机就是我们随意地将消息放置到任意一个分区上,如下面这张图所示。 +这就是所谓的轮询策略。轮询策略是 Kafka Java 生产者 API 默认提供的分区策略。如果你未指定`partitioner.class`参数,那么你的生产者程序会按照轮询的方式在主题的所有分区间均匀地“码放”消息。**轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。** **随机策略** 也称 Randomness 策略。所谓随机就是我们随意地将消息放置到任意一个分区上,如下面这张图所示。 ![img](assets/5b50b76efb8ada0f0779ac3275d215a3.png) @@ -49,7 +49,7 @@ return ThreadLocalRandom.current().nextInt(partitions.size()); 先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。 -本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以 **如果追求数据的均匀分布,还是使用轮询策略比较好** 。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。 **按消息键保序策略** 也称 Key-ordering 策略。有点尴尬的是,这个名词是我自己编的,Kafka 官网上并无这样的提法。 +本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以 **如果追求数据的均匀分布,还是使用轮询策略比较好** 。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。**按消息键保序策略** 也称 Key-ordering 策略。有点尴尬的是,这个名词是我自己编的,Kafka 官网上并无这样的提法。 Kafka 允许为每条消息定义消息键,简称为 Key。这个 Key 的作用非常大,它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务 ID 等;也可以用来表征消息元数据。特别是在 Kafka 不支持时间戳的年代,在一些场景中,工程师们都是直接将消息创建时间封装进 Key 里面的。一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略,如下图所示。 @@ -72,7 +72,7 @@ return Math.abs(key.hashCode()) % partitions.size(); 后来经过了解和调研,我发现这种具有因果关系的消息都有一定的特点,比如在消息体中都封装了固定的标志位,后来我就建议他们对此标志位设定专门的分区策略,保证同一标志位的所有消息都发送到同一分区,这样既可以保证分区内的消息顺序,也可以享受到多分区带来的性能红利。 -这种基于个别字段的分区策略本质上就是按消息键保序的思想,其实更加合适的做法是把标志位数据提取出来统一放到 Key 中,这样更加符合 Kafka 的设计思想。经过改造之后,这个企业的消息处理吞吐量一下提升了 40 多倍,从这个案例你也可以看到自定制分区策略的效果可见一斑。 **其他分区策略** +这种基于个别字段的分区策略本质上就是按消息键保序的思想,其实更加合适的做法是把标志位数据提取出来统一放到 Key 中,这样更加符合 Kafka 的设计思想。经过改造之后,这个企业的消息处理吞吐量一下提升了 40 多倍,从这个案例你也可以看到自定制分区策略的效果可见一斑。**其他分区策略** 上面这几种分区策略都是比较基础的策略,除此之外你还能想到哪些有实际用途的分区策略?其实还有一种比较常见的,即所谓的基于地理位置的分区策略。当然这种策略一般只针对那些大规模的 Kafka 集群,特别是跨城市、跨国家甚至是跨大洲的集群。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" index ada324e29..1e9313ceb 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" @@ -63,7 +63,7 @@ Broker 说:“不好意思,我这边接收的消息必须使用 Snappy 算 那么现在问题来了,Consumer 怎么知道这些消息是用何种压缩算法压缩的呢?其实答案就在消息中。Kafka 会将启用了哪种压缩算法封装进消息集合中,这样当 Consumer 读取到消息集合时,它自然就知道了这些消息使用的是哪种压缩算法。如果用一句话总结一下压缩和解压缩,那么我希望你记住这句话: **Producer 端压缩、Broker 端保持、Consumer 端解压缩。** 除了在 Consumer 端解压缩,Broker 端也会进行解压缩。注意了,这和前面提到消息格式转换时发生的解压缩是不同的场景。每个压缩过的消息集合在 Broker 端写入时都要发生解压缩操作,目的就是为了对消息执行各种验证。我们必须承认这种解压缩对 Broker 端性能是有一定影响的,特别是对 CPU 的使用率而言。 -事实上,最近国内京东的小伙伴们刚刚向社区提出了一个 bugfix,建议去掉因为做消息校验而引入的解压缩。据他们称,去掉了解压缩之后,Broker 端的 CPU 使用率至少降低了 50%。不过有些遗憾的是,目前社区并未采纳这个建议,原因就是这种消息校验是非常重要的,不可盲目去之。毕竟先把事情做对是最重要的,在做对的基础上,再考虑把事情做好做快。针对这个使用场景,你也可以思考一下,是否有一个两全其美的方案,既能避免消息解压缩也能对消息执行校验。 **各种压缩算法对比** ------------ +事实上,最近国内京东的小伙伴们刚刚向社区提出了一个 bugfix,建议去掉因为做消息校验而引入的解压缩。据他们称,去掉了解压缩之后,Broker 端的 CPU 使用率至少降低了 50%。不过有些遗憾的是,目前社区并未采纳这个建议,原因就是这种消息校验是非常重要的,不可盲目去之。毕竟先把事情做对是最重要的,在做对的基础上,再考虑把事情做好做快。针对这个使用场景,你也可以思考一下,是否有一个两全其美的方案,既能避免消息解压缩也能对消息执行校验。**各种压缩算法对比** ------------ 那么我们来谈谈压缩算法。这可是重头戏!之前说了这么多,我们还是要比较一下各个压缩算法的优劣,这样我们才能有针对性地配置适合我们业务的压缩策略。 @@ -77,7 +77,7 @@ Broker 说:“不好意思,我这边接收的消息必须使用 Snappy 算 从表中我们可以发现 zstd 算法有着最高的压缩比,而在吞吐量上的表现只能说中规中矩。反观 LZ4 算法,它在吞吐量方面则是毫无疑问的执牛耳者。当然对于表格中数据的权威性我不做过多解读,只想用它来说明一下当前各种压缩算法的大致表现。 -## 在实际使用中,GZIP、Snappy、LZ4 甚至是 zstd 的表现各有千秋。但对于 Kafka 而言,它们的性能测试结果却出奇得一致,即在吞吐量方面:LZ4 > Snappy > zstd 和 GZIP;而在压缩比方面,zstd > LZ4 > GZIP > Snappy。具体到物理资源,使用 Snappy 算法占用的网络带宽最多,zstd 最少,这是合理的,毕竟 zstd 就是要提供超高的压缩比;在 CPU 使用率方面,各个算法表现得差不多,只是在压缩时 Snappy 算法使用的 CPU 较多一些,而在解压缩时 GZIP 算法则可能使用更多的 CPU。 **最佳实践** +## 在实际使用中,GZIP、Snappy、LZ4 甚至是 zstd 的表现各有千秋。但对于 Kafka 而言,它们的性能测试结果却出奇得一致,即在吞吐量方面:LZ4 > Snappy > zstd 和 GZIP;而在压缩比方面,zstd > LZ4 > GZIP > Snappy。具体到物理资源,使用 Snappy 算法占用的网络带宽最多,zstd 最少,这是合理的,毕竟 zstd 就是要提供超高的压缩比;在 CPU 使用率方面,各个算法表现得差不多,只是在压缩时 Snappy 算法使用的 CPU 较多一些,而在解压缩时 GZIP 算法则可能使用更多的 CPU。**最佳实践** 了解了这些算法对比,我们就能根据自身的实际情况有针对性地启用合适的压缩算法。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" index 127237d9d..64c13296d 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" @@ -16,9 +16,9 @@ 现在你应该能够稍微体会出这里的“有限度”的含义了吧,其实就是说 Kafka 不丢消息是有前提条件的。假如你的消息保存在 N 个 Kafka Broker 上,那么这个前提条件就是这 N 个 Broker 中至少有 1 个存活。只要这个条件成立,Kafka 就能保证你的这条消息永远不会丢失。 -总结一下,Kafka 是能做到不丢失消息的,只不过这些消息必须是已提交的消息,而且还要满足一定的条件。当然,说明这件事并不是要为 Kafka 推卸责任,而是为了在出现该类问题时我们能够明确责任边界。 **“消息丢失”案例** ------------ +总结一下,Kafka 是能做到不丢失消息的,只不过这些消息必须是已提交的消息,而且还要满足一定的条件。当然,说明这件事并不是要为 Kafka 推卸责任,而是为了在出现该类问题时我们能够明确责任边界。**“消息丢失”案例** ------------ -好了,理解了 Kafka 是怎样做到不丢失消息的,那接下来我带你复盘一下那些常见的“Kafka 消息丢失”案例。注意,这里可是带引号的消息丢失哦,其实有些时候我们只是冤枉了 Kafka 而已。 **案例 1:生产者程序丢失数据** Producer 程序丢失消息,这应该算是被抱怨最多的数据丢失场景了。我来描述一个场景:你写了一个 Producer 应用向 Kafka 发送消息,最后发现 Kafka 没有保存,于是大骂:“Kafka 真烂,消息发送居然都能丢失,而且还不告诉我?!”如果你有过这样的经历,那么请先消消气,我们来分析下可能的原因。 +好了,理解了 Kafka 是怎样做到不丢失消息的,那接下来我带你复盘一下那些常见的“Kafka 消息丢失”案例。注意,这里可是带引号的消息丢失哦,其实有些时候我们只是冤枉了 Kafka 而已。**案例 1:生产者程序丢失数据** Producer 程序丢失消息,这应该算是被抱怨最多的数据丢失场景了。我来描述一个场景:你写了一个 Producer 应用向 Kafka 发送消息,最后发现 Kafka 没有保存,于是大骂:“Kafka 真烂,消息发送居然都能丢失,而且还不告诉我?!”如果你有过这样的经历,那么请先消消气,我们来分析下可能的原因。 目前 Kafka Producer 是异步发送消息的,也就是说如果你调用的是 producer.send(msg) 这个 API,那么它通常会立即返回,但此时你不能认为消息发送已成功完成。 @@ -30,7 +30,7 @@ 举例来说,如果是因为那些瞬时错误,那么仅仅让 Producer 重试就可以了;如果是消息不合格造成的,那么可以调整消息格式后再次发送。总之,处理发送失败的责任在 Producer 端而非 Broker 端。 -你可能会问,发送失败真的没可能是由 Broker 端的问题造成的吗?当然可能!如果你所有的 Broker 都宕机了,那么无论 Producer 端怎么重试都会失败的,此时你要做的是赶快处理 Broker 端的问题。但之前说的核心论据在这里依然是成立的:Kafka 依然不认为这条消息属于已提交消息,故对它不做任何持久化保证。 **案例 2:消费者程序丢失数据** Consumer 端丢失数据主要体现在 Consumer 端要消费的消息不见了。Consumer 程序有个“位移”的概念,表示的是这个 Consumer 当前消费到的 Topic 分区的位置。下面这张图来自于官网,它清晰地展示了 Consumer 端的位移数据。 +你可能会问,发送失败真的没可能是由 Broker 端的问题造成的吗?当然可能!如果你所有的 Broker 都宕机了,那么无论 Producer 端怎么重试都会失败的,此时你要做的是赶快处理 Broker 端的问题。但之前说的核心论据在这里依然是成立的:Kafka 依然不认为这条消息属于已提交消息,故对它不做任何持久化保证。**案例 2:消费者程序丢失数据** Consumer 端丢失数据主要体现在 Consumer 端要消费的消息不见了。Consumer 程序有个“位移”的概念,表示的是这个 Consumer 当前消费到的 Topic 分区的位置。下面这张图来自于官网,它清晰地展示了 Consumer 端的位移数据。 ![img](assets/0c97bed3b6350d73a9403d9448290d37.png) @@ -54,7 +54,7 @@ 这里的关键在于 Consumer 自动提交位移,与你没有确认书籍内容被全部读完就将书归还类似,你没有真正地确认消息是否真的被消费就“盲目”地更新了位移。 -这个问题的解决方案也很简单: **如果是多线程异步处理消费消息,Consumer 程序不要开启自动提交位移,而是要应用程序手动提交位移** 。在这里我要提醒你一下,单个 Consumer 程序使用多线程来消费消息说起来容易,写成代码却异常困难,因为你很难正确地处理位移的更新,也就是说避免无消费消息丢失很简单,但极易出现消息被消费了多次的情况。 **最佳实践** -------- +这个问题的解决方案也很简单: **如果是多线程异步处理消费消息,Consumer 程序不要开启自动提交位移,而是要应用程序手动提交位移** 。在这里我要提醒你一下,单个 Consumer 程序使用多线程来消费消息说起来容易,写成代码却异常困难,因为你很难正确地处理位移的更新,也就是说避免无消费消息丢失很简单,但极易出现消息被消费了多次的情况。**最佳实践** -------- 看完这两个案例之后,我来分享一下 Kafka 无消息丢失的配置,每一个其实都能对应上面提到的问题。 @@ -65,7 +65,7 @@ 1. 设置 replication.factor >= 3。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。 1. 设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。 1. 确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1。 -1. 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关重要的。 **小结** +1. 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关重要的。**小结** ______________________________________________________________________ diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" index 9f9f6f3fb..70e3ddf01 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" @@ -16,7 +16,7 @@ Kafka 拦截器借鉴了这样的设计思路。你可以在消息处理的前后多个时点动态植入不同的处理逻辑,比如在消息发送前或者在消息被消费后。 -作为一个非常小众的功能,Kafka 拦截器自 0.10.0.0 版本被引入后并未得到太多的实际应用,我也从未在任何 Kafka 技术峰会上看到有公司分享其使用拦截器的成功案例。但即便如此,在自己的 Kafka 工具箱中放入这么一个有用的东西依然是值得的。今天我们就让它来发挥威力,展示一些非常酷炫的功能。 **Kafka 拦截器** ------------- **Kafka 拦截器分为生产者拦截器和消费者拦截器** 。生产者拦截器允许你在发送消息前以及消息提交成功后植入你的拦截器逻辑;而消费者拦截器支持在消费消息前以及提交位移后编写特定逻辑。值得一提的是,这两种拦截器都支持链的方式,即你可以将一组拦截器串连成一个大的拦截器,Kafka 会按照添加顺序依次执行拦截器逻辑。 +作为一个非常小众的功能,Kafka 拦截器自 0.10.0.0 版本被引入后并未得到太多的实际应用,我也从未在任何 Kafka 技术峰会上看到有公司分享其使用拦截器的成功案例。但即便如此,在自己的 Kafka 工具箱中放入这么一个有用的东西依然是值得的。今天我们就让它来发挥威力,展示一些非常酷炫的功能。**Kafka 拦截器** ------------- **Kafka 拦截器分为生产者拦截器和消费者拦截器** 。生产者拦截器允许你在发送消息前以及消息提交成功后植入你的拦截器逻辑;而消费者拦截器支持在消费消息前以及提交位移后编写特定逻辑。值得一提的是,这两种拦截器都支持链的方式,即你可以将一组拦截器串连成一个大的拦截器,Kafka 会按照添加顺序依次执行拦截器逻辑。 举个例子,假设你想在生产消息前执行两个“前置动作”:第一个是为消息增加一个头信息,封装发送该消息的时间,第二个是更新发送消息数字段,那么当你将这两个拦截器串联在一起统一指定给 Producer 后,Producer 会按顺序执行上面的动作,然后再发送消息。 @@ -41,9 +41,9 @@ props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors); 1. onConsume:该方法在消息返回给 Consumer 程序之前调用。也就是说在开始正式处理消息之前,拦截器会先拦一道,搞一些事情,之后再返回给你。 1. onCommit:Consumer 在提交位移之后调用该方法。通常你可以在该方法中做一些记账类的动作,比如打日志等。 -一定要注意的是, **指定拦截器类时要指定它们的全限定名** ,即 full qualified name。通俗点说就是要把完整包名也加上,不要只有一个类名在那里,并且还要保证你的 Producer 程序能够正确加载你的拦截器类。 **典型使用场景** ---------- +一定要注意的是,**指定拦截器类时要指定它们的全限定名**,即 full qualified name。通俗点说就是要把完整包名也加上,不要只有一个类名在那里,并且还要保证你的 Producer 程序能够正确加载你的拦截器类。**典型使用场景** ---------- -Kafka 拦截器都能用在哪些地方呢?其实,跟很多拦截器的用法相同, **Kafka 拦截器可以应用于包括客户端监控、端到端系统性能检测、消息审计等多种功能在内的场景** 。 +Kafka 拦截器都能用在哪些地方呢?其实,跟很多拦截器的用法相同,**Kafka 拦截器可以应用于包括客户端监控、端到端系统性能检测、消息审计等多种功能在内的场景** 。 我以端到端系统性能检测和消息审计为例来展开介绍下。 @@ -55,7 +55,7 @@ Kafka 拦截器都能用在哪些地方呢?其实,跟很多拦截器的用 我们再来看看消息审计(message audit)的场景。设想你的公司把 Kafka 作为一个私有云消息引擎平台向全公司提供服务,这必然要涉及多租户以及消息审计的功能。 -作为私有云的 PaaS 提供方,你肯定要能够随时查看每条消息是哪个业务方在什么时间发布的,之后又被哪些业务方在什么时刻消费。一个可行的做法就是你编写一个拦截器类,实现相应的消息审计逻辑,然后强行规定所有接入你的 Kafka 服务的客户端程序必须设置该拦截器。 **案例分享** -------- +作为私有云的 PaaS 提供方,你肯定要能够随时查看每条消息是哪个业务方在什么时间发布的,之后又被哪些业务方在什么时刻消费。一个可行的做法就是你编写一个拦截器类,实现相应的消息审计逻辑,然后强行规定所有接入你的 Kafka 服务的客户端程序必须设置该拦截器。**案例分享** -------- 下面我以一个具体的案例来说明一下拦截器的使用。在这个案例中,我们通过编写拦截器类来统计消息端到端处理的延时,非常实用,我建议你可以直接移植到你自己的生产环境中。 @@ -115,7 +115,7 @@ public class AvgLatencyConsumerInterceptor implements ConsumerInterceptor** 。如果你认同这样的结论,那么恭喜你,社区就是这么设计的! +好了,我们来总结一下我们的结论。**位移主题的 Key 中应该保存 3 部分内容:** 。如果你认同这样的结论,那么恭喜你,社区就是这么设计的! 接下来,我们再来看看消息体的设计。也许你会觉得消息体应该很简单,保存一个位移值就可以了。实际上,社区的方案要复杂得多,比如消息体还保存了位移提交的一些其他元数据,诸如时间戳和用户自定义的数据等。保存这些元数据是为了帮助 Kafka 执行各种各样后续的操作,比如删除过期位移消息等。但总体来说,我们还是可以简单地认为消息体就是保存了位移值。 @@ -41,11 +41,11 @@ Okay,我们现在知道 Key 中保存了 Group ID,但是只保存 Group ID 那么,何时会写入这类消息呢?一旦某个 Consumer Group 下的所有 Consumer 实例都停止了,而且它们的位移数据都已被删除时,Kafka 会向位移主题的对应分区写入 tombstone 消息,表明要彻底删除这个 Group 的信息。 -好了,消息格式就说这么多,下面我们来说说位移主题是怎么被创建的。通常来说, **当 Kafka 集群中的第一个 Consumer 程序启动时,Kafka 会自动创建位移主题** 。我们说过,位移主题就是普通的 Kafka 主题,那么它自然也有对应的分区数。但如果是 Kafka 自动创建的,分区数是怎么设置的呢?这就要看 Broker 端参数 offsets.topic.num.partitions 的取值了。它的默认值是 50,因此 Kafka 会自动创建一个 50 分区的位移主题。如果你曾经惊讶于 Kafka 日志路径下冒出很多 \_\_consumer_offsets-xxx 这样的目录,那么现在应该明白了吧,这就是 Kafka 自动帮你创建的位移主题啊。 +好了,消息格式就说这么多,下面我们来说说位移主题是怎么被创建的。通常来说,**当 Kafka 集群中的第一个 Consumer 程序启动时,Kafka 会自动创建位移主题** 。我们说过,位移主题就是普通的 Kafka 主题,那么它自然也有对应的分区数。但如果是 Kafka 自动创建的,分区数是怎么设置的呢?这就要看 Broker 端参数 offsets.topic.num.partitions 的取值了。它的默认值是 50,因此 Kafka 会自动创建一个 50 分区的位移主题。如果你曾经惊讶于 Kafka 日志路径下冒出很多 \_\_consumer_offsets-xxx 这样的目录,那么现在应该明白了吧,这就是 Kafka 自动帮你创建的位移主题啊。 你可能会问,除了分区数,副本数或备份因子是怎么控制的呢?答案也很简单,这就是 Broker 端另一个参数 offsets.topic.replication.factor 要做的事情了。它的默认值是 3。 -总结一下, **如果位移主题是 Kafka 自动创建的,那么该主题的分区数是 50,副本数是 3** 。 +总结一下,**如果位移主题是 Kafka 自动创建的,那么该主题的分区数是 50,副本数是 3** 。 当然,你也可以选择手动创建位移主题,具体方法就是,在 Kafka 集群尚未启动任何 Consumer 之前,使用 Kafka API 创建它。手动创建的好处在于,你可以创建满足你实际场景需要的位移主题。比如很多人说 50 个分区对我来讲太多了,我不想要这么多分区,那么你可以自己创建它,不用理会 offsets.topic.num.partitions 的值。 @@ -53,7 +53,7 @@ Okay,我们现在知道 Key 中保存了 Group ID,但是只保存 Group ID 创建位移主题当然是为了用的,那么什么地方会用到位移主题呢?我们前面一直在说 Kafka Consumer 提交位移时会写入该主题,那 Consumer 是怎么提交位移的呢?目前 Kafka Consumer 提交位移的方式有两种: **自动提交位移和手动提交位移。** Consumer 端有个参数叫 enable.auto.commit,如果值是 true,则 Consumer 在后台默默地为你定期提交位移,提交间隔由一个专属的参数 auto.commit.interval.ms 来控制。自动提交位移有一个显著的优点,就是省事,你不用操心位移提交的事情,就能保证消息消费不会丢失。但这一点同时也是缺点。因为它太省事了,以至于丧失了很大的灵活性和可控性,你完全没法把控 Consumer 端的位移管理。 -事实上,很多与 Kafka 集成的大数据框架都是禁用自动提交位移的,如 Spark、Flink 等。这就引出了另一种位移提交方式: **手动提交位移** ,即设置 enable.auto.commit = false。一旦设置了 false,作为 Consumer 应用开发的你就要承担起位移提交的责任。Kafka Consumer API 为你提供了位移提交的方法,如 consumer.commitSync 等。当调用这些方法时,Kafka 会向位移主题写入相应的消息。 +事实上,很多与 Kafka 集成的大数据框架都是禁用自动提交位移的,如 Spark、Flink 等。这就引出了另一种位移提交方式: **手动提交位移**,即设置 enable.auto.commit = false。一旦设置了 false,作为 Consumer 应用开发的你就要承担起位移提交的责任。Kafka Consumer API 为你提供了位移提交的方法,如 consumer.commitSync 等。当调用这些方法时,Kafka 会向位移主题写入相应的消息。 如果你选择的是自动提交位移,那么就可能存在一个问题:只要 Consumer 一直启动着,它就会无限期地向位移主题写入消息。 @@ -65,7 +65,7 @@ Kafka 是怎么删除位移主题中的过期消息的呢?答案就是 Compact ![img](assets/86a44073aa60ac33e0833e6a9bfd9ae7.jpeg) -图中位移为 0、2 和 3 的消息的 Key 都是 K1。Compact 之后,分区只需要保存位移为 3 的消息,因为它是最新发送的。 **Kafka 提供了专门的后台线程定期地巡检待 Compact 的主题,看看是否存在满足条件的可删除数据** 。这个后台线程叫 Log Cleaner。很多实际生产环境中都出现过位移主题无限膨胀占用过多磁盘空间的问题,如果你的环境中也有这个问题,我建议你去检查一下 Log Cleaner 线程的状态,通常都是这个线程挂掉了导致的。 +图中位移为 0、2 和 3 的消息的 Key 都是 K1。Compact 之后,分区只需要保存位移为 3 的消息,因为它是最新发送的。**Kafka 提供了专门的后台线程定期地巡检待 Compact 的主题,看看是否存在满足条件的可删除数据** 。这个后台线程叫 Log Cleaner。很多实际生产环境中都出现过位移主题无限膨胀占用过多磁盘空间的问题,如果你的环境中也有这个问题,我建议你去检查一下 Log Cleaner 线程的状态,通常都是这个线程挂掉了导致的。 ## 小结 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" index c24a78251..21744acfc 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25417\350\256\262.md" @@ -8,7 +8,7 @@ 具体来讲,Consumer 端应用程序在提交位移时,其实是向 Coordinator 所在的 Broker 提交位移。同样地,当 Consumer 应用启动时,也是向 Coordinator 所在的 Broker 发送各种请求,然后由 Coordinator 负责执行消费者组的注册、成员管理记录等元数据管理操作。 -所有 Broker 在启动时,都会创建和开启相应的 Coordinator 组件。也就是说, **所有 Broker 都有各自的 Coordinator 组件** 。那么,Consumer Group 如何确定为它服务的 Coordinator 在哪台 Broker 上呢?答案就在我们之前说过的 Kafka 内部位移主题 \_\_consumer_offsets 身上。 +所有 Broker 在启动时,都会创建和开启相应的 Coordinator 组件。也就是说,**所有 Broker 都有各自的 Coordinator 组件** 。那么,Consumer Group 如何确定为它服务的 Coordinator 在哪台 Broker 上呢?答案就在我们之前说过的 Kafka 内部位移主题 \_\_consumer_offsets 身上。 目前,Kafka 为某个 Consumer Group 确定 Coordinator 所在的 Broker 的算法有 2 个步骤。 @@ -32,7 +32,7 @@ 总而言之,Rebalance 有以上这三个方面的弊端。你可能会问,这些问题有解吗?特别是针对 Rebalance 慢和影响 TPS 这两个弊端,社区有解决办法吗?针对这两点,我可以很负责任地告诉你:“无解!”特别是 Rebalance 慢这个问题,Kafka 社区对此无能为力。“本事大不如不摊上”,既然我们没办法解决 Rebalance 过程中的各种问题,干脆就避免 Rebalance 吧,特别是那些不必要的 Rebalance。 -就我个人经验而言, **在真实的业务场景中,很多 Rebalance 都是计划外的或者说是不必要的** 。我们应用的 TPS 大多是被这类 Rebalance 拖慢的,因此避免这类 Rebalance 就显得很有必要了。下面我们就来说说如何避免 Rebalance。 +就我个人经验而言,**在真实的业务场景中,很多 Rebalance 都是计划外的或者说是不必要的** 。我们应用的 TPS 大多是被这类 Rebalance 拖慢的,因此避免这类 Rebalance 就显得很有必要了。下面我们就来说说如何避免 Rebalance。 要避免 Rebalance,还是要从 Rebalance 发生的时机入手。我们在前面说过,Rebalance 发生的时机有三个: @@ -66,11 +66,11 @@ Coordinator 会在什么情况下认为某个 Consumer 实例已挂从而要退 将 session.timeout.ms 设置成 6s 主要是为了让 Coordinator 能够更快地定位已经挂掉的 Consumer。毕竟,我们还是希望能尽快揪出那些“尸位素餐”的 Consumer,早日把它们踢出 Group。希望这份配置能够较好地帮助你规避第一类“不必要”的 Rebalance。 -**第二类非必要 Rebalance 是 Consumer 消费时间过长导致的** 。我之前有一个客户,在他们的场景中,Consumer 消费数据时需要将消息处理之后写入到 MongoDB。显然,这是一个很重的消费逻辑。MongoDB 的一丁点不稳定都会导致 Consumer 程序消费时长的增加。此时, **max.poll.interval.ms** 参数值的设置显得尤为关键。如果要避免非预期的 Rebalance,你最好将该参数值设置得大一点,比你的下游最大处理时间稍长一点。就拿 MongoDB 这个例子来说,如果写 MongoDB 的最长时间是 7 分钟,那么你可以将该参数设置为 8 分钟左右。 +**第二类非必要 Rebalance 是 Consumer 消费时间过长导致的** 。我之前有一个客户,在他们的场景中,Consumer 消费数据时需要将消息处理之后写入到 MongoDB。显然,这是一个很重的消费逻辑。MongoDB 的一丁点不稳定都会导致 Consumer 程序消费时长的增加。此时,**max.poll.interval.ms** 参数值的设置显得尤为关键。如果要避免非预期的 Rebalance,你最好将该参数值设置得大一点,比你的下游最大处理时间稍长一点。就拿 MongoDB 这个例子来说,如果写 MongoDB 的最长时间是 7 分钟,那么你可以将该参数设置为 8 分钟左右。 总之,你要为你的业务处理逻辑留下充足的时间。这样,Consumer 就不会因为处理这些消息的时间太长而引发 Rebalance 了。 -如果你按照上面的推荐数值恰当地设置了这几个参数,却发现还是出现了 Rebalance,那么我建议你去排查一下 **Consumer 端的 GC 表现** ,比如是否出现了频繁的 Full GC 导致的长时间停顿,从而引发了 Rebalance。为什么特意说 GC?那是因为在实际场景中,我见过太多因为 GC 设置不合理导致程序频发 Full GC 而引发的非预期 Rebalance 了。 +如果你按照上面的推荐数值恰当地设置了这几个参数,却发现还是出现了 Rebalance,那么我建议你去排查一下 **Consumer 端的 GC 表现**,比如是否出现了频繁的 Full GC 导致的长时间停顿,从而引发了 Rebalance。为什么特意说 GC?那是因为在实际场景中,我见过太多因为 GC 设置不合理导致程序频发 Full GC 而引发的非预期 Rebalance 了。 ## 小结 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" index 994590781..23b73a2a5 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" @@ -10,9 +10,9 @@ 提交位移主要是为了表征 Consumer 的消费进度,这样当 Consumer 发生故障重启之后,就能够从 Kafka 中读取之前提交的位移值,然后从相应的位移处继续消费,从而避免整个消费过程重来一遍。换句话说,位移提交是 Kafka 提供给你的一个工具或语义保障,你负责维持这个语义保障,即如果你提交了位移 X,那么 Kafka 会认为所有位移值小于 X 的消息你都已经成功消费了。 -这一点特别关键。因为位移提交非常灵活,你完全可以提交任何位移值,但由此产生的后果你也要一并承担。假设你的 Consumer 消费了 10 条消息,你提交的位移值却是 20,那么从理论上讲,位移介于 11~19 之间的消息是有可能丢失的;相反地,如果你提交的位移值是 5,那么位移介于 5~9 之间的消息就有可能被重复消费。所以,我想再强调一下, **位移提交的语义保障是由你来负责的,Kafka 只会“无脑”地接受你提交的位移** 。你对位移提交的管理直接影响了你的 Consumer 所能提供的消息语义保障。 +这一点特别关键。因为位移提交非常灵活,你完全可以提交任何位移值,但由此产生的后果你也要一并承担。假设你的 Consumer 消费了 10 条消息,你提交的位移值却是 20,那么从理论上讲,位移介于 11~19 之间的消息是有可能丢失的;相反地,如果你提交的位移值是 5,那么位移介于 5~9 之间的消息就有可能被重复消费。所以,我想再强调一下,**位移提交的语义保障是由你来负责的,Kafka 只会“无脑”地接受你提交的位移** 。你对位移提交的管理直接影响了你的 Consumer 所能提供的消息语义保障。 -鉴于位移提交甚至是位移管理对 Consumer 端的巨大影响,Kafka,特别是 KafkaConsumer API,提供了多种提交位移的方法。 **从用户的角度来说,位移提交分为自动提交和手动提交;从 Consumer 端的角度来说,位移提交分为同步提交和异步提交** 。 +鉴于位移提交甚至是位移管理对 Consumer 端的巨大影响,Kafka,特别是 KafkaConsumer API,提供了多种提交位移的方法。**从用户的角度来说,位移提交分为自动提交和手动提交;从 Consumer 端的角度来说,位移提交分为同步提交和异步提交** 。 我们先来说说自动提交和手动提交。所谓自动提交,就是指 Kafka Consumer 在后台默默地为你提交位移,作为用户的你完全不必操心这些事;而手动提交,则是指你要自己提交位移,Kafka Consumer 压根不管。 @@ -58,7 +58,7 @@ while (true) { 可见,调用 consumer.commitSync() 方法的时机,是在你处理完了 poll() 方法返回的所有消息之后。如果你莽撞地过早提交了位移,就可能会出现消费数据丢失的情况。那么你可能会问,自动提交位移就不会出现消费数据丢失的情况了吗?它能恰到好处地把握时机进行位移提交吗?为了搞清楚这个问题,我们必须要深入地了解一下自动提交位移的顺序。 -一旦设置了 enable.auto.commit 为 true,Kafka 会保证在开始调用 poll 方法时,提交上次 poll 返回的所有消息。从顺序上来说,poll 方法的逻辑是先提交上一批消息的位移,再处理下一批消息,因此它能保证不出现消费丢失的情况。但自动提交位移的一个问题在于, **它可能会出现重复消费** 。 +一旦设置了 enable.auto.commit 为 true,Kafka 会保证在开始调用 poll 方法时,提交上次 poll 返回的所有消息。从顺序上来说,poll 方法的逻辑是先提交上一批消息的位移,再处理下一批消息,因此它能保证不出现消费丢失的情况。但自动提交位移的一个问题在于,**它可能会出现重复消费** 。 在默认情况下,Consumer 每 5 秒自动提交一次位移。现在,我们假设提交位移之后的 3 秒发生了 Rebalance 操作。在 Rebalance 之后,所有 Consumer 从上一次提交的位移处继续消费,但该位移已经是 3 秒前的位移数据了,故在 Rebalance 发生前 3 秒消费的所有数据都要重新再消费一次。虽然你能够通过减少 auto.commit.interval.ms 的值来提高提交频率,但这么做只能缩小重复消费的时间窗口,不可能完全消除它。这是自动提交机制的一个缺陷。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" index 343731e5d..b47216701 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" @@ -2,7 +2,7 @@ 你好,我是胡夕。今天我来跟你聊聊 CommitFailedException 异常的处理。 -说起这个异常,我相信用过 Kafka Java Consumer 客户端 API 的你一定不会感到陌生。 **所谓 CommitFailedException,顾名思义就是 Consumer 客户端在提交位移时出现了错误或异常,而且还是那种不可恢复的严重异常** 。如果异常是可恢复的瞬时错误,提交位移的 API 自己就能规避它们了,因为很多提交位移的 API 方法是支持自动错误重试的,比如我们在上一期中提到的 **commitSync 方法** 。 +说起这个异常,我相信用过 Kafka Java Consumer 客户端 API 的你一定不会感到陌生。**所谓 CommitFailedException,顾名思义就是 Consumer 客户端在提交位移时出现了错误或异常,而且还是那种不可恢复的严重异常** 。如果异常是可恢复的瞬时错误,提交位移的 API 自己就能规避它们了,因为很多提交位移的 API 方法是支持自动错误重试的,比如我们在上一期中提到的 **commitSync 方法** 。 每次和 CommitFailedException 一起出现的,还有一段非常著名的注释。为什么说它很“著名”呢?第一,我想不出在近 50 万行的 Kafka 源代码中,还有哪个异常类能有这种待遇,可以享有这么大段的注释,来阐述其异常的含义;第二,纵然有这么长的文字解释,却依然有很多人对该异常想表达的含义感到困惑。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" index c1620f22f..d35d057ad 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" @@ -28,7 +28,7 @@ 首先,我们要明确的是,KafkaConsumer 类不是线程安全的 (thread-safe)。所有的网络 I/O 处理都是发生在用户主线程中,因此,你在使用过程中必须要确保线程安全。简单来说,就是你不能在多个线程中共享同一个 KafkaConsumer 实例,否则程序会抛出 ConcurrentModificationException 异常。 -当然了,这也不是绝对的。KafkaConsumer 中有个方法是例外的,它就是 **wakeup()** ,你可以在其他线程中安全地调用 **KafkaConsumer.wakeup()** 来唤醒 Consumer。 +当然了,这也不是绝对的。KafkaConsumer 中有个方法是例外的,它就是 **wakeup()**,你可以在其他线程中安全地调用 **KafkaConsumer.wakeup()** 来唤醒 Consumer。 鉴于 KafkaConsumer 不是线程安全的事实,我们能够制定两套多线程方案。 @@ -42,7 +42,7 @@ 总体来说,这两种方案都会创建多个线程,这些线程都会参与到消息的消费过程中,但各自的思路是不一样的。 -我们来打个比方。比如一个完整的消费者应用程序要做的事情是 1、2、3、4、5,那么方案 1 的思路是 **粗粒度化** 的工作划分,也就是说方案 1 会创建多个线程,每个线程完整地执行 1、2、3、4、5,以实现并行处理的目标,它不会进一步分割具体的子任务;而方案 2 则更 **细粒度化** ,它会将 1、2 分割出来,用单线程(也可以是多线程)来做,对于 3、4、5,则用另外的多个线程来做。 +我们来打个比方。比如一个完整的消费者应用程序要做的事情是 1、2、3、4、5,那么方案 1 的思路是 **粗粒度化** 的工作划分,也就是说方案 1 会创建多个线程,每个线程完整地执行 1、2、3、4、5,以实现并行处理的目标,它不会进一步分割具体的子任务;而方案 2 则更 **细粒度化**,它会将 1、2 分割出来,用单线程(也可以是多线程)来做,对于 3、4、5,则用另外的多个线程来做。 这两种方案孰优孰劣呢?应该说是各有千秋。我总结了一下这两种方案的优缺点,我们先来看看下面这张表格。 @@ -64,7 +64,7 @@ 下面我们来说说方案 2。 -与方案 1 的粗粒度不同,方案 2 将任务切分成了 **消息获取** 和 **消息处理** 两个部分,分别由不同的线程处理它们。比起方案 1,方案 2 的最大优势就在于它的 **高伸缩性** ,就是说我们可以独立地调节消息获取的线程数,以及消息处理的线程数,而不必考虑两者之间是否相互影响。如果你的消费获取速度慢,那么增加消费获取的线程数即可;如果是消息的处理速度慢,那么增加 Worker 线程池线程数即可。 +与方案 1 的粗粒度不同,方案 2 将任务切分成了 **消息获取** 和 **消息处理** 两个部分,分别由不同的线程处理它们。比起方案 1,方案 2 的最大优势就在于它的 **高伸缩性**,就是说我们可以独立地调节消息获取的线程数,以及消息处理的线程数,而不必考虑两者之间是否相互影响。如果你的消费获取速度慢,那么增加消费获取的线程数即可;如果是消息的处理速度慢,那么增加 Worker 线程池线程数即可。 不过,这种架构也有它的缺陷。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" index e1bb298b4..b4f0f1332 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" @@ -8,7 +8,7 @@ ## 何时创建 TCP 连接? -我们先从消费者创建 TCP 连接开始讨论。消费者端主要的程序入口是 KafkaConsumer 类。 **和生产者不同的是,构建 KafkaConsumer 实例时是不会创建任何 TCP 连接的** ,也就是说,当你执行完 new KafkaConsumer(properties) 语句后,你会发现,没有 Socket 连接被创建出来。这一点和 Java 生产者是有区别的,主要原因就是生产者入口类 KafkaProducer 在构建实例的时候,会在后台默默地启动一个 Sender 线程,这个 Sender 线程负责 Socket 连接的创建。 +我们先从消费者创建 TCP 连接开始讨论。消费者端主要的程序入口是 KafkaConsumer 类。**和生产者不同的是,构建 KafkaConsumer 实例时是不会创建任何 TCP 连接的**,也就是说,当你执行完 new KafkaConsumer(properties) 语句后,你会发现,没有 Socket 连接被创建出来。这一点和 Java 生产者是有区别的,主要原因就是生产者入口类 KafkaProducer 在构建实例的时候,会在后台默默地启动一个 Sender 线程,这个 Sender 线程负责 Socket 连接的创建。 从这一点上来看,我个人认为 KafkaConsumer 的设计比 KafkaProducer 要好。就像我在第 13 讲中所说的,在 Java 构造函数中启动线程,会造成 this 指针的逃逸,这始终是一个隐患。 @@ -82,11 +82,11 @@ ## 何时关闭 TCP 连接? -和生产者类似,消费者关闭 Socket 也分为主动关闭和 Kafka 自动关闭。主动关闭是指你显式地调用消费者 API 的方法去关闭消费者,具体方式就是 **手动调用 KafkaConsumer.close() 方法,或者是执行 Kill 命令** ,不论是 Kill -2 还是 Kill -9;而 Kafka 自动关闭是由 **消费者端参数 connection.max.idle.ms** 控制的,该参数现在的默认值是 9 分钟,即如果某个 Socket 连接上连续 9 分钟都没有任何请求“过境”的话,那么消费者会强行“杀掉”这个 Socket 连接。 +和生产者类似,消费者关闭 Socket 也分为主动关闭和 Kafka 自动关闭。主动关闭是指你显式地调用消费者 API 的方法去关闭消费者,具体方式就是 **手动调用 KafkaConsumer.close() 方法,或者是执行 Kill 命令**,不论是 Kill -2 还是 Kill -9;而 Kafka 自动关闭是由 **消费者端参数 connection.max.idle.ms** 控制的,该参数现在的默认值是 9 分钟,即如果某个 Socket 连接上连续 9 分钟都没有任何请求“过境”的话,那么消费者会强行“杀掉”这个 Socket 连接。 不过,和生产者有些不同的是,如果在编写消费者程序时,你使用了循环的方式来调用 poll 方法消费消息,那么上面提到的所有请求都会被定期发送到 Broker,因此这些 Socket 连接上总是能保证有请求在发送,从而也就实现了“长连接”的效果。 -针对上面提到的三类 TCP 连接,你需要注意的是, **当第三类 TCP 连接成功创建后,消费者程序就会废弃第一类 TCP 连接** ,之后在定期请求元数据时,它会改为使用第三类 TCP 连接。也就是说,最终你会发现,第一类 TCP 连接会在后台被默默地关闭掉。对一个运行了一段时间的消费者程序来说,只会有后面两类 TCP 连接存在。 +针对上面提到的三类 TCP 连接,你需要注意的是,**当第三类 TCP 连接成功创建后,消费者程序就会废弃第一类 TCP 连接**,之后在定期请求元数据时,它会改为使用第三类 TCP 连接。也就是说,最终你会发现,第一类 TCP 连接会在后台被默默地关闭掉。对一个运行了一段时间的消费者程序来说,只会有后面两类 TCP 连接存在。 ## 可能的问题 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" index 2d6bda3d3..d9db87007 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" @@ -12,7 +12,7 @@ 更可怕的是,由于消费者的速度无法匹及生产者的速度,极有可能导致它消费的数据已经不在操作系统的页缓存中了,那么这些数据就会失去享有 Zero Copy 技术的资格。这样的话,消费者就不得不从磁盘上读取它们,这就进一步拉大了与生产者的差距,进而出现马太效应,即那些 Lag 原本就很大的消费者会越来越慢,Lag 也会越来越大。 -鉴于这些原因, **你在实际业务场景中必须时刻关注消费者的消费进度** 。一旦出现 Lag 逐步增加的趋势,一定要定位问题,及时处理,避免造成业务损失。 +鉴于这些原因,**你在实际业务场景中必须时刻关注消费者的消费进度** 。一旦出现 Lag 逐步增加的趋势,一定要定位问题,及时处理,避免造成业务损失。 既然消费进度这么重要,我们应该怎么监控它呢?简单来说,有 3 种方法。 @@ -24,9 +24,9 @@ ## Kafka 自带命令 -我们先来了解下第一种方法:使用 Kafka 自带的命令行工具 bin/kafka-consumer-groups.sh(bat)。 **kafka-consumer-groups 脚本是 Kafka 为我们提供的最直接的监控消费者消费进度的工具** 。当然,除了监控 Lag 之外,它还有其他的功能。今天,我们主要讨论如何使用它来监控 Lag。 +我们先来了解下第一种方法:使用 Kafka 自带的命令行工具 bin/kafka-consumer-groups.sh(bat)。**kafka-consumer-groups 脚本是 Kafka 为我们提供的最直接的监控消费者消费进度的工具** 。当然,除了监控 Lag 之外,它还有其他的功能。今天,我们主要讨论如何使用它来监控 Lag。 -如果只看名字,你可能会以为它只是操作和管理消费者组的。实际上,它也能够监控独立消费者(Standalone Consumer)的 Lag。我们之前说过, **独立消费者就是没有使用消费者组机制的消费者程序** 。和消费者组相同的是,它们也要配置 group.id 参数值,但和消费者组调用 KafkaConsumer.subscribe() 不同的是,独立消费者调用 KafkaConsumer.assign() 方法直接消费指定分区。今天的重点不是要学习独立消费者,你只需要了解接下来我们讨论的所有内容都适用于独立消费者就够了。 +如果只看名字,你可能会以为它只是操作和管理消费者组的。实际上,它也能够监控独立消费者(Standalone Consumer)的 Lag。我们之前说过,**独立消费者就是没有使用消费者组机制的消费者程序** 。和消费者组相同的是,它们也要配置 group.id 参数值,但和消费者组调用 KafkaConsumer.subscribe() 不同的是,独立消费者调用 KafkaConsumer.assign() 方法直接消费指定分区。今天的重点不是要学习独立消费者,你只需要了解接下来我们讨论的所有内容都适用于独立消费者就够了。 使用 kafka-consumer-groups 脚本很简单。该脚本位于 Kafka 安装目录的 bin 子目录下,我们可以通过下面的命令来查看某个给定消费者的 Lag 值: @@ -126,16 +126,16 @@ Properties props = new Properties(); 你不用完全了解上面这段代码每一行的具体含义,只需要记住我标为橙色的 3 处地方即可:第 1 处是调用 AdminClient.listConsumerGroupOffsets 方法获取给定消费者组的最新消费消息的位移;第 2 处则是获取订阅分区的最新消息位移;最后 1 处就是执行相应的减法操作,获取 Lag 值并封装进一个 Map 对象。 -我把这段代码送给你,你可以将 lagOf 方法直接应用于你的生产环境,以实现程序化监控消费者 Lag 的目的。 **不过请注意,这段代码只适用于 Kafka 2.0.0 及以上的版本** ,2.0.0 之前的版本中没有 AdminClient.listConsumerGroupOffsets 方法。 +我把这段代码送给你,你可以将 lagOf 方法直接应用于你的生产环境,以实现程序化监控消费者 Lag 的目的。**不过请注意,这段代码只适用于 Kafka 2.0.0 及以上的版本**,2.0.0 之前的版本中没有 AdminClient.listConsumerGroupOffsets 方法。 Kafka JMX 监控指标 -------------- 上面这两种方式,都可以很方便地查询到给定消费者组的 Lag 信息。但在很多实际监控场景中,我们借助的往往是现成的监控框架。如果是这种情况,以上这两种办法就不怎么管用了,因为它们都不能集成进已有的监控框架中,如 Zabbix 或 Grafana。下面我们就来看第三种方法,使用 Kafka 默认提供的 JMX 监控指标来监控消费者的 Lag 值。 -当前,Kafka 消费者提供了一个名为 kafka.consumer:type=consumer-fetch-manager-metrics,client-id=“{client-id}”的 JMX 指标,里面有很多属性。和我们今天所讲内容相关的有两组属性: **records-lag-max 和 records-lead-min** ,它们分别表示此消费者在测试窗口时间内曾经达到的最大的 Lag 值和最小的 Lead 值。 +当前,Kafka 消费者提供了一个名为 kafka.consumer:type=consumer-fetch-manager-metrics,client-id=“{client-id}”的 JMX 指标,里面有很多属性。和我们今天所讲内容相关的有两组属性: **records-lag-max 和 records-lead-min**,它们分别表示此消费者在测试窗口时间内曾经达到的最大的 Lag 值和最小的 Lead 值。 -Lag 值的含义我们已经反复讲过了,我就不再重复了。 **这里的 Lead 值是指消费者最新消费消息的位移与分区当前第一条消息位移的差值** 。很显然,Lag 和 Lead 是一体的两个方面: **Lag 越大的话,Lead 就越小,反之也是同理** 。 +Lag 值的含义我们已经反复讲过了,我就不再重复了。**这里的 Lead 值是指消费者最新消费消息的位移与分区当前第一条消息位移的差值** 。很显然,Lag 和 Lead 是一体的两个方面: **Lag 越大的话,Lead 就越小,反之也是同理** 。 你可能会问,为什么要引入 Lead 呢?我只监控 Lag 不就行了吗?这里提 Lead 的原因就在于这部分功能是我实现的。开个玩笑,其实社区引入 Lead 的原因是,只看 Lag 的话,我们也许不能及时意识到可能出现的严重问题。 @@ -143,7 +143,7 @@ Lag 值的含义我们已经反复讲过了,我就不再重复了。 **这里 为什么?我们知道 Kafka 的消息是有留存时间设置的,默认是 1 周,也就是说 Kafka 默认删除 1 周前的数据。倘若你的消费者程序足够慢,慢到它要消费的数据快被 Kafka 删除了,这时你就必须立即处理,否则一定会出现消息被删除,从而导致消费者程序重新调整位移值的情形。这可能产生两个后果:一个是消费者从头消费一遍数据,另一个是消费者从最新的消息位移处开始消费,之前没来得及消费的消息全部被跳过了,从而造成丢消息的假象。 -这两种情形都是不可忍受的,因此必须有一个 JMX 指标,清晰地表征这种情形,这就是引入 Lead 指标的原因。所以,Lag 值从 100 万增加到 200 万这件事情,远不如 Lead 值从 200 减少到 100 这件事来得重要。 **在实际生产环境中,请你一定要同时监控 Lag 值和 Lead 值** 。当然了,这个 lead JMX 指标的确也是我开发的,这一点倒是事实。 +这两种情形都是不可忍受的,因此必须有一个 JMX 指标,清晰地表征这种情形,这就是引入 Lead 指标的原因。所以,Lag 值从 100 万增加到 200 万这件事情,远不如 Lead 值从 200 减少到 100 这件事来得重要。**在实际生产环境中,请你一定要同时监控 Lag 值和 Lead 值** 。当然了,这个 lead JMX 指标的确也是我开发的,这一点倒是事实。 接下来,我给出一张使用 JConsole 工具监控此 JMX 指标的截图。从这张图片中,我们可以看到,client-id 为 consumer-1 的消费者在给定的测量周期内最大的 Lag 值为 714202,最小的 Lead 值是 83,这说明此消费者有很大的消费滞后性。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" index df393f1e0..b4a42a887 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" @@ -16,7 +16,7 @@ 在讨论具体的副本机制之前,我们先花一点时间明确一下副本的含义。 -我们之前谈到过,Kafka 是有主题概念的,而每个主题又进一步划分成若干个分区。副本的概念实际上是在分区层级下定义的,每个分区配置有若干个副本。 **所谓副本(Replica),本质就是一个只能追加写消息的提交日志** 。根据 Kafka 副本机制的定义,同一个分区下的所有副本保存有相同的消息序列,这些副本分散保存在不同的 Broker 上,从而能够对抗部分 Broker 宕机带来的数据不可用。 +我们之前谈到过,Kafka 是有主题概念的,而每个主题又进一步划分成若干个分区。副本的概念实际上是在分区层级下定义的,每个分区配置有若干个副本。**所谓副本(Replica),本质就是一个只能追加写消息的提交日志** 。根据 Kafka 副本机制的定义,同一个分区下的所有副本保存有相同的消息序列,这些副本分散保存在不同的 Broker 上,从而能够对抗部分 Broker 宕机带来的数据不可用。 在实际生产环境中,每台 Broker 都可能保存有各个主题下不同分区的不同副本,因此,单个 Broker 上存有成百上千个副本的现象是非常正常的。 @@ -62,7 +62,7 @@ 基于这个想法,Kafka 引入了 In-sync Replicas,也就是所谓的 ISR 副本集合。ISR 中的副本都是与 Leader 同步的副本,相反,不在 ISR 中的追随者副本就被认为是与 Leader 不同步的。那么,到底什么副本能够进入到 ISR 中呢? -我们首先要明确的是,Leader 副本天然就在 ISR 中。也就是说, **ISR 不只是追随者副本集合,它必然包括 Leader 副本。甚至在某些情况下,ISR 只有 Leader 这一个副本** 。 +我们首先要明确的是,Leader 副本天然就在 ISR 中。也就是说,**ISR 不只是追随者副本集合,它必然包括 Leader 副本。甚至在某些情况下,ISR 只有 Leader 这一个副本** 。 另外,能够进入到 ISR 的追随者副本要满足一定的条件。至于是什么条件,我先卖个关子,我们先来一起看看下面这张图。 @@ -72,7 +72,7 @@ 答案是,要根据具体情况来定。换成英文,就是那句著名的“It depends”。看上去好像 Follower2 的消息数比 Leader 少了很多,它是最有可能与 Leader 不同步的。的确是这样的,但仅仅是可能。 -事实上,这张图中的 2 个 Follower 副本都有可能与 Leader 不同步,但也都有可能与 Leader 同步。也就是说,Kafka 判断 Follower 是否与 Leader 同步的标准,不是看相差的消息数,而是另有“玄机”。 **这个标准就是 Broker 端参数 replica.lag.time.max.ms 参数值** 。这个参数的含义是 Follower 副本能够落后 Leader 副本的最长时间间隔,当前默认值是 10 秒。这就是说,只要一个 Follower 副本落后 Leader 副本的时间不连续超过 10 秒,那么 Kafka 就认为该 Follower 副本与 Leader 是同步的,即使此时 Follower 副本中保存的消息明显少于 Leader 副本中的消息。 +事实上,这张图中的 2 个 Follower 副本都有可能与 Leader 不同步,但也都有可能与 Leader 同步。也就是说,Kafka 判断 Follower 是否与 Leader 同步的标准,不是看相差的消息数,而是另有“玄机”。**这个标准就是 Broker 端参数 replica.lag.time.max.ms 参数值** 。这个参数的含义是 Follower 副本能够落后 Leader 副本的最长时间间隔,当前默认值是 10 秒。这就是说,只要一个 Follower 副本落后 Leader 副本的时间不连续超过 10 秒,那么 Kafka 就认为该 Follower 副本与 Leader 是同步的,即使此时 Follower 副本中保存的消息明显少于 Leader 副本中的消息。 我们在前面说过,Follower 副本唯一的工作就是不断地从 Leader 副本拉取消息,然后写入到自己的提交日志中。如果这个同步过程的速度持续慢于 Leader 副本的消息写入速度,那么在 replica.lag.time.max.ms 时间后,此 Follower 副本就会被认为是与 Leader 副本不同步的,因此不能再放入 ISR 中。此时,Kafka 会自动收缩 ISR 集合,将该副本“踢出”ISR。 @@ -80,7 +80,7 @@ ## Unclean 领导者选举(Unclean Leader Election) -既然 ISR 是可以动态调整的,那么自然就可以出现这样的情形:ISR 为空。因为 Leader 副本天然就在 ISR 中,如果 ISR 为空了,就说明 Leader 副本也“挂掉”了,Kafka 需要重新选举一个新的 Leader。可是 ISR 是空,此时该怎么选举新 Leader 呢? **Kafka 把所有不在 ISR 中的存活副本都称为非同步副本** 。通常来说,非同步副本落后 Leader 太多,因此,如果选择这些副本作为新 Leader,就可能出现数据的丢失。毕竟,这些副本中保存的消息远远落后于老 Leader 中的消息。在 Kafka 中,选举这种副本的过程称为 Unclean 领导者选举。 **Broker 端参数 unclean.leader.election.enable 控制是否允许 Unclean 领导者选举** 。 +既然 ISR 是可以动态调整的,那么自然就可以出现这样的情形:ISR 为空。因为 Leader 副本天然就在 ISR 中,如果 ISR 为空了,就说明 Leader 副本也“挂掉”了,Kafka 需要重新选举一个新的 Leader。可是 ISR 是空,此时该怎么选举新 Leader 呢?**Kafka 把所有不在 ISR 中的存活副本都称为非同步副本** 。通常来说,非同步副本落后 Leader 太多,因此,如果选择这些副本作为新 Leader,就可能出现数据的丢失。毕竟,这些副本中保存的消息远远落后于老 Leader 中的消息。在 Kafka 中,选举这种副本的过程称为 Unclean 领导者选举。**Broker 端参数 unclean.leader.election.enable 控制是否允许 Unclean 领导者选举** 。 开启 Unclean 领导者选举可能会造成数据丢失,但好处是,它使得分区 Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止 Unclean 领导者选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" index 3ee3482a1..d115315ab 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" @@ -6,7 +6,7 @@ Apache Kafka 自己定义了一组请求协议,用于实现各种各样的交互操作。比如常见的 PRODUCE 请求是用于生产消息的,FETCH 请求是用于消费消息的,METADATA 请求是用于请求 Kafka 集群元数据信息的。 -总之,Kafka 定义了很多类似的请求格式。我数了一下,截止到目前最新的 2.3 版本,Kafka 共定义了多达 45 种请求格式。 **所有的请求都是通过 TCP 网络以 Socket 的方式进行通讯的** 。 +总之,Kafka 定义了很多类似的请求格式。我数了一下,截止到目前最新的 2.3 版本,Kafka 共定义了多达 45 种请求格式。**所有的请求都是通过 TCP 网络以 Socket 的方式进行通讯的** 。 今天,我们就来详细讨论一下 Kafka Broker 端处理请求的全流程。 @@ -40,7 +40,7 @@ while (true) { 谈到 Reactor 模式,大神 Doug Lea 的“[Scalable IO in Java](http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf)”应该算是最好的入门教材了。即使你没听说过 Doug Lea,那你应该也用过 ConcurrentHashMap 吧?这个类就是这位大神写的。其实,整个 java.util.concurrent 包都是他的杰作! -好了,我们说回 Reactor 模式。简单来说, **Reactor 模式是事件驱动架构的一种实现方式,特别适合应用于处理多个客户端并发向服务器端发送请求的场景** 。我借用 Doug Lea 的一页 PPT 来说明一下 Reactor 的架构,并借此引出 Kafka 的请求处理模型。 +好了,我们说回 Reactor 模式。简单来说,**Reactor 模式是事件驱动架构的一种实现方式,特别适合应用于处理多个客户端并发向服务器端发送请求的场景** 。我借用 Doug Lea 的一页 PPT 来说明一下 Reactor 的架构,并借此引出 Kafka 的请求处理模型。 Reactor 模式的架构如下图所示: @@ -64,13 +64,13 @@ Acceptor 线程采用轮询的方式将入站请求公平地发到所有网络 当网络线程拿到请求后,它不是自己处理,而是将请求放入到一个共享请求队列中。Broker 端还有个 IO 线程池,负责从该队列中取出请求,执行真正的处理。如果是 PRODUCE 生产请求,则将消息写入到底层的磁盘日志中;如果是 FETCH 请求,则从磁盘或页缓存中读取消息。 -IO 线程池处中的线程才是执行请求逻辑的线程。Broker 端参数 **num.io.threads** 控制了这个线程池中的线程数。 **目前该参数默认值是 8,表示每台 Broker 启动后自动创建 8 个 IO 线程处理请求** 。你可以根据实际硬件条件设置此线程池的个数。 +IO 线程池处中的线程才是执行请求逻辑的线程。Broker 端参数 **num.io.threads** 控制了这个线程池中的线程数。**目前该参数默认值是 8,表示每台 Broker 启动后自动创建 8 个 IO 线程处理请求** 。你可以根据实际硬件条件设置此线程池的个数。 比如,如果你的机器上 CPU 资源非常充裕,你完全可以调大该参数,允许更多的并发请求被同时处理。当 IO 线程处理完请求后,会将生成的响应发送到网络线程池的响应队列中,然后由对应的网络线程负责将 Response 返还给客户端。 细心的你一定发现了请求队列和响应队列的差别: **请求队列是所有网络线程共享的,而响应队列则是每个网络线程专属的** 。这么设计的原因就在于,Dispatcher 只是用于请求分发而不负责响应回传,因此只能让每个网络线程自己发送 Response 给客户端,所以这些 Response 也就没必要放在一个公共的地方。 -我们再来看看刚刚的那张图,图中有一个叫 Purgatory 的组件,这是 Kafka 中著名的“炼狱”组件。它是用来 **缓存延时请求** (Delayed Request)的。 **所谓延时请求,就是那些一时未满足条件不能立刻处理的请求** 。比如设置了 acks=all 的 PRODUCE 请求,一旦设置了 acks=all,那么该请求就必须等待 ISR 中所有副本都接收了消息后才能返回,此时处理该请求的 IO 线程就必须等待其他 Broker 的写入结果。当请求不能立刻处理时,它就会暂存在 Purgatory 中。稍后一旦满足了完成条件,IO 线程会继续处理该请求,并将 Response 放入对应网络线程的响应队列中。 +我们再来看看刚刚的那张图,图中有一个叫 Purgatory 的组件,这是 Kafka 中著名的“炼狱”组件。它是用来 **缓存延时请求** (Delayed Request)的。**所谓延时请求,就是那些一时未满足条件不能立刻处理的请求** 。比如设置了 acks=all 的 PRODUCE 请求,一旦设置了 acks=all,那么该请求就必须等待 ISR 中所有副本都接收了消息后才能返回,此时处理该请求的 IO 线程就必须等待其他 Broker 的写入结果。当请求不能立刻处理时,它就会暂存在 Purgatory 中。稍后一旦满足了完成条件,IO 线程会继续处理该请求,并将 Response 放入对应网络线程的响应队列中。 讲到这里,Kafka 请求流程解析的故事其实已经讲完了,我相信你应该已经了解了 Kafka Broker 是如何从头到尾处理请求的。但是我们不会现在就收尾,我要给今天的内容开个小灶,再说点不一样的东西。 @@ -80,7 +80,7 @@ Kafka 社区把 PRODUCE 和 FETCH 这类请求称为数据类请求,把 Leader 这时,一个尴尬的场面就出现了:如果刚才积压的 PRODUCE 请求都设置了 acks=all,那么这些在 LeaderAndIsr 发送之前的请求就都无法正常完成了。就像前面说的,它们会被暂存在 Purgatory 中不断重试,直到最终请求超时返回给客户端。 -设想一下,如果 Kafka 能够优先处理 LeaderAndIsr 请求,Broker 0 就会立刻抛出 **NOT_LEADER_FOR_PARTITION 异常** ,快速地标识这些积压 PRODUCE 请求已失败,这样客户端不用等到 Purgatory 中的请求超时就能立刻感知,从而降低了请求的处理时间。即使 acks 不是 all,积压的 PRODUCE 请求能够成功写入 Leader 副本的日志,但处理 LeaderAndIsr 之后,Broker 0 上的 Leader 变为了 Follower 副本,也要执行显式的日志截断(Log Truncation,即原 Leader 副本成为 Follower 后,会将之前写入但未提交的消息全部删除),依然做了很多无用功。 +设想一下,如果 Kafka 能够优先处理 LeaderAndIsr 请求,Broker 0 就会立刻抛出 **NOT_LEADER_FOR_PARTITION 异常**,快速地标识这些积压 PRODUCE 请求已失败,这样客户端不用等到 Purgatory 中的请求超时就能立刻感知,从而降低了请求的处理时间。即使 acks 不是 all,积压的 PRODUCE 请求能够成功写入 Leader 副本的日志,但处理 LeaderAndIsr 之后,Broker 0 上的 Leader 变为了 Follower 副本,也要执行显式的日志截断(Log Truncation,即原 Leader 副本成为 Follower 后,会将之前写入但未提交的消息全部删除),依然做了很多无用功。 再举一个例子,同样是在积压大量数据类请求的 Broker 上,当你删除主题的时候,Kafka 控制器(我会在专栏后面的内容中专门介绍它)向该 Broker 发送 StopReplica 请求。如果该请求不能及时处理,主题删除操作会一直 hang 住,从而增加了删除主题的延时。 @@ -88,7 +88,7 @@ Kafka 社区把 PRODUCE 和 FETCH 这类请求称为数据类请求,把 Leader 究其原因,这个方案最大的问题在于,它无法处理请求队列已满的情形。当请求队列已经无法容纳任何新的请求时,纵然有优先级之分,它也无法处理新的控制类请求了。 -那么,社区是如何解决的呢?很简单,你可以再看一遍今天的第三张图,社区完全拷贝了这张图中的一套组件,实现了两类请求的分离。也就是说,Kafka Broker 启动后,会在后台分别创建网络线程池和 IO 线程池,它们分别处理数据类请求和控制类请求。至于所用的 Socket 端口,自然是使用不同的端口了,你需要提供不同的 **listeners 配置** ,显式地指定哪套端口用于处理哪类请求。 +那么,社区是如何解决的呢?很简单,你可以再看一遍今天的第三张图,社区完全拷贝了这张图中的一套组件,实现了两类请求的分离。也就是说,Kafka Broker 启动后,会在后台分别创建网络线程池和 IO 线程池,它们分别处理数据类请求和控制类请求。至于所用的 Socket 端口,自然是使用不同的端口了,你需要提供不同的 **listeners 配置**,显式地指定哪套端口用于处理哪类请求。 ## 小结 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" index f0d7b983a..a65012f44 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" @@ -22,9 +22,9 @@ Kafka Java 消费者需要定期地发送心跳请求(Heartbeat Request)到 Broker 端的协调者,以表明它还存活着。在 Kafka 0.10.1.0 版本之前,发送心跳请求是在 **消费者主线程** 完成的,也就是你写代码调用 KafkaConsumer.poll 方法的那个线程。 -这样做有诸多弊病,最大的问题在于, **消息处理逻辑也是在这个线程中完成的** 。因此,一旦消息处理消耗了过长的时间,心跳请求将无法及时发到协调者那里,导致协调者“错误地”认为该消费者已“死”。自 0.10.1.0 版本开始,社区引入了一个单独的心跳线程来专门执行心跳请求发送,避免了这个问题。 +这样做有诸多弊病,最大的问题在于,**消息处理逻辑也是在这个线程中完成的** 。因此,一旦消息处理消耗了过长的时间,心跳请求将无法及时发到协调者那里,导致协调者“错误地”认为该消费者已“死”。自 0.10.1.0 版本开始,社区引入了一个单独的心跳线程来专门执行心跳请求发送,避免了这个问题。 -但这和重平衡又有什么关系呢?其实, **重平衡的通知机制正是通过心跳线程来完成的** 。当协调者决定开启新一轮重平衡后,它会将“ **REBALANCE_IN_PROGRESS** ”封装进心跳请求的响应中,发还给消费者实例。当消费者实例发现心跳响应中包含了“REBALANCE_IN_PROGRESS”,就能立马知道重平衡又开始了,这就是重平衡的通知机制。 +但这和重平衡又有什么关系呢?其实,**重平衡的通知机制正是通过心跳线程来完成的** 。当协调者决定开启新一轮重平衡后,它会将“ **REBALANCE_IN_PROGRESS** ”封装进心跳请求的响应中,发还给消费者实例。当消费者实例发现心跳响应中包含了“REBALANCE_IN_PROGRESS”,就能立马知道重平衡又开始了,这就是重平衡的通知机制。 对了,很多人还搞不清楚消费者端参数 heartbeat.interval.ms 的真实用途,我来解释一下。从字面上看,它就是设置了心跳的间隔时间,但这个参数的真正作用是控制重平衡通知的频率。如果你想要消费者实例更迅速地得到通知,那么就可以给这个参数设置一个非常小的值,这样消费者就能更快地感知到重平衡已经开启了。 @@ -56,7 +56,7 @@ Kafka Java 消费者需要定期地发送心跳请求(Heartbeat Request)到 当组内成员加入组时,它会向协调者发送 JoinGroup 请求。在该请求中,每个成员都要将自己订阅的主题上报,这样协调者就能收集到所有成员的订阅信息。一旦收集了全部成员的 JoinGroup 请求后,协调者会从这些成员中选择一个担任这个消费者组的领导者。 -通常情况下,第一个发送 JoinGroup 请求的成员自动成为领导者。你一定要注意区分这里的领导者和之前我们介绍的领导者副本,它们不是一个概念。这里的领导者是具体的消费者实例,它既不是副本,也不是协调者。 **领导者消费者的任务是收集所有成员的订阅信息,然后根据这些信息,制定具体的分区消费分配方案。** 选出领导者之后,协调者会把消费者组订阅信息封装进 JoinGroup 请求的响应体中,然后发给领导者,由领导者统一做出分配方案后,进入到下一步:发送 SyncGroup 请求。 +通常情况下,第一个发送 JoinGroup 请求的成员自动成为领导者。你一定要注意区分这里的领导者和之前我们介绍的领导者副本,它们不是一个概念。这里的领导者是具体的消费者实例,它既不是副本,也不是协调者。**领导者消费者的任务是收集所有成员的订阅信息,然后根据这些信息,制定具体的分区消费分配方案。** 选出领导者之后,协调者会把消费者组订阅信息封装进 JoinGroup 请求的响应体中,然后发给领导者,由领导者统一做出分配方案后,进入到下一步:发送 SyncGroup 请求。 在这一步中,领导者向协调者发送 SyncGroup 请求,将刚刚做出的分配方案发给协调者。值得注意的是,其他成员也会向协调者发送 SyncGroup 请求,只不过请求体中并没有实际的内容。这一步的主要目的是让协调者接收分配方案,然后统一以 SyncGroup 响应的方式分发给所有成员,这样组内所有成员就都知道自己该消费哪些分区了。 @@ -72,11 +72,11 @@ Kafka Java 消费者需要定期地发送心跳请求(Heartbeat Request)到 SyncGroup 请求的主要目的,就是让协调者把领导者制定的分配方案下发给各个组内成员。当所有成员都成功接收到分配方案后,消费者组进入到 Stable 状态,即开始正常的消费工作。 -讲完这里, **消费者端** 的重平衡流程我已经介绍完了。接下来,我们从 **协调者端** 来看一下重平衡是怎么执行的。 +讲完这里,**消费者端** 的重平衡流程我已经介绍完了。接下来,我们从 **协调者端** 来看一下重平衡是怎么执行的。 ## Broker 端重平衡场景剖析 -要剖析协调者端处理重平衡的全流程,我们必须要分几个场景来讨论。这几个场景分别是新成员加入组、组成员主动离组、组成员崩溃离组、组成员提交位移。接下来,我们一个一个来讨论。 **场景一:新成员入组。** 新成员入组是指组处于 Stable 状态后,有新成员加入。如果是全新启动一个消费者组,Kafka 是有一些自己的小优化的,流程上会有些许的不同。我们这里讨论的是,组稳定了之后有新成员加入的情形。 +要剖析协调者端处理重平衡的全流程,我们必须要分几个场景来讨论。这几个场景分别是新成员加入组、组成员主动离组、组成员崩溃离组、组成员提交位移。接下来,我们一个一个来讨论。**场景一:新成员入组。** 新成员入组是指组处于 Stable 状态后,有新成员加入。如果是全新启动一个消费者组,Kafka 是有一些自己的小优化的,流程上会有些许的不同。我们这里讨论的是,组稳定了之后有新成员加入的情形。 当协调者收到新的 JoinGroup 请求后,它会通过心跳请求响应的方式通知组内现有的所有成员,强制它们开启新一轮的重平衡。具体的过程和之前的客户端重平衡流程是一样的。现在,我用一张时序图来说明协调者一端是如何处理新成员入组的。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25426\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25426\350\256\262.md" index b84eb6641..f8a2dd4bf 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25426\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25426\350\256\262.md" @@ -4,9 +4,9 @@ **控制器组件(Controller),是 Apache Kafka 的核心组件。它的主要作用是在 Apache ZooKeeper 的帮助下管理和协调整个 Kafka 集群** 。集群中任意一台 Broker 都能充当控制器的角色,但是,在运行过程中,只能有一个 Broker 成为控制器,行使其管理和协调的职责。换句话说,每个正常运转的 Kafka 集群,在任意时刻都有且只有一个控制器。官网上有个名为 activeController 的 JMX 指标,可以帮助我们实时监控控制器的存活状态。这个 JMX 指标非常关键,你在实际运维操作过程中,一定要实时查看这个指标的值。下面,我们就来详细说说控制器的原理和内部运行机制。 -在开始之前,我先简单介绍一下 Apache ZooKeeper 框架。要知道, **控制器是重度依赖 ZooKeeper 的** ,因此,我们有必要花一些时间学习下 ZooKeeper 是做什么的。 **Apache ZooKeeper 是一个提供高可靠性的分布式协调服务框架** 。它使用的数据模型类似于文件系统的树形结构,根目录也是以“/”开始。该结构上的每个节点被称为 znode,用来保存一些元数据协调信息。 +在开始之前,我先简单介绍一下 Apache ZooKeeper 框架。要知道,**控制器是重度依赖 ZooKeeper 的**,因此,我们有必要花一些时间学习下 ZooKeeper 是做什么的。**Apache ZooKeeper 是一个提供高可靠性的分布式协调服务框架** 。它使用的数据模型类似于文件系统的树形结构,根目录也是以“/”开始。该结构上的每个节点被称为 znode,用来保存一些元数据协调信息。 -如果以 znode 持久性来划分, **znode 可分为持久性 znode 和临时 znode** 。持久性 znode 不会因为 ZooKeeper 集群重启而消失,而临时 znode 则与创建该 znode 的 ZooKeeper 会话绑定,一旦会话结束,该节点会被自动删除。 +如果以 znode 持久性来划分,**znode 可分为持久性 znode 和临时 znode** 。持久性 znode 不会因为 ZooKeeper 集群重启而消失,而临时 znode 则与创建该 znode 的 ZooKeeper 会话绑定,一旦会话结束,该节点会被自动删除。 ZooKeeper 赋予客户端监控 znode 变更的能力,即所谓的 Watch 通知功能。一旦 znode 节点被创建、删除,子节点数量发生变化,抑或是 znode 所存的数据本身变更,ZooKeeper 会通过节点变更监听器 (ChangeHandler) 的方式显式通知客户端。 @@ -28,7 +28,7 @@ ZooKeeper 赋予客户端监控 znode 变更的能力,即所谓的 Watch 通 1. **主题管理(创建、删除、增加分区)** 这里的主题管理,就是指控制器帮助我们完成对 Kafka 主题的创建、删除以及分区增加的操作。换句话说,当我们执行 **kafka-topics 脚本** 时,大部分的后台工作都是控制器来完成的。关于 kafka-topics 脚本,我会在专栏后面的内容中,详细介绍它的使用方法。 -2. **分区重分配** 分区重分配主要是指, **kafka-reassign-partitions 脚本** (关于这个脚本,后面我也会介绍)提供的对已有主题分区进行细粒度的分配功能。这部分功能也是控制器实现的。 +2. **分区重分配** 分区重分配主要是指,**kafka-reassign-partitions 脚本** (关于这个脚本,后面我也会介绍)提供的对已有主题分区进行细粒度的分配功能。这部分功能也是控制器实现的。 3. **Preferred 领导者选举** Preferred 领导者选举主要是 Kafka 为了避免部分 Broker 负载过重而提供的一种换 Leader 的方案。在专栏后面说到工具的时候,我们再详谈 Preferred 领导者选举,这里你只需要了解这也是控制器的职责范围就可以了。 @@ -72,26 +72,26 @@ ZooKeeper 赋予客户端监控 znode 变更的能力,即所谓的 Watch 通 在 Kafka 0.11 版本之前,控制器的设计是相当繁琐的,代码更是有些混乱,这就导致社区中很多控制器方面的 Bug 都无法修复。控制器是多线程的设计,会在内部创建很多个线程。比如,控制器需要为每个 Broker 都创建一个对应的 Socket 连接,然后再创建一个专属的线程,用于向这些 Broker 发送特定请求。如果集群中的 Broker 数量很多,那么控制器端需要创建的线程就会很多。另外,控制器连接 ZooKeeper 的会话,也会创建单独的线程来处理 Watch 机制的通知回调。除了以上这些线程,控制器还会为主题删除创建额外的 I/O 线程。 -比起多线程的设计,更糟糕的是,这些线程还会访问共享的控制器缓存数据。我们都知道,多线程访问共享可变数据是维持线程安全最大的难题。为了保护数据安全性,控制器不得不在代码中大量使用 **ReentrantLock 同步机制** ,这就进一步拖慢了整个控制器的处理速度。 +比起多线程的设计,更糟糕的是,这些线程还会访问共享的控制器缓存数据。我们都知道,多线程访问共享可变数据是维持线程安全最大的难题。为了保护数据安全性,控制器不得不在代码中大量使用 **ReentrantLock 同步机制**,这就进一步拖慢了整个控制器的处理速度。 -鉴于这些原因,社区于 0.11 版本重构了控制器的底层设计,最大的改进就是, **把多线程的方案改成了单线程加事件队列的方案** 。我直接使用社区的一张图来说明。 +鉴于这些原因,社区于 0.11 版本重构了控制器的底层设计,最大的改进就是,**把多线程的方案改成了单线程加事件队列的方案** 。我直接使用社区的一张图来说明。 ![img](assets/b14c6f2d246cbf637f2fda5dae1688e5.png) -从这张图中,我们可以看到,社区引入了一个 **事件处理线程** ,统一处理各种控制器事件,然后控制器将原来执行的操作全部建模成一个个独立的事件,发送到专属的事件队列中,供此线程消费。这就是所谓的单线程 + 队列的实现方式。 +从这张图中,我们可以看到,社区引入了一个 **事件处理线程**,统一处理各种控制器事件,然后控制器将原来执行的操作全部建模成一个个独立的事件,发送到专属的事件队列中,供此线程消费。这就是所谓的单线程 + 队列的实现方式。 值得注意的是,这里的单线程不代表之前提到的所有线程都被“干掉”了,控制器只是把缓存状态变更方面的工作委托给了这个线程而已。 这个方案的最大好处在于,控制器缓存中保存的状态只被一个线程处理,因此不再需要重量级的线程同步机制来维护线程安全,Kafka 不用再担心多线程并发访问的问题,非常利于社区定位和诊断控制器的各种问题。事实上,自 0.11 版本重构控制器代码后,社区关于控制器方面的 Bug 明显少多了,这也说明了这种方案是有效的。 -针对控制器的第二个改进就是, **将之前同步操作 ZooKeeper 全部改为异步操作** 。ZooKeeper 本身的 API 提供了同步写和异步写两种方式。之前控制器操作 ZooKeeper 使用的是同步的 API,性能很差,集中表现为, **当有大量主题分区发生变更时,ZooKeeper 容易成为系统的瓶颈** 。新版本 Kafka 修改了这部分设计,完全摒弃了之前的同步 API 调用,转而采用异步 API 写入 ZooKeeper,性能有了很大的提升。根据社区的测试,改成异步之后,ZooKeeper 写入提升了 10 倍! +针对控制器的第二个改进就是,**将之前同步操作 ZooKeeper 全部改为异步操作** 。ZooKeeper 本身的 API 提供了同步写和异步写两种方式。之前控制器操作 ZooKeeper 使用的是同步的 API,性能很差,集中表现为,**当有大量主题分区发生变更时,ZooKeeper 容易成为系统的瓶颈** 。新版本 Kafka 修改了这部分设计,完全摒弃了之前的同步 API 调用,转而采用异步 API 写入 ZooKeeper,性能有了很大的提升。根据社区的测试,改成异步之后,ZooKeeper 写入提升了 10 倍! 除了以上这些,社区最近又发布了一个重大的改进!之前 Broker 对接收的所有请求都是一视同仁的,不会区别对待。这种设计对于控制器发送的请求非常不公平,因为这类请求应该有更高的优先级。 -举个简单的例子,假设我们删除了某个主题,那么控制器就会给该主题所有副本所在的 Broker 发送一个名为 **StopReplica** 的请求。如果此时 Broker 上存有大量积压的 Produce 请求,那么这个 StopReplica 请求只能排队等。如果这些 Produce 请求就是要向该主题发送消息的话,这就显得很讽刺了:主题都要被删除了,处理这些 Produce 请求还有意义吗?此时最合理的处理顺序应该是, **赋予 StopReplica 请求更高的优先级,使它能够得到抢占式的处理。** 这在 2.2 版本之前是做不到的。不过自 2.2 开始,Kafka 正式支持这种不同优先级请求的处理。简单来说,Kafka 将控制器发送的请求与普通数据类请求分开,实现了控制器请求单独处理的逻辑。鉴于这个改进还是很新的功能,具体的效果我们就拭目以待吧。 +举个简单的例子,假设我们删除了某个主题,那么控制器就会给该主题所有副本所在的 Broker 发送一个名为 **StopReplica** 的请求。如果此时 Broker 上存有大量积压的 Produce 请求,那么这个 StopReplica 请求只能排队等。如果这些 Produce 请求就是要向该主题发送消息的话,这就显得很讽刺了:主题都要被删除了,处理这些 Produce 请求还有意义吗?此时最合理的处理顺序应该是,**赋予 StopReplica 请求更高的优先级,使它能够得到抢占式的处理。** 这在 2.2 版本之前是做不到的。不过自 2.2 开始,Kafka 正式支持这种不同优先级请求的处理。简单来说,Kafka 将控制器发送的请求与普通数据类请求分开,实现了控制器请求单独处理的逻辑。鉴于这个改进还是很新的功能,具体的效果我们就拭目以待吧。 ## 小结 -好了,有关 Kafka 控制器的内容,我已经讲完了。最后,我再跟你分享一个小窍门。当你觉得控制器组件出现问题时,比如主题无法删除了,或者重分区 hang 住了,你不用重启 Kafka Broker 或控制器。有一个简单快速的方式是,去 ZooKeeper 中手动删除 /controller 节点。 **具体命令是 rmr /controller** 。这样做的好处是,既可以引发控制器的重选举,又可以避免重启 Broker 导致的消息处理中断。 +好了,有关 Kafka 控制器的内容,我已经讲完了。最后,我再跟你分享一个小窍门。当你觉得控制器组件出现问题时,比如主题无法删除了,或者重分区 hang 住了,你不用重启 Kafka Broker 或控制器。有一个简单快速的方式是,去 ZooKeeper 中手动删除 /controller 节点。**具体命令是 rmr /controller** 。这样做的好处是,既可以引发控制器的重选举,又可以避免重启 Broker 导致的消息处理中断。 ![img](assets/00264a90e6e3a8567b1ad0f5fbb9f492.png) diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25427\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25427\350\256\262.md" index e52f633a2..488d8bb51 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25427\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25427\350\256\262.md" @@ -35,11 +35,11 @@ 我们假设这是某个分区 Leader 副本的高水位图。首先,请你注意图中的“已提交消息”和“未提交消息”。我们之前在专栏\[第 11 讲\]谈到 Kafka 持久性保障的时候,特意对两者进行了区分。现在,我借用高水位再次强调一下。在分区高水位以下的消息被认为是已提交消息,反之就是未提交消息。消费者只能消费已提交消息,即图中位移小于 8 的所有消息。注意,这里我们不讨论 Kafka 事务,因为事务机制会影响消费者所能看到的消息的范围,它不只是简单依赖高水位来判断。它依靠一个名为 LSO(Log Stable Offset)的位移值来判断事务型消费者的可见性。 -另外,需要关注的是, **位移值等于高水位的消息也属于未提交消息。也就是说,高水位上的消息是不能被消费者消费的** 。 +另外,需要关注的是,**位移值等于高水位的消息也属于未提交消息。也就是说,高水位上的消息是不能被消费者消费的** 。 图中还有一个日志末端位移的概念,即 Log End Offset,简写是 LEO。它表示副本写入下一条消息的位移值。注意,数字 15 所在的方框是虚线,这就说明,这个副本当前只有 15 条消息,位移值是从 0 到 14,下一条新消息的位移是 15。显然,介于高水位和 LEO 之间的消息就属于未提交消息。这也从侧面告诉了我们一个重要的事实,那就是: **同一个副本对象,其高水位值不会大于 LEO 值** 。 -**高水位和 LEO 是副本对象的两个重要属性** 。Kafka 所有副本都有对应的高水位和 LEO 值,而不仅仅是 Leader 副本。只不过 Leader 副本比较特殊,Kafka 使用 Leader 副本的高水位来定义所在分区的高水位。换句话说, **分区的高水位就是其 Leader 副本的高水位** 。 +**高水位和 LEO 是副本对象的两个重要属性** 。Kafka 所有副本都有对应的高水位和 LEO 值,而不仅仅是 Leader 副本。只不过 Leader 副本比较特殊,Kafka 使用 Leader 副本的高水位来定义所在分区的高水位。换句话说,**分区的高水位就是其 Leader 副本的高水位** 。 ## 高水位更新机制 @@ -49,7 +49,7 @@ 在这张图中,我们可以看到,Broker 0 上保存了某分区的 Leader 副本和所有 Follower 副本的 LEO 值,而 Broker 1 上仅仅保存了该分区的某个 Follower 副本。Kafka 把 Broker 0 上保存的这些 Follower 副本又称为 **远程副本** (Remote Replica)。Kafka 副本机制在运行过程中,会更新 Broker 1 上 Follower 副本的高水位和 LEO 值,同时也会更新 Broker 0 上 Leader 副本的高水位和 LEO 以及所有远程副本的 LEO,但它不会更新远程副本的高水位值,也就是我在图中标记为灰色的部分。 -为什么要在 Broker 0 上保存这些远程副本呢?其实,它们的主要作用是, **帮助 Leader 副本确定其高水位,也就是分区高水位** 。 +为什么要在 Broker 0 上保存这些远程副本呢?其实,它们的主要作用是,**帮助 Leader 副本确定其高水位,也就是分区高水位** 。 为了帮助你更好地记忆这些值被更新的时机,我做了一张表格。只有搞清楚了更新机制,我们才能开始讨论 Kafka 副本机制的原理,以及它是如何使用高水位来执行副本消息同步的。 @@ -62,7 +62,7 @@ 乍一看,这两个条件好像是一回事,因为目前某个副本能否进入 ISR 就是靠第 2 个条件判断的。但有些时候,会发生这样的情况:即 Follower 副本已经“追上”了 Leader 的进度,却不在 ISR 中,比如某个刚刚重启回来的副本。如果 Kafka 只判断第 1 个条件的话,就可能出现某些副本具备了“进入 ISR”的资格,但却尚未进入到 ISR 中的情况。此时,分区高水位值就可能超过 ISR 中副本 LEO,而高水位 > LEO 的情形是不被允许的。 -下面,我们分别从 Leader 副本和 Follower 副本两个维度,来总结一下高水位和 LEO 的更新机制。 **Leader 副本** 处理生产者请求的逻辑如下: +下面,我们分别从 Leader 副本和 Follower 副本两个维度,来总结一下高水位和 LEO 的更新机制。**Leader 副本** 处理生产者请求的逻辑如下: 1. 写入消息到本地磁盘。 1. 更新分区高水位值。 i. 获取 Leader 副本所在 Broker 端保存的所有远程副本 LEO 值{LEO-1,LEO-2,……,LEO-n}。 ii. 获取 Leader 副本高水位值:currentHW。 iii. 更新 currentHW = min(currentHW, LEO-1,LEO-2,……,LEO-n)。 @@ -73,7 +73,7 @@ 1. 使用 Follower 副本发送请求中的位移值更新远程副本 LEO 值。 -1. 更新分区高水位值(具体步骤与处理生产者请求的步骤相同)。 **Follower 副本** 从 Leader 拉取消息的处理逻辑如下: +1. 更新分区高水位值(具体步骤与处理生产者请求的步骤相同)。**Follower 副本** 从 Leader 拉取消息的处理逻辑如下: 1. 写入消息到本地磁盘。 @@ -101,7 +101,7 @@ Follower 再次尝试从 Leader 拉取消息。和之前不同的是,这次有 ![img](assets/f65911a5c247ad83826788fd275e1ade.png) -这时,Follower 副本也成功地更新 LEO 为 1。此时,Leader 和 Follower 副本的 LEO 都是 1,但各自的高水位依然是 0,还没有被更新。 **它们需要在下一轮的拉取中被更新** ,如下图所示: +这时,Follower 副本也成功地更新 LEO 为 1。此时,Leader 和 Follower 副本的 LEO 都是 1,但各自的高水位依然是 0,还没有被更新。**它们需要在下一轮的拉取中被更新**,如下图所示: ![img](assets/f30a4651605352db542b76b3512df110.png) diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" index 1ca7fc74f..f5bbd5b6d 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" @@ -6,7 +6,7 @@ 所谓的日常管理,无非就是主题的增删改查。你可能会觉得,这有什么好讨论的,官网上不都有命令吗?这部分内容的确比较简单,但它是我们讨论后面内容的基础。而且,在讨论的过程中,我还会向你分享一些小技巧。另外,我们今天讨论的管理手段都是借助于 Kafka 自带的命令。事实上,在专栏后面,我们还会专门讨论如何使用 Java API 的方式来运维 Kafka 集群。 -我们先来学习一下如何使用命令创建 Kafka 主题。 **Kafka 提供了自带的 kafka-topics 脚本,用于帮助用户创建主题** 。该脚本文件位于 Kafka 安装目录的 bin 子目录下。如果你是在 Windows 上使用 Kafka,那么该脚本位于 bin 路径的 windows 子目录下。一个典型的创建命令如下: +我们先来学习一下如何使用命令创建 Kafka 主题。**Kafka 提供了自带的 kafka-topics 脚本,用于帮助用户创建主题** 。该脚本文件位于 Kafka 安装目录的 bin 子目录下。如果你是在 Windows 上使用 Kafka,那么该脚本位于 bin 路径的 windows 子目录下。一个典型的创建命令如下: ```plaintext bin/kafka-topics.sh --bootstrap-server broker_host:port --create --topic my_topic_name --partitions 1 --replication-factor 1 @@ -35,13 +35,13 @@ bin/kafka-topics.sh --bootstrap-server broker_host:port --describe --topic --partitions < 新分区数 > ``` -这里要注意的是,你指定的分区数一定要比原有分区数大,否则 Kafka 会抛出 InvalidPartitionsException 异常。 **2. 修改主题级别参数** 。 +这里要注意的是,你指定的分区数一定要比原有分区数大,否则 Kafka 会抛出 InvalidPartitionsException 异常。**2. 修改主题级别参数** 。 在主题创建之后,我们可以使用 kafka-configs 脚本修改对应的参数。 @@ -51,7 +51,7 @@ bin/kafka-topics.sh --bootstrap-server broker_host:port --alter --topic --alter --add-config max.message.bytes=10485760 ``` -也许你会觉得奇怪,为什么这个脚本就要指定 --zookeeper,而不是 --bootstrap-server 呢?其实,这个脚本也能指定 --bootstrap-server 参数,只是它是用来设置动态参数的。在专栏后面,我会详细介绍什么是动态参数,以及动态参数都有哪些。现在,你只需要了解设置常规的主题级别参数,还是使用 --zookeeper。 **3. 变更副本数。** 使用自带的 kafka-reassign-partitions 脚本,帮助我们增加主题的副本数。这里先留个悬念,稍后我会拿 Kafka 内部主题 \_\_consumer_offsets 来演示如何增加主题副本数。 **4. 修改主题限速。** +也许你会觉得奇怪,为什么这个脚本就要指定 --zookeeper,而不是 --bootstrap-server 呢?其实,这个脚本也能指定 --bootstrap-server 参数,只是它是用来设置动态参数的。在专栏后面,我会详细介绍什么是动态参数,以及动态参数都有哪些。现在,你只需要了解设置常规的主题级别参数,还是使用 --zookeeper。**3. 变更副本数。** 使用自带的 kafka-reassign-partitions 脚本,帮助我们增加主题的副本数。这里先留个悬念,稍后我会拿 Kafka 内部主题 \_\_consumer_offsets 来演示如何增加主题副本数。**4. 修改主题限速。** 这里主要是指设置 Leader 副本和 Follower 副本使用的带宽。有时候,我们想要让某个主题的副本在执行副本同步机制时,不要消耗过多的带宽。Kafka 提供了这样的功能。我来举个例子。假设我有个主题,名为 test,我想让该主题各个分区的 Leader 副本和 Follower 副本在处理副本同步时,不得占用超过 100MBps 的带宽。注意是大写 B,即每秒不超过 100MB。那么,我们应该怎么设置呢? @@ -126,7 +126,7 @@ bin/kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic __consu ## 常见主题错误处理 -最后,我们来说说与主题相关的常见错误,以及相应的处理方法。 **常见错误 1:主题删除失败。** 当运行完上面的删除命令后,很多人发现已删除主题的分区数据依然“躺在”硬盘上,没有被清除。这时该怎么办呢? **实际上,造成主题删除失败的原因有很多,最常见的原因有两个:副本所在的 Broker 宕机了;待删除主题的部分分区依然在执行迁移过程。** 如果是因为前者,通常你重启对应的 Broker 之后,删除操作就能自动恢复;如果是因为后者,那就麻烦了,很可能两个操作会相互干扰。 +最后,我们来说说与主题相关的常见错误,以及相应的处理方法。**常见错误 1:主题删除失败。** 当运行完上面的删除命令后,很多人发现已删除主题的分区数据依然“躺在”硬盘上,没有被清除。这时该怎么办呢?**实际上,造成主题删除失败的原因有很多,最常见的原因有两个:副本所在的 Broker 宕机了;待删除主题的部分分区依然在执行迁移过程。** 如果是因为前者,通常你重启对应的 Broker 之后,删除操作就能自动恢复;如果是因为后者,那就麻烦了,很可能两个操作会相互干扰。 不管什么原因,一旦你碰到主题无法删除的问题,可以采用这样的方法: @@ -136,7 +136,7 @@ bin/kafka-console-consumer.sh --bootstrap-server kafka_host:port --topic __consu 第 3 步,在 ZooKeeper 中执行 rmr /controller,触发 Controller 重选举,刷新 Controller 缓存。 -在执行最后一步时,你一定要谨慎,因为它可能造成大面积的分区 Leader 重选举。事实上,仅仅执行前两步也是可以的,只是 Controller 缓存中没有清空待删除主题罢了,也不影响使用。 **常见错误 2:__consumer_offsets 占用太多的磁盘。** 一旦你发现这个主题消耗了过多的磁盘空间,那么,你一定要显式地用 **jstack 命令** 查看一下 kafka-log-cleaner-thread 前缀的线程状态。通常情况下,这都是因为该线程挂掉了,无法及时清理此内部主题。倘若真是这个原因导致的,那我们就只能重启相应的 Broker 了。另外,请你注意保留出错日志,因为这通常都是 Bug 导致的,最好提交到社区看一下。 +在执行最后一步时,你一定要谨慎,因为它可能造成大面积的分区 Leader 重选举。事实上,仅仅执行前两步也是可以的,只是 Controller 缓存中没有清空待删除主题罢了,也不影响使用。**常见错误 2:__consumer_offsets 占用太多的磁盘。** 一旦你发现这个主题消耗了过多的磁盘空间,那么,你一定要显式地用 **jstack 命令** 查看一下 kafka-log-cleaner-thread 前缀的线程状态。通常情况下,这都是因为该线程挂掉了,无法及时清理此内部主题。倘若真是这个原因导致的,那我们就只能重启相应的 Broker 了。另外,请你注意保留出错日志,因为这通常都是 Bug 导致的,最好提交到社区看一下。 ## 小结 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" index 9f0f7248f..5e877cf01 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" @@ -56,7 +56,7 @@ users 和 clients 则是用于动态调整客户端配额(Quota)的 znode 如果我们再把静态参数加进来一起讨论的话,cluster-wide、per-broker 和 static 参数的优先级是这样的:per-broker 参数 > cluster-wide 参数 > static 参数 > Kafka 默认值。 -另外,如果你仔细查看上图中的 **ephemeralOwner 字段** ,你会发现它们的值都是 0x0。这表示这些 znode 都是持久化节点,它们将一直存在。即使 ZooKeeper 集群重启,这些数据也不会丢失,这样就能保证这些动态参数的值会一直生效。 +另外,如果你仔细查看上图中的 **ephemeralOwner 字段**,你会发现它们的值都是 0x0。这表示这些 znode 都是持久化节点,它们将一直存在。即使 ZooKeeper 集群重启,这些数据也不会丢失,这样就能保证这些动态参数的值会一直生效。 ## 如何配置? @@ -69,7 +69,7 @@ $ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers Completed updating default config for brokers in the cluster, ``` -总体来说命令很简单,但有一点需要注意。 **如果要设置 cluster-wide 范围的动态参数,需要显式指定 entity-default** 。现在,我们使用下面的命令来查看一下刚才的配置是否成功。 +总体来说命令很简单,但有一点需要注意。**如果要设置 cluster-wide 范围的动态参数,需要显式指定 entity-default** 。现在,我们使用下面的命令来查看一下刚才的配置是否成功。 ```plaintext $ bin/kafka-configs.sh --bootstrap-server kafka-host:port --entity-type brokers --entity-default --describe @@ -129,7 +129,7 @@ Configs for broker 1 are: ![img](assets/eb99853bca3d91ad3a53db5dbe3e3b67.png) -看到有这么多动态 Broker 参数,你可能会问:这些我都需要调整吗?你能告诉我最常用的几个吗?根据我的实际使用经验,我来跟你分享一些有较大几率被动态调整值的参数。 **1.log.retention.ms。** 修改日志留存时间应该算是一个比较高频的操作,毕竟,我们不可能完美地预估所有业务的消息留存时长。虽然该参数有对应的主题级别参数可以设置,但拥有在全局层面上动态变更的能力,依然是一个很好的功能亮点。 **2.num.io.threads 和 num.network.threads。** 这是我们在前面提到的两组线程池。就我个人而言,我觉得这是动态 Broker 参数最实用的场景了。毕竟,在实际生产环境中,Broker 端请求处理能力经常要按需扩容。如果没有动态 Broker 参数,我们是无法做到这一点的。 **3. 与 SSL 相关的参数。** 主要是 4 个参数(ssl.keystore.type、ssl.keystore.location、ssl.keystore.password 和 ssl.key.password)。允许动态实时调整它们之后,我们就能创建那些过期时间很短的 SSL 证书。每当我们调整时,Kafka 底层会重新配置 Socket 连接通道并更新 Keystore。新的连接会使用新的 Keystore,阶段性地调整这组参数,有利于增加安全性。 **4.num.replica.fetchers。** +看到有这么多动态 Broker 参数,你可能会问:这些我都需要调整吗?你能告诉我最常用的几个吗?根据我的实际使用经验,我来跟你分享一些有较大几率被动态调整值的参数。**1.log.retention.ms。** 修改日志留存时间应该算是一个比较高频的操作,毕竟,我们不可能完美地预估所有业务的消息留存时长。虽然该参数有对应的主题级别参数可以设置,但拥有在全局层面上动态变更的能力,依然是一个很好的功能亮点。**2.num.io.threads 和 num.network.threads。** 这是我们在前面提到的两组线程池。就我个人而言,我觉得这是动态 Broker 参数最实用的场景了。毕竟,在实际生产环境中,Broker 端请求处理能力经常要按需扩容。如果没有动态 Broker 参数,我们是无法做到这一点的。**3. 与 SSL 相关的参数。** 主要是 4 个参数(ssl.keystore.type、ssl.keystore.location、ssl.keystore.password 和 ssl.key.password)。允许动态实时调整它们之后,我们就能创建那些过期时间很短的 SSL 证书。每当我们调整时,Kafka 底层会重新配置 Socket 连接通道并更新 Keystore。新的连接会使用新的 Keystore,阶段性地调整这组参数,有利于增加安全性。**4.num.replica.fetchers。** 这也是我认为的最实用的动态 Broker 参数之一。Follower 副本拉取速度慢,在线上 Kafka 环境中一直是一个老大难的问题。针对这个问题,常见的做法是增加该参数值,确保有充足的线程可以执行 Follower 副本向 Leader 副本的拉取。现在有了动态参数,你不需要再重启 Broker,就能立即在 Follower 端生效,因此我说这是很实用的应用场景。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" index 685bf49f3..361dd41b4 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" @@ -23,13 +23,13 @@ ![img](assets/eb469122e5af2c9f6baebb173b56bed5.jpeg) -Earliest 策略表示将位移调整到主题当前最早位移处。这个最早位移不一定就是 0,因为在生产环境中,很久远的消息会被 Kafka 自动删除,所以当前最早位移很可能是一个大于 0 的值。 **如果你想要重新消费主题的所有消息,那么可以使用 Earliest 策略** 。 +Earliest 策略表示将位移调整到主题当前最早位移处。这个最早位移不一定就是 0,因为在生产环境中,很久远的消息会被 Kafka 自动删除,所以当前最早位移很可能是一个大于 0 的值。**如果你想要重新消费主题的所有消息,那么可以使用 Earliest 策略** 。 -Latest 策略表示把位移重设成最新末端位移。如果你总共向某个主题发送了 15 条消息,那么最新末端位移就是 15。 **如果你想跳过所有历史消息,打算从最新的消息处开始消费的话,可以使用 Latest 策略。** Current 策略表示将位移调整成消费者当前提交的最新位移。有时候你可能会碰到这样的场景:你修改了消费者程序代码,并重启了消费者,结果发现代码有问题,你需要回滚之前的代码变更,同时也要把位移重设到消费者重启时的位置,那么,Current 策略就可以帮你实现这个功能。 +Latest 策略表示把位移重设成最新末端位移。如果你总共向某个主题发送了 15 条消息,那么最新末端位移就是 15。**如果你想跳过所有历史消息,打算从最新的消息处开始消费的话,可以使用 Latest 策略。** Current 策略表示将位移调整成消费者当前提交的最新位移。有时候你可能会碰到这样的场景:你修改了消费者程序代码,并重启了消费者,结果发现代码有问题,你需要回滚之前的代码变更,同时也要把位移重设到消费者重启时的位置,那么,Current 策略就可以帮你实现这个功能。 -表中第 4 行的 Specified-Offset 策略则是比较通用的策略,表示消费者把位移值调整到你指定的位移处。 **这个策略的典型使用场景是,消费者程序在处理某条错误消息时,你可以手动地“跳过”此消息的处理** 。在实际使用过程中,可能会出现 corrupted 消息无法被消费的情形,此时消费者程序会抛出异常,无法继续工作。一旦碰到这个问题,你就可以尝试使用 Specified-Offset 策略来规避。 +表中第 4 行的 Specified-Offset 策略则是比较通用的策略,表示消费者把位移值调整到你指定的位移处。**这个策略的典型使用场景是,消费者程序在处理某条错误消息时,你可以手动地“跳过”此消息的处理** 。在实际使用过程中,可能会出现 corrupted 消息无法被消费的情形,此时消费者程序会抛出异常,无法继续工作。一旦碰到这个问题,你就可以尝试使用 Specified-Offset 策略来规避。 -如果说 Specified-Offset 策略要求你指定位移的 **绝对数值** 的话,那么 Shift-By-N 策略指定的就是位移的 **相对数值** ,即你给出要跳过的一段消息的距离即可。这里的“跳”是双向的,你既可以向前“跳”,也可以向后“跳”。比如,你想把位移重设成当前位移的前 100 条位移处,此时你需要指定 N 为 -100。 +如果说 Specified-Offset 策略要求你指定位移的 **绝对数值** 的话,那么 Shift-By-N 策略指定的就是位移的 **相对数值**,即你给出要跳过的一段消息的距离即可。这里的“跳”是双向的,你既可以向前“跳”,也可以向后“跳”。比如,你想把位移重设成当前位移的前 100 条位移处,此时你需要指定 N 为 -100。 刚刚讲到的这几种策略都是位移维度的,下面我们来聊聊从时间维度重设位移的 DateTime 和 Duration 策略。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" index 66beda2a3..2e5384d55 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25431\350\256\262.md" @@ -20,7 +20,7 @@ Kafka 默认提供了很多个命令行脚本,用于实现各种各样的功 接下来是 kafka-acls 脚本。它是用于设置 Kafka 权限的,比如设置哪些用户可以访问 Kafka 的哪些主题之类的权限。在专栏后面,我会专门来讲 Kafka 安全设置的内容,到时候我们再细聊这个脚本。 -下面是 kafka-broker-api-versions 脚本。 **这个脚本的主要目的是验证不同 Kafka 版本之间服务器和客户端的适配性** 。我来举个例子,下面这两张图分别展示了 2.2 版本 Server 端与 2.2 版本 Client 端和 1.1.1 版本 Client 端的适配性。 +下面是 kafka-broker-api-versions 脚本。**这个脚本的主要目的是验证不同 Kafka 版本之间服务器和客户端的适配性** 。我来举个例子,下面这两张图分别展示了 2.2 版本 Server 端与 2.2 版本 Client 端和 1.1.1 版本 Client 端的适配性。 ![img](assets/594776ae03ca045b3203f873b75c3139.png) @@ -34,7 +34,7 @@ Kafka 默认提供了很多个命令行脚本,用于实现各种各样的功 请注意这两张图中红线部分的差异。在第一张图中,我们使用 2.2 版本的脚本连接 2.2 版本的 Broker,usable 自然是 7,表示能使用最新版本。在第二张图中,我们使用 1.1 版本的脚本连接 2.2 版本的 Broker,usable 是 5,这表示 1.1 版本的客户端 API 只能发送版本号是 5 的 Produce 请求。 -如果你想了解你的客户端版本与服务器端版本的兼容性,那么最好使用这个命令来检验一下。值得注意的是, **在 0.10.2.0 之前,Kafka 是单向兼容的,即高版本的 Broker 能够处理低版本 Client 发送的请求,反过来则不行。自 0.10.2.0 版本开始,Kafka 正式支持双向兼容,也就是说,低版本的 Broker 也能处理高版本 Client 的请求了** 。 +如果你想了解你的客户端版本与服务器端版本的兼容性,那么最好使用这个命令来检验一下。值得注意的是,**在 0.10.2.0 之前,Kafka 是单向兼容的,即高版本的 Broker 能够处理低版本 Client 发送的请求,反过来则不行。自 0.10.2.0 版本开始,Kafka 正式支持双向兼容,也就是说,低版本的 Broker 也能处理高版本 Client 的请求了** 。 接下来是 kafka-configs 脚本。对于这个脚本,我想你应该已经很熟悉了,我们在讨论参数配置和动态 Broker 参数时都提到过它的用法,这里我就不再赘述了。 @@ -116,7 +116,7 @@ $ bin/kafka-producer-perf-test.sh --topic test-topic --num-records 10000000 --th 上述命令向指定主题发送了 1 千万条消息,每条消息大小是 1KB。该命令允许你在 producer-props 后面指定要设置的生产者参数,比如本例中的压缩算法、延时时间等。 -该命令的输出值得好好说一下。它会打印出测试生产者的吞吐量 (MB/s)、消息发送延时以及各种分位数下的延时。一般情况下,消息延时不是一个简单的数字,而是一组分布。或者说, **我们应该关心延时的概率分布情况,仅仅知道一个平均值是没有意义的** 。这就是这里计算分位数的原因。通常我们关注到 **99th 分位** 就可以了。比如在上面的输出中,99th 值是 604ms,这表明测试生产者生产的消息中,有 99% 消息的延时都在 604ms 以内。你完全可以把这个数据当作这个生产者对外承诺的 SLA。 +该命令的输出值得好好说一下。它会打印出测试生产者的吞吐量 (MB/s)、消息发送延时以及各种分位数下的延时。一般情况下,消息延时不是一个简单的数字,而是一组分布。或者说,**我们应该关心延时的概率分布情况,仅仅知道一个平均值是没有意义的** 。这就是这里计算分位数的原因。通常我们关注到 **99th 分位** 就可以了。比如在上面的输出中,99th 值是 604ms,这表明测试生产者生产的消息中,有 99% 消息的延时都在 604ms 以内。你完全可以把这个数据当作这个生产者对外承诺的 SLA。 ### 测试消费者性能 @@ -160,7 +160,7 @@ baseOffset: 15 lastOffset: 29 count: 15 baseSequence: -1 lastSequence: -1 produc 如果只是指定 --files,那么该命令显示的是消息批次(RecordBatch)或消息集合(MessageSet)的元数据信息,比如创建时间、使用的压缩算法、CRC 校验值等。 -**如果我们想深入看一下每条具体的消息,那么就需要显式指定 --deep-iteration 参数** ,如下所示: +**如果我们想深入看一下每条具体的消息,那么就需要显式指定 --deep-iteration 参数**,如下所示: ```plaintext bin/kafka-dump-log.sh --files ../data_dir/kafka_1/test-topic-1/00000000000000000000.log --deep-iteration @@ -186,7 +186,7 @@ baseOffset: 15 lastOffset: 29 count: 15 baseSequence: -1 lastSequence: -1 produc ...... ``` -在上面的输出中,以竖线开头的就是消息批次下的消息信息。如果你还想看消息里面的实际数据,那么还需要指定 **--print-data-log 参数** ,如下所示: +在上面的输出中,以竖线开头的就是消息批次下的消息信息。如果你还想看消息里面的实际数据,那么还需要指定 **--print-data-log 参数**,如下所示: ```plaintext bin/kafka-dump-log.sh --files ../data_dir/kafka_1/test-topic-1/00000000000000000000.log --deep-iteration --print-data-log @@ -194,7 +194,7 @@ baseOffset: 15 lastOffset: 29 count: 15 baseSequence: -1 lastSequence: -1 produc ### 查询消费者组位移 -接下来,我们来看如何使用 kafka-consumer-groups 脚本查看消费者组位移。在上一讲讨论重设消费者组位移的时候,我们使用的也是这个命令。当时我们用的是 **--reset-offsets 参数** ,今天我们使用的是 **--describe 参数** 。假设我们要查询 Group ID 是 test-group 的消费者的位移,那么命令如图所示: +接下来,我们来看如何使用 kafka-consumer-groups 脚本查看消费者组位移。在上一讲讨论重设消费者组位移的时候,我们使用的也是这个命令。当时我们用的是 **--reset-offsets 参数**,今天我们使用的是 **--describe 参数** 。假设我们要查询 Group ID 是 test-group 的消费者的位移,那么命令如图所示: ![img](assets/f4b7d92cdebff84998506afece1f61ee.png) diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" index 110776617..b16a685e8 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25432\350\256\262.md" @@ -14,7 +14,7 @@ 基于这些原因,社区于 0.11 版本正式推出了 Java 客户端版的 AdminClient,并不断地在后续的版本中对它进行完善。我粗略地计算了一下,有关 AdminClient 的优化和更新的各种提案,社区中有十几个之多,而且贯穿各个大的版本,足见社区对 AdminClient 的重视。 -值得注意的是, **服务器端也有一个 AdminClient** ,包路径是 kafka.admin。这是之前的老运维工具类,提供的功能也比较有限,社区已经不再推荐使用它了。所以,我们最好统一使用客户端的 AdminClient。 +值得注意的是,**服务器端也有一个 AdminClient**,包路径是 kafka.admin。这是之前的老运维工具类,提供的功能也比较有限,社区已经不再推荐使用它了。所以,我们最好统一使用客户端的 AdminClient。 ## 如何使用? @@ -52,7 +52,7 @@ compile group: 'org.apache.kafka', name: 'kafka-clients', version: '2.3.0' ## 工作原理 -在详细介绍 AdminClient 的主要功能之前,我们先简单了解一下 AdminClient 的工作原理。 **从设计上来看,AdminClient 是一个双线程的设计:前端主线程和后端 I/O 线程** 。前端线程负责将用户要执行的操作转换成对应的请求,然后再将请求发送到后端 I/O 线程的队列中;而后端 I/O 线程从队列中读取相应的请求,然后发送到对应的 Broker 节点上,之后把执行结果保存起来,以便等待前端线程的获取。 +在详细介绍 AdminClient 的主要功能之前,我们先简单了解一下 AdminClient 的工作原理。**从设计上来看,AdminClient 是一个双线程的设计:前端主线程和后端 I/O 线程** 。前端线程负责将用户要执行的操作转换成对应的请求,然后再将请求发送到后端 I/O 线程的队列中;而后端 I/O 线程从队列中读取相应的请求,然后发送到对应的 Broker 节点上,之后把执行结果保存起来,以便等待前端线程的获取。 值得一提的是,AdminClient 在内部大量使用生产者 - 消费者模式将请求生成与处理解耦。我在下面这张图中大致描述了它的工作原理。 @@ -63,13 +63,13 @@ compile group: 'org.apache.kafka', name: 'kafka-clients', version: '2.3.0' 1. **构建对应的请求对象** 。比如,如果要创建主题,那么就创建 CreateTopicsRequest;如果是查询消费者组位移,就创建 OffsetFetchRequest。 2. **指定响应的回调逻辑** 。比如从 Broker 端接收到 CreateTopicsResponse 之后要执行的动作。一旦创建好 Call 实例,前端主线程会将其放入到新请求队列(New Call Queue)中,此时,前端主线程的任务就算完成了。它只需要等待结果返回即可。 -剩下的所有事情就都是后端 I/O 线程的工作了。就像图中所展示的那样,该线程使用了 3 个队列来承载不同时期的请求对象,它们分别是新请求队列、待发送请求队列和处理中请求队列。为什么要使用 3 个呢?原因是目前新请求队列的线程安全是由 Java 的 monitor 锁来保证的。 **为了确保前端主线程不会因为 monitor 锁被阻塞,后端 I/O 线程会定期地将新请求队列中的所有 Call 实例全部搬移到待发送请求队列中进行处理** 。图中的待发送请求队列和处理中请求队列只由后端 I/O 线程处理,因此无需任何锁机制来保证线程安全。 +剩下的所有事情就都是后端 I/O 线程的工作了。就像图中所展示的那样,该线程使用了 3 个队列来承载不同时期的请求对象,它们分别是新请求队列、待发送请求队列和处理中请求队列。为什么要使用 3 个呢?原因是目前新请求队列的线程安全是由 Java 的 monitor 锁来保证的。**为了确保前端主线程不会因为 monitor 锁被阻塞,后端 I/O 线程会定期地将新请求队列中的所有 Call 实例全部搬移到待发送请求队列中进行处理** 。图中的待发送请求队列和处理中请求队列只由后端 I/O 线程处理,因此无需任何锁机制来保证线程安全。 当 I/O 线程在处理某个请求时,它会显式地将该请求保存在处理中请求队列。一旦处理完成,I/O 线程会自动地调用 Call 对象中的回调逻辑完成最后的处理。把这些都做完之后,I/O 线程会通知前端主线程说结果已经准备完毕,这样前端主线程能够及时获取到执行操作的结果。AdminClient 是使用 Java Object 对象的 wait 和 notify 实现的这种通知机制。 严格来说,AdminClient 并没有使用 Java 已有的队列去实现上面的请求队列,它是使用 ArrayList 和 HashMap 这样的简单容器类,再配以 monitor 锁来保证线程安全的。不过,鉴于它们充当的角色就是请求队列这样的主体,我还是坚持使用队列来指代它们了。 -了解 AdminClient 工作原理的一个好处在于, **它能够帮助我们有针对性地对调用 AdminClient 的程序进行调试** 。 +了解 AdminClient 工作原理的一个好处在于,**它能够帮助我们有针对性地对调用 AdminClient 的程序进行调试** 。 我们刚刚提到的后端 I/O 线程其实是有名字的,名字的前缀是 kafka-admin-client-thread。有时候我们会发现,AdminClient 程序貌似在正常工作,但执行的操作没有返回结果,或者 hang 住了,现在你应该知道这可能是因为 I/O 线程出现问题导致的。如果你碰到了类似的问题,不妨使用 **jstack 命令** 去查看一下你的 AdminClient 程序,确认下 I/O 线程是否在正常工作。 @@ -77,7 +77,7 @@ compile group: 'org.apache.kafka', name: 'kafka-clients', version: '2.3.0' ## 构造和销毁 AdminClient 实例 -如果你正确地引入了 kafka-clients 依赖,那么你应该可以在编写 Java 程序时看到 AdminClient 对象。 **切记它的完整类路径是 org.apache.kafka.clients.admin.AdminClient,而不是 kafka.admin.AdminClient** 。后者就是我们刚才说的服务器端的 AdminClient,它已经不被推荐使用了。 +如果你正确地引入了 kafka-clients 依赖,那么你应该可以在编写 Java 程序时看到 AdminClient 对象。**切记它的完整类路径是 org.apache.kafka.clients.admin.AdminClient,而不是 kafka.admin.AdminClient** 。后者就是我们刚才说的服务器端的 AdminClient,它已经不被推荐使用了。 创建 AdminClient 实例和创建 KafkaProducer 或 KafkaConsumer 实例的方法是类似的,你需要手动构造一个 Properties 对象或 Map 对象,然后传给对应的方法。社区专门为 AdminClient 提供了几十个专属参数,最常见而且必须要指定的参数,是我们熟知的 **bootstrap.servers 参数** 。如果你想了解完整的参数列表,可以去[官网](https://kafka.apache.org/documentation/#adminclientconfigs)查询一下。如果要销毁 AdminClient 实例,需要显式调用 AdminClient 的 close 方法。 @@ -131,9 +131,9 @@ try (AdminClient client = AdminClient.create(props)) { } ``` -和创建主题的风格一样, **我们调用 AdminClient 的 listConsumerGroupOffsets 方法去获取指定消费者组的位移数据** 。 +和创建主题的风格一样,**我们调用 AdminClient 的 listConsumerGroupOffsets 方法去获取指定消费者组的位移数据** 。 -不过,对于这次返回的结果,我们不能再丢弃不管了, **因为它返回的 Map 对象中保存着按照分区分组的位移数据** 。你可以调用 OffsetAndMetadata 对象的 offset() 方法拿到实际的位移数据。 +不过,对于这次返回的结果,我们不能再丢弃不管了,**因为它返回的 Map 对象中保存着按照分区分组的位移数据** 。你可以调用 OffsetAndMetadata 对象的 offset() 方法拿到实际的位移数据。 ### 获取 Broker 磁盘占用 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" index 18bea50a6..1ba546f3a 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" @@ -20,7 +20,7 @@ 对了,你可能会说,SSL 不是已经过时了吗?现在都叫 TLS(Transport Layer Security)了吧?但是,Kafka 的源码中依然是使用 SSL 而不是 TLS 来表示这类东西的。不过,今天出现的所有 SSL 字眼,你都可以认为它们是和 TLS 等价的。 -Kafka 还支持通过 SASL 做客户端认证。 **SASL 是提供认证和数据安全服务的框架** 。Kafka 支持的 SASL 机制有 5 种,它们分别是在不同版本中被引入的,你需要根据你自己使用的 Kafka 版本,来选择该版本所支持的认证机制。 +Kafka 还支持通过 SASL 做客户端认证。**SASL 是提供认证和数据安全服务的框架** 。Kafka 支持的 SASL 机制有 5 种,它们分别是在不同版本中被引入的,你需要根据你自己使用的 Kafka 版本,来选择该版本所支持的认证机制。 1. GSSAPI:也就是 Kerberos 使用的安全接口,是在 0.9 版本中被引入的。 1. PLAIN:是使用简单的用户名 / 密码认证的机制,在 0.10 版本中被引入。 @@ -36,13 +36,13 @@ Kafka 为我们提供了这么多种认证机制,在实际使用过程中, SASL 下又细分了很多种认证机制,我们应该如何选择呢? -SASL/GSSAPI 主要是给 Kerberos 使用的。如果你的公司已经做了 Kerberos 认证(比如使用 Active Directory),那么使用 GSSAPI 是最方便的了。因为你不需要额外地搭建 Kerberos,只要让你们的 Kerberos 管理员给每个 Broker 和要访问 Kafka 集群的操作系统用户申请 principal 就好了。总之, **GSSAPI 适用于本身已经做了 Kerberos 认证的场景,这样的话,SASL/GSSAPI 可以实现无缝集成** 。 +SASL/GSSAPI 主要是给 Kerberos 使用的。如果你的公司已经做了 Kerberos 认证(比如使用 Active Directory),那么使用 GSSAPI 是最方便的了。因为你不需要额外地搭建 Kerberos,只要让你们的 Kerberos 管理员给每个 Broker 和要访问 Kafka 集群的操作系统用户申请 principal 就好了。总之,**GSSAPI 适用于本身已经做了 Kerberos 认证的场景,这样的话,SASL/GSSAPI 可以实现无缝集成** 。 -而 SASL/PLAIN,就像前面说到的,它是一个简单的用户名 / 密码认证机制,通常与 SSL 加密搭配使用。注意,这里的 PLAIN 和 PLAINTEXT 是两回事。 **PLAIN 在这里是一种认证机制,而 PLAINTEXT 说的是未使用 SSL 时的明文传输** 。对于一些小公司而言,搭建公司级的 Kerberos 可能并没有什么必要,他们的用户系统也不复杂,特别是访问 Kafka 集群的用户可能不是很多。对于 SASL/PLAIN 而言,这就是一个非常合适的应用场景。 **总体来说,SASL/PLAIN 的配置和运维成本相对较小,适合于小型公司中的 Kafka 集群** 。 +而 SASL/PLAIN,就像前面说到的,它是一个简单的用户名 / 密码认证机制,通常与 SSL 加密搭配使用。注意,这里的 PLAIN 和 PLAINTEXT 是两回事。**PLAIN 在这里是一种认证机制,而 PLAINTEXT 说的是未使用 SSL 时的明文传输** 。对于一些小公司而言,搭建公司级的 Kerberos 可能并没有什么必要,他们的用户系统也不复杂,特别是访问 Kafka 集群的用户可能不是很多。对于 SASL/PLAIN 而言,这就是一个非常合适的应用场景。**总体来说,SASL/PLAIN 的配置和运维成本相对较小,适合于小型公司中的 Kafka 集群** 。 但是,SASL/PLAIN 有这样一个弊端:它不能动态地增减认证用户,你必须重启 Kafka 集群才能令变更生效。为什么呢?这是因为所有认证用户信息全部保存在静态文件中,所以只能重启 Broker,才能重新加载变更后的静态文件。 -我们知道,重启集群在很多场景下都是令人不爽的,即使是轮替式升级(Rolling Upgrade)。SASL/SCRAM 就解决了这样的问题。它通过将认证用户信息保存在 ZooKeeper 的方式,避免了动态修改需要重启 Broker 的弊端。在实际使用过程中,你可以使用 Kafka 提供的命令动态地创建和删除用户,无需重启整个集群。因此, **如果你打算使用 SASL/PLAIN,不妨改用 SASL/SCRAM 试试。不过要注意的是,后者是 0.10.2 版本引入的。你至少要升级到这个版本后才能使用** 。 +我们知道,重启集群在很多场景下都是令人不爽的,即使是轮替式升级(Rolling Upgrade)。SASL/SCRAM 就解决了这样的问题。它通过将认证用户信息保存在 ZooKeeper 的方式,避免了动态修改需要重启 Broker 的弊端。在实际使用过程中,你可以使用 Kafka 提供的命令动态地创建和删除用户,无需重启整个集群。因此,**如果你打算使用 SASL/PLAIN,不妨改用 SASL/SCRAM 试试。不过要注意的是,后者是 0.10.2 版本引入的。你至少要升级到这个版本后才能使用** 。 SASL/OAUTHBEARER 是 2.0 版本引入的新认证机制,主要是为了实现与 OAuth 2 框架的集成。OAuth 是一个开发标准,允许用户授权第三方应用访问该用户在某网站上的资源,而无需将用户名和密码提供给第三方应用。Kafka 不提倡单纯使用 OAUTHBEARER,因为它生成的不安全的 JSON Web Token,必须配以 SSL 加密才能用在生产环境中。当然,鉴于它是 2.0 版本才推出来的,而且目前没有太多的实际使用案例,我们可以先观望一段时间,再酌情将其应用于生产环境中。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25434\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25434\350\256\262.md" index 692424594..465067844 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25434\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25434\350\256\262.md" @@ -210,7 +210,7 @@ bin/kafka-console-producer.sh --broker-list localhost:9093 --topic test --produc 好了,现在我们来说说 ACL 的配置。 -如果你在运营一个云上的 Kafka 集群,那么势必会面临多租户的问题。 **除了设置合理的认证机制外,为每个连接 Kafka 集群的客户端授予恰当的权限,也是非常关键的** 。现在我来给出一些最佳实践。 +如果你在运营一个云上的 Kafka 集群,那么势必会面临多租户的问题。**除了设置合理的认证机制外,为每个连接 Kafka 集群的客户端授予恰当的权限,也是非常关键的** 。现在我来给出一些最佳实践。 第一,就像前面说的,要开启 ACL,你需要设置 authorizer.class.name=kafka.security.auth.SimpleAclAuthorizer。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" index 9cdd6a361..2f1e5ba03 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25435\350\256\262.md" @@ -6,7 +6,7 @@ 如果要实现这些需求,除了部署多套 Kafka 集群之外,你还需要某种工具或框架,来帮助你实现数据在集群间的拷贝或镜像。 -值得注意的是, **通常我们把数据在单个集群下不同节点之间的拷贝称为备份,而把数据在集群间的拷贝称为镜像** (Mirroring)。 +值得注意的是,**通常我们把数据在单个集群下不同节点之间的拷贝称为备份,而把数据在集群间的拷贝称为镜像** (Mirroring)。 今天,我来重点介绍一下 Apache Kafka 社区提供的 MirrorMaker 工具,它可以帮我们实现消息或数据从一个集群到另一个集群的拷贝。 @@ -32,8 +32,8 @@ bin/kafka-mirror-maker.sh --consumer.config ./config/consumer.properties --produ 现在我来解释一下这条命令中各个参数的含义。 -- consumer.config 参数。它指定了 MirrorMaker 中消费者的配置文件地址,最主要的配置项是 **bootstrap.servers** ,也就是该 MirrorMaker 从哪个 Kafka 集群读取消息。因为 MirrorMaker 有可能在内部创建多个消费者实例并使用消费者组机制,因此你还需要设置 group.id 参数。另外,我建议你额外配置 auto.offset.reset=earliest,否则的话,MirrorMaker 只会拷贝那些在它启动之后到达源集群的消息。 -- producer.config 参数。它指定了 MirrorMaker 内部生产者组件的配置文件地址。通常来说,Kafka Java Producer 很友好,你不需要配置太多参数。唯一的例外依然是 **bootstrap.servers** ,你必须显式地指定这个参数,配置拷贝的消息要发送到的目标集群。 +- consumer.config 参数。它指定了 MirrorMaker 中消费者的配置文件地址,最主要的配置项是 **bootstrap.servers**,也就是该 MirrorMaker 从哪个 Kafka 集群读取消息。因为 MirrorMaker 有可能在内部创建多个消费者实例并使用消费者组机制,因此你还需要设置 group.id 参数。另外,我建议你额外配置 auto.offset.reset=earliest,否则的话,MirrorMaker 只会拷贝那些在它启动之后到达源集群的消息。 +- producer.config 参数。它指定了 MirrorMaker 内部生产者组件的配置文件地址。通常来说,Kafka Java Producer 很友好,你不需要配置太多参数。唯一的例外依然是 **bootstrap.servers**,你必须显式地指定这个参数,配置拷贝的消息要发送到的目标集群。 - num.streams 参数。我个人觉得,这个参数的名字很容易给人造成误解。第一次看到这个参数名的时候,我一度以为 MirrorMaker 是用 Kafka Streams 组件实现的呢。其实并不是。这个参数就是告诉 MirrorMaker 要创建多少个 KafkaConsumer 实例。当然,它使用的是多线程的方案,即在后台创建并启动多个线程,每个线程维护专属的消费者实例。在实际使用时,你可以根据你的机器性能酌情设置多个线程。 - whitelist 参数。如命令所示,这个参数接收一个正则表达式。所有匹配该正则表达式的主题都会被自动地执行镜像。在这个命令中,我指定了“.\*”,这表明我要同步源集群上的所有主题。 @@ -73,7 +73,7 @@ WARNING: The default partition assignment strategy of the mirror maker will chan 请你一定要仔细阅读这个命令输出中的警告信息。这个警告的意思是,在未来版本中,MirrorMaker 内部消费者会使用轮询策略(Round-robin)来为消费者实例分配分区,现阶段使用的默认策略依然是基于范围的分区策略(Range)。Range 策略的思想很朴素,它是将所有分区根据一定的顺序排列在一起,每个消费者依次顺序拿走各个分区。 -Round-robin 策略的推出时间要比 Range 策略晚。通常情况下,我们可以认为,社区推出的比较晚的分区分配策略会比之前的策略好。这里的好指的是能实现更均匀的分配效果。该警告信息的最后一部分内容提示我们, **如果我们想提前“享用”轮询策略,需要手动地在 consumer.properties 文件中增加 partition.assignment.strategy 的设置** 。 +Round-robin 策略的推出时间要比 Range 策略晚。通常情况下,我们可以认为,社区推出的比较晚的分区分配策略会比之前的策略好。这里的好指的是能实现更均匀的分配效果。该警告信息的最后一部分内容提示我们,**如果我们想提前“享用”轮询策略,需要手动地在 consumer.properties 文件中增加 partition.assignment.strategy 的设置** 。 ### 第 3 步:验证消息是否拷贝成功 @@ -92,7 +92,7 @@ test:0:5000000 讲到这里,你一定会觉得很奇怪吧,我们明明在源集群创建了一个 4 分区的主题,为什么到了目标集群,就变成单分区了呢? -原因很简单。 **MirrorMaker 在执行消息镜像的过程中,如果发现要同步的主题在目标集群上不存在的话,它就会根据 Broker 端参数 num.partitions 和 default.replication.factor 的默认值,自动将主题创建出来** 。在这个例子中,我们在目标集群上没有创建过任何主题,因此,在镜像开始时,MirrorMaker 自动创建了一个名为 test 的单分区单副本的主题。 +原因很简单。**MirrorMaker 在执行消息镜像的过程中,如果发现要同步的主题在目标集群上不存在的话,它就会根据 Broker 端参数 num.partitions 和 default.replication.factor 的默认值,自动将主题创建出来** 。在这个例子中,我们在目标集群上没有创建过任何主题,因此,在镜像开始时,MirrorMaker 自动创建了一个名为 test 的单分区单副本的主题。 **在实际使用场景中,我推荐你提前把要同步的所有主题按照源集群上的规格在目标集群上等价地创建出来** 。否则,极有可能出现刚刚的这种情况,这会导致一些很严重的问题。比如原本在某个分区的消息同步到了目标集群以后,却位于其他的分区中。如果你的消息处理逻辑依赖于这样的分区映射,就必然会出现问题。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" index 69fbf2700..d53c0f380 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25436\350\256\262.md" @@ -8,7 +8,7 @@ ## 主机监控 -主机级别的监控,往往是揭示线上问题的第一步。 **所谓主机监控,指的是监控 Kafka 集群 Broker 所在的节点机器的性能** 。通常来说,一台主机上运行着各种各样的应用进程,这些进程共同使用主机上的所有硬件资源,比如 CPU、内存或磁盘等。 +主机级别的监控,往往是揭示线上问题的第一步。**所谓主机监控,指的是监控 Kafka 集群 Broker 所在的节点机器的性能** 。通常来说,一台主机上运行着各种各样的应用进程,这些进程共同使用主机上的所有硬件资源,比如 CPU、内存或磁盘等。 常见的主机监控指标包括但不限于以下几种: @@ -55,13 +55,13 @@ 这个 Broker JVM 进程默认使用了 G1 的 GC 算法,当 cleanup 步骤结束后,堆上活跃对象大小从 827MB 缩减成 645MB。另外,你可以根据前面的时间戳来计算每次 GC 的间隔和频率。 -自 0.9.0.0 版本起,社区将默认的 GC 收集器设置为 G1,而 G1 中的 Full GC 是由单线程执行的,速度非常慢。因此, **你一定要监控你的 Broker GC 日志,即以 kafkaServer-gc.log 开头的文件** 。注意不要出现 Full GC 的字样。一旦你发现 Broker 进程频繁 Full GC,可以开启 G1 的 -XX:+PrintAdaptiveSizePolicy 开关,让 JVM 告诉你到底是谁引发了 Full GC。 +自 0.9.0.0 版本起,社区将默认的 GC 收集器设置为 G1,而 G1 中的 Full GC 是由单线程执行的,速度非常慢。因此,**你一定要监控你的 Broker GC 日志,即以 kafkaServer-gc.log 开头的文件** 。注意不要出现 Full GC 的字样。一旦你发现 Broker 进程频繁 Full GC,可以开启 G1 的 -XX:+PrintAdaptiveSizePolicy 开关,让 JVM 告诉你到底是谁引发了 Full GC。 ## 集群监控 说完了主机和 JVM 监控,现在我来给出监控 Kafka 集群的几个方法。 -**1. 查看 Broker 进程是否启动,端口是否建立。** 千万不要小看这一点。在很多容器化的 Kafka 环境中,比如使用 Docker 启动 Kafka Broker 时,容器虽然成功启动了,但是里面的网络设置如果配置有误,就可能会出现进程已经启动但端口未成功建立监听的情形。因此,你一定要同时检查这两点,确保服务正常运行。 **2. 查看 Broker 端关键日志。** 这里的关键日志,主要涉及 Broker 端服务器日志 server.log,控制器日志 controller.log 以及主题分区状态变更日志 state-change.log。其中,server.log 是最重要的,你最好时刻对它保持关注。很多 Broker 端的严重错误都会在这个文件中被展示出来。因此,如果你的 Kafka 集群出现了故障,你要第一时间去查看对应的 server.log,寻找和定位故障原因。 **3. 查看 Broker 端关键线程的运行状态。** +**1. 查看 Broker 进程是否启动,端口是否建立。** 千万不要小看这一点。在很多容器化的 Kafka 环境中,比如使用 Docker 启动 Kafka Broker 时,容器虽然成功启动了,但是里面的网络设置如果配置有误,就可能会出现进程已经启动但端口未成功建立监听的情形。因此,你一定要同时检查这两点,确保服务正常运行。**2. 查看 Broker 端关键日志。** 这里的关键日志,主要涉及 Broker 端服务器日志 server.log,控制器日志 controller.log 以及主题分区状态变更日志 state-change.log。其中,server.log 是最重要的,你最好时刻对它保持关注。很多 Broker 端的严重错误都会在这个文件中被展示出来。因此,如果你的 Kafka 集群出现了故障,你要第一时间去查看对应的 server.log,寻找和定位故障原因。**3. 查看 Broker 端关键线程的运行状态。** 这些关键线程的意外挂掉,往往无声无息,但是却影响巨大。比方说,Broker 后台有个专属的线程执行 Log Compaction 操作,由于源代码的 Bug,这个线程有时会无缘无故地“死掉”,社区中很多 Jira 都曾报出过这个问题。当这个线程挂掉之后,作为用户的你不会得到任何通知,Kafka 集群依然会正常运转,只是所有的 Compaction 操作都不能继续了,这会导致 Kafka 内部的位移主题所占用的磁盘空间越来越大。因此,我们有必要对这些关键线程的状态进行监控。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25437\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25437\350\256\262.md" index 4fff4d77e..57aa4b0af 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25437\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25437\350\256\262.md" @@ -86,7 +86,7 @@ bin/kafka-manager -Dconfig.file=conf/application.conf -Dhttp.port=8080 除了丰富的监控功能之外,Kafka Manager 还提供了很多运维管理操作,比如执行主题的创建、Preferred Leader 选举等。在生产环境中,这可能是一把双刃剑,毕竟这意味着每个访问 Kafka Manager 的人都能执行这些运维操作。这显然是不能被允许的。因此,很多 Kafka Manager 用户都有这样一个诉求:把 Kafka Manager 变成一个纯监控框架,关闭非必要的管理功能。 -庆幸的是,Kafka Manager 提供了这样的功能。 **你可以修改 config 下的 application.conf 文件,删除 application.features 中的值** 。比如,如果我想禁掉 Preferred Leader 选举功能,那么我就可以删除对应 KMPreferredReplicaElectionFeature 项。删除完之后,我们重启 Kafka Manager,再次进入到主界面,我们就可以发现之前的 Preferred Leader Election 菜单项已经没有了。 +庆幸的是,Kafka Manager 提供了这样的功能。**你可以修改 config 下的 application.conf 文件,删除 application.features 中的值** 。比如,如果我想禁掉 Preferred Leader 选举功能,那么我就可以删除对应 KMPreferredReplicaElectionFeature 项。删除完之后,我们重启 Kafka Manager,再次进入到主界面,我们就可以发现之前的 Preferred Leader Election 菜单项已经没有了。 ![img](assets/16b5ac5eeb4f32f872265ec91d130401.png) @@ -94,7 +94,7 @@ bin/kafka-manager -Dconfig.file=conf/application.conf -Dhttp.port=8080 ## Burrow -我要介绍的第二个 Kafka 开源监控框架是 Burrow。 **Burrow 是 LinkedIn 开源的一个专门监控消费者进度的框架** 。事实上,当初其开源时,我对它还是挺期待的。毕竟是 LinkedIn 公司开源的一个框架,而 LinkedIn 公司又是 Kafka 创建并发展壮大的地方。Burrow 应该是有机会成长为很好的 Kafka 监控框架的。 +我要介绍的第二个 Kafka 开源监控框架是 Burrow。**Burrow 是 LinkedIn 开源的一个专门监控消费者进度的框架** 。事实上,当初其开源时,我对它还是挺期待的。毕竟是 LinkedIn 公司开源的一个框架,而 LinkedIn 公司又是 Kafka 创建并发展壮大的地方。Burrow 应该是有机会成长为很好的 Kafka 监控框架的。 然而令人遗憾的是,它后劲不足,发展非常缓慢,目前已经有几个月没有更新了。而且这个框架是用 Go 写的,安装时要求必须有 Go 运行环境,所以,Burrow 在普及率上不如其他框架。另外,Burrow 没有 UI 界面,只是开放了一些 HTTP Endpoint,这对于“想偷懒”的运维来说,更是一个减分项。 @@ -117,7 +117,7 @@ $GOPATH/bin/Burrow --config-dir /path/containing/config ## JMXTrans + InfluxDB + Grafana -除了刚刚说到的专属开源 Kafka 监控框架之外,其实现在更流行的做法是, **在一套通用的监控框架中监控 Kafka** ,比如使用 **JMXTrans + InfluxDB + Grafana 的组合** 。由于 Grafana 支持对 **JMX 指标** 的监控,因此很容易将 Kafka 各种 JMX 指标集成进来。 +除了刚刚说到的专属开源 Kafka 监控框架之外,其实现在更流行的做法是,**在一套通用的监控框架中监控 Kafka**,比如使用 **JMXTrans + InfluxDB + Grafana 的组合** 。由于 Grafana 支持对 **JMX 指标** 的监控,因此很容易将 Kafka 各种 JMX 指标集成进来。 我们来看一张生产环境中的监控截图。图中集中了很多监控指标,比如 CPU 使用率、GC 收集数据、内存使用情况等。除此之外,这个仪表盘面板还囊括了很多关键的 Kafka JMX 指标,比如 BytesIn、BytesOut 和每秒消息数等。将这么多数据统一集成进一个面板上直观地呈现出来,是这套框架非常鲜明的特点。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25438\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25438\350\256\262.md" index a63eda5e9..8e923bf72 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25438\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25438\350\256\262.md" @@ -20,7 +20,7 @@ ![img](assets/94486dc0eb55b68855478ef7e5709359.png) -**第 1 层:应用程序层** 。它是指优化 Kafka 客户端应用程序代码。比如,使用合理的数据结构、缓存计算开销大的运算结果,抑或是复用构造成本高的对象实例等。这一层的优化效果最为明显,通常也是比较简单的。 **第 2 层:框架层** 。它指的是合理设置 Kafka 集群的各种参数。毕竟,直接修改 Kafka 源码进行调优并不容易,但根据实际场景恰当地配置关键参数的值,还是很容易实现的。 **第 3 层:JVM 层** 。Kafka Broker 进程是普通的 JVM 进程,各种对 JVM 的优化在这里也是适用的。优化这一层的效果虽然比不上前两层,但有时也能带来巨大的改善效果。 **第 4 层:操作系统层** 。对操作系统层的优化很重要,但效果往往不如想象得那么好。与应用程序层的优化效果相比,它是有很大差距的。 +**第 1 层:应用程序层** 。它是指优化 Kafka 客户端应用程序代码。比如,使用合理的数据结构、缓存计算开销大的运算结果,抑或是复用构造成本高的对象实例等。这一层的优化效果最为明显,通常也是比较简单的。**第 2 层:框架层** 。它指的是合理设置 Kafka 集群的各种参数。毕竟,直接修改 Kafka 源码进行调优并不容易,但根据实际场景恰当地配置关键参数的值,还是很容易实现的。**第 3 层:JVM 层** 。Kafka Broker 进程是普通的 JVM 进程,各种对 JVM 的优化在这里也是适用的。优化这一层的效果虽然比不上前两层,但有时也能带来巨大的改善效果。**第 4 层:操作系统层** 。对操作系统层的优化很重要,但效果往往不如想象得那么好。与应用程序层的优化效果相比,它是有很大差距的。 ## 基础性调优 @@ -48,15 +48,15 @@ 在很多公司的实际环境中,这个大小已经被证明是非常合适的,你可以安心使用。如果你想精确调整的话,我建议你可以查看 GC log,特别是关注 Full GC 之后堆上存活对象的总大小,然后把堆大小设置为该值的 1.5~2 倍。如果你发现 Full GC 没有被执行过,手动运行 jmap -histo:live \< pid > 就能人为触发 Full GC。 -2.GC 收集器的选择。 **我强烈建议你使用 G1 收集器,主要原因是方便省事,至少比 CMS 收集器的优化难度小得多** 。另外,你一定要尽力避免 Full GC 的出现。其实,不论使用哪种收集器,都要竭力避免 Full GC。在 G1 中,Full GC 是单线程运行的,它真的非常慢。如果你的 Kafka 环境中经常出现 Full GC,你可以配置 JVM 参数 -XX:+PrintAdaptiveSizePolicy,来探查一下到底是谁导致的 Full GC。 +2.GC 收集器的选择。**我强烈建议你使用 G1 收集器,主要原因是方便省事,至少比 CMS 收集器的优化难度小得多** 。另外,你一定要尽力避免 Full GC 的出现。其实,不论使用哪种收集器,都要竭力避免 Full GC。在 G1 中,Full GC 是单线程运行的,它真的非常慢。如果你的 Kafka 环境中经常出现 Full GC,你可以配置 JVM 参数 -XX:+PrintAdaptiveSizePolicy,来探查一下到底是谁导致的 Full GC。 -使用 G1 还很容易碰到的一个问题,就是 **大对象(Large Object)** ,反映在 GC 上的错误,就是“too many humongous allocations”。所谓的大对象,一般是指至少占用半个区域(Region)大小的对象。举个例子,如果你的区域尺寸是 2MB,那么超过 1MB 大小的对象就被视为是大对象。要解决这个问题,除了增加堆大小之外,你还可以适当地增加区域大小,设置方法是增加 JVM 启动参数 -XX:+G1HeapRegionSize=N。默认情况下,如果一个对象超过了 N/2,就会被视为大对象,从而直接被分配在大对象区。如果你的 Kafka 环境中的消息体都特别大,就很容易出现这种大对象分配的问题。 +使用 G1 还很容易碰到的一个问题,就是 **大对象(Large Object)**,反映在 GC 上的错误,就是“too many humongous allocations”。所谓的大对象,一般是指至少占用半个区域(Region)大小的对象。举个例子,如果你的区域尺寸是 2MB,那么超过 1MB 大小的对象就被视为是大对象。要解决这个问题,除了增加堆大小之外,你还可以适当地增加区域大小,设置方法是增加 JVM 启动参数 -XX:+G1HeapRegionSize=N。默认情况下,如果一个对象超过了 N/2,就会被视为大对象,从而直接被分配在大对象区。如果你的 Kafka 环境中的消息体都特别大,就很容易出现这种大对象分配的问题。 ### Broker 端调优 我们继续沿着漏斗往上走,来看看 Broker 端的调优。 -Broker 端调优很重要的一个方面,就是合理地设置 Broker 端参数值,以匹配你的生产环境。不过,后面我们在讨论具体的调优目标时再详细说这部分内容。这里我想先讨论另一个优化手段, **即尽力保持客户端版本和 Broker 端版本一致** 。不要小看版本间的不一致问题,它会令 Kafka 丧失很多性能收益,比如 Zero Copy。下面我用一张图来说明一下。 +Broker 端调优很重要的一个方面,就是合理地设置 Broker 端参数值,以匹配你的生产环境。不过,后面我们在讨论具体的调优目标时再详细说这部分内容。这里我想先讨论另一个优化手段,**即尽力保持客户端版本和 Broker 端版本一致** 。不要小看版本间的不一致问题,它会令 Kafka 丧失很多性能收益,比如 Zero Copy。下面我用一张图来说明一下。 ![img](assets/5310d7d29235b080c872e0a9eb396e6e.png) @@ -90,11 +90,11 @@ Broker 端调优很重要的一个方面,就是合理地设置 Broker 端参 我稍微解释一下表格中的内容。 -Broker 端参数 num.replica.fetchers 表示的是 Follower 副本用多少个线程来拉取消息,默认使用 1 个线程。如果你的 Broker 端 CPU 资源很充足,不妨适当调大该参数值,加快 Follower 副本的同步速度。因为在实际生产环境中, **配置了 acks=all 的 Producer 程序吞吐量被拖累的首要因素,就是副本同步性能** 。增加这个值后,你通常可以看到 Producer 端程序的吞吐量增加。 +Broker 端参数 num.replica.fetchers 表示的是 Follower 副本用多少个线程来拉取消息,默认使用 1 个线程。如果你的 Broker 端 CPU 资源很充足,不妨适当调大该参数值,加快 Follower 副本的同步速度。因为在实际生产环境中,**配置了 acks=all 的 Producer 程序吞吐量被拖累的首要因素,就是副本同步性能** 。增加这个值后,你通常可以看到 Producer 端程序的吞吐量增加。 -另外需要注意的,就是避免经常性的 Full GC。目前不论是 CMS 收集器还是 G1 收集器,其 Full GC 采用的是 Stop The World 的单线程收集策略,非常慢,因此一定要避免。 **在 Producer 端,如果要改善吞吐量,通常的标配是增加消息批次的大小以及批次缓存时间,即 batch.size 和 linger.ms** 。目前它们的默认值都偏小,特别是默认的 16KB 的消息批次大小一般都不适用于生产环境。假设你的消息体大小是 1KB,默认一个消息批次也就大约 16 条消息,显然太小了。我们还是希望 Producer 能一次性发送更多的消息。 +另外需要注意的,就是避免经常性的 Full GC。目前不论是 CMS 收集器还是 G1 收集器,其 Full GC 采用的是 Stop The World 的单线程收集策略,非常慢,因此一定要避免。**在 Producer 端,如果要改善吞吐量,通常的标配是增加消息批次的大小以及批次缓存时间,即 batch.size 和 linger.ms** 。目前它们的默认值都偏小,特别是默认的 16KB 的消息批次大小一般都不适用于生产环境。假设你的消息体大小是 1KB,默认一个消息批次也就大约 16 条消息,显然太小了。我们还是希望 Producer 能一次性发送更多的消息。 -除了这两个,你最好把压缩算法也配置上,以减少网络 I/O 传输量,从而间接提升吞吐量。当前,和 Kafka 适配最好的两个压缩算法是 **LZ4 和 zstd** ,不妨一试。 +除了这两个,你最好把压缩算法也配置上,以减少网络 I/O 传输量,从而间接提升吞吐量。当前,和 Kafka 适配最好的两个压缩算法是 **LZ4 和 zstd**,不妨一试。 同时,由于我们的优化目标是吞吐量,最好不要设置 acks=all 以及开启重试。前者引入的副本同步时间通常都是吞吐量的瓶颈,而后者在执行过程中也会拉低 Producer 应用的吞吐量。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" index fee750201..fde8321bb 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25439\350\256\262.md" @@ -24,7 +24,7 @@ ## Kafka Connect 组件 -我们先利用 Kafka Connect 组件 **收集数据** 。如前所述,Kafka Connect 组件负责连通 Kafka 与外部数据系统。连接外部数据源的组件叫连接器(Connector)。 **常见的外部数据源包括数据库、KV 存储、搜索系统或文件系统等。** 今天我们使用文件连接器(File Connector)实时读取 Nginx 的 access 日志。假设 access 日志的格式如下: +我们先利用 Kafka Connect 组件 **收集数据** 。如前所述,Kafka Connect 组件负责连通 Kafka 与外部数据系统。连接外部数据源的组件叫连接器(Connector)。**常见的外部数据源包括数据库、KV 存储、搜索系统或文件系统等。** 今天我们使用文件连接器(File Connector)实时读取 Nginx 的 access 日志。假设 access 日志的格式如下: ```plaintext 10.10.13.41 - - [13/Aug/2019:03:46:54 +0800] "GET /v1/open/product_list?user_key= **** &user_phone= **** &screen_height=1125&screen_width=2436&from_page=1&user_type=2&os_type=ios HTTP/1.0" 200 1321 @@ -46,7 +46,7 @@ rest.host.name=localhost rest.port=8083 ``` -第 1 项是指定 **要连接的 Kafka 集群** ,第 2 项和第 3 项分别指定 Connect 组件开放的 REST 服务的 **主机名和端口** 。保存这些变更之后,我们运行下面的命令启动 Connect。 +第 1 项是指定 **要连接的 Kafka 集群**,第 2 项和第 3 项分别指定 Connect 组件开放的 REST 服务的 **主机名和端口** 。保存这些变更之后,我们运行下面的命令启动 Connect。 ```plaintext cd kafka_2.12-2.3.0 @@ -195,13 +195,13 @@ bootstrap.servers 参数你应该已经很熟悉了,我就不多讲了。这 代码中的 mapValues 调用将接收到的 JSON 串转换成 LogLine 对象,之后再次调用 mapValues 方法,提取出 LogLine 对象中的 payload 字段,这个字段保存了真正的日志数据。这样,经过两次 mapValues 方法调用之后,我们成功地将原始数据转换成了实际的 Nginx 日志行数据。 -值得注意的是,代码使用的是 Kafka Streams 提供的 mapValues 方法。顾名思义, **这个方法就是只对消息体(Value)进行转换,而不变更消息的键(Key)** 。 +值得注意的是,代码使用的是 Kafka Streams 提供的 mapValues 方法。顾名思义,**这个方法就是只对消息体(Value)进行转换,而不变更消息的键(Key)** 。 -其实,Kafka Streams 也提供了 map 方法,允许你同时修改消息 Key。通常来说,我们认为 **mapValues 要比 map 方法更高效** ,因为 Key 的变更可能导致下游处理算子(Operator)的重分区,降低性能。如果可能的话最好尽量使用 mapValues 方法。 +其实,Kafka Streams 也提供了 map 方法,允许你同时修改消息 Key。通常来说,我们认为 **mapValues 要比 map 方法更高效**,因为 Key 的变更可能导致下游处理算子(Operator)的重分区,降低性能。如果可能的话最好尽量使用 mapValues 方法。 -拿到真实日志行数据之后,我们调用 groupBy 方法进行统计计数。由于我们要统计双端(ios 端和 android 端)的请求数,因此,我们 groupBy 的 Key 是 ios 或 android。在上面的那段代码中,我仅仅依靠日志行中是否包含特定关键字的方式来确定是哪一端。更正宗的做法应该是, **分析 Nginx 日志格式,提取对应的参数值,也就是 os_type 的值** 。 +拿到真实日志行数据之后,我们调用 groupBy 方法进行统计计数。由于我们要统计双端(ios 端和 android 端)的请求数,因此,我们 groupBy 的 Key 是 ios 或 android。在上面的那段代码中,我仅仅依靠日志行中是否包含特定关键字的方式来确定是哪一端。更正宗的做法应该是,**分析 Nginx 日志格式,提取对应的参数值,也就是 os_type 的值** 。 -做完 groupBy 之后,我们还需要限定要统计的时间窗口范围,即我们统计的双端请求数是在哪个时间窗口内计算的。在这个例子中,我调用了 **windowedBy 方法** ,要求 Kafka Streams 每 2 秒统计一次双端的请求数。设定好了时间窗口之后,下面就是调用 **count 方法** 进行统计计数了。 +做完 groupBy 之后,我们还需要限定要统计的时间窗口范围,即我们统计的双端请求数是在哪个时间窗口内计算的。在这个例子中,我调用了 **windowedBy 方法**,要求 Kafka Streams 每 2 秒统计一次双端的请求数。设定好了时间窗口之后,下面就是调用 **count 方法** 进行统计计数了。 这一切都做完了之后,我们需要调用 **toStream 方法** 将刚才统计出来的表(Table)转换成事件流,这样我们就能实时观测它里面的内容。我会在专栏的最后几讲中解释下流处理领域内的流和表的概念以及它们的区别。这里你只需要知道 toStream 是将一个 Table 变成一个 Stream 即可。 @@ -227,7 +227,7 @@ bootstrap.servers 参数你应该已经很熟悉了,我就不多讲了。这 …… ``` -由于我们统计的结果是某个时间窗口范围内的,因此承载这个统计结果的消息的 Key 封装了该时间窗口信息,具体格式是: **[ios 或 [[email protected]](/cdn-cgi/l/email-protection)开始时间 / 结束时间]** ,而消息的 Value 就是一个简单的数字,表示这个时间窗口内的总请求数。 +由于我们统计的结果是某个时间窗口范围内的,因此承载这个统计结果的消息的 Key 封装了该时间窗口信息,具体格式是: **[ios 或 [[email protected]](/cdn-cgi/l/email-protection)开始时间 / 结束时间]**,而消息的 Value 就是一个简单的数字,表示这个时间窗口内的总请求数。 如果把上面 ios 相邻输出行中的开始时间相减,我们就会发现,它们的确是每 2 秒输出一次,每次输出会同时计算出 ios 端和 android 端的总请求数。接下来,你可以订阅这个 Kafka 主题,将结果实时导出到你期望的其他数据存储上。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25440\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25440\350\256\262.md" index eab5fa539..5630d11e7 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25440\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25440\350\256\262.md" @@ -22,7 +22,7 @@ 因此,业界的大神们扬长避短,将两者结合在一起使用。一方面,利用流处理快速地给出不那么精确的结果;另一方面,依托于批处理,最终实现数据一致性。这就是所谓的 **Lambda 架构** 。 -延时低是个很好的特性,但如果计算结果不准确,流处理是无法完全替代批处理的。所谓计算结果准确,在教科书或文献中有个专属的名字,叫正确性(Correctness)。可以这么说, **目前难以实现正确性是流处理取代批处理的最大障碍** ,而实现正确性的基石是精确一次处理语义(Exactly Once Semantics,EOS)。 +延时低是个很好的特性,但如果计算结果不准确,流处理是无法完全替代批处理的。所谓计算结果准确,在教科书或文献中有个专属的名字,叫正确性(Correctness)。可以这么说,**目前难以实现正确性是流处理取代批处理的最大障碍**,而实现正确性的基石是精确一次处理语义(Exactly Once Semantics,EOS)。 这里的精确一次是流处理平台能提供的一类一致性保障。常见的一致性保障有三类: @@ -32,13 +32,13 @@ 注意,我这里说的都是 **对应用状态的影响** 。对于很多有副作用(Side Effect)的操作而言,实现精确一次语义几乎是不可能的。举个例子,假设流处理中的某个步骤是发送邮件操作,当邮件发送出去后,倘若后面出现问题要回滚整个流处理流程,已发送的邮件是没法追回的,这就是所谓的副作用。当你的流处理逻辑中存在包含副作用的操作算子时,该操作算子的执行是无法保证精确一次处理的。因此,我们通常只是保证这类操作对应用状态的影响精确一次罢了。后面我们会重点讨论 Kafka Streams 是如何实现 EOS 的。 -我们今天讨论的流处理既包含真正的实时流处理,也包含微批化(Microbatch)的流处理。 **所谓的微批化,其实就是重复地执行批处理引擎来实现对无限数据集的处理** 。典型的微批化实现平台就是 **Spark Streaming** 。 +我们今天讨论的流处理既包含真正的实时流处理,也包含微批化(Microbatch)的流处理。**所谓的微批化,其实就是重复地执行批处理引擎来实现对无限数据集的处理** 。典型的微批化实现平台就是 **Spark Streaming** 。 ## Kafka Streams 的特色 -相比于其他流处理平台, **Kafka Streams 最大的特色就是它不是一个平台** ,至少它不是一个具备完整功能(Full-Fledged)的平台,比如其他框架中自带的调度器和资源管理器,就是 Kafka Streams 不提供的。 +相比于其他流处理平台,**Kafka Streams 最大的特色就是它不是一个平台**,至少它不是一个具备完整功能(Full-Fledged)的平台,比如其他框架中自带的调度器和资源管理器,就是 Kafka Streams 不提供的。 -Kafka 官网明确定义 Kafka Streams 是一个 **Java 客户端库** (Client Library)。 **你可以使用这个库来构建高伸缩性、高弹性、高容错性的分布式应用以及微服务** 。 +Kafka 官网明确定义 Kafka Streams 是一个 **Java 客户端库** (Client Library)。**你可以使用这个库来构建高伸缩性、高弹性、高容错性的分布式应用以及微服务** 。 使用 Kafka Streams API 构建的应用就是一个普通的 Java 应用程序。你可以选择任何熟悉的技术或框架对其进行编译、打包、部署和上线。 @@ -66,27 +66,27 @@ Java 客户端库的定位既可以说是特色,也可以说是一个缺陷。 ### 上下游数据源 -谈完了部署方式的差异,我们来说说连接上下游数据源方面的差异。简单来说, **Kafka Streams 目前只支持从 Kafka 读数据以及向 Kafka 写数据** 。在没有 Kafka Connect 组件的支持下,Kafka Streams 只能读取 Kafka 集群上的主题数据,在完成流处理逻辑后也只能将结果写回到 Kafka 主题上。 +谈完了部署方式的差异,我们来说说连接上下游数据源方面的差异。简单来说,**Kafka Streams 目前只支持从 Kafka 读数据以及向 Kafka 写数据** 。在没有 Kafka Connect 组件的支持下,Kafka Streams 只能读取 Kafka 集群上的主题数据,在完成流处理逻辑后也只能将结果写回到 Kafka 主题上。 反观 Spark Streaming 和 Flink 这类框架,它们都集成了丰富的上下游数据源连接器(Connector),比如常见的连接器 MySQL、ElasticSearch、HBase、HDFS、Kafka 等。如果使用这些框架,你可以很方便地集成这些外部框架,无需二次开发。 -当然,由于开发 Connector 通常需要同时掌握流处理框架和外部框架,因此在实际使用过程中,Connector 的质量参差不齐,在具体使用的时候,你可以多查查对应的 **jira 官网** ,看看有没有明显的“坑”,然后再决定是否使用。 +当然,由于开发 Connector 通常需要同时掌握流处理框架和外部框架,因此在实际使用过程中,Connector 的质量参差不齐,在具体使用的时候,你可以多查查对应的 **jira 官网**,看看有没有明显的“坑”,然后再决定是否使用。 在这个方面,我是有前车之鉴的。曾经,我使用过一个 Connector,我发现它在读取 Kafka 消息向其他系统写入的时候似乎总是重复消费。费了很多周折之后,我才发现这是一个已知的 Bug,而且早就被记录在 jira 官网上了。因此,我推荐你多逛下 jira,也许能提前避开一些“坑”。 **总之,目前 Kafka Streams 只支持与 Kafka 集群进行交互,它没有提供开箱即用的外部数据源连接器。** ### 协调方式 -在分布式协调方面,Kafka Streams 应用依赖于 Kafka 集群提供的协调功能,来提供 **高容错性和高伸缩性** 。 **Kafka Streams 应用底层使用了消费者组机制来实现任意的流处理扩缩容** 。应用的每个实例或节点,本质上都是相同消费者组下的独立消费者,彼此互不影响。它们之间的协调工作,由 Kafka 集群 Broker 上对应的协调者组件来完成。当有实例增加或退出时,协调者自动感知并重新分配负载。 +在分布式协调方面,Kafka Streams 应用依赖于 Kafka 集群提供的协调功能,来提供 **高容错性和高伸缩性** 。**Kafka Streams 应用底层使用了消费者组机制来实现任意的流处理扩缩容** 。应用的每个实例或节点,本质上都是相同消费者组下的独立消费者,彼此互不影响。它们之间的协调工作,由 Kafka 集群 Broker 上对应的协调者组件来完成。当有实例增加或退出时,协调者自动感知并重新分配负载。 我画了一张图来展示每个 Kafka Streams 实例内部的构造,从这张图中,我们可以看出,每个实例都由一个消费者实例、特定的流处理逻辑,以及一个生产者实例组成,而这些实例中的消费者实例,共同构成了一个消费者组。 ![img](assets/c2ef5ebb19a75f3a16017c0c1f7a9a0b.png) -通过这个机制,Kafka Streams 应用同时实现了 **高伸缩性和高容错性** ,而这一切都是自动提供的,不需要你手动实现。 +通过这个机制,Kafka Streams 应用同时实现了 **高伸缩性和高容错性**,而这一切都是自动提供的,不需要你手动实现。 而像 Flink 这样的框架,它的容错性和扩展性是通过专属的主节点(Master Node)全局来协调控制的。 -Flink 支持通过 ZooKeeper 实现主节点的高可用性,避免单点失效: **某个节点出现故障会自动触发恢复操作** 。 **这种全局性协调模型对于流处理中的作业而言非常实用,但不太适配单独的流处理应用程序** 。原因就在于它不像 Kafka Streams 那样轻量级,应用程序必须要实现特定的 API 来开启检查点机制(checkpointing),同时还需要亲身参与到错误恢复的过程中。 +Flink 支持通过 ZooKeeper 实现主节点的高可用性,避免单点失效: **某个节点出现故障会自动触发恢复操作** 。**这种全局性协调模型对于流处理中的作业而言非常实用,但不太适配单独的流处理应用程序** 。原因就在于它不像 Kafka Streams 那样轻量级,应用程序必须要实现特定的 API 来开启检查点机制(checkpointing),同时还需要亲身参与到错误恢复的过程中。 应该这样说,在不同的场景下,Kafka Streams 和 Flink 这种重量级的协调模型各有优劣。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25441\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25441\350\256\262.md" index bf8c3461b..aa8f1540b 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25441\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25441\350\256\262.md" @@ -56,13 +56,13 @@ movies.filter((title, movie) -> movie.getGenre().equals(" 动作片 ")).xxx()... 流和表的概念在流处理领域非常关键。在 Kafka Streams DSL 中,流用 **KStream** 表示,而表用 **KTable** 表示。 -Kafka Streams 还定义了 **GlobalKTable** 。本质上它和 KTable 都表征了一个表,里面封装了事件变更流,但是它和 KTable 的最大不同在于,当 Streams 应用程序读取 Kafka 主题数据到 GlobalKTable 时,它会读取主题 **所有分区的数据** ,而对 KTable 而言,Streams 程序实例只会读取 **部分分区的数据** ,这主要取决于 Streams 实例的数量。 +Kafka Streams 还定义了 **GlobalKTable** 。本质上它和 KTable 都表征了一个表,里面封装了事件变更流,但是它和 KTable 的最大不同在于,当 Streams 应用程序读取 Kafka 主题数据到 GlobalKTable 时,它会读取主题 **所有分区的数据**,而对 KTable 而言,Streams 程序实例只会读取 **部分分区的数据**,这主要取决于 Streams 实例的数量。 ### 时间 在流处理领域内,精确定义事件时间是非常关键的:一方面,它是决定流处理应用能否实现正确性的前提;另一方面,流处理中时间窗口等操作依赖于时间概念才能正常工作。 -常见的时间概念有两类:事件发生时间(Event Time)和事件处理时间(Processing Time)。理想情况下,我们希望这两个时间相等,即事件一旦发生就马上被处理,但在实际场景中,这是不可能的, **Processing Time 永远滞后于 Event Time** ,而且滞后程度又是一个高度变化,无法预知,就像“Streaming Systems”一书中的这张图片所展示的那样: +常见的时间概念有两类:事件发生时间(Event Time)和事件处理时间(Processing Time)。理想情况下,我们希望这两个时间相等,即事件一旦发生就马上被处理,但在实际场景中,这是不可能的,**Processing Time 永远滞后于 Event Time**,而且滞后程度又是一个高度变化,无法预知,就像“Streaming Systems”一书中的这张图片所展示的那样: ![img](assets/ab8e5206729b484b0a16f2bb9de3a792.png) @@ -162,7 +162,7 @@ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \ ### 常见操作算子 -操作算子的丰富程度和易用性是衡量流处理框架受欢迎程度的重要依据之一。Kafka Streams DSL 提供了很多开箱即用的操作算子,大体上分为两大类: **无状态算子和有状态算子** 。下面我就向你分别介绍几个经常使用的算子。 **在无状态算子中,filter 的出场率是极高的** 。它执行的就是过滤的逻辑。依然拿 WordCount 为例,假设我们只想统计那些以字母 s 开头的单词的个数,我们可以在执行完 flatMapValues 后增加一行代码,代码如下: +操作算子的丰富程度和易用性是衡量流处理框架受欢迎程度的重要依据之一。Kafka Streams DSL 提供了很多开箱即用的操作算子,大体上分为两大类: **无状态算子和有状态算子** 。下面我就向你分别介绍几个经常使用的算子。**在无状态算子中,filter 的出场率是极高的** 。它执行的就是过滤的逻辑。依然拿 WordCount 为例,假设我们只想统计那些以字母 s 开头的单词的个数,我们可以在执行完 flatMapValues 后增加一行代码,代码如下: ```plaintext .filter(((key, value) -> value.startsWith("s"))) diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25442\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25442\350\256\262.md" index d957b24aa..a0b246e21 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25442\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25442\350\256\262.md" @@ -8,7 +8,7 @@ 众所周知,金融领域内的获客成本是相当高的,一线城市高净值白领的获客成本通常可达上千元。面对如此巨大的成本压力,金融企业一方面要降低广告投放的获客成本,另一方面要做好精细化运营,实现客户生命周期内价值(Custom Lifecycle Value, CLV)的最大化。 -**实现价值最大化的一个重要途径就是做好用户洞察,而用户洞察要求你要更深度地了解你的客户** ,即所谓的 Know Your Customer(KYC),真正做到以客户为中心,不断地满足客户需求。 +**实现价值最大化的一个重要途径就是做好用户洞察,而用户洞察要求你要更深度地了解你的客户**,即所谓的 Know Your Customer(KYC),真正做到以客户为中心,不断地满足客户需求。 为了实现 KYC,传统的做法是花费大量的时间与客户见面,做面对面的沟通以了解客户的情况。但是,用这种方式得到的数据往往是不真实的,毕竟客户内心是有潜在的自我保护意识的,短时间内的面对面交流很难真正洞察到客户的真实诉求。 @@ -46,7 +46,7 @@ 消息流中的每个事件或每条消息包含的是一个未知用户的某种信息,它可以是用户在页面的访问记录数据,也可以是用户的购买行为数据。这些消息中可能会包含我们刚才提到的若干种 ID 信息,比如页面访问信息中可能包含设备 ID,也可能包含注册账号,而购买行为信息中可能包含身份证信息和手机号等。 -连接的另一方表保存的是 **用户所有的 ID 信息** ,随着连接的不断深入,表中保存的 ID 品类会越来越丰富,也就是说,流中的数据会被不断地补充进表中,最终实现对用户所有 ID 的打通。 +连接的另一方表保存的是 **用户所有的 ID 信息**,随着连接的不断深入,表中保存的 ID 品类会越来越丰富,也就是说,流中的数据会被不断地补充进表中,最终实现对用户所有 ID 的打通。 ## Kafka Streams 实现 @@ -65,7 +65,7 @@ } ``` -顺便说一下, **Avro 是 Java 或大数据生态圈常用的序列化编码机制** ,比如直接使用 JSON 或 XML 保存对象。Avro 能极大地节省磁盘占用空间或网络 I/O 传输量,因此普遍应用于大数据量下的数据传输。 +顺便说一下,**Avro 是 Java 或大数据生态圈常用的序列化编码机制**,比如直接使用 JSON 或 XML 保存对象。Avro 能极大地节省磁盘占用空间或网络 I/O 传输量,因此普遍应用于大数据量下的数据传输。 在这个场景下,我们需要两个 Kafka 主题,一个用于构造表,另一个用于构建流。这两个主题的消息格式都是上面的 IDMapping 对象。 @@ -165,7 +165,7 @@ public class IDMappingStreams { } ``` -这个 Java 类代码中最重要的方法是 **buildTopology 函数** ,它构造了我们打通 ID Mapping 的所有逻辑。 +这个 Java 类代码中最重要的方法是 **buildTopology 函数**,它构造了我们打通 ID Mapping 的所有逻辑。 在该方法中,我们首先构造了 StreamsBuilder 对象实例,这是构造任何 Kafka Streams 应用的第一步。之后我们读取配置文件,获取了要读写的所有 Kafka 主题名。在这个例子中,我们需要用到 4 个主题,它们的作用如下: diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" index 4f106f671..7ab5765a7 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25443\350\256\262.md" @@ -89,7 +89,7 @@ See https://docs.gradle.org/5.3/userguide/command_line_interface.html#sec:comman 1. **log 包** 。log 包中定义了 Broker 底层消息和索引保存机制以及物理格式,非常值得一读。特别是 Log、LogSegment 和 LogManager 这几个类,几乎定义了 Kafka 底层的消息存储机制,一定要重点关注。 2. **controller 包** 。controller 包实现的是 Kafka Controller 的所有功能,特别是里面的 KafkaController.scala 文件,它封装了 Controller 的所有事件处理逻辑。如果你想弄明白 Controller 的工作原理,最好多读几遍这个将近 2000 行的大文件。 -3. **coordinator 包下的 group 包代码** 。当前,coordinator 包有两个子 package:group 和 transaction。前者封装的是 Consumer Group 所用的 Coordinator;后者封装的是支持 Kafka 事务的 Transaction Coordinator。我个人觉得你最好把 group 包下的代码通读一遍,了解下 Broker 端是如何管理 Consumer Group 的。这里比较重要的是 **GroupMetadataManager 和 GroupCoordinator 类** ,它们定义了 Consumer Group 的元数据信息以及管理这些元数据的状态机机制。 +3. **coordinator 包下的 group 包代码** 。当前,coordinator 包有两个子 package:group 和 transaction。前者封装的是 Consumer Group 所用的 Coordinator;后者封装的是支持 Kafka 事务的 Transaction Coordinator。我个人觉得你最好把 group 包下的代码通读一遍,了解下 Broker 端是如何管理 Consumer Group 的。这里比较重要的是 **GroupMetadataManager 和 GroupCoordinator 类**,它们定义了 Consumer Group 的元数据信息以及管理这些元数据的状态机机制。 4. **network 包代码以及 server 包下的部分代码** 。如果你还有余力的话,可以再读一下这些代码。前者的 SocketServer 实现了 Broker 接收外部请求的完整网络流程。我们在专栏第 24 讲说过,Kafka 用的是 Reactor 模式。如果你想搞清楚 Reactor 模式是怎么在 Kafka“落地”的,就把这个类搞明白吧。 从总体流程上看,Broker 端顶部的入口类是 KafkaApis.scala。这个类是处理所有入站请求的总入口,下图展示了部分请求的处理方法: @@ -113,7 +113,7 @@ See https://docs.gradle.org/5.3/userguide/command_line_interface.html#sec:comman 第 1 个不得不提的当然就是[Kafka 官网](https://kafka.apache.org/documentation/)。很多人会忽视官网,但其实官网才是最重要的学习资料。你只需要通读几遍官网,并切实掌握里面的内容,就已经能够较好地掌握 Kafka 了。 -第 2 个是 Kafka 的\[JIRA 列表\]( %3D KAFKA ORDER BY created DESC)。当你碰到 Kafka 抛出的异常的时候,不妨使用异常的关键字去 JIRA 中搜索一下,看看是否是已知的 Bug。很多时候,我们碰到的问题早就已经被别人发现并提交到社区了。此时, **JIRA 列表就是你排查问题的好帮手** 。 +第 2 个是 Kafka 的\[JIRA 列表\]( %3D KAFKA ORDER BY created DESC)。当你碰到 Kafka 抛出的异常的时候,不妨使用异常的关键字去 JIRA 中搜索一下,看看是否是已知的 Bug。很多时候,我们碰到的问题早就已经被别人发现并提交到社区了。此时,**JIRA 列表就是你排查问题的好帮手** 。 第 3 个是[Kafka KIP 列表](https://cwiki.apache.org/confluence/display/KAFKA/Kafka+Improvement+Proposals)。KIP 的全称是 Kafka Improvement Proposals,即 Kafka 新功能提议。你可以看到 Kafka 的新功能建议及其讨论。如果你想了解 Kafka 未来的发展路线,KIP 是不能不看的。当然,如果你想到了一些 Kafka 暂时没有的新功能,也可以在 KIP 中提交自己的提议申请,等待社区的评审。 diff --git "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25444\350\256\262.md" "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25444\350\256\262.md" index b647886d8..7d68317a8 100644 --- "a/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25444\350\256\262.md" +++ "b/docs/Middleware/Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230/\347\254\25444\350\256\262.md" @@ -8,7 +8,7 @@ 虽然专栏更新结束了,但是我相信我们的 Kafka 学习之旅不会结束。相反,这对于你来说,或许是一个新的开始。 -还记得开篇词里的那句话吧:“ **Stay focused and work hard!** ”我一直觉得,学习任何技术,甚至是搞定任何事情,只要下足了功夫,理论上你可以藐视一切学习方法或捷径。但是,如果你忽视了毅力和坚持,再多的速成教程也无法引领你达到你期望的高度。著名的“10000 小时定律”就明确表示, **10000 个小时的锤炼,是所有人从平凡人变成世界级大师的必要条件** 。 +还记得开篇词里的那句话吧:“ **Stay focused and work hard!** ”我一直觉得,学习任何技术,甚至是搞定任何事情,只要下足了功夫,理论上你可以藐视一切学习方法或捷径。但是,如果你忽视了毅力和坚持,再多的速成教程也无法引领你达到你期望的高度。著名的“10000 小时定律”就明确表示,**10000 个小时的锤炼,是所有人从平凡人变成世界级大师的必要条件** 。 还是那句话,只要你持之以恒地投入时间去学习,你就能成为某个领域的专家。因此,从某种意义上说,我这碗“鸡汤”的配料非常简单,就四个字: **干就完了** 。 @@ -18,19 +18,19 @@ **首先,最重要的就是夯实技术基本功。这是我们 IT 从业者赖以生存的基石。** 这里的基本功包含很多方面,比如 **操作系统** 、 **数据结构** 等,但我更想说的,还是 **对 Java 语言的掌握** 。 -目前,大数据框架多是以 Java 或 JVM 系语言开发而成的,因此, **熟练掌握甚至精通 Java,是学好大数据框架的基石** !所谓精通,不仅仅是要求你熟练使用 Java 进行代码开发,更要求你对 JVM 底层有详细的了解。就这个层面的学习而言,我想给你 3 条建议。 +目前,大数据框架多是以 Java 或 JVM 系语言开发而成的,因此,**熟练掌握甚至精通 Java,是学好大数据框架的基石**!所谓精通,不仅仅是要求你熟练使用 Java 进行代码开发,更要求你对 JVM 底层有详细的了解。就这个层面的学习而言,我想给你 3 条建议。 1. **持续精进自己的 Java 功底** 。比如,你可以去 Java 官网上,把 Java 语言规范和 JVM 规范熟读一遍。很多人都不太重视语言规范文档,但实际上,Java 中关于线程和同步的知识,在 Java 语言规范中都有相关的阐释。 2. **提升自己的 Java 多线程开发以及 I/O 开发能力** 。很多大数据框架底层都大量使用 Java 多线程能力以及 NIO 帮助实现自身功能。就拿 Kafka 来说,多线程自不必说,Kafka 可是大量使用 NIO 实现网络通信的。所以,这部分的知识是你必须要熟练掌握的。 -3. **掌握 JVM 调优和 GC** 。我推荐你去读一读“Java Performance”这本书。虽然目前 GC 收集器大部分演进到了 G1 时代,但书中大部分的调优内容依然是适用的。调优 Kafka 的 JVM,也要依赖这部分知识给予我们指导。 **除此之外,你还要学习分布式系统的设计。** 分布式系统领域内的诸多经典问题,是设计并开发任何一款分布式系统都要面临和解决的问题,比如我们耳熟能详的一致性问题、领导者选举问题、分区备份问题等。这些问题在 Kafka 中都有体现,我们在专栏里面也有所涉及。因此, **分布式系统的诸多基础性概念,是帮助你日后深入掌握大数据分布式框架的重要因素** 。 +3. **掌握 JVM 调优和 GC** 。我推荐你去读一读“Java Performance”这本书。虽然目前 GC 收集器大部分演进到了 G1 时代,但书中大部分的调优内容依然是适用的。调优 Kafka 的 JVM,也要依赖这部分知识给予我们指导。**除此之外,你还要学习分布式系统的设计。** 分布式系统领域内的诸多经典问题,是设计并开发任何一款分布式系统都要面临和解决的问题,比如我们耳熟能详的一致性问题、领导者选举问题、分区备份问题等。这些问题在 Kafka 中都有体现,我们在专栏里面也有所涉及。因此,**分布式系统的诸多基础性概念,是帮助你日后深入掌握大数据分布式框架的重要因素** 。 而且,很多经典的分布式问题在业界早已被研究多年,无论是理论还是实践案例,都有着翔实的记录。比如我们在专栏前面谈到的分区概念,分区在分布式系统设计中早就不是什么新鲜的概念了,早在上世纪六七十年代,就已经有行业专家在研究分区数据库的实现问题了。要较好地掌握大数据框架中的分区或分片,是不可能绕过分布式系统中的分区以及分区机制的。 这些经验都偏重理论的学习。你千万不要小看理论的价值,毕竟,列宁说过:“没有革命的理论,就没有革命的运动。”这里的“运动”就是一种实践。先让理论指导实践,再借助实践补充理论,才是学习任何东西无往而不利的最佳法则。 -强调完理论,自然就要引出实践了。我这里所说的实践不仅仅是对框架的简单使用。你从官网上下载 Kafka,启动它,然后创建一个生产者和一个消费者,跑通端到端的消息发送,这不叫实践,这只是应用罢了。 **真正的实践一定要包含你自己的思考和验证,而且要与真实业务相绑定** 。我不排斥你单纯地学习某个框架,但以我个人的经验而言,在实际工作中进行学习,往往是学得最快、掌握得也最扎实的学习方式。 +强调完理论,自然就要引出实践了。我这里所说的实践不仅仅是对框架的简单使用。你从官网上下载 Kafka,启动它,然后创建一个生产者和一个消费者,跑通端到端的消息发送,这不叫实践,这只是应用罢了。**真正的实践一定要包含你自己的思考和验证,而且要与真实业务相绑定** 。我不排斥你单纯地学习某个框架,但以我个人的经验而言,在实际工作中进行学习,往往是学得最快、掌握得也最扎实的学习方式。 -另外, **在实际学习过程中,你最好记录下遇到问题、解决问题的点点滴滴,并不断积累** 。要知道,很多技术大家之所以成为技术大家,不仅仅是因为理论掌握得很牢固,填过的“坑”多,更重要的是, **他们不重复犯错** 。 +另外,**在实际学习过程中,你最好记录下遇到问题、解决问题的点点滴滴,并不断积累** 。要知道,很多技术大家之所以成为技术大家,不仅仅是因为理论掌握得很牢固,填过的“坑”多,更重要的是,**他们不重复犯错** 。 孔子曾经称赞他的学生颜回“不贰过”。“不贰过”也就是不重复犯错。在我看来,在实践方面,一个不犯相同过错的人,就已经可以被称为大家了。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25401\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25401\350\256\262.md" index be3e86518..f4873ec02 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25401\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25401\350\256\262.md" @@ -22,7 +22,7 @@ ls -l 解压后的文件如下图所示: ![1](assets/20200726210733673.png) -其中 conf 文件夹存放的是 RocketMQ 的配置文件,提供了各种部署结构的示例配置。例如 2m-2s-async 是 2 主 2 从异步复制的配置示例;2m-noslave 是 2 主的示例配置。由于本文主要是搭建一个学习环境,故采取的部署架构为 1 主的部署架构,关于生产环境下如何搭建 RocketMQ 集群、如何调优参数将在该专栏的后续文章中专门介绍。 **Step3:修改 NameServer JVM 参数** +其中 conf 文件夹存放的是 RocketMQ 的配置文件,提供了各种部署结构的示例配置。例如 2m-2s-async 是 2 主 2 从异步复制的配置示例;2m-noslave 是 2 主的示例配置。由于本文主要是搭建一个学习环境,故采取的部署架构为 1 主的部署架构,关于生产环境下如何搭建 RocketMQ 集群、如何调优参数将在该专栏的后续文章中专门介绍。**Step3:修改 NameServer JVM 参数** ```bash cd bin @@ -38,7 +38,7 @@ JAVA_OPT="{JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:Max JAVA_OPT="{JAVA_OPT} -server -Xms512M -Xmx512M -Xmn256M -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" ``` -> 温馨提示:这里修改 JVM 参数主要目的是个人学习电脑内存不够,默认 NameServer 会占用 4G。 **Step4:启动 NameServer** +> 温馨提示:这里修改 JVM 参数主要目的是个人学习电脑内存不够,默认 NameServer 会占用 4G。**Step4:启动 NameServer** ```plaintext > nohup ./mqnamesrv & @@ -106,7 +106,7 @@ sh ./mqadmin clusterList -n 127.0.0.1:9876 #### **安装 RocketMQ-Console** 使用运维命令不太直观,学习成本较大,为此 RocketMQ 官方提供了一个运维管理界面 RokcetMQ-Console,用于对 RocketMQ 集群提供常用的运维功能,故本节主要讲解如何在 Linux 环境安装 RokcetMQ-Console -RocketMQ 官方并未提供 RokcetMQ-Console 的安装包,故需要通过源码进行编译。 **Step1:下载源码** +RocketMQ 官方并未提供 RokcetMQ-Console 的安装包,故需要通过源码进行编译。**Step1:下载源码** ```plaintext wget @@ -162,11 +162,11 @@ nohup java -jar rocketmq-console-ng-1.0.0.jar & ![9](assets/20200726210852167.png) -设置环境变量名称:ROCKETMQ\_HOME,其值用于指定 RocketMQ 运行的主目录,笔者设置的路径为:/home/dingwpmz/tmp/rocketmq。 **Step3:将 distribution/conf/logback_namesrv.xml 文件拷贝到 Step2 中设置的主目录下** 执行后的效果如下图所示: +设置环境变量名称:ROCKETMQ\_HOME,其值用于指定 RocketMQ 运行的主目录,笔者设置的路径为:/home/dingwpmz/tmp/rocketmq。**Step3:将 distribution/conf/logback_namesrv.xml 文件拷贝到 Step2 中设置的主目录下** 执行后的效果如下图所示: ![10](assets/20200726210902103.png) -> 温馨提示:该文件为 NameServer 的日志路劲,可以手动修改 logback\_namesrv.xml 文件中的日志目录,由于这是 Logback 的基础知识,这里就不再详细介绍 Logback 的配置方法。 **Step4:以 Debug 方法运行 NamesrvStartup,执行效果如下图所示,表示启动成功** ![11](assets/20200726210914125.png) **Step5:将 distribution/conf/logback_brokerxml、broker.conf 文件拷贝到 Step2 中设置的主目录下** 执行后的效果如下图所示: +> 温馨提示:该文件为 NameServer 的日志路劲,可以手动修改 logback\_namesrv.xml 文件中的日志目录,由于这是 Logback 的基础知识,这里就不再详细介绍 Logback 的配置方法。**Step4:以 Debug 方法运行 NamesrvStartup,执行效果如下图所示,表示启动成功**![11](assets/20200726210914125.png) **Step5:将 distribution/conf/logback_brokerxml、broker.conf 文件拷贝到 Step2 中设置的主目录下** 执行后的效果如下图所示: ![12](assets/20200726210923668.png) **Step6:修改 broker.conf 中的配置,主要设置 NameServer 的地址、Broker 的名称等相关属性** diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25402\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25402\350\256\262.md" index 44e3e1352..883eb0cba 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25402\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25402\350\256\262.md" @@ -8,7 +8,7 @@ 在 RocketMQ 主要的组件如下。 -**NameServer** NameServer 集群,Topic 的路由注册中心,为客户端根据 Topic 提供路由服务,从而引导客户端向 Broker 发送消息。NameServer 之间的节点不通信。路由信息在 NameServer 集群中数据一致性采取的最终一致性。 **Broker** 消息存储服务器,分为两种角色:Master 与 Slave,上图中呈现的就是 2 主 2 从的部署架构,在 RocketMQ 中,主服务承担读写操作,从服务器作为一个备份,当主服务器存在压力时,从服务器可以承担读服务(消息消费)。所有 Broker,包含 Slave 服务器每隔 30s 会向 NameServer 发送心跳包,心跳包中会包含存在在 Broker 上所有的 Topic 的路由信息。 **Client** +**NameServer** NameServer 集群,Topic 的路由注册中心,为客户端根据 Topic 提供路由服务,从而引导客户端向 Broker 发送消息。NameServer 之间的节点不通信。路由信息在 NameServer 集群中数据一致性采取的最终一致性。**Broker** 消息存储服务器,分为两种角色:Master 与 Slave,上图中呈现的就是 2 主 2 从的部署架构,在 RocketMQ 中,主服务承担读写操作,从服务器作为一个备份,当主服务器存在压力时,从服务器可以承担读服务(消息消费)。所有 Broker,包含 Slave 服务器每隔 30s 会向 NameServer 发送心跳包,心跳包中会包含存在在 Broker 上所有的 Topic 的路由信息。**Client** 消息客户端,包括 Producer(消息发送者)和 Consumer(消费消费者)。客户端在同一时间只会连接一台 NameServer,只有在连接出现异常时才会向尝试连接另外一台。客户端每隔 30s 向 NameServer 发起 Topic 的路由信息查询。 @@ -42,7 +42,7 @@ #### **消费队列负载算法与重平衡机制** 那集群模式下,消费者是如何来分配消息的呢? -例如上面实例中 order_topic 有 16 个队列,那一个拥有 3 个消费者的消费组如何来分配队列中。 **在 MQ 领域有一个不成文的约定:同一个消费者同一时间可以分配多个队列,但一个队列同一时间只会分配给一个消费者。** **RocketMQ 提供了众多的队列负载算法** ,其中最常用的两种平均分配算法。 +例如上面实例中 order_topic 有 16 个队列,那一个拥有 3 个消费者的消费组如何来分配队列中。**在 MQ 领域有一个不成文的约定:同一个消费者同一时间可以分配多个队列,但一个队列同一时间只会分配给一个消费者。** **RocketMQ 提供了众多的队列负载算法**,其中最常用的两种平均分配算法。 - AllocateMessageQueueAveragely:平均分配 - AllocateMessageQueueAveragelyByCircle:轮流平均分配 @@ -84,11 +84,11 @@ AllocateMessageQueueAveragelyByCircle 分配算法的队列负载机制如下: 上述整个过程无需应用程序干预,由 RocketMQ 完成。大概的做法就是将将原先分配给自己但这次不属于的队列进行丢弃,新分配的队列则创建新的拉取任务。 -#### **消费进度** 消费者消费一条消息后需要记录消费的位置,这样在消费端重启的时候,继续从上一次消费的位点开始进行处理新的消息。在 RocketMQ 中,消息消费位点的存储是以消费组为单位的。 **集群模式** 下,消息消费进度存储在 Broker 端,`{ROCKETMQ_HOME}/store/config/consumerOffset.json` 是其具体的存储文件,其中内容截图如下 +#### **消费进度** 消费者消费一条消息后需要记录消费的位置,这样在消费端重启的时候,继续从上一次消费的位点开始进行处理新的消息。在 RocketMQ 中,消息消费位点的存储是以消费组为单位的。**集群模式** 下,消息消费进度存储在 Broker 端,`{ROCKETMQ_HOME}/store/config/consumerOffset.json` 是其具体的存储文件,其中内容截图如下 ![4](assets/20200726212330669.png) -可见消费进度的 Key 为 [\[email protected\]](/cdn-cgi/l/email-protection),然后每一个队列一个偏移量。 **广播模式** 的消费进度文件存储在用户的主目录,默认文件全路劲名:`{USER_HOME}/.rocketmq_offsets`。 +可见消费进度的 Key 为 [\[email protected\]](/cdn-cgi/l/email-protection),然后每一个队列一个偏移量。**广播模式** 的消费进度文件存储在用户的主目录,默认文件全路劲名:`{USER_HOME}/.rocketmq_offsets`。 #### **消费模型** diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25403\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25403\350\256\262.md" index 9c1519223..c1f23db62 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25403\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25403\350\256\262.md" @@ -179,7 +179,7 @@ SendResult send(Collection msgs, long timeout) Message request(Message msg, long timeout) ``` -RocketMQ 在 4.6.0 版本中引入了 request-response 请求模型,就消息发送者发送到 Broker,需要等消费者处理完才返回,该 request 的重载方法与 send 方法一样,在此不再重复介绍。 **ClientConfig** +RocketMQ 在 4.6.0 版本中引入了 request-response 请求模型,就消息发送者发送到 Broker,需要等消费者处理完才返回,该 request 的重载方法与 send 方法一样,在此不再重复介绍。**ClientConfig** 客户端配置相关。这里先简单介绍几个核心参数,后续在实践部分还会重点介绍。 @@ -188,7 +188,7 @@ RocketMQ 在 4.6.0 版本中引入了 request-response 请求模型,就消息 - String instanceName:客户端的实例名称 - String namespace:客户端所属的命名空 -**DefaultMQProducer** 消息发送者默认实现类。 **TransactionMQProducer** 事务消息发送者默认实现类。 +**DefaultMQProducer** 消息发送者默认实现类。**TransactionMQProducer** 事务消息发送者默认实现类。 ### 消息发送 API 简单使用示例 @@ -262,7 +262,7 @@ Namespace,顾名思义,命名空间,为消息发送者、消息消费者 > [全链路压测之上下文环境管理](https://mp.weixin.qq.com/s/a6IGrOtn1mi0r05355L5Ng) -当然消息发送者与消息消费者的命名空间必须一样,才能彼此协作。 **一言以蔽之,Namespace 主要为消息发送者、消息消费者进行分组,底层的逻辑是改变 Topic 的名称。** #### **request-response 响应模型 API** RocketMQ 在 4.6.0 版本中引入了 request-response 请求模型,就是消息发送者发送到 Broker,需要等消费者处理完才返回。其相关的 API 如下: +当然消息发送者与消息消费者的命名空间必须一样,才能彼此协作。**一言以蔽之,Namespace 主要为消息发送者、消息消费者进行分组,底层的逻辑是改变 Topic 的名称。** #### **request-response 响应模型 API** RocketMQ 在 4.6.0 版本中引入了 request-response 请求模型,就是消息发送者发送到 Broker,需要等消费者处理完才返回。其相关的 API 如下: ![8](assets/20200726212659392.png) diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25404\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25404\350\256\262.md" index f3f0d80a7..0bc9df797 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25404\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25404\350\256\262.md" @@ -42,7 +42,7 @@ public static void main(String[] args) throws Exception{ } ``` -Oneway 方式通常用于发送一些不太重要的消息,例如操作日志,偶然出现消息丢失对业务无影响。 **那实际生产中,同步发送与异步发送该如何选择呢?** +Oneway 方式通常用于发送一些不太重要的消息,例如操作日志,偶然出现消息丢失对业务无影响。**那实际生产中,同步发送与异步发送该如何选择呢?** 在回答如何选择同步发送还是异步发送时,首先简单介绍一下异步发送的实现原理: diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25405\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25405\350\256\262.md" index 6643da2ad..dccaaecd5 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25405\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25405\350\256\262.md" @@ -89,11 +89,11 @@ latencyMax ClientConfig 顾名思义,客户端的配置,在 RocketMQ 中消息发送者(Producer)和消息消费者(Consumer),即上面的配置生产者、消费者是通用的。 -**namesrvAddr** NameServer 的地址列表。 **clientIP** 客户端 IP,通过 `RemotingUtil#getLocalAddress` 方法获取,在 4.7.0 版本中优先会返回不是 127.0.0.1 和 192.168 开头的最后一个 IPV4 或第一个 IPV6。客户端 IP 主要是用来定位消费者的,clientIP 会当成客户端 id 的组成部分。 +**namesrvAddr** NameServer 的地址列表。**clientIP** 客户端 IP,通过 `RemotingUtil#getLocalAddress` 方法获取,在 4.7.0 版本中优先会返回不是 127.0.0.1 和 192.168 开头的最后一个 IPV4 或第一个 IPV6。客户端 IP 主要是用来定位消费者的,clientIP 会当成客户端 id 的组成部分。 如下图所示:在菜单 \[Consumer\] 列表中点击一个消费组,点击按钮 \[client\] 可以查阅其客户端(消费者)。 -![3](assets/20200801154637230.png) **instanceName** 客户端实例名称,是客户端标识 CID 的组成部分,在第三篇会详细其 CID 与场景的使用问题。 **unitName** 定义一个单元,主要用途:客户端 CID 的组成部分;如果获取 NameServer 的地址是通过 URL 进行动态更新的话,会将该值附加到当中,即可以区分不同的获取 NameServer 地址的服务。 **clientCallbackExecutorThreads** 客户端 public 回调的线程池线程数量,默认为 CPU 核数,不建议改变该值。 **namespace** 客户端命名空间,从 4.5.1 版本被引入,在第三篇中已详细介绍。 **pollNameServerInterval** 客户端从 NameServer 更新 Topic 的间隔,默认值 30s,就 Producer、Consumer 会每隔 30s 向 NameServer 更新 Topic 的路由信息,该值不建议修改。 **heartbeatBrokerInterval** 客户端向 Broker 发送心跳包的时间间隔,默认为 30s,该值不建议修改。 **persistConsumerOffsetInterval** 客户端持久化消息消费进度的间隔,默认为 5s,该值不建议修改。 +![3](assets/20200801154637230.png) **instanceName** 客户端实例名称,是客户端标识 CID 的组成部分,在第三篇会详细其 CID 与场景的使用问题。**unitName** 定义一个单元,主要用途:客户端 CID 的组成部分;如果获取 NameServer 的地址是通过 URL 进行动态更新的话,会将该值附加到当中,即可以区分不同的获取 NameServer 地址的服务。**clientCallbackExecutorThreads** 客户端 public 回调的线程池线程数量,默认为 CPU 核数,不建议改变该值。**namespace** 客户端命名空间,从 4.5.1 版本被引入,在第三篇中已详细介绍。**pollNameServerInterval** 客户端从 NameServer 更新 Topic 的间隔,默认值 30s,就 Producer、Consumer 会每隔 30s 向 NameServer 更新 Topic 的路由信息,该值不建议修改。**heartbeatBrokerInterval** 客户端向 Broker 发送心跳包的时间间隔,默认为 30s,该值不建议修改。**persistConsumerOffsetInterval** 客户端持久化消息消费进度的间隔,默认为 5s,该值不建议修改。 ### 核心参数工作机制与使用建议 @@ -109,7 +109,7 @@ RocketMQ 为了保证消息发送的高可用性,在内部引入了重试机 正如上图所 topicA 在 broker-a、broker-b 上分别创建了 4 个队列,例如一个线程使用 Producer 发送消息时,通过对 sendWhichQueue getAndIncrement() 方法获取下一个队列。 -例如在发送之前 sendWhichQueue 该值为 broker-a 的 q1,如果由于此时 broker-a 的突发流量异常大导致消息发送失败,会触发重试,按照轮循机制,下一个选择的队列为 broker-a 的 q2 队列,此次消息发送大概率还是会失败, **即尽管会重试 2 次,但都是发送给同一个 Broker 处理,此过程会显得不那么靠谱,即大概率还是会失败,那这样重试的意义将大打折扣。** 故 RocketMQ 为了解决该问题,引入了 **故障规避机制** ,在消息重试的时候,会尽量规避上一次发送的 Broker,回到上述示例,当消息发往 broker-a q1 队列时返回发送失败,那重试的时候,会先排除 broker-a 中所有队列,即这次会选择 broker-b q1 队列,增大消息发送的成功率。 +例如在发送之前 sendWhichQueue 该值为 broker-a 的 q1,如果由于此时 broker-a 的突发流量异常大导致消息发送失败,会触发重试,按照轮循机制,下一个选择的队列为 broker-a 的 q2 队列,此次消息发送大概率还是会失败,**即尽管会重试 2 次,但都是发送给同一个 Broker 处理,此过程会显得不那么靠谱,即大概率还是会失败,那这样重试的意义将大打折扣。** 故 RocketMQ 为了解决该问题,引入了 **故障规避机制**,在消息重试的时候,会尽量规避上一次发送的 Broker,回到上述示例,当消息发往 broker-a q1 队列时返回发送失败,那重试的时候,会先排除 broker-a 中所有队列,即这次会选择 broker-b q1 队列,增大消息发送的成功率。 上述规避思路是默认生效的,即无需干预。 @@ -174,7 +174,7 @@ public static void main(String[] args) throws Exception{ ![6](assets/20200801154707534.png) -在向集群 2 发送消息时出现 Topic 不存在,但明明创建了 dw_test_02,而且如果单独向集群 2 的 dw_test_02 发送消息确能成功, **初步排查是创建了两个到不同集群的 Producer 引起的,那这是为什么呢?如果解决呢?** **1. 问题分析** +在向集群 2 发送消息时出现 Topic 不存在,但明明创建了 dw_test_02,而且如果单独向集群 2 的 dw_test_02 发送消息确能成功,**初步排查是创建了两个到不同集群的 Producer 引起的,那这是为什么呢?如果解决呢?** **1. 问题分析** 要解决该问题,首先得理解 RocketMQ Client 的核心组成部分,如下图所示: diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25406\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25406\350\256\262.md" index 694cd922e..949737e84 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25406\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25406\350\256\262.md" @@ -76,7 +76,7 @@ RocketMQ 会每一分钟打印前一分钟内消息发送的耗时情况分布 在 RocketMQ 中通常遇到网络超时,通常与网络的抖动有关系,但由于我对网络不是特别擅长,故暂时无法找到直接证据,但能找到一些间接证据,例如在一个应用中同时连接了 Kafka、RocketMQ 集群,发现在出现超时的同一时间发现连接到 RocketMQ 集群内所有 Broker,连接到 Kafka 集群都出现了超时。 -**但出现网络超时,我们总得解决,那有什么解决方案吗?** 我们对消息中间件的最低期望就是高并发低延迟,从上面的消息发送耗时分布情况也可以看出 RocketMQ 确实符合我们的期望,绝大部分请求都是在微妙级别内,故我给出的方案时, **减少消息发送的超时时间,增加重试次数,并增加快速失败的最大等待时长** 。具体措施如下。 +**但出现网络超时,我们总得解决,那有什么解决方案吗?** 我们对消息中间件的最低期望就是高并发低延迟,从上面的消息发送耗时分布情况也可以看出 RocketMQ 确实符合我们的期望,绝大部分请求都是在微妙级别内,故我给出的方案时,**减少消息发送的超时时间,增加重试次数,并增加快速失败的最大等待时长** 。具体措施如下。 \\1. 增加 Broker 端快速失败的时长,建议为 1000,在 Broker 的配置文件中增加如下配置: @@ -84,7 +84,7 @@ RocketMQ 会每一分钟打印前一分钟内消息发送的耗时情况分布 maxWaitTimeMillsInQueue=1000 ``` -主要原因是在当前的 RocketMQ 版本中,快速失败导致的错误为 system_busy,并不会触发重试,适当增大该值,尽可能避免触发该机制,详情可以参考本专栏第 3 部分内容,会重点介绍 system_busy、broker_busy。 **如果 RocketMQ 的客户端版本为 4.3.0 以下版本(不含 4.3.0):** 将超时时间设置消息发送的超时时间为 500ms,并将重试次数设置为 6 次(这个可以适当进行调整,尽量大于 3),其背后的哲学是尽快超时,并进行重试,因为发现局域网内的网络抖动是瞬时的,下次重试的是就能恢复,并且 RocketMQ 有故障规避机制,重试的时候会尽量选择不同的 Broker,相关的代码如下: +主要原因是在当前的 RocketMQ 版本中,快速失败导致的错误为 system_busy,并不会触发重试,适当增大该值,尽可能避免触发该机制,详情可以参考本专栏第 3 部分内容,会重点介绍 system_busy、broker_busy。**如果 RocketMQ 的客户端版本为 4.3.0 以下版本(不含 4.3.0):** 将超时时间设置消息发送的超时时间为 500ms,并将重试次数设置为 6 次(这个可以适当进行调整,尽量大于 3),其背后的哲学是尽快超时,并进行重试,因为发现局域网内的网络抖动是瞬时的,下次重试的是就能恢复,并且 RocketMQ 有故障规避机制,重试的时候会尽量选择不同的 Broker,相关的代码如下: ```java DefaultMQProducer producer = new DefaultMQProducer("dw_test_producer_group"); @@ -147,7 +147,7 @@ too many requests and system thread pool busy ![8](assets/20200802175003961.png) -根据上述 5 类错误日志,其触发的原由可以归纳为如下 3 种。 **1. PageCache 压力较大** 其中如下三类错误属于此种情况: +根据上述 5 类错误日志,其触发的原由可以归纳为如下 3 种。**1. PageCache 压力较大** 其中如下三类错误属于此种情况: ```plaintext \[REJECTREQUEST\]system busy @@ -155,9 +155,9 @@ too many requests and system thread pool busy \[PCBUSY_CLEAN_QUEUE\]broker busy ``` -判断 PageCache 是否忙的依据就是,在写入消息、向内存追加消息时加锁的时间,默认的判断标准是加锁时间超过 1s,就认为是 PageCache 压力大,向客户端抛出相关的错误日志。 **2. 发送线程池挤压的拒绝策略** 在 RocketMQ 中处理消息发送的,是一个只有一个线程的线程池,内部会维护一个有界队列,默认长度为 1W。如果当前队列中挤压的数量超过 1w,执行线程池的拒绝策略,从而抛出 \[too many requests and system thread pool busy\] 错误。 **3. Broker 端快速失败** 默认情况下 Broker 端开启了快速失败机制,就是在 Broker 端还未发生 PageCache 繁忙(加锁超过 1s)的情况,但存在一些请求在消息发送队列中等待 200ms 的情况,RocketMQ 会不再继续排队,直接向客户端返回 System busy,但由于 RocketMQ 客户端目前对该错误没有进行重试处理,所以在解决这类问题的时候需要额外处理。 +判断 PageCache 是否忙的依据就是,在写入消息、向内存追加消息时加锁的时间,默认的判断标准是加锁时间超过 1s,就认为是 PageCache 压力大,向客户端抛出相关的错误日志。**2. 发送线程池挤压的拒绝策略** 在 RocketMQ 中处理消息发送的,是一个只有一个线程的线程池,内部会维护一个有界队列,默认长度为 1W。如果当前队列中挤压的数量超过 1w,执行线程池的拒绝策略,从而抛出 \[too many requests and system thread pool busy\] 错误。**3. Broker 端快速失败** 默认情况下 Broker 端开启了快速失败机制,就是在 Broker 端还未发生 PageCache 繁忙(加锁超过 1s)的情况,但存在一些请求在消息发送队列中等待 200ms 的情况,RocketMQ 会不再继续排队,直接向客户端返回 System busy,但由于 RocketMQ 客户端目前对该错误没有进行重试处理,所以在解决这类问题的时候需要额外处理。 -#### **PageCache 繁忙解决方案** 一旦消息服务器出现大量 PageCache 繁忙(在向内存追加数据加锁超过 1s)的情况,这个是比较严重的问题,需要人为进行干预解决,解决的问题思路如下。 **1. transientStorePoolEnable** +#### **PageCache 繁忙解决方案** 一旦消息服务器出现大量 PageCache 繁忙(在向内存追加数据加锁超过 1s)的情况,这个是比较严重的问题,需要人为进行干预解决,解决的问题思路如下。**1. transientStorePoolEnable** 开启 transientStorePoolEnable 机制,即在 Broker 中配置文件中增加如下配置: diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25407\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25407\350\256\262.md" index ed78d99d7..a00067496 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25407\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25407\350\256\262.md" @@ -93,19 +93,19 @@ RocketMQ 消息服务器开启一个定时任务,消费 RMQ_SYS_TRANS_HALF_TOP 事务实战的代码位于 rocketmq-example-user-service 模块。 -#### **RocketMQ 生产者初始化** ![6](assets/20200808130128165.png) +#### **RocketMQ 生产者初始化**![6](assets/20200808130128165.png) ![7](assets/20200808130134947.png) 代码解读:上述引入了 TransactionMQProducerContainer,主要的目的是事务消息需要关联一个事务回调监听器,故这里采取的是每一个 Topic 单独一个 TransactionMQProducer,所属的生产者组为一个固定前缀与 Topic 的组合。 -#### **UserServiceImpl 业务方法实现概述** ![8](assets/202008081301429.png) +#### **UserServiceImpl 业务方法实现概述**![8](assets/202008081301429.png) 代码解决:UserServiceImpl 的 login 就是实现登录的处理逻辑,首先先执行普通的业务逻辑,即验证登录用户的用户名与密码,如果不匹配,返回对应的业务错误提示;如果符合登录后,构建登录日志,并为登录日志生成全局唯一的业务流水号。该流水号将贯穿整个业务处理路程,即事务回调、消息消费等各个环节。 > 注意:这里并不会操作有数据库的写入操作,数据库的写入操作放在了事务消息的监听器中。 -#### **事件回调监听器** ![9](assets/20200808130149837.png) +#### **事件回调监听器**![9](assets/20200808130149837.png) 代码解读: @@ -118,7 +118,7 @@ RocketMQ 消息服务器开启一个定时任务,消费 RMQ_SYS_TRANS_HALF_TOP checkLocalTransaction ``` -该方法由 RocketMQ Broker 会主动调用,在该方法我们如果能根据唯一流水号查询到记录,则任务事务成功提交,则返回 COMMIT_MESSAGE,RocketMQ 会提交事务,将处于 PREPARE 状态的消息发送到用户真实的 Topic 中,这样消费端就能正常消费消息;如果从本地事务表中未查询到消息,返回 UNOWN 即可,不能直接返回 ROLL_BACK,只有当 RocketMQ 在指定回查次数后还未查询到,则会回滚该条消息,客户端不会消费到消息, **实现业务与消息发送的分布式事务一致性** 。 +该方法由 RocketMQ Broker 会主动调用,在该方法我们如果能根据唯一流水号查询到记录,则任务事务成功提交,则返回 COMMIT_MESSAGE,RocketMQ 会提交事务,将处于 PREPARE 状态的消息发送到用户真实的 Topic 中,这样消费端就能正常消费消息;如果从本地事务表中未查询到消息,返回 UNOWN 即可,不能直接返回 ROLL_BACK,只有当 RocketMQ 在指定回查次数后还未查询到,则会回滚该条消息,客户端不会消费到消息,**实现业务与消息发送的分布式事务一致性** 。 上面有增加测试代码,就是该事务成功与事务失败,看数据库与 MQ 是否是一致性。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25408\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25408\350\256\262.md" index 145e15999..952091a0a 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25408\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25408\350\256\262.md" @@ -10,13 +10,13 @@ RocketMQ 消费端的 API 如下图所示: 其核心类图如下所示。 -**MQAdmin** MQ 一些基本的管理功能,例如创建 Topic,这里稍微有点奇怪,消费端应该不需要继承该接口。该类在消息发送 API 章节已详细介绍,再次不再重复说明。 **MQConsumer** MQ 消费者,这个接口定义得过于简单,如果该接口需要,可以将其子接口一些共同的方法提取到该接口中。 +**MQAdmin** MQ 一些基本的管理功能,例如创建 Topic,这里稍微有点奇怪,消费端应该不需要继承该接口。该类在消息发送 API 章节已详细介绍,再次不再重复说明。**MQConsumer** MQ 消费者,这个接口定义得过于简单,如果该接口需要,可以将其子接口一些共同的方法提取到该接口中。 ```plaintext Set fetchSubscribeMessageQueues(final String topic) ``` -获取分配该 Topic 所有的读队列。 **MQPushConsumer** RocketMQ 支持推、拉两种模式,该接口是 **拉模式** 的接口定义。 +获取分配该 Topic 所有的读队列。**MQPushConsumer** RocketMQ 支持推、拉两种模式,该接口是 **拉模式** 的接口定义。 ```plaintext void start() @@ -157,7 +157,7 @@ void resume() 恢复继续消费。 -**DefaultMQPushConsumer** RocketMQ 消息推模式默认实现类。 **DefaultMQPullConsumer** +**DefaultMQPushConsumer** RocketMQ 消息推模式默认实现类。**DefaultMQPullConsumer** RocketMQ 消息拉取模式默认实现类。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25409\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25409\350\256\262.md" index 0867be556..74a2a5f53 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25409\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25409\350\256\262.md" @@ -6,13 +6,13 @@ PUSH 模式是对 PULL 模式的封装,类似于一个高级 API,用户使 DefaultMQPushConsumer 的核心参数一览如下。 -**InternalLogger log** 这个是消费者一个 final 的属性,用来记录 RocketMQ Consumer 在运作过程中的一些日志,其日志文件默认路径为 `{user.home}/logs/rocketmqlogs/rocketmq_cliente.log`。 **String consumerGroup** 消费组的名称,在 RocketMQ 中,对于消费中来说,一个消费组就是一个独立的隔离单位,例如多个消费组订阅同一个主题,其消息进度(消息处理的进展)是相互独立的,两者不会有任何的干扰。 **MessageModel messageModel** 消息组消息消费模式,在 RocketMQ 中支持集群模式、广播模式。集群模式值得是一个消费组内多个消费者共同消费一个 Topic 中的消息,即一条消息只会被集群内的某一个消费者处理;而广播模式是指一个消费组内的每一个消费者负责 Topic 中的所有消息。 **ConsumeFromWhere consumeFromWhere** **一个消费者初次启动时** (即消费进度管理器中无法查询到该消费组的进度)时从哪个位置开始消费的策略,可选值如下所示: +**InternalLogger log** 这个是消费者一个 final 的属性,用来记录 RocketMQ Consumer 在运作过程中的一些日志,其日志文件默认路径为 `{user.home}/logs/rocketmqlogs/rocketmq_cliente.log`。**String consumerGroup** 消费组的名称,在 RocketMQ 中,对于消费中来说,一个消费组就是一个独立的隔离单位,例如多个消费组订阅同一个主题,其消息进度(消息处理的进展)是相互独立的,两者不会有任何的干扰。**MessageModel messageModel** 消息组消息消费模式,在 RocketMQ 中支持集群模式、广播模式。集群模式值得是一个消费组内多个消费者共同消费一个 Topic 中的消息,即一条消息只会被集群内的某一个消费者处理;而广播模式是指一个消费组内的每一个消费者负责 Topic 中的所有消息。**ConsumeFromWhere consumeFromWhere** **一个消费者初次启动时** (即消费进度管理器中无法查询到该消费组的进度)时从哪个位置开始消费的策略,可选值如下所示: - CONSUME_FROM_LAST_OFFSET:从最新的消息开始消费。 - CONSUME_FROM_FIRST_OFFSET:从最新的位点开始消费。 - CONSUME_FROM_TIMESTAMP:从指定的时间戳开始消费,这里的实现思路是从 Broker 服务器寻找消息的存储时间小于或等于指定时间戳中最大的消息偏移量的消息,从这条消息开始消费。 -**String consumeTimestamp** 指定从什么时间戳开始消费,其格式为 yyyyMMddHHmmss,默认值为 30 分钟之前,该参数只在 consumeFromWhere 为 CONSUME_FROM_TIMESTAMP 时生效。 **AllocateMessageQueueStrategy allocateMessageQueueStrategy** +**String consumeTimestamp** 指定从什么时间戳开始消费,其格式为 yyyyMMddHHmmss,默认值为 30 分钟之前,该参数只在 consumeFromWhere 为 CONSUME_FROM_TIMESTAMP 时生效。**AllocateMessageQueueStrategy allocateMessageQueueStrategy** 消息队列负载算法。主要解决的问题是消息消费队列在各个消费者之间的负载均衡策略,例如一个 Topic 有8个队列,一个消费组中有3个消费者,那这三个消费者各自去消费哪些队列。 @@ -24,7 +24,7 @@ RocketMQ 默认提供了如下负载均衡算法: - AllocateMessageQueueByConfig:手动指定,这个通常需要配合配置中心,在消费者启动时,首先先创建 AllocateMessageQueueByConfig 对象,然后根据配置中心的配置,再根据当前的队列信息,进行分配,即该方法不具备队列的自动负载,在 Broker 端进行队列扩容时,无法自动感知,需要手动变更配置。 - AllocateMessageQueueByMachineRoom:消费指定机房中的队列,该分配算法首先需要调用该策略的 `setConsumeridcs(Set consumerIdCs)` 方法,用于设置需要消费的机房,将刷选出来的消息按平均连续分配算法进行队列负载。 -**AllocateMessageQueueConsistentHash** 一致性 Hash 算法。 **OffsetStore offsetStore** +**AllocateMessageQueueConsistentHash** 一致性 Hash 算法。**OffsetStore offsetStore** 消息进度存储管理器,该属性为私有属性,不能通过 API 进行修改,该参数主要是根据消费模式在内部自动创建,RocketMQ 在广播消息、集群消费两种模式下消息消费进度的存储策略会有所不同。 @@ -118,7 +118,7 @@ long consumeTimeout = 15 ### 核心参数工作原理 -#### **消息消费队列负载算法** 本节将使用图解的方式来阐述 RocketMQ 默认提供的消息消费队列负载机制。 **AllocateMessageQueueAveragely** 平均连续分配算法。主要的特点是一个消费者分配的消息队列是连续的 +#### **消息消费队列负载算法** 本节将使用图解的方式来阐述 RocketMQ 默认提供的消息消费队列负载机制。**AllocateMessageQueueAveragely** 平均连续分配算法。主要的特点是一个消费者分配的消息队列是连续的 ![3](assets/20200814230902879.png) **AllocateMessageQueueAveragelyByCircle** 平均轮流分配算法,其分配示例图如下: @@ -126,11 +126,11 @@ long consumeTimeout = 15 ![5](assets/20200814230918209.png) -上述的背景是一个 MQ 集群的两台 Broker 分别部署在两个不同的机房,每一个机房中都部署了一些消费者,其队列的负载情况是同机房中的消费队列优先被同机房的消费者进行分配,其分配算法可以指定其他的算法,例如示例中的平均分配,但如果机房 B 中的消费者宕机,B 机房中没有存活的消费者,那该机房中的队列会被其他机房中的消费者获取进行消费。 **AllocateMessageQueueByConfig** 手动指定,这个通常需要配合配置中心,在消费者启动时,首先先创建 AllocateMessageQueueByConfig 对象,然后根据配置中心的配置,再根据当前的队列信息,进行分配,即该方法不具备队列的自动负载,在 Broker 端进行队列扩容时,无法自动感知,需要手动变更配置。 **AllocateMessageQueueByMachineRoom** 消费指定机房中的队列,该分配算法首先需要调用该策略的 `setConsumeridcs(Set consumerIdCs)` 方法,用于设置需要消费的机房,将刷选出来的消息按平均连续分配算法进行队列负载,其分配示例图如下所示: +上述的背景是一个 MQ 集群的两台 Broker 分别部署在两个不同的机房,每一个机房中都部署了一些消费者,其队列的负载情况是同机房中的消费队列优先被同机房的消费者进行分配,其分配算法可以指定其他的算法,例如示例中的平均分配,但如果机房 B 中的消费者宕机,B 机房中没有存活的消费者,那该机房中的队列会被其他机房中的消费者获取进行消费。**AllocateMessageQueueByConfig** 手动指定,这个通常需要配合配置中心,在消费者启动时,首先先创建 AllocateMessageQueueByConfig 对象,然后根据配置中心的配置,再根据当前的队列信息,进行分配,即该方法不具备队列的自动负载,在 Broker 端进行队列扩容时,无法自动感知,需要手动变更配置。**AllocateMessageQueueByMachineRoom** 消费指定机房中的队列,该分配算法首先需要调用该策略的 `setConsumeridcs(Set consumerIdCs)` 方法,用于设置需要消费的机房,将刷选出来的消息按平均连续分配算法进行队列负载,其分配示例图如下所示: ![6](assets/20200814230925817.png) -由于设置 consumerIdCs 为 A 机房,故 B 机房中的队列并不会消息。 **AllocateMessageQueueConsistentHash** 一致性 Hash 算法,讲真,在消息队列负载这里使用一致性算法,没有任何实际好处,一致性 Hash 算法最佳的使用场景用在 Redis 缓存的分布式领域最适宜。 +由于设置 consumerIdCs 为 A 机房,故 B 机房中的队列并不会消息。**AllocateMessageQueueConsistentHash** 一致性 Hash 算法,讲真,在消息队列负载这里使用一致性算法,没有任何实际好处,一致性 Hash 算法最佳的使用场景用在 Redis 缓存的分布式领域最适宜。 #### **PUSH 模型消息拉取机制** 在介绍消息消费端限流机制时,首先用如下简图简单介绍一下 RocketMQ 消息拉取执行模型 @@ -154,7 +154,7 @@ long consumeTimeout = 15 有读者朋友说,消息 msg3 处理完,当然是向 Broker 汇报 msg3 的偏移量作为消息消费进度呀。但细心思考一下,发现如果提交 msg3 的偏移量为消息消费进度,那汇报完毕后如果消费者发生内存溢出等问题导致 JVM 异常退出,msg1 的消息还未处理,然后重启消费者,由于消息消费进度文件中存储的是 msg3 的消息偏移量,会继续从 msg3 开始消费,会造成 **消息丢失** 。显然这种方式并不可取。 -RocketMQ 采取的方式是处理完 msg3 之后,会将 msg3 从消息处理队列中移除,但在向 Broker 汇报消息消费进度时是 **取 ProceeQueue 中最小的偏移量为消息消费进度** ,即汇报的消息消费进度是 0。 +RocketMQ 采取的方式是处理完 msg3 之后,会将 msg3 从消息处理队列中移除,但在向 Broker 汇报消息消费进度时是 **取 ProceeQueue 中最小的偏移量为消息消费进度**,即汇报的消息消费进度是 0。 ![9](assets/20200814230953825.png) diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25410\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25410\350\256\262.md" index 16f412c6a..22b755dc5 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25410\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25410\350\256\262.md" @@ -47,7 +47,7 @@ sh ./mqadmin resetOffsetByTime -n 127.0.0.1:9876 -g CID_CONSUMER_TEST -t TopicT 在 RocketMQ 集群在多机房部署方案中本场景下,将一个 RocketMQ 集群部署在两个机房中,即每一个机房都各自部署一个 Broker,两个 Broker 共同承担消息的写入与消费。并且在两个机房都部署了两个消费者。 -从消费者的角度来看,如果采取平均分配,特别是采取 AllocateMessageQueueAveragelyByCircle 方案,会出现消费者跨消费这种情况, **如果能实现本机房的消费者优先消费本机房中的消息,可有效避免消息跨机房消费** 。值得庆幸的是,RocketMQ 设计者已经为我们了提供了解决方案——AllocateMachineRoomNearby。 +从消费者的角度来看,如果采取平均分配,特别是采取 AllocateMessageQueueAveragelyByCircle 方案,会出现消费者跨消费这种情况,**如果能实现本机房的消费者优先消费本机房中的消息,可有效避免消息跨机房消费** 。值得庆幸的是,RocketMQ 设计者已经为我们了提供了解决方案——AllocateMachineRoomNearby。 接下来我们来介绍一下,如何使用 AllocateMachineRoomNearby 队列负载算法。 @@ -77,7 +77,7 @@ consumer 默认的 clientIP 为 RemotingUtil.getLocalAddress(),即本机的 IP 其含义分别如下: -**1. AllocateMessageQueueStrategy allocateMessageQueueStrategy** 内部分配算法,可以看成机房就近分配算法,其实是一个代理,内部还是需要持有一种分配算法,例如平均分配算法。 **2. MachineRoomResolver machineRoomResolver** +**1. AllocateMessageQueueStrategy allocateMessageQueueStrategy** 内部分配算法,可以看成机房就近分配算法,其实是一个代理,内部还是需要持有一种分配算法,例如平均分配算法。**2. MachineRoomResolver machineRoomResolver** 多机房解析器,即从 brokerName、客户端 clientId 中识别出所在的机房。 @@ -261,7 +261,7 @@ RocketMQ 的 clientId 的生成规则与 Producer 一样,如果两者出现重 明明客户端有两个,但为什么有一半的队列没有分配到消费者呢? -这就是因为 clientID 相同导致的。我们不妨以平均分配算法为例进行思考,队列负载算法时,首先会向 NameServer 查询 Topic 的路由信息,这里会返回队列个数为 4,然后向 Broker 查询当前活跃的消费者个数,会返回 2,然后开始分配。队列负载算法分配时,首先会将队列,消费者的 cid 进行排序,第一消费者分配前面 2 个队列,第二个消费者分配后面两个队列,但由于两个 cid 是相同的,这样会造成两个消费者在分配队列时,都认为自己是第一个消费者,故都分配到了前 2 个队列,即前面两个队列会被两个消费者都分配到,造成消息 **重复消费** ,并且有些队列却 **无法被消费** 。 +这就是因为 clientID 相同导致的。我们不妨以平均分配算法为例进行思考,队列负载算法时,首先会向 NameServer 查询 Topic 的路由信息,这里会返回队列个数为 4,然后向 Broker 查询当前活跃的消费者个数,会返回 2,然后开始分配。队列负载算法分配时,首先会将队列,消费者的 cid 进行排序,第一消费者分配前面 2 个队列,第二个消费者分配后面两个队列,但由于两个 cid 是相同的,这样会造成两个消费者在分配队列时,都认为自己是第一个消费者,故都分配到了前 2 个队列,即前面两个队列会被两个消费者都分配到,造成消息 **重复消费**,并且有些队列却 **无法被消费** 。 最佳实践:建议大家对 clientIP 进行定制化,最好是客户端 IP + 时间戳,甚至于客户端 IP + uuid。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25411\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25411\350\256\262.md" index bb0cc56c6..84e450292 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25411\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25411\350\256\262.md" @@ -218,7 +218,7 @@ boolean autoCommit int pullThreadNums ``` -消息拉取线程数量,默认为 20 个,注意这个是每一个消费者默认 20 个线程往 Broker 拉取消息。 **这个应该是 Lite PULL 模式对比 PUSH 模式一个非常大的优势。** +消息拉取线程数量,默认为 20 个,注意这个是每一个消费者默认 20 个线程往 Broker 拉取消息。**这个应该是 Lite PULL 模式对比 PUSH 模式一个非常大的优势。** ```plaintext long autoCommitIntervalMillis @@ -317,11 +317,11 @@ public class LitePullConsumerSubscribe02 { 从上面的示例可以看出 Lite PULL 相关的 API 比 4.6.0 之前的 DefaultMQPullConsumer 的使用上要简便不少,从编程风格上已非常接近了 PUSH 模式,其底层的实现原理是否也一致呢?显然不是的,请听我我慢慢道来。 -不知大家是否注意到,Lite PULL 模式下只是通过 poll() 方法拉取一批消息,然后提交给应用程序处理, **采取自动提交模式下位点的提交与消费结果并没有直接挂钩,即消息如果处理失败,其消费位点还是继续向前继续推进,缺乏消息的重试机制。** 为了论证笔者的观点,这里给出 DefaultLitePullConsumer 的 poll() 方法执行流程图,请大家重点关注位点提交所处的位置。 +不知大家是否注意到,Lite PULL 模式下只是通过 poll() 方法拉取一批消息,然后提交给应用程序处理,**采取自动提交模式下位点的提交与消费结果并没有直接挂钩,即消息如果处理失败,其消费位点还是继续向前继续推进,缺乏消息的重试机制。** 为了论证笔者的观点,这里给出 DefaultLitePullConsumer 的 poll() 方法执行流程图,请大家重点关注位点提交所处的位置。 ![4](assets/20200817190919223.png) -**Lite Pull 模式的自动提交位点,一个非常重要的特征是 poll() 方法一返回,这批消息就默认是消费成功了** ,一旦没有处理好,就会造成消息丢失,那有没有方法解决上述这个问题呢, **seek 方法就闪亮登场了** ,在业务方法处理过程中,如果处理失败,可以通过 seek 方法重置消费位点,即在捕获到消息业务处理后,需要根据返回的第一条消息中(MessageExt)信息构建一个 MessageQueue 对象以及需要重置的位点。 +**Lite Pull 模式的自动提交位点,一个非常重要的特征是 poll() 方法一返回,这批消息就默认是消费成功了**,一旦没有处理好,就会造成消息丢失,那有没有方法解决上述这个问题呢,**seek 方法就闪亮登场了**,在业务方法处理过程中,如果处理失败,可以通过 seek 方法重置消费位点,即在捕获到消息业务处理后,需要根据返回的第一条消息中(MessageExt)信息构建一个 MessageQueue 对象以及需要重置的位点。 Lite Pull 模式的消费者相比 PUSH 模式的另外一个不同点事 Lite Pull 模式没有消息消费重试机制,PUSH 模式在并发消费模式下默认提供了 16 次重试,并且每一次重试的间隔不一致,极大的简化了编程模型。在这方面 Lite Pull 模型还是会稍显复杂。 @@ -340,9 +340,9 @@ PULL 模式通常适合大数据领域的批处理操作,对消息的实时性 - Broker 端没有新消息,立即返回,拉取结果中不包含任何消息。 - 当前拉取请求在 Broker 端挂起,在 Broker 端挂起,并且轮询 Broker 端是否有新消息,即轮询机制。 -上面说的第二种方式,有一个“高大上”的名字—— **轮询** ,根据轮询的方式又可以分为 **长轮询、短轮询** 。 +上面说的第二种方式,有一个“高大上”的名字—— **轮询**,根据轮询的方式又可以分为 **长轮询、短轮询** 。 -- **短轮询** :第一次未拉取到消息后等待一个时间间隔后再试,默认为 1s,可以在 Broker 的配置文件中设置 shortPollingTimeMills 改变默认值,即轮询一次, **注意:只轮询一次** 。 +- **短轮询** :第一次未拉取到消息后等待一个时间间隔后再试,默认为 1s,可以在 Broker 的配置文件中设置 shortPollingTimeMills 改变默认值,即轮询一次,**注意:只轮询一次** 。 - **长轮询** :可以由 PULL 客户端设置在 Broker 端挂起的超时时间,默认为 20s,然后在 Broker 端没有拉取到消息后默认每隔 5s 一次轮询,并且在 Broker 端获取到新消息后,会唤醒拉取线程,结束轮询,尝试一次消息拉取,然后返回一批消息到客户端,长轮询的时序图如下所示: ![6](assets/20200817190938839.png) diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25413\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25413\350\256\262.md" index 6595ef7ba..dced3e98e 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25413\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25413\350\256\262.md" @@ -32,9 +32,9 @@ RocketMQ 消息消费端按照消费组进行的线程隔离,即每一个消 **所以要保证消费端对单队列中的消息顺序处理,故多线程处理,需要按照消息消费队列进行加锁。** 故顺序消费在消费端的并发度并不取决消费端线程池的大小,而是取决于分给给消费者的队列数量,故如果一个 Topic 是用在顺序消费场景中,建议消费者的队列数设置增多,可以适当为非顺序消费的 2~3 倍,这样有利于提高消费端的并发度,方便横向扩容。 -消费端的横向扩容或 Broker 端队列个数的变更都会触发消息消费队列的重新负载,在并发消息时在队列负载的时候一个消费队列有可能被多个消费者同时消息,但顺序消费时并不会出现这种情况,因为顺序消息不仅仅在消费消息时会锁定消息消费队列,在分配到消息队列时, **能从该队列拉取消息还需要在 Broker 端申请该消费队列的锁** ,即同一个时间只有一个消费者能拉取该队列中的消息,确保顺序消费的语义。 +消费端的横向扩容或 Broker 端队列个数的变更都会触发消息消费队列的重新负载,在并发消息时在队列负载的时候一个消费队列有可能被多个消费者同时消息,但顺序消费时并不会出现这种情况,因为顺序消息不仅仅在消费消息时会锁定消息消费队列,在分配到消息队列时,**能从该队列拉取消息还需要在 Broker 端申请该消费队列的锁**,即同一个时间只有一个消费者能拉取该队列中的消息,确保顺序消费的语义。 -从前面的文章中也介绍到并发消费模式在消费失败是有重试机制,默认重试 16 次,而且重试时是先将消息发送到 Broker,然后再次拉取到消息,这种机制就会丧失其消费的顺序性,故如果是顺序消费模式,消息重试时在消费端不停的重试,重试次数为 Integer.MAX_VALUE, **即如果一条消息如果一直不能消费成功,其消息消费进度就会一直无法向前推进,即会造成消息积压现象。** > 温馨提示:顺序消息时一定要捕捉异常,必须能区分是系统异常还是业务异常,更加准确的要能区分哪些异常是通过重试能恢复的,哪些是通过重试无法恢复的。无法恢复的一定要尽量在发送到 MQ 之前就要拦截,并且需要提高告警功能。 +从前面的文章中也介绍到并发消费模式在消费失败是有重试机制,默认重试 16 次,而且重试时是先将消息发送到 Broker,然后再次拉取到消息,这种机制就会丧失其消费的顺序性,故如果是顺序消费模式,消息重试时在消费端不停的重试,重试次数为 Integer.MAX_VALUE,**即如果一条消息如果一直不能消费成功,其消息消费进度就会一直无法向前推进,即会造成消息积压现象。** > 温馨提示:顺序消息时一定要捕捉异常,必须能区分是系统异常还是业务异常,更加准确的要能区分哪些异常是通过重试能恢复的,哪些是通过重试无法恢复的。无法恢复的一定要尽量在发送到 MQ 之前就要拦截,并且需要提高告警功能。 ### 消息过滤实战 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25414\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25414\350\256\262.md" index f3bd61b49..98b22e7f0 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25414\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25414\350\256\262.md" @@ -67,11 +67,11 @@ RocketMQ 消息消费端会从 3 个维度进行限流: PullMessageService 线程会按照队列向 Broker 拉取一批消息,然后会存入到 ProcessQueue 队列中,即所谓的处理队列,然后再提交到消费端线程池中进行消息消费,消息消费完成后会将对应的消息从 ProcessQueue 中移除,然后向 Broker 端提交消费进度,提交的消费偏移量为 ProceeQueue 中的最小偏移量。 -**规则一:消息消费端队列中积压的消息超过 1000 条值的就是 ProcessQueue 中存在的消息条数超过指定值** ,默认为 1000 条,就触发限流,限流的具体做法就是暂停向 Broker 拉取该队列中的消息,但并不会阻止其他队列的消息拉取。例如如果 q0 中积压的消息超过 1000 条,但 q1 中积压的消息不足 1000,那 q1 队列中的消息会继续消费。其目的就是担心积压的消息太多,如果再继续拉取,会造成内存溢出。 **规则二:消息在 ProcessQueue 中实际上维护的是一个 TreeMap** ,key 为消息的偏移量、vlaue 为消息对象,由于 TreeMap 本身是排序的,故很容易得出最大偏移量与最小偏移量的差值,即有可能存在处理队列中其实就只有 3 条消息,但偏移量确超过了 2000,例如如下图所示: +**规则一:消息消费端队列中积压的消息超过 1000 条值的就是 ProcessQueue 中存在的消息条数超过指定值**,默认为 1000 条,就触发限流,限流的具体做法就是暂停向 Broker 拉取该队列中的消息,但并不会阻止其他队列的消息拉取。例如如果 q0 中积压的消息超过 1000 条,但 q1 中积压的消息不足 1000,那 q1 队列中的消息会继续消费。其目的就是担心积压的消息太多,如果再继续拉取,会造成内存溢出。**规则二:消息在 ProcessQueue 中实际上维护的是一个 TreeMap**,key 为消息的偏移量、vlaue 为消息对象,由于 TreeMap 本身是排序的,故很容易得出最大偏移量与最小偏移量的差值,即有可能存在处理队列中其实就只有 3 条消息,但偏移量确超过了 2000,例如如下图所示: ![9](assets/20200823231015957.png) -出现这种情况也是非常有可能的,其主要原因就是消费偏移量为 100 的这个线程由于某种情况卡主了(“阻塞”了),其他消息却能正常消费,这种情况虽然不会造成内存溢出,但大概率会造成大量消息重复消费,究其原因与消息消费进度的提交机制有关,在 RocketMQ 中,例如消息偏移量为 2001 的消息消费成功后,向服务端汇报消费进度时并不是报告 2001,而是取处理队列中最小偏移量 100,这样虽然消息一直在处理,但消息消费进度始终无法向前推进,试想一下如果此时最大的消息偏移量为 1000,项目组发现出现了消息积压,然后重启消费端,那消息就会从 100 开始重新消费,会造成大量消息重复消费,RocketMQ 为了避免出现大量消息重复消费,故对此种情况会对其进行限制,超过 2000 就不再拉取消息了。 **规则三:消息处理队列中积压的消息总大小超过 100M。** +出现这种情况也是非常有可能的,其主要原因就是消费偏移量为 100 的这个线程由于某种情况卡主了(“阻塞”了),其他消息却能正常消费,这种情况虽然不会造成内存溢出,但大概率会造成大量消息重复消费,究其原因与消息消费进度的提交机制有关,在 RocketMQ 中,例如消息偏移量为 2001 的消息消费成功后,向服务端汇报消费进度时并不是报告 2001,而是取处理队列中最小偏移量 100,这样虽然消息一直在处理,但消息消费进度始终无法向前推进,试想一下如果此时最大的消息偏移量为 1000,项目组发现出现了消息积压,然后重启消费端,那消息就会从 100 开始重新消费,会造成大量消息重复消费,RocketMQ 为了避免出现大量消息重复消费,故对此种情况会对其进行限制,超过 2000 就不再拉取消息了。**规则三:消息处理队列中积压的消息总大小超过 100M。** 这个就更加直接了,不仅从消息数量考虑,再结合从消息体大小考虑,处理队列中消息总大小超过 100M 进行限流,这个显而易见就是为了避免内存溢出。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25415\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25415\350\256\262.md" index 7a10eed14..f106b640f 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25415\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25415\350\256\262.md" @@ -64,7 +64,7 @@ SPACE 磁盘已使用的占比 -#### **集群中资源吞吐** 命令 statsAll 可以查看集群中所有主题/消费组的实时吞吐情况。 **命令示例** +#### **集群中资源吞吐** 命令 statsAll 可以查看集群中所有主题/消费组的实时吞吐情况。**命令示例** ```plaintext bin/mqadmin statsAll -n x.x.x.x:9876 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25417\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25417\350\256\262.md" index 24c5e6283..1e6b43e58 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25417\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25417\350\256\262.md" @@ -259,7 +259,7 @@ waitTimeMillsInSendQueue 发送消息时在队列中等待时间,超过会抛出超时错误 -#### **调优建议** 对 Broker 的几个属性可能影响到集群性能的稳定性,下面进行特别说明。 **1. 开启异步刷盘** 除了一些支付类场景、或者 TPS 较低的场景(例如:TPS 在 2000 以下)生产环境建议开启异步刷盘,提高集群吞吐 +#### **调优建议** 对 Broker 的几个属性可能影响到集群性能的稳定性,下面进行特别说明。**1. 开启异步刷盘** 除了一些支付类场景、或者 TPS 较低的场景(例如:TPS 在 2000 以下)生产环境建议开启异步刷盘,提高集群吞吐 ```plaintext flushDiskType=ASYNC_FLUSH diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25418\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25418\350\256\262.md" index b96277364..e941e2eaa 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25418\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25418\350\256\262.md" @@ -71,7 +71,7 @@ broker-a、broker-b、broker-c、broker-d 及其从节点为 CentOS 6。broker-a ![img](assets/20200816103630396.png) -#### **注意事项** 在扩容中,我们将新申请的 8 台 CentOS 7 节点,命名为 broker-a1、broker-b1、broker-c1、broker-d1 的形式,而不是 broker-e、broker-f、broker-g、broker-h。下面看下这么命名的原因,客户端消费默认采用平均分配算法,假设有四个消费节点。 **第一种形式** 扩容后排序如下,即新加入的节点 broker-e 会排在原集群的最后。 +#### **注意事项** 在扩容中,我们将新申请的 8 台 CentOS 7 节点,命名为 broker-a1、broker-b1、broker-c1、broker-d1 的形式,而不是 broker-e、broker-f、broker-g、broker-h。下面看下这么命名的原因,客户端消费默认采用平均分配算法,假设有四个消费节点。**第一种形式** 扩容后排序如下,即新加入的节点 broker-e 会排在原集群的最后。 ```plaintext broker-a,broker-b,broker-c,broker-d,broker-e,broker-f,broker-g,broker-h @@ -79,7 +79,7 @@ broker-a,broker-b,broker-c,broker-d,broker-e,broker-f,broker-g,broker-h ![img](assets/20200816105925559.png) -注:当缩容摘除 broker-a、broker-b、broker-c、broker-d 的流量时,会发现 consumer-01、consumer-02 没有不能分到 Broker 节点,造成流量偏移,存在剩余的一半节点无法承载流量压力的隐患。 **第二种形式** +注:当缩容摘除 broker-a、broker-b、broker-c、broker-d 的流量时,会发现 consumer-01、consumer-02 没有不能分到 Broker 节点,造成流量偏移,存在剩余的一半节点无法承载流量压力的隐患。**第二种形式** 扩容后的排序如下,即新加入的主节点 broker-a1 紧跟着原来的主节点 broker-a。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25420\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25420\350\256\262.md" index a24760b06..812efa3b0 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25420\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25420\350\256\262.md" @@ -10,9 +10,9 @@ ![img](assets/20200827092950837.png) -#### **主题监控** 从发送速度、发送耗时、消息大小、日消息量方面整理主题监控项,下面分别介绍这些监控项的重要性。 **发送速度** 通过实时采集主题的发送速度,来掌握主题的流量情况。例如:有些业务场景不允许主题的发送速度掉为 0,那通过实时采集发送速度指标,为将来告警做准备。 **发送变化率** 发送变化率是指,特定时间内主题的发送速度变化了多少。例如:5 分钟内发送速率陡增了 2 倍。通常用于两方面,一个是保护集群,某个 Topic 过高的瞬时流量可能对集群安全造成影响。例如:一个发送速率为 5000 的主题,在 3 分钟内陡增了 5 倍,到了 25000 的高度,这种流量对集群存在安全隐患。另一个是使用角度检测业务是否正常,比如一个发送速率为 8000 的主题,在 3 分钟内掉为 80,类似这种断崖式下跌是否是业务正常逻辑,可以对业务健康情况反向检测。 **发送耗时** 通过采集发送消息的耗时分布情况,了解客户端的发送情况,耗时分布可以为下面区间,单位毫秒。\[0, 1), \[1, 5), \[5, 10), \[10, 50), \[50, 100), \[100, 500), \[500, 1000), \[1000, ∞)。例如:如果发送的消息耗时分布集中在大于 500ms~1000ms,那需要介入分析原因为何耗时如此长。 **消息大小** 通过采集消息大小的分布情况,了解那些客户端存在大消息。发送速率过高的大消息同样存在集群的安全隐患。比如那些主题发送的消息大于 5K,为日后需要专项治理或者实时告警提供数据支撑。消息大小分布区间如下参考,单位 KB。\[0, 1), \[1, 5), \[5, 10), \[10, 50), \[50, 100), \[500, 1000), \[1000, ∞)。 **日消息量** 日消息量是指通过每天采集的发送的消息数量,形成时间曲线。可以分析一周、一月的消息总量变化情况 +#### **主题监控** 从发送速度、发送耗时、消息大小、日消息量方面整理主题监控项,下面分别介绍这些监控项的重要性。**发送速度** 通过实时采集主题的发送速度,来掌握主题的流量情况。例如:有些业务场景不允许主题的发送速度掉为 0,那通过实时采集发送速度指标,为将来告警做准备。**发送变化率** 发送变化率是指,特定时间内主题的发送速度变化了多少。例如:5 分钟内发送速率陡增了 2 倍。通常用于两方面,一个是保护集群,某个 Topic 过高的瞬时流量可能对集群安全造成影响。例如:一个发送速率为 5000 的主题,在 3 分钟内陡增了 5 倍,到了 25000 的高度,这种流量对集群存在安全隐患。另一个是使用角度检测业务是否正常,比如一个发送速率为 8000 的主题,在 3 分钟内掉为 80,类似这种断崖式下跌是否是业务正常逻辑,可以对业务健康情况反向检测。**发送耗时** 通过采集发送消息的耗时分布情况,了解客户端的发送情况,耗时分布可以为下面区间,单位毫秒。\[0, 1), \[1, 5), \[5, 10), \[10, 50), \[50, 100), \[100, 500), \[500, 1000), \[1000, ∞)。例如:如果发送的消息耗时分布集中在大于 500ms~1000ms,那需要介入分析原因为何耗时如此长。**消息大小** 通过采集消息大小的分布情况,了解那些客户端存在大消息。发送速率过高的大消息同样存在集群的安全隐患。比如那些主题发送的消息大于 5K,为日后需要专项治理或者实时告警提供数据支撑。消息大小分布区间如下参考,单位 KB。\[0, 1), \[1, 5), \[5, 10), \[10, 50), \[50, 100), \[500, 1000), \[1000, ∞)。**日消息量** 日消息量是指通过每天采集的发送的消息数量,形成时间曲线。可以分析一周、一月的消息总量变化情况 -#### **消费监控** **消费速度** 通过实时采集消费速度指标,掌握消费组健康情况。同样有些场景对消费速度大小比较关心。通过采集实时消息消费速率情况,为告警提供数据支撑。 **消费积压** 消费积压是指某一时刻还有多少消息没有消费,消费积压 = 发送消息总量 - 消费消息总量。消息积压是消费组监控指标中最重要的一项,有一些准实时场景对积压有着严苛的要求,那对消费积压指标的采集和告警就尤为重要。 **消费耗时** 消费耗时是从客户端采集的指标,通过采集客户端消费耗时分布情况检测客户端消费情况。通过消费耗时可以观察到客户端是否有阻塞情况、以及协助使用同学排查定位问题 +#### **消费监控** **消费速度** 通过实时采集消费速度指标,掌握消费组健康情况。同样有些场景对消费速度大小比较关心。通过采集实时消息消费速率情况,为告警提供数据支撑。**消费积压** 消费积压是指某一时刻还有多少消息没有消费,消费积压 = 发送消息总量 - 消费消息总量。消息积压是消费组监控指标中最重要的一项,有一些准实时场景对积压有着严苛的要求,那对消费积压指标的采集和告警就尤为重要。**消费耗时** 消费耗时是从客户端采集的指标,通过采集客户端消费耗时分布情况检测客户端消费情况。通过消费耗时可以观察到客户端是否有阻塞情况、以及协助使用同学排查定位问题 ### 监控开发实战 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25422\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25422\350\256\262.md" index c3110257a..bf3ccffbc 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25422\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25422\350\256\262.md" @@ -32,7 +32,7 @@ nohup sh bin/mqbroker -c conf/broker-a.conf & ![img](assets/20200910105701883.png) -说明:中间缺失部分为掉线,没有采集到的情况。 **系统错误日志一** +说明:中间缺失部分为掉线,没有采集到的情况。**系统错误日志一** ```java 2020-03-16T17:56:07.505715+08:00 VECS0xxxx kernel: \[\] ? \_\_alloc_pages_nodemask+0x7e1/0x960 @@ -61,7 +61,7 @@ nohup sh bin/mqbroker -c conf/broker-a.conf & 34 2020-03-27T10:35:28.777500+08:00 VECS0xxxx kernel: ffff8803ef75baa0 0000000000000082 ffff8803ef75ba68 ffff8803ef75ba64 ``` -说明:系统日志显示错误“page allocation failure”和“blocked for more than 120 second”错误,日志目录 /var/log/messages。 **GC 日志** +说明:系统日志显示错误“page allocation failure”和“blocked for more than 120 second”错误,日志目录 /var/log/messages。**GC 日志** ```sql 2020-03-16T17:49:13.785+0800: 13484510.599: Total time for which application threads were stopped: 0.0072354 seconds, Stopping threads took: 0.0001536 seconds @@ -127,7 +127,7 @@ nohup sh bin/mqbroker -c conf/broker-a.conf & [Times: user=0.05 sys=0.00, real=0.01 secs] ``` -说明:GC 日志正常。 **Broker 错误日志** +说明:GC 日志正常。**Broker 错误日志** ```plaintext 2020-03-16 17:55:15 ERROR BrokerControllerScheduledThread1 - SyncTopicConfig Exception, x.x.x.x:10911 @@ -153,11 +153,11 @@ Linux version 3.10.0-1062.4.1.el7.x86_64 (\[email protected\]) (gcc version 4.8 ```plaintext ### 集群频繁抖动发送超时 -#### **现象描述** 监控和业务同学反馈发送超时,而且频繁出现。具体现象如下图。 **预热现象** ![img](assets/20200910095246617.jpg) +#### **现象描述** 监控和业务同学反馈发送超时,而且频繁出现。具体现象如下图。**预热现象**![img](assets/20200910095246617.jpg) ![img](assets/20200910095328878.jpg) -说明:上图分别为开启预热时(`warmMapedFileEnable=true`)集群的发送 RT 监控、Broker 开启预热设置时的日志。 **内存传输现象** ![CPU 抖动](assets/20200910095358560.jpg) +说明:上图分别为开启预热时(`warmMapedFileEnable=true`)集群的发送 RT 监控、Broker 开启预热设置时的日志。**内存传输现象**![CPU 抖动](assets/20200910095358560.jpg) ![img](assets/20200910095449761.jpg) diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25423\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25423\350\256\262.md" index a642d6976..7b18c4c94 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25423\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25423\350\256\262.md" @@ -28,7 +28,7 @@ this(null, producerGroup, null, enableMsgTrace, customizedTraceTopic); } ``` -说明:enableMsgTrace 是否开启发送轨迹,默认 false;customizedTraceTopic 设置收集消息轨迹的自定义主题,默认为 RMQ\_SYS\_TRACE\_TOPIC。 **发送代码示例** +说明:enableMsgTrace 是否开启发送轨迹,默认 false;customizedTraceTopic 设置收集消息轨迹的自定义主题,默认为 RMQ\_SYS\_TRACE\_TOPIC。**发送代码示例** ```java public static void main(String[] args) throws MQClientException, InterruptedException { @@ -66,7 +66,7 @@ public static void main(String[] args) throws MQClientException, InterruptedExce } ``` -说明:创建 DefaultMQProducer 时将 enableMsgTrace 设置为 true 开启发送消息轨迹。 **3. 消费端使用** **消费轨迹 API** +说明:创建 DefaultMQProducer 时将 enableMsgTrace 设置为 true 开启发送消息轨迹。**3. 消费端使用** **消费轨迹 API** ```plaintext public DefaultMQPushConsumer(final String consumerGroup, boolean enableMsgTrace, final String customizedTraceTopic) { @@ -78,7 +78,7 @@ this(null, consumerGroup, null, new AllocateMessageQueueAveragely(), enableMsgTr } ``` -说明:enableMsgTrace 是否开启消费轨迹,默认 false;customizedTraceTopic 设置收集消息轨迹的自定义主题,默认为 RMQ\_SYS\_TRACE\_TOPIC。 **消费代码示例** +说明:enableMsgTrace 是否开启消费轨迹,默认 false;customizedTraceTopic 设置收集消息轨迹的自定义主题,默认为 RMQ\_SYS\_TRACE\_TOPIC。**消费代码示例** ```java public static void main(String\[\] args) throws InterruptedException, MQClientException { @@ -118,7 +118,7 @@ DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1",true); 说明:创建 DefaultMQPushConsumer 将 enableMsgTrace 设置为 true 开启消费消息轨迹。 -**4. 消息轨迹效果** 通过发送和消费一条消息,在 RocketMQ-Console 中看下消息轨迹的效果截图。 **发送消息内容** +**4. 消息轨迹效果** 通过发送和消费一条消息,在 RocketMQ-Console 中看下消息轨迹的效果截图。**发送消息内容** ```plaintext SendResult [sendStatus=SEND_OK, msgId=A9FE1075810A18B4AAC24A40738B0000, offsetMsgId=A9FE107500002A9F0000000000002147, messageQueue=MessageQueue [topic=TopicTest, brokerName=liangyong, queueId=1], queueOffset=2] @@ -144,7 +144,7 @@ Receive New Messages: [MessageExt [brokerName=liangyong, queueId=1, storeSize=18 ![img](assets/20200918101244746.png) -#### **轨迹格式说明** 消息轨迹类型有三种,Pub 指发送轨迹,SubBefore 指消费前轨迹,SubAfter 指消费后轨迹。 **发送轨迹 Pub** 名称 +#### **轨迹格式说明** 消息轨迹类型有三种,Pub 指发送轨迹,SubBefore 指消费前轨迹,SubAfter 指消费后轨迹。**发送轨迹 Pub** 名称 说明 @@ -277,7 +277,7 @@ Broker 返回的消费状态,0:SUCCESS,1:TIME\_OUT,2:EXCEPTION,3:RETURNN ### ACL -#### **ACL 含义** 访问控制表(Access Control List,ACL)描述用户或角色对资源的访问控制权限,RocketMQ 中的 ACL 见下表说明。 **RocketMQ 中的 ACL 含义说明:** 含义 +#### **ACL 含义** 访问控制表(Access Control List,ACL)描述用户或角色对资源的访问控制权限,RocketMQ 中的 ACL 见下表说明。**RocketMQ 中的 ACL 含义说明:** 含义 说明 @@ -297,7 +297,7 @@ admin 和其他角色 DENY 表示无权限 ANY 表示拥有 PUB 或者 SUB 权限 PUB 表示拥有主题发送权限 SUB 表示拥有消费组订阅权限 -#### **ACL 使用示例** 将 `aclEnable = true` 添加到 Broker 配置文件中,另外添加 `{ROCKETMQ_HOME}/conf/plain_acl.yml` 文件,用于 ACL 控制。 **1. Broker 配置** +#### **ACL 使用示例** 将 `aclEnable = true` 添加到 Broker 配置文件中,另外添加 `{ROCKETMQ_HOME}/conf/plain_acl.yml` 文件,用于 ACL 控制。**1. Broker 配置** ```plaintext brokerClusterName = DefaultCluster @@ -392,7 +392,7 @@ groupPerms 详细的消费组权限 -**3. ACL 发送示例** 在上面的配置文件中,将 TopicTes1 设置了 DENY 权限,即禁止发送和消费;将 TopicTest2 设置成了 PUB|SUB 权限,即允许发送和订阅权限。下面例子尝试向主题 TopicTes1 发送消息,观察其是否可以成功。 **禁止发送示例** +**3. ACL 发送示例** 在上面的配置文件中,将 TopicTes1 设置了 DENY 权限,即禁止发送和消费;将 TopicTest2 设置成了 PUB|SUB 权限,即允许发送和订阅权限。下面例子尝试向主题 TopicTes1 发送消息,观察其是否可以成功。**禁止发送示例** ```java public class AclSendTest { @@ -426,7 +426,7 @@ public class AclSendTest { } ``` -**禁止发送截图** ![img](assets/20200920202144594.jpg) **禁止发送说明** 用户 RocketMQ 向主题 TopicTes1 发送消息时抛出 AclException,拒绝访问,如果将代码中主题换成 TopicTes2,则可以发送成功,接着看下文。 **4. ACL 消费示例** 在上面的配置文件中,将 consumerTest 设置了 DENY 权限,即禁止消费消息。由于 TopicTes2 设置为允许发送,我们下面尝试向 TopicTes2 发送一条消息,consumerTest 订阅了 TopicTes2 观察其是否可以消费。 **允许发送示例** +**禁止发送截图**![img](assets/20200920202144594.jpg) **禁止发送说明** 用户 RocketMQ 向主题 TopicTes1 发送消息时抛出 AclException,拒绝访问,如果将代码中主题换成 TopicTes2,则可以发送成功,接着看下文。**4. ACL 消费示例** 在上面的配置文件中,将 consumerTest 设置了 DENY 权限,即禁止消费消息。由于 TopicTes2 设置为允许发送,我们下面尝试向 TopicTes2 发送一条消息,consumerTest 订阅了 TopicTes2 观察其是否可以消费。**允许发送示例** ```java public class AclSendTest { @@ -544,9 +544,9 @@ private static void printBody(List msg) { } ``` -**禁止消费截图** ![img](assets/20200920203904115.jpg) **禁止消费说明** 我们向 TopicTest2 成功发送了一条消息,但由于消费组 consumerTest 被设置成禁止消费,所有未能收到该消息。 +**禁止消费截图**![img](assets/20200920203904115.jpg) **禁止消费说明** 我们向 TopicTest2 成功发送了一条消息,但由于消费组 consumerTest 被设置成禁止消费,所有未能收到该消息。 -#### **ACL 命令汇总** RocketMQ 提供了一系列命令动态更新 Acl 配置文件,使设置的权限及时生效。 **1. 获取 ACL 配置版本** 使用 clusterAclConfigVersion 命令查看版本信息。 **参数说明** 参数 +#### **ACL 命令汇总** RocketMQ 提供了一系列命令动态更新 Acl 配置文件,使设置的权限及时生效。**1. 获取 ACL 配置版本** 使用 clusterAclConfigVersion 命令查看版本信息。**参数说明** 参数 说明 @@ -571,7 +571,7 @@ DefaultCluster broker-a x.x.x.x:10911 0 get cluster's plain access config version success. ``` -**2. 获取 Acl 权限配置** 使用 getAccessConfigSubCommand 获取 ACL 的配置信息。 **参数说明** 参数 +**2. 获取 Acl 权限配置** 使用 getAccessConfigSubCommand 获取 ACL 的配置信息。**参数说明** 参数 说明 @@ -611,7 +611,7 @@ topicPerms : groupPerms : ``` -**3. 更新全局白名单** 使用 updateGlobalWhiteAddr 对 ACL 的全局白名单 globalWhiteRemoteAddresses 进行变更。 **参数说明** 参数 +**3. 更新全局白名单** 使用 updateGlobalWhiteAddr 对 ACL 的全局白名单 globalWhiteRemoteAddresses 进行变更。**参数说明** 参数 说明 @@ -666,7 +666,7 @@ groupPerms : 说明:全局白名单已经被更新。 -**4. 更新用户配置** 对于用户账户的配置的变更通过 updateAclConfig 来实现。 **参数说明** 参数 +**4. 更新用户配置** 对于用户账户的配置的变更通过 updateAclConfig 来实现。**参数说明** 参数 说明 @@ -749,7 +749,7 @@ groupPerms : \[groupA=DENY, groupB=PUB|SUB, groupC=SUB\] 说明:用户 RocketMQ 的密钥 secretKey 和主题权限 topicPerms 已变更生效。 -**5. 删除用户配置** 通过 deleteAccessConfig 删除指定用户的 ACL 配置信息。 **参数说明** 参数 +**5. 删除用户配置** 通过 deleteAccessConfig 删除指定用户的 ACL 配置信息。**参数说明** 参数 说明 @@ -822,7 +822,7 @@ RocketMQ 开源版本在 4.5.0 版本开始支持多副本(DLedger),在以 多副本使用 Raft 协议在节点意外掉线后能够完成自动选主,提高集群的高可用和保证数据的一致性。 -#### **多副本搭建** 由于 DLedger 基于 Raft 协议开发的功能,需要过半数选举,最少 3 个节点组成一个 Raft 组。 **broker-n0.conf** +#### **多副本搭建** 由于 DLedger 基于 Raft 协议开发的功能,需要过半数选举,最少 3 个节点组成一个 Raft 组。**broker-n0.conf** ```plaintext brokerClusterName = RaftCluster @@ -893,7 +893,7 @@ RaftCluster RaftNode00 1 x.x.x.x:30911 V4_7_0 RaftCluster RaftNode00 3 x.x.x.x:30931 V4_7_0 0.00(0,0ms) 0.00(0,0ms) 0 444663.49 -1.0000 ``` -说明:BID 为 0 表示表示 Master,其他两个均为 Follower。 **控制台截图:** ![img](assets/20200922235326160.jpg) **查看发送消息:** ![img](assets/20200922235557734.jpg) +说明:BID 为 0 表示表示 Master,其他两个均为 Follower。**控制台截图:**![img](assets/20200922235326160.jpg) **查看发送消息:**![img](assets/20200922235557734.jpg) 说明:通过以上步骤,我们完成多副本的搭建过程。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25424\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25424\350\256\262.md" index ea4b71e55..9f34b2cb2 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25424\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25424\350\256\262.md" @@ -1,6 +1,6 @@ # 24 RocketMQ-Console 常用页面指标获取逻辑 -本文的目的不是详细介绍 RocketMQ-Console 的使用方法, **主要对一些关键点(更多是会有疑问的点)进行介绍** ,避免对返回结果进行想当然。 +本文的目的不是详细介绍 RocketMQ-Console 的使用方法,**主要对一些关键点(更多是会有疑问的点)进行介绍**,避免对返回结果进行想当然。 ### 集群信息一览 @@ -40,6 +40,6 @@ RocketMQ 的数据统计是基于时间窗口,并且数据是存储在内存 通常大家会看到 lastTimestamp 的时间会显示 1970 年,这是为什么呢? -首先 lastTimestamp 在“查看消息消费进度”时表示的意思是当前消费到的消息的存储时间, **即消息消费进度中当前的偏移量对应的消息在 Broker 中的存储时间** 。再结合 RocketMQ 消息消费过期删除机制,默认一条消息只存储 3 天,三天过后这条消息会被删除,如果此时一直没有消费,消息消费进度代表的 **当前偏移量所对应的消息已被删除** ,则会显示 1970。 +首先 lastTimestamp 在“查看消息消费进度”时表示的意思是当前消费到的消息的存储时间,**即消息消费进度中当前的偏移量对应的消息在 Broker 中的存储时间** 。再结合 RocketMQ 消息消费过期删除机制,默认一条消息只存储 3 天,三天过后这条消息会被删除,如果此时一直没有消费,消息消费进度代表的 **当前偏移量所对应的消息已被删除**,则会显示 1970。 -RocketMQ 的使用还是比较简单的, **本文重点展示的是一些容易引起“误会”的点** ,暂时想到就只有如上如果大家对 RocketMQ-Console 的使用有有其他一些疑问,欢迎大家加入到官方创建的微信群。 +RocketMQ 的使用还是比较简单的,**本文重点展示的是一些容易引起“误会”的点**,暂时想到就只有如上如果大家对 RocketMQ-Console 的使用有有其他一些疑问,欢迎大家加入到官方创建的微信群。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25425\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25425\350\256\262.md" index 9203f3009..a6f4ad388 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25425\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25425\350\256\262.md" @@ -41,11 +41,11 @@ RocketMQ 的 Nameserver 并没有采用诸如 ZooKeeper 的注册中心,而是 1. Broker 每 30s 向 Nameserver 发送心跳包,心跳包中包含主题的路由信息(主题的读写队列数、操作权限等),Nameserver 会通过 HashMap 更新 Topic 的路由信息,并记录最后一次收到 Broker 的时间戳。 1. Nameserver 以每 10s 的频率清除已宕机的 Broker,Nameserver 认为 Broker 宕机的依据是如果当前系统时间戳减去最后一次收到 Broker 心跳包的时间戳大于 120s。 -1. 消息生产者以每 30s 的频率去拉取主题的路由信息,即消息生产者并不会立即感知 Broker 服务器的新增与删除。 **PULL 模式的一个典型特征是即使注册中心中存储的路由信息发生变化后,客户端无法实时感知,只能依靠客户端的定时更新更新任务,这样会引发一些问题** 。例如大促结束后要对集群进行缩容,对集群进行下线,如果是直接停止进程,由于是网络连接直接断开,Nameserver 能立即感知 Broker 的下线,会及时存储在内存中的路由信息,但并不会立即推送给 Producer、Consumer,而是需要等到 Producer 定时向 Nameserver 更新路由信息,那在更新之前,进行消息队列负载时,会选择已经下线的 Broker 上的队列,这样会造成消息发送失败。 +1. 消息生产者以每 30s 的频率去拉取主题的路由信息,即消息生产者并不会立即感知 Broker 服务器的新增与删除。**PULL 模式的一个典型特征是即使注册中心中存储的路由信息发生变化后,客户端无法实时感知,只能依靠客户端的定时更新更新任务,这样会引发一些问题** 。例如大促结束后要对集群进行缩容,对集群进行下线,如果是直接停止进程,由于是网络连接直接断开,Nameserver 能立即感知 Broker 的下线,会及时存储在内存中的路由信息,但并不会立即推送给 Producer、Consumer,而是需要等到 Producer 定时向 Nameserver 更新路由信息,那在更新之前,进行消息队列负载时,会选择已经下线的 Broker 上的队列,这样会造成消息发送失败。 在 RocketMQ 中 Nameserver 集群中的节点相互之间不通信,各节点相互独立,实现非常简单,但同样会带来一个问题:Topic 的路由信息在各个节点上会出现不一致。 -那 Nameserver 如何解决上述这两个问题呢?RocketMQ 的设计者采取的方案是不解决,即为了保证 Nameserver 的高性能,允许存在这些缺陷,这些缺陷由其使用者去解决。 **由于消息发送端无法及时感知路由信息的变化,引入了消息发送重试与故障规避机制来保证消息的发送高可用** ,这部分内容已经在前面的文章中详细介绍。 +那 Nameserver 如何解决上述这两个问题呢?RocketMQ 的设计者采取的方案是不解决,即为了保证 Nameserver 的高性能,允许存在这些缺陷,这些缺陷由其使用者去解决。**由于消息发送端无法及时感知路由信息的变化,引入了消息发送重试与故障规避机制来保证消息的发送高可用**,这部分内容已经在前面的文章中详细介绍。 那 Nameserver 之间数据的不一致,会造成什么重大问题吗? @@ -57,7 +57,7 @@ Nameserver 数据不一致示例图如下: ![4](assets/20200825230543596.png) -#### **对消息发送端的影响** 正如上图所示,Producer-1 连接 Nameserver-1,而 Producer-2 连接 Nameserver-2,例如这个两个消息发送者都需要发送消息到主题 order_topic。 **由于 Nameserver 中存储的路由信息不一致,对消息发送的影响不大,只是会造成消息分布不均衡** ,会导致消息大部分会发送到 broker-a 上,只要不出现网络分区的情况,Nameserver 中的数据会最终达到一致,数据不均衡问题会很快得到解决。故从消息发送端来看,Nameserver 中路由数据的不一致性并不会产生严重的问题 +#### **对消息发送端的影响** 正如上图所示,Producer-1 连接 Nameserver-1,而 Producer-2 连接 Nameserver-2,例如这个两个消息发送者都需要发送消息到主题 order_topic。**由于 Nameserver 中存储的路由信息不一致,对消息发送的影响不大,只是会造成消息分布不均衡**,会导致消息大部分会发送到 broker-a 上,只要不出现网络分区的情况,Nameserver 中的数据会最终达到一致,数据不均衡问题会很快得到解决。故从消息发送端来看,Nameserver 中路由数据的不一致性并不会产生严重的问题 #### **对消息消费端的影响** @@ -66,6 +66,6 @@ Nameserver 数据不一致示例图如下: - c1:在消息队列负载的时查询到 order_topic 的队列数量为 8 个(broker-a、broker-b 各 2 个),查询到该消费组在线的消费者为 2 个,那按照平均分配算法,会分配到 4 个队列,分别为 broker-a:q0、q1、q2、q3。 - c2:在消息队列负载时查询到 order_topic 的队列个数为 4 个(broker-a),查询到该消费组在线的消费者 2 个,按照平均分配算法,会分配到 2 个队列,由于 c2 在整个消费列表中处于第二个位置,故分配到队列为 broker-a:q2、q3。 -将出现的问题一目了然了吧: **会出现 broker-b 上的队列分配不到消费者,并且 broker-a 上的 q2、q3 这两个队列会被两个消费者同时消费,造成消息的重复处理** ,如果 **消费端实现了幂等** ,也不会造成太大的影响,无法就是有些队列消息未处理,结合监控机制,这种情况很快能被监控并通知人工进行干预。 +将出现的问题一目了然了吧: **会出现 broker-b 上的队列分配不到消费者,并且 broker-a 上的 q2、q3 这两个队列会被两个消费者同时消费,造成消息的重复处理**,如果 **消费端实现了幂等**,也不会造成太大的影响,无法就是有些队列消息未处理,结合监控机制,这种情况很快能被监控并通知人工进行干预。 当然随着 Nameserver 路由信息最终实现一致,同一个消费组中所有消费组,最终维护的路由信息会达到一致,这样消息消费队列最终会被正常负载,故只要消费端实现幂等,造成的影响也是可控的,不会造成不可估量的损失,就是因为这个原因,RocketMQ 的设计者们为了达到简单、高效之目的,在 Nameserver 的设计上允许出现一些缺陷,给我们做架构设计方案时起到了一个非常好的示范作用,无需做到尽善尽美,懂得抉择、权衡。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25426\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25426\350\256\262.md" index b352f0b48..3af005b39 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25426\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25426\350\256\262.md" @@ -149,6 +149,6 @@ JDK 的 Future 模型通常需要一个线程池对象、一个任务请求 Task ![12](assets/20200908221449539.png) -然后调用 thenApply 方法,为 CompletableFuture 注册一个异步回掉,即在异步回掉的时候,将结果通过网络传入到客户端,实现消息发送线程与结果返回的解耦。 **思考:CompletableFuture 的 thenApply 方法在哪个线程中执行呢?** +然后调用 thenApply 方法,为 CompletableFuture 注册一个异步回掉,即在异步回掉的时候,将结果通过网络传入到客户端,实现消息发送线程与结果返回的解耦。**思考:CompletableFuture 的 thenApply 方法在哪个线程中执行呢?** 其实在 CompletableFuture 中会内置一个线程池 ForkJoin 线程池,用来执行器异步回调。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25427\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25427\350\256\262.md" index 43cd08451..f97a27381 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25427\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25427\350\256\262.md" @@ -8,7 +8,7 @@ ![1](assets/20200909224001643.png) -从这里我们可以得到一种通用的数据存储格式定义实践: **通常存储协议遵循 Header + Body** ,并且 **Header 部分是定长** 的,存放一些基本信息,body 存储数据,在 RocketMQ 的消息存储协议,我们可以将消息体的大小这 4 个字节看成是 Header,后面所有的字段认为是与消息相关的业务属性,按照指定格式进行组装即可。 +从这里我们可以得到一种通用的数据存储格式定义实践: **通常存储协议遵循 Header + Body**,并且 **Header 部分是定长** 的,存放一些基本信息,body 存储数据,在 RocketMQ 的消息存储协议,我们可以将消息体的大小这 4 个字节看成是 Header,后面所有的字段认为是与消息相关的业务属性,按照指定格式进行组装即可。 针对 Header + Body 这种协议,我们通常的提取一条消息会分成两个步骤,先将 Header 读取到 ByteBuffer 中,在 RocketMQ 中的消息体,会读出一条消息的长度,然后就可以从 **消息的开头** 处读取该条消息长度的字节,然后就按照预先定义的格式解析各个部分即可。 @@ -28,7 +28,7 @@ ![3](assets/2020090922401933.png) -consumequeue 设计极具技巧性,其每个条目使用固定长度(8 字节 commitlog 物理偏移量、4 字节消息长度、8 字节 tag hashcode),这里不是存储 tag 的原始字符串,而是存储 hashcode,目的就是确保每个条目的长度固定,可以使用访问类似数组下标的方式来快速定位条目,极大的提高了 ConsumeQueue 文件的读取性能。 **故基于文件的存储设计,需要针对性的设计一些索引,索引文件的设计,要确保条目的固定长度,使之可以使用类似访问数组的方式快速定位数据。** +consumequeue 设计极具技巧性,其每个条目使用固定长度(8 字节 commitlog 物理偏移量、4 字节消息长度、8 字节 tag hashcode),这里不是存储 tag 的原始字符串,而是存储 hashcode,目的就是确保每个条目的长度固定,可以使用访问类似数组下标的方式来快速定位条目,极大的提高了 ConsumeQueue 文件的读取性能。**故基于文件的存储设计,需要针对性的设计一些索引,索引文件的设计,要确保条目的固定长度,使之可以使用类似访问数组的方式快速定位数据。** ### 内存映射与页缓存 @@ -52,19 +52,19 @@ MappedByteBuffer mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, **在 Linux 操作系统中,MappedByteBuffer 基本可以看成是页缓存(PageCache)。** 在 Linux 操作系统中的内存使用策略时,会最大可能的利用机器的物理内存,并常驻内存中,就是所谓的页缓存,只有当操作系统的内存不够的情况下,会采用缓存置换算法例如 LRU,将不常用的页缓存回收,即操作系统会自动管理这部分内存,无需使用者关心。如果从页缓存中查询数据时未命中,会产生缺页中断,由操作系统自动将文件中的内容加载到页缓存。 -内存映射,将磁盘数据映射到磁盘,通过向内存映射中写入数据,这些数据并不会立即同步到磁盘,需用定时刷盘或由操作系统决定何时将数据持久化到磁盘。故存储的在页缓存的中的数据,如果 RocketMQ Broker 进程异常退出,存储在页缓存中的数据并不会丢失,操作系统会定时页缓存中的数据持久化到磁盘,做到安全可靠。 **不过如果是机器断电等异常情况,存储在页缓存中的数据就有可能丢失。** ### 顺序写 +内存映射,将磁盘数据映射到磁盘,通过向内存映射中写入数据,这些数据并不会立即同步到磁盘,需用定时刷盘或由操作系统决定何时将数据持久化到磁盘。故存储的在页缓存的中的数据,如果 RocketMQ Broker 进程异常退出,存储在页缓存中的数据并不会丢失,操作系统会定时页缓存中的数据持久化到磁盘,做到安全可靠。**不过如果是机器断电等异常情况,存储在页缓存中的数据就有可能丢失。** ### 顺序写 基于磁盘的读写,提高其写入性能的另外一个设计原理是 **磁盘顺序写** 。磁盘顺序写广泛用在基于文件的存储模型中,大家不妨思考一下 MySQL Redo 日志的引入目的,我们知道在 MySQL InnoDB 的存储引擎中,会有一个内存 Pool,用来缓存磁盘的文件块,当更新语句将数据修改后,会首先在内存中进行修改,然后将变更写入到 redo 文件(关键是会执行一次 force,同步刷盘,确保数据被持久化到磁盘中),但此时并不会同步数据文件,其操作流程如下图所示: ![5](assets/20200909224036499.png) -如果不引入 redo,更新 order,更新 user,首先会更新 InnoDB Pool(更新内存),然后定时刷写到磁盘,由于不同的表对应的数据文件不一致,故如果每更新内存中的数据就刷盘,那就是大量的随机写磁盘,性能低下,故为了避免这个问题,首先引入一个顺序写 redo 日志,然后定时同步内存中的数据到数据文件,虽然引入了多余的 redo 顺序写,但整体上获得的性能更好,从这里也可以看出顺序写的性能比随机写要高不少。 **故基于文件的编程模型中,设计时一定要设计成顺序写,顺序写一个非常的特点是只追究,不更新。** ### 引用计数器 +如果不引入 redo,更新 order,更新 user,首先会更新 InnoDB Pool(更新内存),然后定时刷写到磁盘,由于不同的表对应的数据文件不一致,故如果每更新内存中的数据就刷盘,那就是大量的随机写磁盘,性能低下,故为了避免这个问题,首先引入一个顺序写 redo 日志,然后定时同步内存中的数据到数据文件,虽然引入了多余的 redo 顺序写,但整体上获得的性能更好,从这里也可以看出顺序写的性能比随机写要高不少。**故基于文件的编程模型中,设计时一定要设计成顺序写,顺序写一个非常的特点是只追究,不更新。** ### 引用计数器 在面向文件基于 NIO 的编程中,基本都是面向 ByteBuffer 进行编程,并且对 ByteBuffer 进行读操作,通常会使用其 slince 方法,两个 ByteBuffer 对象的内存地址相同,但指针不一样,通常使用示例如下: ![6](assets/2020090922404497.png) -上面的方法的作用就是从一个映射文件,例如 commitlog、ConsumeQueue 文件中的某一个位置读取指定长度的数据,这里就是从内存映射 MappedBytebuffer slice 一个对象,共享其内部的存储,但维护独立的指针,这样做的好处就是避免了内存的拷贝,但与之带来的弊端就是较难管理,主要是 ByteBuffer 对象的释放会变得复杂起来。 **需要跟踪该 MappedByteBuffer 会 slice 多少次** ,在这些对象的声明周期没有结束后,不能随意的关闭 MappedByteBuffer,否则其他对象的内存无法访问,造成不可控制的错误,那 RocketMQ 是如何解决这个问题的呢? **其解决方案是引入了引用计数器** ,即每次 slice 后 引用计数器增加一,释放后引用计数器减一,只有当前的引用计数器为 0,才可以真正释放。在 RocketMQ 中关于引用计数的实现如下: +上面的方法的作用就是从一个映射文件,例如 commitlog、ConsumeQueue 文件中的某一个位置读取指定长度的数据,这里就是从内存映射 MappedBytebuffer slice 一个对象,共享其内部的存储,但维护独立的指针,这样做的好处就是避免了内存的拷贝,但与之带来的弊端就是较难管理,主要是 ByteBuffer 对象的释放会变得复杂起来。**需要跟踪该 MappedByteBuffer 会 slice 多少次**,在这些对象的声明周期没有结束后,不能随意的关闭 MappedByteBuffer,否则其他对象的内存无法访问,造成不可控制的错误,那 RocketMQ 是如何解决这个问题的呢?**其解决方案是引入了引用计数器**,即每次 slice 后 引用计数器增加一,释放后引用计数器减一,只有当前的引用计数器为 0,才可以真正释放。在 RocketMQ 中关于引用计数的实现如下: ![7](assets/20200909224052719.png) diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25428\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25428\350\256\262.md" index afffdf759..a0cc01931 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25428\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25428\350\256\262.md" @@ -4,13 +4,13 @@ 基于文件的编程模型中为了提高文件的写入性能,通常会引入内存映射机制,但凡事都有利弊,引入了内存映射、页缓存等机制,数据首先写入到页缓存,此时并没有真正的持久化到磁盘,那 Broker 收到客户端的消息发送请求时是存储到页缓存中就直接返回成功,还是要持久化到磁盘中才返回成功呢? -这里又是一个抉择,是在性能与消息可靠性方面进行的权衡,为此 RocketMQ 提供了多种持久化策略: **同步刷盘、异步刷盘。** **“刷盘”** 这个名词是不是听起来很高大上,其实这并不是一个什么神秘高深的词语,所谓的 **刷盘就是将内存中的数据同步到磁盘** ,在代码层面其实是调用了 **FileChannel 或 MappedBytebuffer 的 force 方法** ,其截图如下: +这里又是一个抉择,是在性能与消息可靠性方面进行的权衡,为此 RocketMQ 提供了多种持久化策略: **同步刷盘、异步刷盘。** **“刷盘”** 这个名词是不是听起来很高大上,其实这并不是一个什么神秘高深的词语,所谓的 **刷盘就是将内存中的数据同步到磁盘**,在代码层面其实是调用了 **FileChannel 或 MappedBytebuffer 的 force 方法**,其截图如下: ![1](assets/20200912110913230.png) 接下来分别介绍同步刷盘与异步刷盘的实现技巧。 -#### **同步刷盘** 同步刷盘指的 Broker 端收到消息发送者的消息后,先写入内存,然后同时将内容持久化到磁盘后才向客户端返回消息发送成功。 **提出思考:那在 RocketMQ 的同步刷盘是一次消息写入就只将一条消息刷写到磁盘?答案是否定的。** +#### **同步刷盘** 同步刷盘指的 Broker 端收到消息发送者的消息后,先写入内存,然后同时将内容持久化到磁盘后才向客户端返回消息发送成功。**提出思考:那在 RocketMQ 的同步刷盘是一次消息写入就只将一条消息刷写到磁盘?答案是否定的。** 在 RocketMQ 中同步刷盘的入口为 commitlog 的 handleDiskFlush,同步刷盘的截图如下: @@ -96,7 +96,7 @@ RocketMQ 正常退出时可以从倒数第三个文件开始恢复,这个看 基于 checkpoint 文件的特点,异常退出时定位文件恢复的策略如下: - 恢复 ConsumeQueue 时是按照 topic 进行恢复的,从第一文件开始恢复。 -- 从最后一个 commitlog 文件逐步向前寻找,在寻找时读取该文件中的 **第一条消息的存储时间** ,如果这个存储时间小于 checkpoint 文件中的刷盘时间,就可以从这个文件开始恢复,如果这个文件中第一条消息的存储时间大于刷盘点,说明不能从这个文件开始恢复,需要找上一个文件,因为 checkpoint 的文件中的刷盘点代表的是百分之百可靠的消息。 +- 从最后一个 commitlog 文件逐步向前寻找,在寻找时读取该文件中的 **第一条消息的存储时间**,如果这个存储时间小于 checkpoint 文件中的刷盘时间,就可以从这个文件开始恢复,如果这个文件中第一条消息的存储时间大于刷盘点,说明不能从这个文件开始恢复,需要找上一个文件,因为 checkpoint 的文件中的刷盘点代表的是百分之百可靠的消息。 文件恢复的具体代码我这里就不做过多阐述了,根据上面的设计理念,自己顺藤摸瓜,效果应该会事半功倍。文件恢复的入口:DefaultMessageStore#recover。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25429\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25429\350\256\262.md" index da0341198..9c65ed58a 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25429\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25429\350\256\262.md" @@ -72,7 +72,7 @@ void invokeOneway(Channel channel, RemotingCommand request, long timeoutMillis) Oneway 请求调用。 -#### **NettyRemotingAbstract** Netty 远程服务抽象实现类,定义网络远程调用、请求,响应等处理逻辑,其核心方法与核心方法的设计理念如下。 **NettyRemotingAbstract 核心属性:** +#### **NettyRemotingAbstract** Netty 远程服务抽象实现类,定义网络远程调用、请求,响应等处理逻辑,其核心方法与核心方法的设计理念如下。**NettyRemotingAbstract 核心属性:** - Semaphore semaphoreOneway:控制 oneway 发送方式的并发度的信号量,默认为 65535 个许可。 @@ -136,8 +136,8 @@ Oneway 请求调用。 > 上述流程图将省略 NettyRemotingClient、NettyRemotingServer 的初始化流程,因为这些将在下文详细阐述。 - NettyRemotingClient 会在需要连接到指定地址先通过 Netty 相关 API 创建 Channel,并进行缓存,下一次请求如果还是发送到该地址时可重复利用。 -- 然后调用 NettyRemotingClient 的 invokeAsync 等方法进行网络发送,在发送时在 Netty 中会进行一个 **非常重要的步骤:对请求编码** ,主要是将需要发送的请求,例如 RemotingCommand,将该对象按照 **特定的格式(协议)** 转换成二进制流。 -- NettyRemotingServer 端接收到二进制后,网络读请求就绪,进行读请求事件处理流程。首先需要从 **二进制流中识别一个完整的请求包,这就是所谓的解码** ,即将二进制流转换为请求对象,解码成 RemotingCommand,然后读事件会传播到 NettyServerHandler,最终执行 NettyRemotingAbstract 的 processRequestCommand,主要是根据 requestCode 获取指定的命令执行线程池与 NettyRequestProcessor,并执行对应的逻辑,然后通过网络将执行结果返回给客户端。 +- 然后调用 NettyRemotingClient 的 invokeAsync 等方法进行网络发送,在发送时在 Netty 中会进行一个 **非常重要的步骤:对请求编码**,主要是将需要发送的请求,例如 RemotingCommand,将该对象按照 **特定的格式(协议)** 转换成二进制流。 +- NettyRemotingServer 端接收到二进制后,网络读请求就绪,进行读请求事件处理流程。首先需要从 **二进制流中识别一个完整的请求包,这就是所谓的解码**,即将二进制流转换为请求对象,解码成 RemotingCommand,然后读事件会传播到 NettyServerHandler,最终执行 NettyRemotingAbstract 的 processRequestCommand,主要是根据 requestCode 获取指定的命令执行线程池与 NettyRequestProcessor,并执行对应的逻辑,然后通过网络将执行结果返回给客户端。 - 客户端收到服务端的响应后,读事件触发,执行解码(NettyDecoder),然后读事件会传播到 NettyClientHandler,并处理响应结果。 ### Netty 网络编程要点 @@ -157,8 +157,8 @@ Oneway 请求调用。 例如一个订单服务 order-serevice-app,用户会发起多个下单服务,在 order-service-app 中就会对应多个线程,订单服务需要调用优惠券相关的微服务,多个线程通过 dubbo client 向优惠券发起 RPC 调用,这个过程至少需要做哪些操作呢? 1. 创建 TCP 连接,默认情况下 Dubbo 客户端和 Dubbo 服务端会保持一条长连接,用一条连接发送该客户端到服务端的所有网络请求。 -1. 将请求转换为二进制流,试想一下,多个请求依次通过一条连接发送消息,那服务端如何从二级制流中解析出一个完整的请求呢,例如 Dubbo 请求的请求体中至少需要封装需要调用的远程服务名、请求参数等。这里其实就是涉及所谓的 **自定义协议** ,即需要制定一套 **通信规范** 。 -1. 客户端根据通信协议对将请求转换为二进制的过程称之为 **编码** ,服务端根据通信协议从二级制流中识别出一个个请求,称之为 **解码** 。 +1. 将请求转换为二进制流,试想一下,多个请求依次通过一条连接发送消息,那服务端如何从二级制流中解析出一个完整的请求呢,例如 Dubbo 请求的请求体中至少需要封装需要调用的远程服务名、请求参数等。这里其实就是涉及所谓的 **自定义协议**,即需要制定一套 **通信规范** 。 +1. 客户端根据通信协议对将请求转换为二进制的过程称之为 **编码**,服务端根据通信协议从二级制流中识别出一个个请求,称之为 **解码** 。 1. 服务端解码请求后,需要按照请求执行对应的业务逻辑处理,这里在网络通信中通常涉及到两类线程:IO 线程和业务线程池,通常 IO 线程负责请求解析,而业务线程池执行业务逻辑,最大可能的解耦 IO 读写与业务的处理逻辑。 接下来我们将从 RocketMQ 中是如何使用的,从而来探究 Netty 的学习与使用。 @@ -234,11 +234,11 @@ public boolean isOK() { ![8](assets/20200920112528931.png) -通常 Boos Group 默认使用一个线程,而 Work 线程组通常为 CPU 的合数,Work 线程组通常为 IO 线程池,处理读写事件。 **Step2** :创建默认事件执行线程组。 +通常 Boos Group 默认使用一个线程,而 Work 线程组通常为 CPU 的合数,Work 线程组通常为 IO 线程池,处理读写事件。**Step2** :创建默认事件执行线程组。 ![9](assets/20200920112618407.png) -关于该线程池的作用与客户端类似,故不重复介绍。 **Step3** :使用 Netty ServerBootstrap 服务端启动类构建服务端。(模板) +关于该线程池的作用与客户端类似,故不重复介绍。**Step3** :使用 Netty ServerBootstrap 服务端启动类构建服务端。(模板) ![10](assets/20200920112639194.png) @@ -255,11 +255,11 @@ public boolean isOK() { ![11](assets/20200920112647675.png) -ServerBootstrap 的 bind 的方法是一个非阻塞方法,调用 sync() 方法会变成阻塞方法,即等待服务端启动完成。 **2. Netty ServerHandler 编写示例** 服务端在网络通信方面无非就是接受请求并处理,然后将响应发送到客户端,处理请求的入口通常通过定义 ChannelHandler,我们来看一下 RocketMQ 中编写的 Handler。 +ServerBootstrap 的 bind 的方法是一个非阻塞方法,调用 sync() 方法会变成阻塞方法,即等待服务端启动完成。**2. Netty ServerHandler 编写示例** 服务端在网络通信方面无非就是接受请求并处理,然后将响应发送到客户端,处理请求的入口通常通过定义 ChannelHandler,我们来看一下 RocketMQ 中编写的 Handler。 ![12](assets/20200920112656230.png) -服务端的业务处理 Handler 主要是接受客户端的请求,故通常关注的是读事件,可以通常继承 SimpleChannelInboundHandler,并实现 channelRead0,由于已经经过了解码器(NettyDecoder),已经将请求解码成具体的请求对象了,在 RocketMQ 中使用 RemotingCommand 对象,只需面向该对象进行编程,processMessageReceived 该方法是 NettyRemotingClient、NettyRemotingServer 的父类,故对于服务端来会调用 processReqeustCommand 方法。 **在基于 Netty4 的编程,在 ChannelHandler 加上@ChannelHandler.Sharable 可实现线程安全。** > 温馨提示:在 ChannelHandler 中通常不会执行具体的业务逻辑,通常是只负责请求的分发,其背后会引入线程池进行异步解耦,在 RocketMQ 的实现中更加如此,在 RocketMQ 提供了基于“业务”的线程池隔离,例如会为消息发送、消息拉取分别创建不同的线程池。这部分内容将在下文详细介绍。 +服务端的业务处理 Handler 主要是接受客户端的请求,故通常关注的是读事件,可以通常继承 SimpleChannelInboundHandler,并实现 channelRead0,由于已经经过了解码器(NettyDecoder),已经将请求解码成具体的请求对象了,在 RocketMQ 中使用 RemotingCommand 对象,只需面向该对象进行编程,processMessageReceived 该方法是 NettyRemotingClient、NettyRemotingServer 的父类,故对于服务端来会调用 processReqeustCommand 方法。**在基于 Netty4 的编程,在 ChannelHandler 加上@ChannelHandler.Sharable 可实现线程安全。** > 温馨提示:在 ChannelHandler 中通常不会执行具体的业务逻辑,通常是只负责请求的分发,其背后会引入线程池进行异步解耦,在 RocketMQ 的实现中更加如此,在 RocketMQ 提供了基于“业务”的线程池隔离,例如会为消息发送、消息拉取分别创建不同的线程池。这部分内容将在下文详细介绍。 #### **协议编码解码器** @@ -280,7 +280,7 @@ ServerBootstrap 的 bind 的方法是一个非阻塞方法,调用 sync() 方 ### 线程隔离机制 -通常服务端接收请求,经过解码器解码后转换成请求对象,服务端需要根据请求对象进行对应的业务处理,避免业务处理阻塞 IO 读取线程,通常业务的处理会采用额外的线程池,即 **业务线程池** ,RocketMQ 在这块采用的方式值得我们借鉴,提供了不同业务采用不同的线程池,实现线程隔离机制。 +通常服务端接收请求,经过解码器解码后转换成请求对象,服务端需要根据请求对象进行对应的业务处理,避免业务处理阻塞 IO 读取线程,通常业务的处理会采用额外的线程池,即 **业务线程池**,RocketMQ 在这块采用的方式值得我们借鉴,提供了不同业务采用不同的线程池,实现线程隔离机制。 RocketMQ 为每一个请求进行编码,然后每一类请求会对应一个 Proccess(业务处理逻辑),并且将 Process 注册到指定线程池,实现线程隔离机制。 diff --git "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25430\350\256\262.md" "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25430\350\256\262.md" index 0fc2eee7c..759658a98 100644 --- "a/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25430\350\256\262.md" +++ "b/docs/Middleware/RocketMQ \345\256\236\346\210\230\344\270\216\350\277\233\351\230\266/\347\254\25430\350\256\262.md" @@ -8,7 +8,7 @@ **笔者个人的建议** :在无法实际使用时,应该去研究主流技术的原理,为使用做好准备,不要因为没有接触到而放弃学习,机会是留给有准备的人,如果你对某一项技术研究有一定深度时,当项目中需要使用时,你可以立马施展你的才华,很容易脱颖而出。 -我在学习 RocketMQ 之前在实际工作中没有接触过任意一款消息中间件,更别谈使用了,促成我学习 RocketMQ 的原因是我得知 RocketMQ 被捐献给 Apache 基金会,而且还听说 RocketMQ 支撑了阿里双十一的具大流量,让我比较好奇,想一睹一款高性能的分布式消息中间件的风采,从此踏上了学习 RocketMQ 的历程。 **确定好目标后,该怎么学习 RocketMQ 呢?** **1. 通读 RocketMQ 官方设计手册** +我在学习 RocketMQ 之前在实际工作中没有接触过任意一款消息中间件,更别谈使用了,促成我学习 RocketMQ 的原因是我得知 RocketMQ 被捐献给 Apache 基金会,而且还听说 RocketMQ 支撑了阿里双十一的具大流量,让我比较好奇,想一睹一款高性能的分布式消息中间件的风采,从此踏上了学习 RocketMQ 的历程。**确定好目标后,该怎么学习 RocketMQ 呢?** **1. 通读 RocketMQ 官方设计手册** 通常开始学习一个开源框架(产品),建议大家首先去官网查看其用户手册、设计手册,从而对该框架能解决什么问题,基本的工作原理、涵盖了哪些知识点(后续可以对这些知识点一一突破),从全局上掌握这块中间件。我还清晰的记得我在看 RokcetMQ 设计手册时,我不仅将一些属于理解透彻,并且一些与性能方面的“高级”名词深深的吸引了我,例如: @@ -21,7 +21,7 @@ 看过设计手册后,让我产生了极大的兴趣,下决心从源码角度对其进行深入研究,立下了不仅深入研究 RocketMQ 的工作原理与实现细节,更是想掌握基于文件编程的设计理念,如何在实践中使用内存映射。 -**2. 下载源码,跑通 Demo** 每一个开源框架,都会提供完备的测试案例,RokcetMQ 也不例外,RokcetMQ 的源码中有一个单独的模块 example,里面放了很多使用 Demo,按需运行一些测试用例,能让你掌握 RokcetMQ 的基本使用,算是入了一个门。 **3. 源码研究 RocketMQ** +**2. 下载源码,跑通 Demo** 每一个开源框架,都会提供完备的测试案例,RokcetMQ 也不例外,RokcetMQ 的源码中有一个单独的模块 example,里面放了很多使用 Demo,按需运行一些测试用例,能让你掌握 RokcetMQ 的基本使用,算是入了一个门。**3. 源码研究 RocketMQ** 通过前面两个步骤,对设计原理有了一个全局的理解,同时掌握了 RocketMQ 的基本使用,接下来需要深入探究 RocketMQ,特别是如果大家认真阅读了本专栏的所有实战类文章,那是时候研究其源码了。 @@ -30,7 +30,7 @@ - 深入研究其实现原理,成体系化的研读 RocketMQ,对 RocketMQ 更具掌控性。通常对应消息中间件,如果出现故障,通常会给公司业务造成较大损失,当出现问题时快速止血固然重要,更难能可贵的时预判风险,避免生产故障发生,要做到预判风险,成体系化研究 RocketMQ 显得非常必要。 - 学习优秀的 RocketMQ 框架,提升编程技能,例如高并发、基于文件编程相关的技巧,我们都可以从中得到一些启发。 -**那如何阅读 RocketMQ 源码呢?** 阅读源码之前还是需要具备一定的基础,建议在阅读 RokcetMQ 源码之前,先尝试阅读一下 Java 数据结构相关的源码,例如 HashMap、ArrayList,主要是培养自己阅读源码的方法, **通常我阅读源码的方法:先主流程、后分支流程** 。 +**那如何阅读 RocketMQ 源码呢?** 阅读源码之前还是需要具备一定的基础,建议在阅读 RokcetMQ 源码之前,先尝试阅读一下 Java 数据结构相关的源码,例如 HashMap、ArrayList,主要是培养自己阅读源码的方法,**通常我阅读源码的方法:先主流程、后分支流程** 。 我举一个简单的例子来说明先主流程、后分支流程。 diff --git "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25408\350\256\262.md" "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25408\350\256\262.md" index c33da9bcc..a658470fd 100644 --- "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25408\350\256\262.md" +++ "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25408\350\256\262.md" @@ -12,17 +12,17 @@ 服务可以对应成一个“应用”,这个应用由很多函数共同组成,这些函数具有相同的访问权限、网络配置,日志也记录到相同的 logstore。这些函数本身的配置可以各不相同,比如同一服务下有的函数内存是3G,有的函数内存是 512M,有些函数用 Python 写,有些函数用 Node.js 写。 -当然,如果应用比较复杂,同一个应用也可以对应多个服务,这里没有强绑定关系。 **1)服务配置** ![image.png](assets/2020-08-10-091918.png) +当然,如果应用比较复杂,同一个应用也可以对应多个服务,这里没有强绑定关系。**1)服务配置**![image.png](assets/2020-08-10-091918.png) 接下来我们介绍服务的几个核心配置: **日志配置:** 开发者的代码在函数计算平台运行,如何查看函数运行产生的日志呢?在Server 化的开发方式中,日志都打到统一的文件中,通过 Logstash/Fluentd 这种日志收集工具收集到 ElasticSearch 中,并通过 Kibana 这种可视化工具查看日志及指标。但是在函数计算中,运行代码的机器由函数计算动态分配,开发者无法自己收集日志,函数计算需要帮助开发者投递日志。日志配置就是起到这个作用,配置 LogConfig 设置日志服务的 Project 和 Logstore,函数计算会将函数运行中产生的日志投递到开发者的 Logstore 里。 -但是为了成功投递日志,单单配置 Logtore 还不够,函数计算是没有权限向开发者的 Logstore 投递日志的,需要用户授予函数计算向指定的 Logstore 写数据的权限,有了这个授权后,函数计算就可以名正言顺地向开发者的 Logstore 投递日志了。 **文件存储配置:** 函数计算中每个函数都是独立的,在不同的执行环境中执行,如果用户有一些公共文件希望多个函数共享怎么办呢?在传统 Server 化的开发方式中,好办,将公共文件放到磁盘就好了,各个都去磁盘的同一位置读取,函数计算中的机器是函数计算动态分配的,开发者无法事先将文件存入磁盘,那怎么办呢?可以挂载 NAS,在服务中挂载 NAS 后,函数就可以像访问本地文件系统一样访问 NAS 上的文件了。 **网络配置:** 网络配置顾名思义就是设置函数的网络访问能力,主要有两种,一个是函数中是否可以访问公网,这是个布尔型的开关,默认是开启的,如果不需要访问公网可以关闭开关。另一个是函数是否可以访问指定 VPC,VPC 是专有网络,专有网络内的数据比较机密,是不能通过公共互联网访问的。如果需要函数访问 VPC 内的资源,比如函数需要访问 VPC 内的 RDS,那就需要授予函数计算访问指定 VPC 的能力,原理是用户授权赋予弹性网卡 ENI 访问 VPC ,并将此 ENI 插入到 FC 中执行用户函数的机器上,从而使函数可以访问 VPC 内资源。 **权限:** 函数计算是云原生的架构,与云上许多服务产生交互,阿里云有非常严格的权限限制,函数计算是没有能力访问开发者的其他云资源的,当开发者需要函数计算访问其他云服务的时候就需要显示授予函数计算权限。 +但是为了成功投递日志,单单配置 Logtore 还不够,函数计算是没有权限向开发者的 Logstore 投递日志的,需要用户授予函数计算向指定的 Logstore 写数据的权限,有了这个授权后,函数计算就可以名正言顺地向开发者的 Logstore 投递日志了。**文件存储配置:** 函数计算中每个函数都是独立的,在不同的执行环境中执行,如果用户有一些公共文件希望多个函数共享怎么办呢?在传统 Server 化的开发方式中,好办,将公共文件放到磁盘就好了,各个都去磁盘的同一位置读取,函数计算中的机器是函数计算动态分配的,开发者无法事先将文件存入磁盘,那怎么办呢?可以挂载 NAS,在服务中挂载 NAS 后,函数就可以像访问本地文件系统一样访问 NAS 上的文件了。**网络配置:** 网络配置顾名思义就是设置函数的网络访问能力,主要有两种,一个是函数中是否可以访问公网,这是个布尔型的开关,默认是开启的,如果不需要访问公网可以关闭开关。另一个是函数是否可以访问指定 VPC,VPC 是专有网络,专有网络内的数据比较机密,是不能通过公共互联网访问的。如果需要函数访问 VPC 内的资源,比如函数需要访问 VPC 内的 RDS,那就需要授予函数计算访问指定 VPC 的能力,原理是用户授权赋予弹性网卡 ENI 访问 VPC ,并将此 ENI 插入到 FC 中执行用户函数的机器上,从而使函数可以访问 VPC 内资源。**权限:** 函数计算是云原生的架构,与云上许多服务产生交互,阿里云有非常严格的权限限制,函数计算是没有能力访问开发者的其他云资源的,当开发者需要函数计算访问其他云服务的时候就需要显示授予函数计算权限。 权限主要有两个应用场景:一个是授予函数计算访问其他服务的权限,比如刚才提到的授权函数计算访问开发者的日志服务、授权函数计算创建 ENI。另一个是授权函数可以访问开发者的云资源,这个是什么呢?举个例子,函数中需要访问 OSS 获取对象,但是又不想暴露 AK,那怎么办呢?开发者可以配置服务中的 Role 有访问 OSS 的权限,函数执行过程中,函数计算会 assumeRole 生成一个临时 AK ,并将这个 AK 存储到函数的上下文 `context.credentials` 里,开发者在代码中使用``` context.credentials.access_key_id``/``context.credentials.access_key_secret``/``context.credentials``.``security_token``  ``` 去创建 OSS Client 就可以了。 #### 2. 函数 -“函数计算”中函数可谓是核心概念,函数是管理、运行的基本单元,一个函数通常由一系列配置与可运行代码包组成。 **1)函数配置** +“函数计算”中函数可谓是核心概念,函数是管理、运行的基本单元,一个函数通常由一系列配置与可运行代码包组成。**1)函数配置** ![image.png](assets/2020-08-10-091919.png) @@ -50,7 +50,7 @@ 版本相当于服务的快照,包括服务的配置、服务内的函数代码及函数配置。当您开发和测试完成后,就发布一个版本,版本单调递增,版本发布后,已发布的版本不能更改,您可以继续在 Latest 版本上开发测试,不会影响已发布的版本。调用函数时,只需要指定版本就可以调用指定版本的函数。 -那新问题又来了,版本名称是函数计算指定的单调递增的,每次发布版本,都会有一个新的版本, **那每次发完版本后,客户端还要改代码执行最新的版本吗?** 为了解决这个问题呢,我们引入了别名,别名就是指向特定服务版本的指针,发布后,只需要将别名指向发布的版本,再次发布后,再切换别名指向最新的版本,客户端只需要指定别名就可以保证调用线上最新的代码。同时别名支持灰度发布的功能,即有 10% 的流量指向最新版本,90% 理论指向老版本。回滚也非常简单,只需要将别名指向之前的版本即可快速完成回滚。 +那新问题又来了,版本名称是函数计算指定的单调递增的,每次发布版本,都会有一个新的版本,**那每次发完版本后,客户端还要改代码执行最新的版本吗?** 为了解决这个问题呢,我们引入了别名,别名就是指向特定服务版本的指针,发布后,只需要将别名指向发布的版本,再次发布后,再切换别名指向最新的版本,客户端只需要指定别名就可以保证调用线上最新的代码。同时别名支持灰度发布的功能,即有 10% 的流量指向最新版本,90% 理论指向老版本。回滚也非常简单,只需要将别名指向之前的版本即可快速完成回滚。 ### 开发流程 diff --git "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25409\350\256\262.md" "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25409\350\256\262.md" index def0821d5..ac2761f2b 100644 --- "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25409\350\256\262.md" +++ "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25409\350\256\262.md" @@ -24,17 +24,17 @@ 但是稍微麻烦一些的项目,可能就不太适合在线上调试了,这时,我们就需要本地开发和调试方法,毕竟大部分代码开发应该都在本地,虽然大家都说云端写代码、开发、debug 是未来的趋势,但是至少目前来看,还是本地开发更习惯、更靠谱。 -所以这时就需要用我们的插件或者命令行工具了。 **2)命令行工具** ![图片 2.jpg](assets/2020-08-31-034450.jpg) +所以这时就需要用我们的插件或者命令行工具了。**2)命令行工具**![图片 2.jpg](assets/2020-08-31-034450.jpg) (命令行工具本地调试) -我们在安装之后,如果想进行本地调试,还要安装 Docker,安装之后,我们可以通过 invoke local 的指令来进行本地的调试。例如上图中,我们可以看到,当我执行完了 fun local invoke demo_03/demo_03,顺利输出了结果。当然如果你是第一次使用,可能还会涉及到通过 Docker 拉取镜像的过程。 **3)VSCode 插件** 如果要在编辑器中写代码,该怎么调试?非常简单,使用 VSCode 插件,你只需要点击 VSCode 插件的运行功能,插件就可以自动拉起 Docker,帮助我们本地调试代码。 +我们在安装之后,如果想进行本地调试,还要安装 Docker,安装之后,我们可以通过 invoke local 的指令来进行本地的调试。例如上图中,我们可以看到,当我执行完了 fun local invoke demo_03/demo_03,顺利输出了结果。当然如果你是第一次使用,可能还会涉及到通过 Docker 拉取镜像的过程。**3)VSCode 插件** 如果要在编辑器中写代码,该怎么调试?非常简单,使用 VSCode 插件,你只需要点击 VSCode 插件的运行功能,插件就可以自动拉起 Docker,帮助我们本地调试代码。 ![图片 3.jpg](assets/2020-08-31-034451.jpg) 从上图中可以看到,我们已经顺利输出了结果。 -这时就会有人问:还要安装 Docker 吗?没有 Docker 行不行?没有 Docker 当然是不行的,因为这个调试的机制本身就依赖 Docker。但是我们人类往往是具有创造力的:没有条件,就创造条件,所以,下面再给大家分享一个无工具的调试方案。 **4)无工具调试** ![图片 4.jpg](assets/2020-08-31-034453.jpg) +这时就会有人问:还要安装 Docker 吗?没有 Docker 行不行?没有 Docker 当然是不行的,因为这个调试的机制本身就依赖 Docker。但是我们人类往往是具有创造力的:没有条件,就创造条件,所以,下面再给大家分享一个无工具的调试方案。**4)无工具调试**![图片 4.jpg](assets/2020-08-31-034453.jpg) 如上图,以 Python 为例,我们只需要增加一段代码,来调用我们的方法,至于 event 可以采用我们即将使用的触发器情况,这样就可以实现简单的调试方法了。 diff --git "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25411\350\256\262.md" "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25411\350\256\262.md" index dd84ba16b..b492d6d25 100644 --- "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25411\350\256\262.md" +++ "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25411\350\256\262.md" @@ -20,16 +20,16 @@ 在函数计算中如何查看函数日志呢?在传统服务器开发方式中,可以将日志记录到磁盘中的某个文件中,再通过日志收集工具收集文件的内容;而在函数计算中,开发者不需要维护服务器了,那如何收集代码里打印的日志呢? -**1)配置日志** 函数计算与日志服务无缝集成,可以将函数日志记录到开发者提供的日志仓库(Logstore)中。日志是服务配置中的一项,为服务配置 LogProject 和 Logstore,同一服务下所有函数通过 stdout 打印的日志,都会收集到对应的 Logstore 中。 **2)记录日志** 那日志怎么打呢?在代码中直接通过 console.log/print 打印的日志可以收集到吗?答案是可以的。各个开发语言提供的打印日志的库都将日志打印到 stdout,比如 node.js 的 console.log()、python 的 print()、golang 的 fmt.Println() 等。函数计算收集所有打印到 stdout 的日志并将其上传到 Logstore 中。 +**1)配置日志** 函数计算与日志服务无缝集成,可以将函数日志记录到开发者提供的日志仓库(Logstore)中。日志是服务配置中的一项,为服务配置 LogProject 和 Logstore,同一服务下所有函数通过 stdout 打印的日志,都会收集到对应的 Logstore 中。**2)记录日志** 那日志怎么打呢?在代码中直接通过 console.log/print 打印的日志可以收集到吗?答案是可以的。各个开发语言提供的打印日志的库都将日志打印到 stdout,比如 node.js 的 console.log()、python 的 print()、golang 的 fmt.Println() 等。函数计算收集所有打印到 stdout 的日志并将其上传到 Logstore 中。 -函数计算的调用是请求维度的,每次调用对应一个请求,也就对应一个 requestID。当请求量很大时,会有海量日志,如何区分哪些日志属于哪个请求呢?这就需要把 requestID 一起记录到日志中。函数计算提供内置的日志语句,打印的每条日志前都会带上请求 ID,方便日志的筛选。 **3)查看日志** +函数计算的调用是请求维度的,每次调用对应一个请求,也就对应一个 requestID。当请求量很大时,会有海量日志,如何区分哪些日志属于哪个请求呢?这就需要把 requestID 一起记录到日志中。函数计算提供内置的日志语句,打印的每条日志前都会带上请求 ID,方便日志的筛选。**3)查看日志** 当函数日志被收集到日志服务的 Logstore 中,可以登录日志服务控制台查看日志。 同时,函数计算控制台也集成了日志服务,可以在函数计算控制台上查看日志。函数计算控制台有两种查询方式: - **简单查询** :简单查询中列出每个 requestID 对应的日志,可以通过 requestID 对日志进行筛选; -- **高级查询** :高级查询嵌入了日志服务,可以通过 SQL 语句进行查询。 **点击链接观看 Demo 演示:** [ **https://developer.aliyun.com/lesson202418996** ](https://developer.aliyun.com/lesson_2024_18996) +- **高级查询** :高级查询嵌入了日志服务,可以通过 SQL 语句进行查询。**点击链接观看 Demo 演示:** [ **https://developer.aliyun.com/lesson202418996** ](https://developer.aliyun.com/lesson_2024_18996) #### 2. 指标 @@ -52,7 +52,7 @@ 函数计算提供了很多可观测性相关的功能,那究竟怎样定位问题呢?以几个场景为例。 -**场景一:新版本发布后,函数错误率升高** 首先发布版本后要观察函数各项指标,一旦错误率升高要立即回滚避免故障,查看函数日志定位错误原因,修复问题再次上线。 **场景二:函数性能差,总是执行时间很长,甚至超时** 开启 tracing 功能,在函数内部可能耗时的地方进行埋点,查看请求的瀑布图,定位执行时间长的原因,修复问题。 **场景三:业务量迅速扩张,并发度即将到达并发度限制** +**场景一:新版本发布后,函数错误率升高** 首先发布版本后要观察函数各项指标,一旦错误率升高要立即回滚避免故障,查看函数日志定位错误原因,修复问题再次上线。**场景二:函数性能差,总是执行时间很长,甚至超时** 开启 tracing 功能,在函数内部可能耗时的地方进行埋点,查看请求的瀑布图,定位执行时间长的原因,修复问题。**场景三:业务量迅速扩张,并发度即将到达并发度限制** 通过 metrics 查看当前并发度,观察到并发度持续上升时,及时联系函数计算开发同学,提升并发度。 diff --git "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25422\350\256\262.md" "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25422\350\256\262.md" index 4981efa45..1dc8ace59 100644 --- "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25422\350\256\262.md" +++ "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25422\350\256\262.md" @@ -22,9 +22,9 @@ SAE 除上述控制台界面部署的方式之外,还支持通过 Maven 插件 如何使用 Maven 插件进行部署?首先需要为应用添加 Maven 依赖 toolkit-maven-plugin,接下来需要编写配置文件来配置插件的具体行为,这里定义了三个配置文件: -- **toolkit_profile.yaml 账号配置文件** ,用来配置阿里云 ak、sk 来标识阿里云用户,这里推荐使用子账号 ak、sk 以降低安全风险。 -- **toolkit_package.yaml 打包配置文件** ,用来声明部署应用的类型,可以选择 war、jar、url 以及镜像的方式来进行部署,若采用 war、jar 的方式,则会将当前应用进行打包上传,而 url 或者镜像的方式则要显示的填写对应的包地址或者镜像地址进行部署。 -- **toolkit_deploy.yaml 部署配置** ,即可以配置该应用的环境变量、启动参数、健康检查等内容,这与控制台上的配置选项是一致的。 +- **toolkit_profile.yaml 账号配置文件**,用来配置阿里云 ak、sk 来标识阿里云用户,这里推荐使用子账号 ak、sk 以降低安全风险。 +- **toolkit_package.yaml 打包配置文件**,用来声明部署应用的类型,可以选择 war、jar、url 以及镜像的方式来进行部署,若采用 war、jar 的方式,则会将当前应用进行打包上传,而 url 或者镜像的方式则要显示的填写对应的包地址或者镜像地址进行部署。 +- **toolkit_deploy.yaml 部署配置**,即可以配置该应用的环境变量、启动参数、健康检查等内容,这与控制台上的配置选项是一致的。 这三个文件都有对应的模板,具体的模板选项可以查看[产品文档](https://help.aliyun.com/document_detail/110639.html?spm=a2c4g.11186623.6.611.5a3473c76owo99),接下来通过运行 Maven 打包部署命令 mvn clean package toolkit:deploy 即可自动化部署到 SAE 上。 @@ -36,7 +36,7 @@ SAE 除上述控制台界面部署的方式之外,还支持通过 Maven 插件 ### 总结 -相信您通过本文已经了解了 SAE 的几种部署方式和基本使用,在这里也推荐您选用 SAE,在不改变当前开发运维方式的同时,享受 Serverless 技术带来的价值。 **相关文档:** +相信您通过本文已经了解了 SAE 的几种部署方式和基本使用,在这里也推荐您选用 SAE,在不改变当前开发运维方式的同时,享受 Serverless 技术带来的价值。**相关文档:** [通过 Maven 插件自动部署应用](https://help.aliyun.com/document_detail/110639.html?spm=a2c4g.11186623.6.611.5a3473c76owo99) [通过 IntelliJ IDEA 插件部署应用](https://help.aliyun.com/document_detail/110665.html?spm=a2c4g.11186623.6.612.77f16905iduxEH) [通过 Eclipse 插件一键部署应用](https://help.aliyun.com/document_detail/110664.html?spm=a2c4g.11186623.6.613.616144e2vDAuFc) diff --git "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25429\350\256\262.md" "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25429\350\256\262.md" index e208064d1..65a0e6396 100644 --- "a/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25429\350\256\262.md" +++ "b/docs/Middleware/Serverless \346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25429\350\256\262.md" @@ -12,7 +12,7 @@ 在这个过程中,涉及到调度、云资源创建和挂载、镜像拉取、容器环境创建、应用进程创建等步骤,应用的创建效率与这些过程紧密相关。 -我们很自然而然地能想到,这其中部分过程是否能并行,以减少整个创建的耗时呢?经过对每个过程的耗时分析,我们发现其中的一些瓶颈点,并且部分执行步骤之间是解耦独立的,比如云弹性网卡的创建挂载和应用镜像拉取,就是相互独立的过程。 **基于此,我们将其中独立的过程做了并行化处理,在不影响创建链路的同时,降低了应用创建的时耗。** ### 应用部署 +我们很自然而然地能想到,这其中部分过程是否能并行,以减少整个创建的耗时呢?经过对每个过程的耗时分析,我们发现其中的一些瓶颈点,并且部分执行步骤之间是解耦独立的,比如云弹性网卡的创建挂载和应用镜像拉取,就是相互独立的过程。**基于此,我们将其中独立的过程做了并行化处理,在不影响创建链路的同时,降低了应用创建的时耗。** ### 应用部署 应用的部署,即应用升级。我们知道,传统的应用部署过程可以分为以下几个步骤: @@ -24,7 +24,7 @@ 上文我们讲到,应用实例的创建过程包括调度、云资源创建挂载、镜像拉取、容器环境创建、应用进程拉起等步骤,对于应用部署而言,完全可以不用重走一遍所有的流程,因为我们需要的仅仅是基于新的镜像,创建新的应用执行环境和进程而已。 -因此, **我们实现了原地部署的功能** ,在滚动升级过程中,保留原来待升级应用实例及其挂载的云网络、云存储资源,只更新实例的执行环境,无需经过调度、云资源创建等过程。这样,原来的部署流程也简化为: +因此,**我们实现了原地部署的功能**,在滚动升级过程中,保留原来待升级应用实例及其挂载的云网络、云存储资源,只更新实例的执行环境,无需经过调度、云资源创建等过程。这样,原来的部署流程也简化为: > 摘流,将运行实例从 SLB 后端摘除 -> 原地升级实例 -> 接入流量 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25400\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25400\350\256\262.md" index fbebe874e..99fb59c9c 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25400\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25400\350\256\262.md" @@ -19,9 +19,9 @@ 分布式数据库中间件 ShardingSphere 作为一个分库分表的“利器”,可以很好地解决这些痛点问题,并且相比其他分库分表框架(如 Cobar、MyCat 等)具有以下几点优势: -- **技术权威性** ,是 Apache 基金会历史上第一个分布式数据库中间件项目,代表着这一领域的最新技术方向; -- **解决方案完备性** ,它集客户端分片、代理服务器,以及分布式数据库的核心功能于一身,提供了一套适用于互联网应用架构、云服务架构的,完整的开源分布式数据库中间件解决方案和生态圈。 -- **开发友好性** ,提供了友好的集成方式,业务开发人员只需要引入一个 JAR 包就能在业务代码中嵌入数据分片、读写分离、分布式事务、数据库治理等一系列功能。 +- **技术权威性**,是 Apache 基金会历史上第一个分布式数据库中间件项目,代表着这一领域的最新技术方向; +- **解决方案完备性**,它集客户端分片、代理服务器,以及分布式数据库的核心功能于一身,提供了一套适用于互联网应用架构、云服务架构的,完整的开源分布式数据库中间件解决方案和生态圈。 +- **开发友好性**,提供了友好的集成方式,业务开发人员只需要引入一个 JAR 包就能在业务代码中嵌入数据分片、读写分离、分布式事务、数据库治理等一系列功能。 - **可插拔的系统扩展性** :它的很多核心功能均通过插件的形式提供,供开发者排列组合来定制属于自己的独特系统。 这些优秀的特性,让 ShardingSphere 在分库分表中间件领域占据了领先地位,并被越来越多的知名企业(比如京东、当当、电信、中通快递、哔哩哔哩等)用来构建自己强大而健壮的数据平台。如果你苦于找不到一款成熟稳定的分库分表中间件,那么 ShardingSphere 恰能帮助你解决这个痛点。 @@ -54,7 +54,7 @@ 此外,课程中的核心功能部分,我是基于具体的案例分析并给出详细的代码实现和配置方案,方便你进行学习和改造。课程配套代码,你可以在 [https://github.com/tianyilan12/shardingsphere-demo](https://github.com/tianyilan12/shardingsphere-demo) 下载。 -### 你将获得 **1.** **分库分表的应用方式和实现原理** 帮你理解 ShardingSphere 的核心功能特性,来满足日常开发工作所需,同时基于源码给出这些功能的设计原理和实现机制。 **2.** **学习优秀的开源框架,提高技术理解与应用能力** 技术原理是具有相通性的。以 ZooKeeper 这个分布式协调框架为例,ShardingSphere 和 Dubbo 中都使用它来完成了注册中心的构建 +### 你将获得 **1.** **分库分表的应用方式和实现原理** 帮你理解 ShardingSphere 的核心功能特性,来满足日常开发工作所需,同时基于源码给出这些功能的设计原理和实现机制。**2.** **学习优秀的开源框架,提高技术理解与应用能力** 技术原理是具有相通性的。以 ZooKeeper 这个分布式协调框架为例,ShardingSphere 和 Dubbo 中都使用它来完成了注册中心的构建 ![image](assets/CgqCHl7nA4GAbjKUAABqNKIcNmc812.png) @@ -62,7 +62,7 @@ 随着对 ShardingSphere 的深入学习,你会发现类似的例子还有很多,包括基于 SPI 机制的微内核架构、基于雪花算法的分布式主键、基于 Apollo 的配置中心、基于 Nacos 的注册中心、基于 Seata 的柔性事务、基于 OpenTracing 规范的链路跟踪等。 -而这些技术体系在 Dubbo、Spring Cloud 等主流开发框架中也多有体现。因此这个课程除了可以强化你对这些技术体系的系统化理解,还可以让你掌握这些技术体系的具体应用场景和实现方式,从而实现触类旁通。 **3.** **学习从源码分析到日常开发的技巧** +而这些技术体系在 Dubbo、Spring Cloud 等主流开发框架中也多有体现。因此这个课程除了可以强化你对这些技术体系的系统化理解,还可以让你掌握这些技术体系的具体应用场景和实现方式,从而实现触类旁通。**3.** **学习从源码分析到日常开发的技巧** 从源码解析到日常应用是本课程的一个核心目标。基于 ShardingSphere 这款优秀的开源框架,可以提炼出一系列包括设计模式的应用(如工厂模式、策略模式、模板方法等)、微内核架构等架构模式、组件设计和类层结构划分的思想和实现策略、常见缓存的应用以及自定义缓存机制的实现、Spring 家族框架的集成和整合等开发技巧,这些开发技巧都能够直接应用到日常开发过程。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25401\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25401\350\256\262.md" index 9ffccdb7a..ed7ef9235 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25401\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25401\350\256\262.md" @@ -14,9 +14,9 @@ 既然以 MySQL 为代表的关系型数据库中的单表无法支持大数据量的存储和访问方案,自然而然的,你可能会想到是否可以采用诸如 MongoDB 等 NoSQL 的方式来管理数据? -但这并不是一个很好的选项,原因有很多:一方面, **关系型生态系统非常完善** ,关系型数据库经过几十年的持续发展,具有 NoSQL 无法比拟的稳定性和可靠性;另一方面, **关系型数据库的事务特性** ,也是其他数据存储工具所不具备的一项核心功能。目前绝大部分公司的核心数据都存储在关系型数据库中,就互联网公司而言,MySQL 是主流的数据存储方案。 +但这并不是一个很好的选项,原因有很多:一方面,**关系型生态系统非常完善**,关系型数据库经过几十年的持续发展,具有 NoSQL 无法比拟的稳定性和可靠性;另一方面,**关系型数据库的事务特性**,也是其他数据存储工具所不具备的一项核心功能。目前绝大部分公司的核心数据都存储在关系型数据库中,就互联网公司而言,MySQL 是主流的数据存储方案。 -现在,我们选择了关系型数据库,就可以考虑采用分库分表的方案来解决单库表的瓶颈问题,这是目前互联网行业处理海量数据的通用方法。 **分库分表方案更多的是对关系型数据库数据存储和访问机制的一种补充,而不是颠覆** 。那么究竟什么是分库分表呢? +现在,我们选择了关系型数据库,就可以考虑采用分库分表的方案来解决单库表的瓶颈问题,这是目前互联网行业处理海量数据的通用方法。**分库分表方案更多的是对关系型数据库数据存储和访问机制的一种补充,而不是颠覆** 。那么究竟什么是分库分表呢? ### 什么是数据分库分表? @@ -36,7 +36,7 @@ ![image](assets/Ciqc1F7nGDCARTTCAAC8BTUPGAU300.png) -从这里可以看到, **垂直分表的处理方式就是将一个表按照字段分成多张表,每个表存储其中一部分字段。** 在实现上,我们通常会把头像等 blob 类型的大字段数据或热度较低的数据放在一张独立的表中,将经常需要组合查询的列放在一张表中,这也可以认为是分表操作的一种表现形式。 +从这里可以看到,**垂直分表的处理方式就是将一个表按照字段分成多张表,每个表存储其中一部分字段。** 在实现上,我们通常会把头像等 blob 类型的大字段数据或热度较低的数据放在一张独立的表中,将经常需要组合查询的列放在一张表中,这也可以认为是分表操作的一种表现形式。 通过垂直分表能得到一定程度的性能提升,但数据毕竟仍然位于同一个数据库中,也就是把操作范围限制在一台服务器上,每个表还是会竞争同一台服务器中的 CPU、内存、网络 IO 等资源。基于这个考虑,在有了垂直分表之后,就可以进一步引入垂直分库。 @@ -52,11 +52,11 @@ 可以看到,水平分库是把同一个表的数据按一定规则拆分到不同的数据库中,每个库同样可以位于不同的服务器上。这种方案往往能解决单库存储量及性能瓶颈问题,但由于同一个表被分配在不同的数据库中,数据的访问需要额外的路由工作,因此大大提升了系统复杂度。这里所谓的规则实际上就是一系列的算法,常见的包括: -- **取模算法** ,取模的方式有很多,比如前面介绍的按照用户 ID 进行取模,当然也可以通过表的一列或多列字段进行 hash 求值来取模; -- **范围限定算法** ,范围限定也很常见,比如可以采用按年份、按时间等策略路由到目标数据库或表; -- **预定义算法** ,是指事先规划好具体库或表的数量,然后直接路由到指定库或表中。 +- **取模算法**,取模的方式有很多,比如前面介绍的按照用户 ID 进行取模,当然也可以通过表的一列或多列字段进行 hash 求值来取模; +- **范围限定算法**,范围限定也很常见,比如可以采用按年份、按时间等策略路由到目标数据库或表; +- **预定义算法**,是指事先规划好具体库或表的数量,然后直接路由到指定库或表中。 -按照水平分库的思路,也可以对用户库中的用户表进行水平拆分,效果如下图所示。也就是说, **水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中** 。 +按照水平分库的思路,也可以对用户库中的用户表进行水平拆分,效果如下图所示。也就是说,**水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中** 。 ![image](assets/CgqCHl7nGFGAXKk0AACCxF6OwYE181.png) @@ -74,7 +74,7 @@ #### 分库分表与读写分离 -说到分库分表,我们不得不介绍另一个解决数据访问瓶颈的技术体系: **读写分离** ,这个技术与数据库主从架构有关。我们知道像 MySQL 这样的数据库提供了完善的主从架构,能够确保主数据库与从数据库之间的数据同步。基于主从架构,就可以按照操作要求对读操作和写操作进行分离,从而提升访问效率。读写分离的基本原理是这样的: +说到分库分表,我们不得不介绍另一个解决数据访问瓶颈的技术体系: **读写分离**,这个技术与数据库主从架构有关。我们知道像 MySQL 这样的数据库提供了完善的主从架构,能够确保主数据库与从数据库之间的数据同步。基于主从架构,就可以按照操作要求对读操作和写操作进行分离,从而提升访问效率。读写分离的基本原理是这样的: ![image](assets/Ciqc1F7nGF-AaBJ0AACkmf13Mrs619.png) @@ -96,7 +96,7 @@ 所谓客户端分片,相当于在数据库的客户端就实现了分片规则。显然,这种方式将分片处理的工作进行前置,客户端管理和维护着所有的分片逻辑,并决定每次 SQL 执行所对应的目标数据库和数据表。 -客户端分片这一解决方案也有不同的表现形式,其中最为简单的方式就是 **应用层分片** ,也就是说在应用程序中直接维护着分片规则和分片逻辑: +客户端分片这一解决方案也有不同的表现形式,其中最为简单的方式就是 **应用层分片**,也就是说在应用程序中直接维护着分片规则和分片逻辑: ![image](assets/Ciqc1F7nGHqAdJD9AACzVB1hs2Y585.png) diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25402\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25402\350\256\262.md" index e85f5f206..626f34551 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25402\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25402\350\256\262.md" @@ -22,7 +22,7 @@ 对于一款开源中间件来说,要得到长足的发展,一方面依赖于社区的贡献,另外在很大程度上还取决于自身的设计和发展理念。 -ShardingSphere 的定位非常明确,就是一种关系型数据库中间件,而并非一个全新的关系型数据库。ShardingSphere 认为,在当下,关系型数据库依然占有巨大市场,但凡涉及数据的持久化,关系型数据库仍然是系统的标准配置,也是各个公司核心业务的基石,在可预见的未来中,这点很难撼动。所以, **ShardingSphere 在当前阶段更加关注在原有基础上进行兼容和扩展,而非颠覆** 。那么 ShardingSphere 是如何做到这一点呢? +ShardingSphere 的定位非常明确,就是一种关系型数据库中间件,而并非一个全新的关系型数据库。ShardingSphere 认为,在当下,关系型数据库依然占有巨大市场,但凡涉及数据的持久化,关系型数据库仍然是系统的标准配置,也是各个公司核心业务的基石,在可预见的未来中,这点很难撼动。所以,**ShardingSphere 在当前阶段更加关注在原有基础上进行兼容和扩展,而非颠覆** 。那么 ShardingSphere 是如何做到这一点呢? ShardingSphere 构建了一个生态圈,这个生态圈由一套开源的分布式数据库中间件解决方案所构成。按照目前的规划,ShardingSphere 由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar 这三款相互独立的产品组成,其中前两款已经正式发布,而 Sharding-Sidecar 正在规划中。我们可以从这三款产品出发,分析 ShardingSphere 的设计理念。 @@ -30,11 +30,11 @@ ShardingSphere 构建了一个生态圈,这个生态圈由一套开源的分 ShardingSphere 的前身是 Sharding-JDBC,所以这是整个框架中最为成熟的组件。Sharding-JDBC 的定位是一个轻量级 Java 框架,在 JDBC 层提供了扩展性服务。我们知道 JDBC 是一种开发规范,指定了 DataSource、Connection、Statement、PreparedStatement、ResultSet 等一系列接口。而各大数据库供应商通过实现这些接口提供了自身对 JDBC 规范的支持,使得 JDBC 规范成为 Java 领域中被广泛采用的数据库访问标准。 -基于这一点,Sharding-JDBC 一开始的设计就完全兼容 JDBC 规范,Sharding-JDBC 对外暴露的一套分片操作接口与 JDBC 规范中所提供的接口完全一致。开发人员只需要了解 JDBC,就可以使用 Sharding-JDBC 来实现分库分表,Sharding-JDBC 内部屏蔽了所有的分片规则和处理逻辑的复杂性。显然, **这种方案天生就是一种具有高度兼容性的方案,能够为开发人员提供最简单、最直接的开发支持** 。关于 Sharding-JDBC 与 JDBC 规范的兼容性话题,我们将会在下一课时中详细讨论。 +基于这一点,Sharding-JDBC 一开始的设计就完全兼容 JDBC 规范,Sharding-JDBC 对外暴露的一套分片操作接口与 JDBC 规范中所提供的接口完全一致。开发人员只需要了解 JDBC,就可以使用 Sharding-JDBC 来实现分库分表,Sharding-JDBC 内部屏蔽了所有的分片规则和处理逻辑的复杂性。显然,**这种方案天生就是一种具有高度兼容性的方案,能够为开发人员提供最简单、最直接的开发支持** 。关于 Sharding-JDBC 与 JDBC 规范的兼容性话题,我们将会在下一课时中详细讨论。 ![4.png](assets/CgqCHl7rSOuAXZt6AAC4cmjERnk488.png) Sharding-JDBC 与 JDBC 规范的兼容性示意图 -在实际开发过程中,Sharding-JDBC 以 JAR 包的形式提供服务。 **开发人员可以使用这个 JAR 包直连数据库,无需额外的部署和依赖管理** 。在应用 Sharding-JDBC 时,需要注意到 Sharding-JDBC 背后依赖的是一套完整而强大的分片引擎: +在实际开发过程中,Sharding-JDBC 以 JAR 包的形式提供服务。**开发人员可以使用这个 JAR 包直连数据库,无需额外的部署和依赖管理** 。在应用 Sharding-JDBC 时,需要注意到 Sharding-JDBC 背后依赖的是一套完整而强大的分片引擎: ![5.png](assets/CgqCHl7rSPSAUJHuAADsN1Pqjqs981.png) @@ -64,7 +64,7 @@ Sidecar 设计模式受到了越来越多的关注和采用,这个模式的目 ### ShardingSphere 的核心功能:从数据分片到编排治理 -介绍完 ShardingSphere 的设计理念之后,我们再来关注它的核心功能和实现机制。这里把 ShardingSphere 的整体功能拆分成四大部分,即 **基础设施** 、 **分片引擎** 、 **分布式事务** 和 **治理与集成** ,这四大部分也构成了本课程介绍 ShardingSphere 的整体行文结构,下面我们来分别进行介绍: +介绍完 ShardingSphere 的设计理念之后,我们再来关注它的核心功能和实现机制。这里把 ShardingSphere 的整体功能拆分成四大部分,即 **基础设施** 、 **分片引擎** 、 **分布式事务** 和 **治理与集成**,这四大部分也构成了本课程介绍 ShardingSphere 的整体行文结构,下面我们来分别进行介绍: #### 基础设施 @@ -72,7 +72,7 @@ Sidecar 设计模式受到了越来越多的关注和采用,这个模式的目 - 微内核架构 -ShardingSphere 在设计上采用了 **微内核(MicroKernel)架构模式** ,来确保系统具有高度可扩展性。微内核架构包含两部分组件,即内核系统和插件。使用微内核架构对系统进行升级,要做的只是用新插件替换旧插件,而不需要改变整个系统架构: +ShardingSphere 在设计上采用了 **微内核(MicroKernel)架构模式**,来确保系统具有高度可扩展性。微内核架构包含两部分组件,即内核系统和插件。使用微内核架构对系统进行升级,要做的只是用新插件替换旧插件,而不需要改变整个系统架构: ![8.png](assets/CgqCHl7rSSCAcY4sAABRDnG4TnQ180.png) @@ -112,7 +112,7 @@ ShardingSphere 内置了一组分布式事务的实现方案,其中强一致 - 数据脱敏 -数据脱敏是确保数据访问安全的常见需求,通常做法是对原始的 SQL 进行改写,从而实现对原文数据进行加密。当我们想要获取原始数据时,在实现上就需要通过对数据库中所存储的密文数据进行解密才能完成。我们可以根据需要实现一套类似的加解密机制,但 ShardingSphere 的强大之处在于, **它将这套机制内嵌到了 SQL 的执行过程中,业务开发人员不需要关注具体的加解密实现细节,而只要通过简单的配置就能实现数据的自动脱敏。** +数据脱敏是确保数据访问安全的常见需求,通常做法是对原始的 SQL 进行改写,从而实现对原文数据进行加密。当我们想要获取原始数据时,在实现上就需要通过对数据库中所存储的密文数据进行解密才能完成。我们可以根据需要实现一套类似的加解密机制,但 ShardingSphere 的强大之处在于,**它将这套机制内嵌到了 SQL 的执行过程中,业务开发人员不需要关注具体的加解密实现细节,而只要通过简单的配置就能实现数据的自动脱敏。** - 配置中心 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25403\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25403\350\256\262.md" index 9d7173fe4..01f177a4f 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25403\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25403\350\256\262.md" @@ -2,15 +2,15 @@ 我们知道 ShardingSphere 是一种典型的客户端分片解决方案,而客户端分片的实现方式之一就是重写 JDBC 规范。在上一课时中,我们也介绍了,ShardingSphere 在设计上从一开始就完全兼容 JDBC 规范,ShardingSphere 对外暴露的一套分片操作接口与 JDBC 规范中所提供的接口完全一致。 -讲到这里,你可能会觉得有点神奇, **ShardingSphere 究竟是通过什么方式,实现了与 JDBC 规范完全兼容的 API 来提供分片功能呢?** 这个问题非常重要,值得我们专门花一个课时的内容来进行分析和讲解。可以说, **理解 JDBC 规范以及 ShardingSphere 对 JDBC 规范的重写方式,是正确使用 ShardingSphere 实现数据分片的前提** 。今天,我们就深入讨论 JDBC 规范与 ShardingSphere 的这层关系,帮你从底层设计上解开其中的神奇之处。 +讲到这里,你可能会觉得有点神奇,**ShardingSphere 究竟是通过什么方式,实现了与 JDBC 规范完全兼容的 API 来提供分片功能呢?** 这个问题非常重要,值得我们专门花一个课时的内容来进行分析和讲解。可以说,**理解 JDBC 规范以及 ShardingSphere 对 JDBC 规范的重写方式,是正确使用 ShardingSphere 实现数据分片的前提** 。今天,我们就深入讨论 JDBC 规范与 ShardingSphere 的这层关系,帮你从底层设计上解开其中的神奇之处。 ### JDBC 规范简介 -ShardingSphere 提供了与 JDBC 规范完全兼容的实现过程,在对这一过程进行详细展开之前,先来回顾一下 JDBC 规范。 **JDBC(Java Database Connectivity)的设计初衷是提供一套用于各种数据库的统一标准** ,而不同的数据库厂家共同遵守这套标准,并提供各自的实现方案供应用程序调用。作为统一标准,JDBC 规范具有完整的架构体系,如下图所示: +ShardingSphere 提供了与 JDBC 规范完全兼容的实现过程,在对这一过程进行详细展开之前,先来回顾一下 JDBC 规范。**JDBC(Java Database Connectivity)的设计初衷是提供一套用于各种数据库的统一标准**,而不同的数据库厂家共同遵守这套标准,并提供各自的实现方案供应用程序调用。作为统一标准,JDBC 规范具有完整的架构体系,如下图所示: ![Drawing 0.png](assets/CgqCHl7xtaiASay6AAB0vuO1kAA457.png) -JDBC 架构中的 Driver Manager 负责加载各种不同的驱动程序(Driver),并根据不同的请求,向调用者返回相应的数据库连接(Connection)。而应用程序通过调用 JDBC API 来实现对数据库的操作。 **对于开发人员而言,JDBC API 是我们访问数据库的主要途径,也是 ShardingSphere 重写 JDBC 规范并添加分片功能的入口** 。如果我们使用 JDBC 开发一个访问数据库的处理流程,常见的代码风格如下所示: +JDBC 架构中的 Driver Manager 负责加载各种不同的驱动程序(Driver),并根据不同的请求,向调用者返回相应的数据库连接(Connection)。而应用程序通过调用 JDBC API 来实现对数据库的操作。**对于开发人员而言,JDBC API 是我们访问数据库的主要途径,也是 ShardingSphere 重写 JDBC 规范并添加分片功能的入口** 。如果我们使用 JDBC 开发一个访问数据库的处理流程,常见的代码风格如下所示: ```java // 创建池化的数据源 @@ -70,7 +70,7 @@ DataSource 的目的是获取 Connection 对象,我们可以把 Connection 理 #### Statement -JDBC 规范中的 Statement 存在两种类型,一种是 **普通的 Statement** ,一种是 **支持预编译的 PreparedStatement** 。所谓预编译,是指数据库的编译器会对 SQL 语句提前编译,然后将预编译的结果缓存到数据库中,这样下次执行时就可以替换参数并直接使用编译过的语句,从而提高 SQL 的执行效率。当然,这种预编译也需要成本,所以在日常开发中,对数据库只执行一次性读写操作时,用 Statement 对象进行处理比较合适;而当涉及 SQL 语句的多次执行时,可以使用 PreparedStatement。 +JDBC 规范中的 Statement 存在两种类型,一种是 **普通的 Statement**,一种是 **支持预编译的 PreparedStatement** 。所谓预编译,是指数据库的编译器会对 SQL 语句提前编译,然后将预编译的结果缓存到数据库中,这样下次执行时就可以替换参数并直接使用编译过的语句,从而提高 SQL 的执行效率。当然,这种预编译也需要成本,所以在日常开发中,对数据库只执行一次性读写操作时,用 Statement 对象进行处理比较合适;而当涉及 SQL 语句的多次执行时,可以使用 PreparedStatement。 如果需要查询数据库中的数据,只需要调用 Statement 或 PreparedStatement 对象的 executeQuery 方法即可,该方法以 SQL 语句作为参数,执行完后返回一个 JDBC 的 ResultSet 对象。当然,Statement 或 PreparedStatement 中提供了一大批执行 SQL 更新和查询的重载方法。在 ShardingSphere 中,同样也提供了 ShardingStatement 和 ShardingPreparedStatement 这两个支持分片操作的 Statement 对象。 @@ -86,7 +86,7 @@ ShardingSphere 提供了与 JDBC 规范完全兼容的 API。也就是说,开 ### 基于适配器模式的 JDBC 重写实现方案 -在 ShardingSphere 中,实现与 JDBC 规范兼容性的基本策略就是采用了设计模式中的适配器模式(Adapter Pattern)。 **适配器模式通常被用作连接两个不兼容接口之间的桥梁,涉及为某一个接口加入独立的或不兼容的功能。** +在 ShardingSphere 中,实现与 JDBC 规范兼容性的基本策略就是采用了设计模式中的适配器模式(Adapter Pattern)。**适配器模式通常被用作连接两个不兼容接口之间的桥梁,涉及为某一个接口加入独立的或不兼容的功能。** 作为一套适配 JDBC 规范的实现方案,ShardingSphere 需要对上面介绍的 JDBC API 中的 DataSource、Connection、Statement 及 ResultSet 等核心对象都完成重写。虽然这些对象承载着不同功能,但重写机制应该是共通的,否则就需要对不同对象都实现定制化开发,显然,这不符合我们的设计原则。为此,ShardingSphere 抽象并开发了一套基于适配器模式的实现方案,整体结构是这样的,如下图所示: diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25404\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25404\350\256\262.md" index df101b081..14e1a4500 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25404\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25404\350\256\262.md" @@ -4,7 +4,7 @@ ### 如何抽象开源框架的应用方式? -当我们自己在设计和实现一款开源框架时,如何规划它的应用方式呢?作为一款与数据库访问相关的开源框架,ShardingSphere 提供了多个维度的应用方式,我们可以对这些应用方式进行抽象,从而提炼出一种模版。这个模版由四个维度组成,分别是 **底层工具、基础规范、开发框架和领域框架** ,如下图所示: +当我们自己在设计和实现一款开源框架时,如何规划它的应用方式呢?作为一款与数据库访问相关的开源框架,ShardingSphere 提供了多个维度的应用方式,我们可以对这些应用方式进行抽象,从而提炼出一种模版。这个模版由四个维度组成,分别是 **底层工具、基础规范、开发框架和领域框架**,如下图所示: ![2.png](assets/CgqCHl75qv-AFbZvAACz7F_yXRM280.png) @@ -12,7 +12,7 @@ 底层工具指的是这个开源框架所面向的目标工具或所依赖的第三方工具。这种底层工具往往不是框架本身可以控制和管理的,框架的作用只是在它上面添加一个应用层,用于封装对这些底层工具的使用方式。 -对于 ShardingSphere 而言, **这里所说的底层工具实际上指的是关系型数据库** 。目前,ShardingSphere 支持包括 MySQL、Oracle、SQLServer、PostgreSQL 以及任何遵循 SQL92 标准的数据库。 +对于 ShardingSphere 而言,**这里所说的底层工具实际上指的是关系型数据库** 。目前,ShardingSphere 支持包括 MySQL、Oracle、SQLServer、PostgreSQL 以及任何遵循 SQL92 标准的数据库。 #### 基础规范 @@ -24,11 +24,11 @@ 开源框架本身也是一个开发框架,但我们通常不会自己设计和实现一个全新的开发框架,而是更倾向于与现有的主流开发框架进行集成。目前,Java 世界中最主流的开发框架就是 Spring 家族系列框架。 -ShardingSphere 同时集成了 Spring 和 Spring Boot 这两款 Spring 家族的主流开发框架。 **熟悉这两款框架的开发人员在应用 ShardingSphere 进行开发时将不需要任何学习成本** 。 +ShardingSphere 同时集成了 Spring 和 Spring Boot 这两款 Spring 家族的主流开发框架。**熟悉这两款框架的开发人员在应用 ShardingSphere 进行开发时将不需要任何学习成本** 。 #### 领域框架 -对于某些开源框架而言,也需要考虑和领域框架进行集成,以便提供更好的用户体验和使用友好性,区别于前面提到的适用于任何场景的开发框架。 **所谓领域框架,是指与所设计的开源框架属于同一专业领域的开发框架。** 业务开发人员已经习惯在日常开发过程中使用这些特定于某一领域的开发框架,所以在设计自己的开源框架时,也需要充分考虑与这些框架的整合和集成。 +对于某些开源框架而言,也需要考虑和领域框架进行集成,以便提供更好的用户体验和使用友好性,区别于前面提到的适用于任何场景的开发框架。**所谓领域框架,是指与所设计的开源框架属于同一专业领域的开发框架。** 业务开发人员已经习惯在日常开发过程中使用这些特定于某一领域的开发框架,所以在设计自己的开源框架时,也需要充分考虑与这些框架的整合和集成。 对于 ShardingSphere 而言,领域框架指的是 MyBatis、Hibernate 等常见的 ORM 框架。ShardingSphere 对这领域框架提供了无缝集成的实现方案,熟悉 ORM 框架的开发人员在应用 ShardingSphere 进行开发时同样不需要任何学习成本。 @@ -175,7 +175,7 @@ public final class DataSourceHelper{ #### Spring -如果使用 Spring 作为我们的开发框架,那么 JDBC 中各个核心对象的创建过程都会交给 Spring 容器进行完成。 **ShardingSphere 中基于命名空间(NameSpace)机制完成了与 Spring 框架的无缝集成。要想使用这种机制,需要先引入对应的 Maven 依赖** : +如果使用 Spring 作为我们的开发框架,那么 JDBC 中各个核心对象的创建过程都会交给 Spring 容器进行完成。**ShardingSphere 中基于命名空间(NameSpace)机制完成了与 Spring 框架的无缝集成。要想使用这种机制,需要先引入对应的 Maven 依赖** : ```plaintext @@ -332,7 +332,7 @@ public class UserApplication ### 小结 -作为一个优秀的开源框架,ShardingSphere 提供了多方面的集成方式供广大开发人员在业务系统中使用它来完成分库分表操作。在这一课时中,我们先梳理了作为一个开源框架所应该具备的应用方式,并分析了这些应用方式在 ShardingSphere 中的具体实现机制。可以看到, **从 JDBC 规范,到 Spring、Spring Boot 开发框架,再到 JPA、MyBatis 等主流 ORM 框架,ShardingSphere 都提供了完善的集成方案。** +作为一个优秀的开源框架,ShardingSphere 提供了多方面的集成方式供广大开发人员在业务系统中使用它来完成分库分表操作。在这一课时中,我们先梳理了作为一个开源框架所应该具备的应用方式,并分析了这些应用方式在 ShardingSphere 中的具体实现机制。可以看到,**从 JDBC 规范,到 Spring、Spring Boot 开发框架,再到 JPA、MyBatis 等主流 ORM 框架,ShardingSphere 都提供了完善的集成方案。** 这里给你留一道思考题:为了实现框架的易用性,ShardingSphere 为开发人员提供了哪些工具和规范的集成? diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25405\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25405\350\256\262.md" index ab4ed52de..5c537f5b7 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25405\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25405\350\256\262.md" @@ -30,7 +30,7 @@ ShardingRuleConfiguration 中所需要配置的规则比较多,我们可以通 ![image](assets/Ciqc1F75rTaADRO6AAD5MCLrohA562.png) -这里引入了一些新的概念,包括绑定表、广播表等,这些概念在下一课时介绍到 ShardingSphere 的分库分表操作时都会详细展开,这里不做具体介绍。 **事实上,对于 ShardingRuleConfiguration 而言,必须要设置的只有一个配置项,即 TableRuleConfiguration。** +这里引入了一些新的概念,包括绑定表、广播表等,这些概念在下一课时介绍到 ShardingSphere 的分库分表操作时都会详细展开,这里不做具体介绍。**事实上,对于 ShardingRuleConfiguration 而言,必须要设置的只有一个配置项,即 TableRuleConfiguration。** #### TableRuleConfiguration @@ -89,7 +89,7 @@ Java 代码配置是使用 ShardingSphere 所提供的底层 API 来完成配置 Yaml 配置是 ShardingSphere 所推崇的一种配置方式。Yaml 的语法和其他高级语言类似,并且可以非常直观地描述多层列表和对象等数据形态,特别适合用来表示或编辑数据结构和各种配置文件。 -在语法上,常见的"!!"表示实例化该类;以"-"开头的多行构成一个数组;以":"表示键值对;以"#"表示注释。关于 Yaml 语法的更多介绍可以参考百度百科 [https://baike.baidu.com/item/YAML](https://baike.baidu.com/item/YAML)。 **请注意,Yaml 大小写敏感,并使用缩进表示层级关系。** 这里给出一个基于 ShardingSphere 实现读写分离场景下的配置案例: +在语法上,常见的"!!"表示实例化该类;以"-"开头的多行构成一个数组;以":"表示键值对;以"#"表示注释。关于 Yaml 语法的更多介绍可以参考百度百科 [https://baike.baidu.com/item/YAML](https://baike.baidu.com/item/YAML)。**请注意,Yaml 大小写敏感,并使用缩进表示层级关系。** 这里给出一个基于 ShardingSphere 实现读写分离场景下的配置案例: ```plaintext dataSources: diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25409\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25409\350\256\262.md" index b236add07..d2c8a97bb 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25409\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25409\350\256\262.md" @@ -226,7 +226,7 @@ private void insertHealthRecord(HealthRecord healthRecord, Connection connection } ``` -首先通过 Connection 对象构建一个 PreparedStatement。请注意, **由于我们需要通过 ShardingSphere 的主键自动生成机制,所以在创建 PreparedStatement 时需要进行特殊地设置:** +首先通过 Connection 对象构建一个 PreparedStatement。请注意,**由于我们需要通过 ShardingSphere 的主键自动生成机制,所以在创建 PreparedStatement 时需要进行特殊地设置:** ```plaintext connection.prepareStatement(sql_health_record_insert, Statement.RETURN_GENERATED_KEYS) diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25410\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25410\350\256\262.md" index bacb2efaf..c91f2bd97 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25410\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25410\350\256\262.md" @@ -14,7 +14,7 @@ 关于这个问题,要讨论的点在于是否需要将敏感数据以明文形式存储在数据库中。这个问题的答案并不是绝对的。 -我们先来考虑第一种情况。 **对于一些敏感数据而言,我们显然应该直接以密文的形式将加密之后的数据进行存储,防止有任何一种途径能够从数据库中获取这些数据明文。** 在这类敏感数据中,最典型的就是用户密码,我们通常会采用 MD5 等不可逆的加密算法对其进行加密,而使用这些数据的方法也只是依赖于它的密文形式,不会涉及对明文的直接处理。 **但对于用户姓名、手机号等信息,由于统计分析等方面的需要,显然我们不能直接采用不可逆的加密算法对其进行加密,还需要将明文信息进行处理** **。** 一种常见的处理方式是将一个字段用两列来进行保存,一列保存明文,一列保存密文,这就是第二种情况。 +我们先来考虑第一种情况。**对于一些敏感数据而言,我们显然应该直接以密文的形式将加密之后的数据进行存储,防止有任何一种途径能够从数据库中获取这些数据明文。** 在这类敏感数据中,最典型的就是用户密码,我们通常会采用 MD5 等不可逆的加密算法对其进行加密,而使用这些数据的方法也只是依赖于它的密文形式,不会涉及对明文的直接处理。**但对于用户姓名、手机号等信息,由于统计分析等方面的需要,显然我们不能直接采用不可逆的加密算法对其进行加密,还需要将明文信息进行处理** **。** 一种常见的处理方式是将一个字段用两列来进行保存,一列保存明文,一列保存密文,这就是第二种情况。 显然,我们可以将第一种情况看作是第二种情况的特例。也就是说,在第一种情况中没有明文列,只有密文列。 @@ -24,7 +24,7 @@ ShardingSphere 同样基于这两种情况进行了抽象,它将这里的明 #### 敏感数据如何加解密? -数据脱敏本质上就是一种加解密技术应用场景,自然少不了对各种加解密算法和技术的封装。 **传统的加解密方式有两种,一种是对称加密,常见的包括 DEA 和 AES;另一种是非对称加密,常见的包括 RSA。** ShardingSphere 内部也抽象了一个 ShardingEncryptor 组件专门封装各种加解密操作: +数据脱敏本质上就是一种加解密技术应用场景,自然少不了对各种加解密算法和技术的封装。**传统的加解密方式有两种,一种是对称加密,常见的包括 DEA 和 AES;另一种是非对称加密,常见的包括 RSA。** ShardingSphere 内部也抽象了一个 ShardingEncryptor 组件专门封装各种加解密操作: ```java public interface ShardingEncryptor extends TypeBasedSPI { @@ -91,7 +91,7 @@ public class EncryptUser { ``` -请注意, **我们在 resultMap 中并没有指定 user_name_plain 字段,同时,insert 语句中同样没有指定这个字段。** +请注意,**我们在 resultMap 中并没有指定 user_name_plain 字段,同时,insert 语句中同样没有指定这个字段。** 有了 Mapper,我们就可以构建 Service 层组件。在这个 EncryptUserServiceImpl 类中,我们分别提供了 processEncryptUsers 和 getEncryptUsers 方法来插入用户以及获取用户列表。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25411\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25411\350\256\262.md" index ff64bc0e4..5d7d95030 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25411\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25411\350\256\262.md" @@ -18,7 +18,7 @@ ShardingSphere 的编排治理功能非常丰富,与日常开发紧密相关 在 ShardingSphere 中,提供了多种配置中心的实现方案,包括主流的 ZooKeeeper、Etcd、Apollo 和 Nacos。开发人员也可以根据需要实现自己的配置中心并通过 SPI 机制加载到 ShardingSphere 运行时环境中。 -另一方面,配置信息不是一成不变的。 **对修改后的配置信息的统一分发,是配置中心可以提供的另一个重要能力** 。配置中心中配置信息的任何变化都可以实时同步到各个服务实例中。在 ShardingSphere 中,通过配置中心可以支持数据源、数据表、分片以及读写分离策略的动态切换。 +另一方面,配置信息不是一成不变的。**对修改后的配置信息的统一分发,是配置中心可以提供的另一个重要能力** 。配置中心中配置信息的任何变化都可以实时同步到各个服务实例中。在 ShardingSphere 中,通过配置中心可以支持数据源、数据表、分片以及读写分离策略的动态切换。 同时,在集中式配置信息管理方案的基础上,ShardingSphere 也支持从本地加载配置信息的实现方案。如果我们希望以本地的配置信息为准,并将本地配置覆盖配置中心的配置,通过一个开关就可以做到这一点。 @@ -150,7 +150,7 @@ spring.shardingsphere.masterslave.slave-data-source-names=dsslave0,dsslave1 spring.shardingsphere.props.sql.show=true ``` -接下来指定配置中心,我们将 overwrite 设置为 true, **这意味着前面的这些本地配置项会覆盖保存在 ZooKeeper 服务器上的配置项,也就是说我们采用的是本地配置模式** 。然后我们设置配置中心类型为 zookeeper,服务器列表为 localhost:2181,并将命名空间设置为 orchestration-health_ms。 +接下来指定配置中心,我们将 overwrite 设置为 true,**这意味着前面的这些本地配置项会覆盖保存在 ZooKeeper 服务器上的配置项,也就是说我们采用的是本地配置模式** 。然后我们设置配置中心类型为 zookeeper,服务器列表为 localhost:2181,并将命名空间设置为 orchestration-health_ms。 ```plaintext spring.shardingsphere.orchestration.name=health_ms @@ -182,7 +182,7 @@ spring.shardingsphere.orchestration.registry.namespace=orchestration-health_ms 由于我们在本地配置文件中将 spring.shardingsphere.orchestration.overwrite 配置项设置为 true,本地配置的变化就会影响到服务器端配置,进而影响到所有使用这些配置的应用程序。如果不希望产生这种影响,而是统一使用位于配置中心上的配置,应该怎么做呢? -很简单,我们只需要将 spring.shardingsphere.orchestration.overwrite 设置为 false 即可。 **将这个配置开关进行关闭,意味着我们将只从配置中心读取配置,也就是说,本地不需要保存任何配置信息** ,只包含指定配置中心的相关内容了: +很简单,我们只需要将 spring.shardingsphere.orchestration.overwrite 设置为 false 即可。**将这个配置开关进行关闭,意味着我们将只从配置中心读取配置,也就是说,本地不需要保存任何配置信息**,只包含指定配置中心的相关内容了: ```plaintext spring.shardingsphere.orchestration.name=health_ms diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25412\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25412\350\256\262.md" index b7529bb6a..8a71ee7dd 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25412\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25412\350\256\262.md" @@ -4,11 +4,11 @@ ### 如何系统剖析 ShardingSphere 的代码结构? -在阅读开源框架时,我们碰到的一大问题在于, **常常会不由自主地陷入代码的细节而无法把握框架代码的整体结构** 。市面上主流的、被大家所熟知而广泛应用的代码框架肯定考虑得非常周全,其代码结构不可避免存在一定的复杂性。对 ShardingSphere 而言,情况也是一样,我们发现 ShardingSphere 源码的一级代码结构目录就有 15 个,而这些目录内部包含的具体 Maven 工程则多达 50 余个: +在阅读开源框架时,我们碰到的一大问题在于,**常常会不由自主地陷入代码的细节而无法把握框架代码的整体结构** 。市面上主流的、被大家所熟知而广泛应用的代码框架肯定考虑得非常周全,其代码结构不可避免存在一定的复杂性。对 ShardingSphere 而言,情况也是一样,我们发现 ShardingSphere 源码的一级代码结构目录就有 15 个,而这些目录内部包含的具体 Maven 工程则多达 50 余个: ![Drawing 0.png](assets/CgqCHl8ZTt2ASVxWAAAShIkwDl8738.png) ShardingSphere 源码一级代码结构目录 -**如何快速把握 ShardingSphere 的代码结构呢?这是我们剖析源码时需要回答的第一个问题** ,为此我们需要梳理剖析 ShardingSphere 框架代码结构的系统方法。 +**如何快速把握 ShardingSphere 的代码结构呢?这是我们剖析源码时需要回答的第一个问题**,为此我们需要梳理剖析 ShardingSphere 框架代码结构的系统方法。 本课时我们将对如何系统剖析 ShardingSphere 代码结构这一话题进行抽象,梳理出应对这一问题的六大系统方法(如下图): @@ -22,7 +22,7 @@ ShardingSphere 在设计上采用了微内核架构模式来确保系统具有 ![Drawing 3.png](assets/CgqCHl8ZTvyAET3QAABeRzWl3zI113.png) ShardingSphere 中 TypeBasedSPI 接口的类层结构 -这些接口的实现都遵循了 JDK 提供的 SPI 机制。在我们阅读 ShardingSphere 的各个代码工程时, **一旦发现在代码工程中的 META-INF/services 目录里创建了一个以服务接口命名的文件,就说明这个代码工程中包含了用于实现扩展性的 SPI 定义** 。 +这些接口的实现都遵循了 JDK 提供的 SPI 机制。在我们阅读 ShardingSphere 的各个代码工程时,**一旦发现在代码工程中的 META-INF/services 目录里创建了一个以服务接口命名的文件,就说明这个代码工程中包含了用于实现扩展性的 SPI 定义** 。 在 ShardingSphere 中,大量使用了微内核架构和 SPI 机制实现系统的扩展性。只要掌握了微内核架构的基本原理以及 SPI 的实现方式就会发现,原来在 ShardingSphere 中,很多代码结构上的组织方式就是为了满足这些扩展性的需求。ShardingSphere 中实现微内核架构的方式就是直接对 JDK 的 ServiceLoader 类进行一层简单的封装,并添加属性设置等自定义的功能,其本身并没有太多复杂的内容。 @@ -30,7 +30,7 @@ ShardingSphere 在设计上采用了微内核架构模式来确保系统具有 #### 基于分包设计原则阅读源码 -分包(Package)设计原则可以用来设计和规划开源框架的代码结构。 **对于一个包结构而言,最核心的设计要点就是高内聚和低耦合** 。我们刚开始阅读某个框架的源码时,为了避免过多地扎进细节而只关注某一个具体组件,同样可以使用这些原则来管理我们的学习预期。 +分包(Package)设计原则可以用来设计和规划开源框架的代码结构。**对于一个包结构而言,最核心的设计要点就是高内聚和低耦合** 。我们刚开始阅读某个框架的源码时,为了避免过多地扎进细节而只关注某一个具体组件,同样可以使用这些原则来管理我们的学习预期。 以 ShardingSphere 为例,我们在分析它的路由引擎时发现了两个代码工程,一个是 sharding-core-route,一个是 sharding-core-entry。从代码结构上讲,尽管这两个代码工程都不是直接面向业务开发人员,但 sharding-core-route 属于路由引擎的底层组件,包含了路由引擎的核心类 ShardingRouter。 @@ -69,7 +69,7 @@ public final class ShardingDataSourceFactory { 那么,对于 ShardingSphere 框架而言,什么才是它的主流程呢?这个问题其实不难回答。事实上,JDBC 规范为我们实现数据存储和访问提供了基本的开发流程。我们可以从 DataSource 入手,逐步引入 Connection、Statement 等对象,并完成 SQL 执行的主流程。这是从框架提供的核心功能角度梳理的一种主流程。 -对于框架内部的代码组织结构而言,实际上也存在着核心流程的概念。最典型的就是 ShardingSphere 的分片引擎结构,整个分片引擎执行流程可以非常清晰的分成五个组成部分, **分别是解析引擎、路由引擎、改写引擎、执行引擎和归并引擎** : +对于框架内部的代码组织结构而言,实际上也存在着核心流程的概念。最典型的就是 ShardingSphere 的分片引擎结构,整个分片引擎执行流程可以非常清晰的分成五个组成部分,**分别是解析引擎、路由引擎、改写引擎、执行引擎和归并引擎** : ![Drawing 8.png](assets/Ciqc1F8ZTzuASMVSAACEHFtHTxA442.png) @@ -79,7 +79,7 @@ ShardingSphere 对每个引擎都进行了明确地命名,在代码工程的 #### 基于框架演进过程阅读源码 -ShardingSphere 经历了从 1.X 到 4.X 版本的发展,功能越来越丰富,目前的代码结构已经比较复杂。但我相信 ShardingSphere 的开发人员也不是一开始就把 ShardingSphere 设计成现在这种代码结构。换个角度,如果我们自己来设计这样一个框架,通常会采用一定的策略,从简单到复杂、从核心功能到辅助机制,逐步实现和完善框架,这也是软件开发的一个基本规律。针对这个角度,当我们想要解读 ShardingSphere 的代码结构而又觉得无从下手时,可以考虑一个核心问题: **如何从易到难对框架进行逐步拆解** ? +ShardingSphere 经历了从 1.X 到 4.X 版本的发展,功能越来越丰富,目前的代码结构已经比较复杂。但我相信 ShardingSphere 的开发人员也不是一开始就把 ShardingSphere 设计成现在这种代码结构。换个角度,如果我们自己来设计这样一个框架,通常会采用一定的策略,从简单到复杂、从核心功能到辅助机制,逐步实现和完善框架,这也是软件开发的一个基本规律。针对这个角度,当我们想要解读 ShardingSphere 的代码结构而又觉得无从下手时,可以考虑一个核心问题: **如何从易到难对框架进行逐步拆解**? 实际上,在前面几个课时介绍 ShardingSphere 的核心功能时已经回答了这个问题。我们首先介绍的是分库分表功能,然后扩展到读写分离,然后再到数据脱敏。从这些功能的演进我们可以推演其背后的代码结构的演进。这里以数据脱敏功能的实现过程为例来解释这一观点。 @@ -120,9 +120,9 @@ ShardingSphere 经历了从 1.X 到 4.X 版本的发展,功能越来越丰富 ![Drawing 12.png](assets/CgqCHl8ZT5KASWyUAAAJVU7jHKk131.png) sharding-transaction-2pc 代码工程下的子工程 -在翻阅这些代码工程时,会发现每个工程中的类都很少,原因就在于, **这些类都只是完成与第三方框架的集成而已** 。所以,只要我们对这些第三方框架有一定了解,阅读这部分代码就会显得非常简单。 +在翻阅这些代码工程时,会发现每个工程中的类都很少,原因就在于,**这些类都只是完成与第三方框架的集成而已** 。所以,只要我们对这些第三方框架有一定了解,阅读这部分代码就会显得非常简单。 -再举一个例子,我们知道 ZooKeeper 可以同时用来实现配置中心和注册中心。作为一款主流的分布式协调框架,基本的工作原理就是采用了它所提供的临时节点以及监听机制。基于 ZooKeeper 的这一原理,我们可以把当前 ShardingSphere 所使用的各个 DataSource 注册到 ZooKeeper 中,并根据 DataSource 的运行时状态来动态对数据库实例进行治理,以及实现访问熔断机制。 **事实上,ShardingSphere 能做到这一点,依赖的就是 ZooKeeper 所提供的基础功能** 。只要我们掌握了这些功能,理解这块代码就不会很困难,而 ShardingSphere 本身并没有使用 ZooKeeper 中任何复杂的功能。 +再举一个例子,我们知道 ZooKeeper 可以同时用来实现配置中心和注册中心。作为一款主流的分布式协调框架,基本的工作原理就是采用了它所提供的临时节点以及监听机制。基于 ZooKeeper 的这一原理,我们可以把当前 ShardingSphere 所使用的各个 DataSource 注册到 ZooKeeper 中,并根据 DataSource 的运行时状态来动态对数据库实例进行治理,以及实现访问熔断机制。**事实上,ShardingSphere 能做到这一点,依赖的就是 ZooKeeper 所提供的基础功能** 。只要我们掌握了这些功能,理解这块代码就不会很困难,而 ShardingSphere 本身并没有使用 ZooKeeper 中任何复杂的功能。 ### 如何梳理ShardingSphere中的核心技术体系? @@ -132,7 +132,7 @@ ShardingSphere 中包含了很多技术体系,在本课程中,我们将从 这里定义基础架构的标准是,属于基础架构类的技术可以脱离 ShardingSphere 框架本身独立运行。也就是说,这些技术可以单独抽离出来,供其他框架直接使用。我们认为 ShardingSphere 所实现的微内核架构和分布式主键可以归到基础架构。 -#### 分片引擎 **分片引擎是 ShardingSphere 最核心的技术体系,包含了解析引擎、路由引擎、改写引擎、执行引擎、归并引擎和读写分离等 6 大主题** ,我们对每个主题都会详细展开。分片引擎在整个 ShardingSphere 源码解析内容中占有最大篇幅 +#### 分片引擎 **分片引擎是 ShardingSphere 最核心的技术体系,包含了解析引擎、路由引擎、改写引擎、执行引擎、归并引擎和读写分离等 6 大主题**,我们对每个主题都会详细展开。分片引擎在整个 ShardingSphere 源码解析内容中占有最大篇幅 对于解析引擎而言,我们重点梳理 SQL 解析流程所包含的各个阶段;对于路由引擎,我们将在介绍路由基本原理的基础上,给出数据访问的分片路由和广播路由,以及如何在路由过程中集成多种分片策略和分片算法的实现过程;改写引擎相对比较简单,我们将围绕如何基于装饰器模式完成 SQL 改写实现机制这一主题展开讨论;而对于执行引擎,首先需要梳理和抽象分片环境下 SQL 执行的整体流程,然后把握 ShardingSphere 中的 Executor 执行模型;在归并引擎中,我们将分析数据归并的类型,并阐述各种归并策略的实现过程;最后,我们将关注普通主从架构和分片主从架构下读写分离的实现机制。 @@ -144,7 +144,7 @@ ShardingSphere 中包含了很多技术体系,在本课程中,我们将从 在治理和集成部分,从源码角度讨论的话题包括数据脱敏、配置中心、注册中心、链路跟踪以及系统集成。 -对于数据脱敏, **我们会在改写引擎的基础上给出如何实现低侵入性的数据脱敏方案** ;配置中心用来完成配置信息的动态化管理,而注册中心则实现了数据库访问熔断机制,这两种技术可以采用通用的框架进行实现,只是面向了不同的业务场景,我们会分析通用的实现原理以及面向业务场景的差异性;ShardingSphere 中实现了一系列的 Hook 机制,我们将基于这些 Hook 机制以及 OpenTracing 协议来剖析实现数据访问链路跟踪的工作机制;当然,作为一款主流的开源框架,ShardingSphere 也完成与 Spring 以及 SpringBoot 的无缝集成,对系统集成方式的分析可以更好地帮助我们使用这个框架。 +对于数据脱敏,**我们会在改写引擎的基础上给出如何实现低侵入性的数据脱敏方案** ;配置中心用来完成配置信息的动态化管理,而注册中心则实现了数据库访问熔断机制,这两种技术可以采用通用的框架进行实现,只是面向了不同的业务场景,我们会分析通用的实现原理以及面向业务场景的差异性;ShardingSphere 中实现了一系列的 Hook 机制,我们将基于这些 Hook 机制以及 OpenTracing 协议来剖析实现数据访问链路跟踪的工作机制;当然,作为一款主流的开源框架,ShardingSphere 也完成与 Spring 以及 SpringBoot 的无缝集成,对系统集成方式的分析可以更好地帮助我们使用这个框架。 ### 从源码解析到日常开发 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25413\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25413\350\256\262.md" index 189ceacb0..8722d2790 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25413\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25413\350\256\262.md" @@ -4,13 +4,13 @@ ### 什么是微内核架构? -**微内核是一种典型的架构模式** ,区别于普通的设计模式,架构模式是一种高层模式,用于描述系统级的结构组成、相互关系及相关约束。微内核架构在开源框架中的应用也比较广泛,除了 ShardingSphere 之外,在主流的 PRC 框架 Dubbo 中也实现了自己的微内核架构。那么,在介绍什么是微内核架构之前,我们有必要先阐述这些开源框架会使用微内核架构的原因。 +**微内核是一种典型的架构模式**,区别于普通的设计模式,架构模式是一种高层模式,用于描述系统级的结构组成、相互关系及相关约束。微内核架构在开源框架中的应用也比较广泛,除了 ShardingSphere 之外,在主流的 PRC 框架 Dubbo 中也实现了自己的微内核架构。那么,在介绍什么是微内核架构之前,我们有必要先阐述这些开源框架会使用微内核架构的原因。 -#### 为什么要使用微内核架构? **微内核架构本质上是为了提高系统的扩展性** 。所谓扩展性,是指系统在经历不可避免的变更时所具有的灵活性,以及针对提供这样的灵活性所需要付出的成本间的平衡能力。也就是说,当在往系统中添加新业务时,不需要改变原有的各个组件,只需把新业务封闭在一个新的组件中就能完成整体业务的升级,我们认为这样的系统具有较好的可扩展性 +#### 为什么要使用微内核架构?**微内核架构本质上是为了提高系统的扩展性** 。所谓扩展性,是指系统在经历不可避免的变更时所具有的灵活性,以及针对提供这样的灵活性所需要付出的成本间的平衡能力。也就是说,当在往系统中添加新业务时,不需要改变原有的各个组件,只需把新业务封闭在一个新的组件中就能完成整体业务的升级,我们认为这样的系统具有较好的可扩展性 就架构设计而言,扩展性是软件设计的永恒话题。而要实现系统扩展性,一种思路是提供可插拔式的机制来应对所发生的变化。当系统中现有的某个组件不满足要求时,我们可以实现一个新的组件来替换它,而整个过程对于系统的运行而言应该是无感知的,我们也可以根据需要随时完成这种新旧组件的替换。 -比如在下个课时中我们将要介绍的 ShardingSphere 中提供的分布式主键功能,分布式主键的实现可能有很多种,而扩展性在这个点上的体现就是, **我们可以使用任意一种新的分布式主键实现来替换原有的实现,而不需要依赖分布式主键的业务代码做任何的改变** 。 +比如在下个课时中我们将要介绍的 ShardingSphere 中提供的分布式主键功能,分布式主键的实现可能有很多种,而扩展性在这个点上的体现就是,**我们可以使用任意一种新的分布式主键实现来替换原有的实现,而不需要依赖分布式主键的业务代码做任何的改变** 。 ![image.png](assets/CgqCHl8esVaAVlUFAACJmGjQZDA482.png) @@ -18,15 +18,15 @@ #### 什么是微内核架构? -从组成结构上讲, **微内核架构包含两部分组件:内核系统和插件** 。这里的内核系统通常提供系统运行所需的最小功能集,而插件是独立的组件,包含自定义的各种业务代码,用来向内核系统增强或扩展额外的业务能力。在 ShardingSphere 中,前面提到的分布式主键就是插件,而 ShardingSphere 的运行时环境构成了内核系统。 +从组成结构上讲,**微内核架构包含两部分组件:内核系统和插件** 。这里的内核系统通常提供系统运行所需的最小功能集,而插件是独立的组件,包含自定义的各种业务代码,用来向内核系统增强或扩展额外的业务能力。在 ShardingSphere 中,前面提到的分布式主键就是插件,而 ShardingSphere 的运行时环境构成了内核系统。 ![image](assets/CgqCHl8esWOAJ-5cAACfxz06p_E616.png) -那么这里的插件具体指的是什么呢?这就需要我们明确两个概念,一个概念就是经常在说的 **API** ,这是系统对外暴露的接口。而另一个概念就是 **SPI** (Service Provider Interface,服务提供接口),这是插件自身所具备的扩展点。就两者的关系而言,API 面向业务开发人员,而 SPI 面向框架开发人员,两者共同构成了 ShardingSphere 本身。 +那么这里的插件具体指的是什么呢?这就需要我们明确两个概念,一个概念就是经常在说的 **API**,这是系统对外暴露的接口。而另一个概念就是 **SPI** (Service Provider Interface,服务提供接口),这是插件自身所具备的扩展点。就两者的关系而言,API 面向业务开发人员,而 SPI 面向框架开发人员,两者共同构成了 ShardingSphere 本身。 ![image](assets/Ciqc1F8esXOADonEAACE9HEUTJc298.png) -可插拔式的实现机制说起来简单,做起来却不容易,我们需要考虑两方面内容。一方面,我们需要梳理系统的变化并把它们抽象成多个 SPI 扩展点。另一方面, **当我们实现了这些 SPI 扩展点之后,就需要构建一个能够支持这种可插拔机制的具体实现,从而提供一种 SPI 运行时环境** 。 +可插拔式的实现机制说起来简单,做起来却不容易,我们需要考虑两方面内容。一方面,我们需要梳理系统的变化并把它们抽象成多个 SPI 扩展点。另一方面,**当我们实现了这些 SPI 扩展点之后,就需要构建一个能够支持这种可插拔机制的具体实现,从而提供一种 SPI 运行时环境** 。 那么,ShardingSphere 是如何实现微内核架构的呢?让我们来一起看一下。 @@ -34,7 +34,7 @@ 事实上,JDK 已经为我们提供了一种微内核架构的实现方式,这种实现方式针对如何设计和实现 SPI 提出了一些开发和配置上的规范,ShardingSphere 使用的就是这种规范。首先,我们需要设计一个服务接口,并根据需要提供不同的实现类。接下来,我们将模拟实现分布式主键的应用场景。 -基于 SPI 的约定,创建一个单独的工程来存放服务接口,并给出接口定义。请注意 **这个服务接口的完整类路径为 com.tianyilan.KeyGenerator** ,接口中只包含一个获取目标主键的简单示例方法。 +基于 SPI 的约定,创建一个单独的工程来存放服务接口,并给出接口定义。请注意 **这个服务接口的完整类路径为 com.tianyilan.KeyGenerator**,接口中只包含一个获取目标主键的简单示例方法。 ```plaintext package com.tianyilan; @@ -60,7 +60,7 @@ public class SnowflakeKeyGenerator implements KeyGenerator { } ``` -接下来的这个步骤很关键, **在这个代码工程的 META-INF/services/ 目录下,需要创建一个以服务接口完整类路径 com.tianyilan.KeyGenerator 命名的文件** ,文件的内容是指向该接口所对应的两个实现类的完整类路径 com.tianyilan.UUIDKeyGenerator 和 com.tianyilan. SnowflakeKeyGenerator。 +接下来的这个步骤很关键,**在这个代码工程的 META-INF/services/ 目录下,需要创建一个以服务接口完整类路径 com.tianyilan.KeyGenerator 命名的文件**,文件的内容是指向该接口所对应的两个实现类的完整类路径 com.tianyilan.UUIDKeyGenerator 和 com.tianyilan. SnowflakeKeyGenerator。 我们把这个代码工程打成一个 jar 包,然后新建另一个代码工程,该代码工程需要这个 jar 包,并完成如下所示的 Main 函数。 @@ -199,7 +199,7 @@ TypeBasedSPIServiceLoader 对外暴露了服务的接口,对通过 loadTypeBas } ``` -这样,shardingsphere-spi 代码工程中的内容就介绍完毕。 **这部分内容相当于是 ShardingSphere 中所提供的插件运行时环境** 。下面我们基于 ShardingSphere 中提供的几个典型应用场景来讨论这个运行时环境的具体使用方法。 +这样,shardingsphere-spi 代码工程中的内容就介绍完毕。**这部分内容相当于是 ShardingSphere 中所提供的插件运行时环境** 。下面我们基于 ShardingSphere 中提供的几个典型应用场景来讨论这个运行时环境的具体使用方法。 #### 微内核架构在 ShardingSphere 中的应用 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25414\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25414\350\256\262.md" index 15088beb1..9645bacfd 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25414\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25414\350\256\262.md" @@ -22,7 +22,7 @@ GeneratedKey 并不是 ShardingSphere 所创造的概念。如果你熟悉 Mybat 在执行这个 insert 语句时,返回的对象中自动包含了生成的主键值。当然,这种方式能够生效的前提是对应的数据库本身支持自增长的主键。 -当我们使用 ShardingSphere 提供的自动生成键方案时,开发过程以及效果和上面描述的完全一致。在 ShardingSphere 中,同样实现了一个 GeneratedKey 类。 **请注意,该类位于 sharding-core-route 工程下** 。我们先看该类提供的 getGenerateKey 方法: +当我们使用 ShardingSphere 提供的自动生成键方案时,开发过程以及效果和上面描述的完全一致。在 ShardingSphere 中,同样实现了一个 GeneratedKey 类。**请注意,该类位于 sharding-core-route 工程下** 。我们先看该类提供的 getGenerateKey 方法: ```java public static Optional getGenerateKey(final ShardingRule shardingRule, final TableMetas tableMetas, final List parameters, final InsertStatement insertStatement) { @@ -145,7 +145,7 @@ public final class UUIDShardingKeyGenerator implements ShardingKeyGenerator { - 时间戳位 -第二个部分是 41 个 bit,表示的是时间戳。41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是365 *24* 60 *60* 1000,即 69.73 年。 **也就是说,ShardingSphere 的 SnowFlake 算法的时间纪元从 2016 年 11 月 1 日零点开始,可以使用到 2086 年** ,相信能满足绝大部分系统的要求。 +第二个部分是 41 个 bit,表示的是时间戳。41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是365 *24* 60 *60* 1000,即 69.73 年。**也就是说,ShardingSphere 的 SnowFlake 算法的时间纪元从 2016 年 11 月 1 日零点开始,可以使用到 2086 年**,相信能满足绝大部分系统的要求。 - 工作进程位 @@ -155,7 +155,7 @@ public final class UUIDShardingKeyGenerator implements ShardingKeyGenerator { 第四个部分是 12 个 bit,表示序号,也就是某个机房某台机器上在一毫秒内同时生成的 ID 序号。如果在这个毫秒内生成的数量超过 4096(即 2 的 12 次幂),那么生成器会等待下个毫秒继续生成。 -因为 SnowFlake 算法依赖于时间戳,所以还需要考虑时钟回拨这种场景。 **所谓时钟回拨,是指服务器因为时间同步,导致某一部分机器的时钟回到了过去的时间点** 。显然,时间戳的回滚会导致生成一个已经使用过的 ID,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。ShardingSphere 中最大容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。 +因为 SnowFlake 算法依赖于时间戳,所以还需要考虑时钟回拨这种场景。**所谓时钟回拨,是指服务器因为时间同步,导致某一部分机器的时钟回到了过去的时间点** 。显然,时间戳的回滚会导致生成一个已经使用过的 ID,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。ShardingSphere 中最大容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。 了解了 SnowFlake 算法的基本概念之后,我们来看 SnowflakeShardingKeyGenerator 类的具体实现。首先在 SnowflakeShardingKeyGenerator 类中存在一批常量的定义,用于维护 SnowFlake 算法中各个 bit 之间的关系,同时还存在一个 TimeService 用于获取当前的时间戳。而 SnowflakeShardingKeyGenerator 的核心方法 generateKey 负责生成具体的 ID,我们这里给出详细的代码,并为每行代码都添加注释: diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25415\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25415\350\256\262.md" index 6c4d15d82..d3c5f230e 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25415\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25415\350\256\262.md" @@ -35,7 +35,7 @@ shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStr return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new Properties()); ``` -可以看到,上述代码构建了几个数据源,加上分库、分表策略以及分片规则,然后通过 ShardingDataSourceFactory 获取了目前数据源 DataSource 。显然,对于应用开发而言, **DataSource 就是我们使用 ShardingSphere 框架的入口** 。事实上,对于 ShardingSphere 内部的运行机制而言,DataSource 同样是引导我们进入分片引擎的入口。围绕 DataSource,通过跟踪代码的调用链路,我们可以得到如下所示的类层结构图: +可以看到,上述代码构建了几个数据源,加上分库、分表策略以及分片规则,然后通过 ShardingDataSourceFactory 获取了目前数据源 DataSource 。显然,对于应用开发而言,**DataSource 就是我们使用 ShardingSphere 框架的入口** 。事实上,对于 ShardingSphere 内部的运行机制而言,DataSource 同样是引导我们进入分片引擎的入口。围绕 DataSource,通过跟踪代码的调用链路,我们可以得到如下所示的类层结构图: ![Drawing 2.png](assets/Ciqc1F8nyriAPY8tAAB8wwhtMU4809.png) @@ -47,7 +47,7 @@ return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardin ![Drawing 4.png](assets/Ciqc1F8nysKAKGdhAABINS6qFpI839.png) -从作用上讲,外观模式能够起到 **客户端与后端服务之间的隔离作用** ,随着业务需求的变化和时间的演进,外观背后各个子系统的划分和实现可能需要进行相应的调整和升级,这种调整和升级需要做到 **对客户端透明** 。在设计诸如 ShardingSphere 这样的中间件框架时,这种隔离性尤为重要。 +从作用上讲,外观模式能够起到 **客户端与后端服务之间的隔离作用**,随着业务需求的变化和时间的演进,外观背后各个子系统的划分和实现可能需要进行相应的调整和升级,这种调整和升级需要做到 **对客户端透明** 。在设计诸如 ShardingSphere 这样的中间件框架时,这种隔离性尤为重要。 对于 SQL 解析引擎而言,情况同样类似。不同之处在于,SQLParseEngine 本身并不提供外观作用,而是把这部分功能委托给了另一个核心类 SQLParseKernel。从命名上看,这个类才是 SQL 解析的内核类,也是所谓的外观类。SQLParseKernel 屏蔽了后端服务中复杂的 SQL 抽象语法树对象 SQLAST、SQL 片段对象 SQLSegment ,以及最终的 SQL 语句 SQLStatement 对象的创建和管理过程。上述这些类之间的关系如下所示: diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25416\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25416\350\256\262.md" index 6df602c21..55fdf02e8 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25416\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25416\350\256\262.md" @@ -165,7 +165,7 @@ public final class SQLSegmentsExtractorEngine { #### 2.第三阶段:填充 SQL 语句 -完成所有 SQLSegment 的提取之后,我们就来到了解析引擎的最后一个阶段,即填充 SQLStatement。所谓的 **填充过程** ,就是通过填充器 SQLSegmentFiller 为 SQLStatement 注入具体 SQLSegment 的过程。这点从 SQLSegmentFiller 接口定义中的各个参数就可以得到明确,如下所示: +完成所有 SQLSegment 的提取之后,我们就来到了解析引擎的最后一个阶段,即填充 SQLStatement。所谓的 **填充过程**,就是通过填充器 SQLSegmentFiller 为 SQLStatement 注入具体 SQLSegment 的过程。这点从 SQLSegmentFiller 接口定义中的各个参数就可以得到明确,如下所示: ```java public interface SQLSegmentFiller { @@ -276,7 +276,7 @@ public final class SQLStatementFillerEngine { **工厂模式** 的应用比较简单,作用也比较直接。例如,SQLParseEngineFactory 工厂类用于创建 SQLParseEngine,而 SQLParserFactory 工厂类用于创建 SQLParser。 -相比工厂模式, **外观类** 通常比较难识别和把握,因此,我们也花了一定篇幅介绍了 SQL 解析引擎中的外观类 SQLParseKernel,以及与 SQLParseEngine 之间的委托关系。 +相比工厂模式,**外观类** 通常比较难识别和把握,因此,我们也花了一定篇幅介绍了 SQL 解析引擎中的外观类 SQLParseKernel,以及与 SQLParseEngine 之间的委托关系。 #### 2.缓存的实现方式 @@ -296,7 +296,7 @@ Cache cache = CacheBuilder.newBuilder().softValues().initi #### 4.回调机制 -所谓 **回调** ,本质上就是一种 **双向调用模式** ,也就是说,被调用方在被调用的同时也会调用对方。在实现上,我们可以提取一个用于业务接口作为一种 Callback 接口,然后让具体的业务对象去实现这个接口。这样,当外部对象依赖于这个业务场景时,只需要依赖这个 Callback 接口,而不需要关心这个接口的具体实现类。 +所谓 **回调**,本质上就是一种 **双向调用模式**,也就是说,被调用方在被调用的同时也会调用对方。在实现上,我们可以提取一个用于业务接口作为一种 Callback 接口,然后让具体的业务对象去实现这个接口。这样,当外部对象依赖于这个业务场景时,只需要依赖这个 Callback 接口,而不需要关心这个接口的具体实现类。 这在软件设计和实现过程中是一种常见的消除业务对象和外部对象之间循环依赖的处理方式。ShardingSphere 中大量采用了这种实现方式来确保代码的可维护性,这非常值得我们学习。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25417\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25417\350\256\262.md" index 8f002a9f0..398643783 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25417\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25417\350\256\262.md" @@ -2,7 +2,7 @@ 前面我们花了几个课时对 ShardingSphere 中的 SQL 解析引擎做了介绍,我们明白 SQL 解析的作用就是根据输入的 SQL 语句生成一个 SQLStatement 对象。 -从今天开始,我们将进入 **ShardingSphere 的路由(Routing)引擎部分的源码解析** 。从流程上讲, **路由引擎** 是整个分片引擎执行流程中的第二步,即基于 SQL 解析引擎所生成的 SQLStatement,通过解析执行过程中所携带的上下文信息,来获取匹配数据库和表的分片策略,并生成路由结果。 +从今天开始,我们将进入 **ShardingSphere 的路由(Routing)引擎部分的源码解析** 。从流程上讲,**路由引擎** 是整个分片引擎执行流程中的第二步,即基于 SQL 解析引擎所生成的 SQLStatement,通过解析执行过程中所携带的上下文信息,来获取匹配数据库和表的分片策略,并生成路由结果。 ### 分层:路由引擎整体架构 @@ -16,11 +16,11 @@ #### 1.sharding-core-route 工程 -我们先来看图中的 ShardingRouter 类,该类是整个路由流程的启动点。ShardingRouter 类直接依赖于解析引擎 SQLParseEngine 类完成 SQL 解析并获取 SQLStatement 对象,然后供 PreparedStatementRoutingEngine 和 StatementRoutingEngine 进行使用。注意到这几个类都位于 sharding-core-route 工程中, **处于底层组件** 。 +我们先来看图中的 ShardingRouter 类,该类是整个路由流程的启动点。ShardingRouter 类直接依赖于解析引擎 SQLParseEngine 类完成 SQL 解析并获取 SQLStatement 对象,然后供 PreparedStatementRoutingEngine 和 StatementRoutingEngine 进行使用。注意到这几个类都位于 sharding-core-route 工程中,**处于底层组件** 。 #### 2.sharding-core-entry 工程 -另一方面,上图中的 PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 则位于 sharding-core-entry 工程中。从包的命名上看,entry 相当于是访问的入口,所以我们可以判断这个工程中所提供的类 **属于面向应用层组件** ,处于更加上层的位置。PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 的使用者分别是 ShardingPreparedStatement 和 ShardingStatement。这两个类再往上就是 ShardingConnection 以及 ShardingDataSource 这些直接面向应用层的类了。 +另一方面,上图中的 PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 则位于 sharding-core-entry 工程中。从包的命名上看,entry 相当于是访问的入口,所以我们可以判断这个工程中所提供的类 **属于面向应用层组件**,处于更加上层的位置。PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 的使用者分别是 ShardingPreparedStatement 和 ShardingStatement。这两个类再往上就是 ShardingConnection 以及 ShardingDataSource 这些直接面向应用层的类了。 ### 路由核心类:ShardingRouter @@ -73,7 +73,7 @@ ShardingRule 的内容非常丰富,但其定位更多是提供规则信息, ![image](assets/CgqCHl8xJyqAHmcfAACVSxCxm4s053.png) -ShardingRouter 是路由引擎的核心类, **在接下来的内容中,我们将对上图中的 6 个步骤分别一 一 详细展开,帮忙你理解一个路由引擎的设计思想和实现机制。** +ShardingRouter 是路由引擎的核心类,**在接下来的内容中,我们将对上图中的 6 个步骤分别一 一 详细展开,帮忙你理解一个路由引擎的设计思想和实现机制。** #### 1.分片合理性验证 @@ -163,7 +163,7 @@ public final class SQLStatementContextFactory { - InsertSQLStatementContext - CommonSQLStatementContext -它们都实现了 SQLStatementContext 接口,顾名思义,所谓的 **SQLStatementContext 就是一种上下文对象** ,保存着与特定 SQLStatement 相关的上下文信息,用于为后续处理提供数据存储和传递的手段。 +它们都实现了 SQLStatementContext 接口,顾名思义,所谓的 **SQLStatementContext 就是一种上下文对象**,保存着与特定 SQLStatement 相关的上下文信息,用于为后续处理提供数据存储和传递的手段。 我们可以想象在 SQLStatementContext 中势必都持有 SQLStatement 对象以及与表结构信息相关的上下文 TablesContext。 @@ -470,6 +470,6 @@ public final class SimpleQueryShardingEngine extends BaseShardingEngine { ### 小结与预告 -作为 ShardingSphere 分片引擎的第二个核心组件,路由引擎的目的在于生成 SQLRouteResult目标对象。而整个路由引擎中最核心的就是 ShardingRouter 类。今天,我们对 ShardingRouter 的整体执行流程进行了详细的讨论,同时也引出了路由引擎中的底层对象 RoutingEngine。 **这里给你留一道思考题:ShardingSphere 中,一个完整的路由执行过程需要经历哪些步骤?** 欢迎你在留言区与大家讨论,我将一一点评解答。 +作为 ShardingSphere 分片引擎的第二个核心组件,路由引擎的目的在于生成 SQLRouteResult目标对象。而整个路由引擎中最核心的就是 ShardingRouter 类。今天,我们对 ShardingRouter 的整体执行流程进行了详细的讨论,同时也引出了路由引擎中的底层对象 RoutingEngine。**这里给你留一道思考题:ShardingSphere 中,一个完整的路由执行过程需要经历哪些步骤?** 欢迎你在留言区与大家讨论,我将一一点评解答。 在今天的课程中,我们也提到了 ShardingSphere 中存在多种 RoutingEngine。在下一课时的内容中,我们将关注于这些 RoutingEngine 的具体实现过程。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25418\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25418\350\256\262.md" index 960247b99..3b5450527 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25418\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25418\350\256\262.md" @@ -10,7 +10,7 @@ ### 分片路由 -对于分片路由而言,我们将重点介绍 **标准路由** ,标准路由是 ShardingSphere 推荐使用的分片方式。 +对于分片路由而言,我们将重点介绍 **标准路由**,标准路由是 ShardingSphere 推荐使用的分片方式。 在使用过程中,我们需要首先考虑标准路由的适用范围。标准路由适用范围有两大场景:一种面向不包含关联查询的 SQL;另一种则适用于仅包含绑定表关联查询的 SQL。前面一种场景比较好理解,而针对后者,我们就需要引入绑定表这个 ShardingSphere 中的重要概念。 @@ -150,7 +150,7 @@ private boolean isRoutingByHint(final TableRule tableRule) { 在 ShardingSphere 中,Hint 代表的是一种强制路由的方法,是一条流程的支线。然后,我们再看 getDataNodes 方法中的 isRoutingByShardingConditions 判断。想要判断是否根据分片条件进行路由,其逻辑在于 DatabaseShardingStrategy 和 TableShardingStrategy 都不是 HintShardingStrategy 时就走这个代码分支。而最终如果 isRoutingByHint 和 isRoutingByShardingConditions 都不满足,也就是说,DatabaseShardingStrategy 或 TableShardingStrategy 中任意一个是 HintShardingStrategy,则执行 routeByMixedConditions 这一混合的路由方式。 -以上三条代码分支虽然处理方式有所不同,但 **本质上都是获取 RouteValue 的集合** ,我们在上一课时中介绍路由条件 ShardingCondition 时知道,RouteValue 保存的就是用于路由的表名和列名。在获取了所需的 RouteValue 之后,在 StandardRoutingEngine 中,以上三种场景最终都会调用 route0 基础方法进行路由,该方法的作用就是根据这些 RouteValue 得出目标 DataNode 的集合。同样,我们也知道 DataNode 中保存的就是具体的目标节点,包括 dataSourceName和tableName。route0 方法如下所示: +以上三条代码分支虽然处理方式有所不同,但 **本质上都是获取 RouteValue 的集合**,我们在上一课时中介绍路由条件 ShardingCondition 时知道,RouteValue 保存的就是用于路由的表名和列名。在获取了所需的 RouteValue 之后,在 StandardRoutingEngine 中,以上三种场景最终都会调用 route0 基础方法进行路由,该方法的作用就是根据这些 RouteValue 得出目标 DataNode 的集合。同样,我们也知道 DataNode 中保存的就是具体的目标节点,包括 dataSourceName和tableName。route0 方法如下所示: ```java private Collection route0(final TableRule tableRule, final List databaseShardingValues, final List tableShardingValues) { diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25419\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25419\350\256\262.md" index d31f8c84a..05c67308b 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25419\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25419\350\256\262.md" @@ -1,6 +1,6 @@ # 19 路由引擎:如何在路由过程中集成多种路由策略和路由算法? -上一课时《18 | 路由引擎:如何实现数据访问的分片路由和广播路由?》,我们在介绍 ShardingRule 对象时,引出了 ShardingSphere 路由引擎中的 **分片策略 ShardingStrategy** ,分片策略是路由引擎中的一个核心概念,直接影响了最终的路由结果。今天,我们将围绕这一核心概念展开讨论。 +上一课时《18 | 路由引擎:如何实现数据访问的分片路由和广播路由?》,我们在介绍 ShardingRule 对象时,引出了 ShardingSphere 路由引擎中的 **分片策略 ShardingStrategy**,分片策略是路由引擎中的一个核心概念,直接影响了最终的路由结果。今天,我们将围绕这一核心概念展开讨论。 ### 分片策略整体结构 @@ -21,7 +21,7 @@ public interface ShardingStrategy { ShardingStrategy 实现类图 -如果我们翻阅这些具体 ShardingStrategy 实现类的代码,会发现每个 ShardingStrategy 中都会包含另一个与路由相关的核心概念,即 **分片算法 ShardingAlgorithm** ,我们发现 ShardingAlgorithm 是一个空接口,但包含了 **四个继承接口** ,即 +如果我们翻阅这些具体 ShardingStrategy 实现类的代码,会发现每个 ShardingStrategy 中都会包含另一个与路由相关的核心概念,即 **分片算法 ShardingAlgorithm**,我们发现 ShardingAlgorithm 是一个空接口,但包含了 **四个继承接口**,即 - PreciseShardingAlgorithm - RangeShardingAlgorithm @@ -34,7 +34,7 @@ ShardingStrategy 实现类图 ShardingAlgorithm 子接口和实现类图 -请注意,ShardingStrategy 与 ShardingAlgorithm 之间并不是一对一的关系。 **在一个 ShardingStrategy 中,可以同时使用多个 ShardingAlgorithm 来完成具体的路由执行策略** 。因此,我们具有如下所示的类层结构关系图: +请注意,ShardingStrategy 与 ShardingAlgorithm 之间并不是一对一的关系。**在一个 ShardingStrategy 中,可以同时使用多个 ShardingAlgorithm 来完成具体的路由执行策略** 。因此,我们具有如下所示的类层结构关系图: ![Drawing 2.png](assets/Ciqc1F86Zm-AE3bOAACLylkVuks873.png) @@ -76,7 +76,7 @@ public final class NoneShardingStrategy implements ShardingStrategy { > 关于 Hint 的概念和前置路由的应用方式,可以回顾 \[《07 | 数据分片:如何实现分库、分表、分库+分表以及强制路由(下)?》\]中的内容。 -基于 HintShardingStrategy,我们可以通过 Hint 而非 SQL 解析的方式执行分片策略。而 HintShardingStrategy 的实现依赖于 **HintShardingAlgorithm** ,HintShardingAlgorithm 继承了 ShardingAlgorithm 接口。 +基于 HintShardingStrategy,我们可以通过 Hint 而非 SQL 解析的方式执行分片策略。而 HintShardingStrategy 的实现依赖于 **HintShardingAlgorithm**,HintShardingAlgorithm 继承了 ShardingAlgorithm 接口。 其定义如下所示,可以看到该接口同样存在一个 doSharding 方法: @@ -132,7 +132,7 @@ public final class HintShardingStrategy implements ShardingStrategy { StandardShardingStrategy 是一种标准分片策略,提供对 SQL 语句中的=, >, \<, >=, \<=, IN 和 BETWEEN AND 等操作的分片支持。 -我们知道分片策略相当于分片算法与分片键的组合。对于 StandardShardingStrategy 而言,它 **只支持单分片键** ,并提供 **PreciseShardingAlgorithm** 和 **RangeShardingAlgorithm** 这两个分片算法。 +我们知道分片策略相当于分片算法与分片键的组合。对于 StandardShardingStrategy 而言,它 **只支持单分片键**,并提供 **PreciseShardingAlgorithm** 和 **RangeShardingAlgorithm** 这两个分片算法。 - PreciseShardingAlgorithm 是必选的,用于处理 = 和 IN 的分片; - RangeShardingAlgorithm 是可选的,用于处理 BETWEEN AND, >, \<, >=, \<= 分片。 @@ -158,7 +158,7 @@ public final class PreciseModuloDatabaseShardingAlgorithm implements PreciseShar } ``` -可以看到,这里对 PreciseShardingValue 进行了对 2 的取模计算,并与传入的 availableTargetNames 进行比对,从而决定目标数据库。 **(2)RangeShardingAlgorithm** 而对于 RangeShardingAlgorithm 而言,情况就相对复杂。RangeShardingAlgorithm 同样具有两个实现类:分别为 RangeModuloDatabaseShardingAlgorithm 和 RangeModuloTableShardingAlgorithm,它们的命名和代码风格与 PreciseShardingAlgorithm 的实现类非常类似。 +可以看到,这里对 PreciseShardingValue 进行了对 2 的取模计算,并与传入的 availableTargetNames 进行比对,从而决定目标数据库。**(2)RangeShardingAlgorithm** 而对于 RangeShardingAlgorithm 而言,情况就相对复杂。RangeShardingAlgorithm 同样具有两个实现类:分别为 RangeModuloDatabaseShardingAlgorithm 和 RangeModuloTableShardingAlgorithm,它们的命名和代码风格与 PreciseShardingAlgorithm 的实现类非常类似。 这里也以 RangeModuloDatabaseShardingAlgorithm 为例,它的实现如下所示: @@ -181,7 +181,7 @@ public final class RangeModuloDatabaseShardingAlgorithm implements RangeSharding } ``` -与 PreciseModuloDatabaseShardingAlgorithm 相比,这里多了一层 for 循环,在该循环中添加了对范围 ValueRange 的 lowerEndpoint() 到 upperEndpoint() 中各个值的计算和比对。 **(3) StandardShardingStrategy 类** 介绍完分片算法之后,我们回到 StandardShardingStrategy 类,我们来看它的 doSharding 方法,如下所示: +与 PreciseModuloDatabaseShardingAlgorithm 相比,这里多了一层 for 循环,在该循环中添加了对范围 ValueRange 的 lowerEndpoint() 到 upperEndpoint() 中各个值的计算和比对。**(3) StandardShardingStrategy 类** 介绍完分片算法之后,我们回到 StandardShardingStrategy 类,我们来看它的 doSharding 方法,如下所示: ```java @Override @@ -363,7 +363,7 @@ ShardingStrategyConfiguration 相关类的包结构 ### 从源码解析到日常开发 -在我们设计软件系统的过程中,面对复杂业务场景时, **职责分离** 始终是需要考虑的一个设计点。ShardingSphere 对于分片策略的设计和实现很好地印证了这一观点。 +在我们设计软件系统的过程中,面对复杂业务场景时,**职责分离** 始终是需要考虑的一个设计点。ShardingSphere 对于分片策略的设计和实现很好地印证了这一观点。 分片策略在 ShardingSphere 中实际上是一个比较复杂的概念,但通过将分片的具体算法分离出去并提炼 ShardingAlgorithm 接口,并构建 ShardingStrategy 和 ShardingAlgorithm 之间一对多的灵活关联关系,我们可以更好地把握整个分片策略体系的类层结构,这种职责分离机制同样可以应用与日常开发过程中。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25420\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25420\350\256\262.md" index 2a00db376..78d642c13 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25420\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25420\350\256\262.md" @@ -4,7 +4,7 @@ **SQL 改写** 在分库分表框架中通常位于路由之后,也是整个 SQL 执行流程中的重要环节,因为开发人员是面向逻辑库与逻辑表所书写的 SQL,并不能够直接在真实的数据库中执行,SQL 改写,用于将逻辑 SQL 改写为在真实数据库中可以正确执行的 SQL。 -事实上,我们已经在前面的案例中看到了 SQL 改写的应用场景,这个场景就是分布式主键的自动生成过程。在关系型数据库中, **自增主键** 是常见的功能特性,而对于 ShardingSphere 而言,这也是 SQL 改写的典型应用场景。 +事实上,我们已经在前面的案例中看到了 SQL 改写的应用场景,这个场景就是分布式主键的自动生成过程。在关系型数据库中,**自增主键** 是常见的功能特性,而对于 ShardingSphere 而言,这也是 SQL 改写的典型应用场景。 今天,我们就将基于自增主键这一场景来探讨 **ShardingSphere 中 SQL 改写的实现过程** 。 @@ -72,7 +72,7 @@ private final SQLTokenGenerators sqlTokenGenerators = new SQLTokenGenerators(); #### 2. SQLToken -接下来,我们来看一下 SQLToken 对象,该对象在改写引擎中重要性很高, **SQLRewriteEngine 正是基于 SQLToken 实现了 SQL 改写** ,SQLToken 类的定义如下所示: +接下来,我们来看一下 SQLToken 对象,该对象在改写引擎中重要性很高,**SQLRewriteEngine 正是基于 SQLToken 实现了 SQL 改写**,SQLToken 类的定义如下所示: ```java @RequiredArgsConstructor @@ -220,7 +220,7 @@ public final class ShardingSQLRewriteContextDecorator implements SQLRewriteConte #### 1. 参数改写 -参数改写部分又引入了几个新类。首当其冲的是 ParameterRewriter 以及构建它的 ParameterRewriterBuilder。 **(1)ParameterRewriter** 我们先来看 ParameterRewriter 的定义: +参数改写部分又引入了几个新类。首当其冲的是 ParameterRewriter 以及构建它的 ParameterRewriterBuilder。**(1)ParameterRewriter** 我们先来看 ParameterRewriter 的定义: ```plaintext public interface ParameterRewriter { @@ -240,7 +240,7 @@ public boolean isNeedRewrite(final SQLStatementContext sqlStatementContext) { } ``` -显然,输入的 SQL 应该是一种 InsertSQLStatement,并且只有在路由结果已经包含了 GeneratedKey 的情况下才执行这种改写。 **(2)ParameterRewriterBuilder** 在介绍 rewrite 方法之前,我们先来理解 **ParameterBuilder** 的概念,ParameterBuilder 是一种参数构建器: +显然,输入的 SQL 应该是一种 InsertSQLStatement,并且只有在路由结果已经包含了 GeneratedKey 的情况下才执行这种改写。**(2)ParameterRewriterBuilder** 在介绍 rewrite 方法之前,我们先来理解 **ParameterBuilder** 的概念,ParameterBuilder 是一种参数构建器: ```plaintext public interface ParameterBuilder { @@ -334,7 +334,7 @@ public interface SQLRewriteEngine { SQLRewriteEngine 接口只有一个方法,即根据输入的 SQLRewriteContext 返回一个 SQLRewriteResult 对象。我们通过前面的介绍已经了解到,可以通过装饰器类对 SQLRewriteContext 进行装饰,从而满足不同场景的需要。 -注意到 SQLRewriteEngine 接口只有两个实现类:分别是 DefaultSQLRewriteEngine 和 ShardingSQLRewriteEngine。我们重点关注 ShardingSQLRewriteEngine,但在介绍这个改写引擎类之前,我们先要介绍一下 **SQLBuilder 接口** ,从定义上可以看出 SQLBuilder 的目的就是构建最终可以执行的 SQL 语句: +注意到 SQLRewriteEngine 接口只有两个实现类:分别是 DefaultSQLRewriteEngine 和 ShardingSQLRewriteEngine。我们重点关注 ShardingSQLRewriteEngine,但在介绍这个改写引擎类之前,我们先要介绍一下 **SQLBuilder 接口**,从定义上可以看出 SQLBuilder 的目的就是构建最终可以执行的 SQL 语句: ```plaintext public interface SQLBuilder { @@ -363,7 +363,7 @@ public final String toSQL() { } ``` -可以看到,如果 SQLRewriteContext 的 sqlTokens 为空,就直接返回保存在 SQLRewriteContext 中的最终 SQL;反之,会构建一个保存 SQL的StringBuilder,然后依次添加每个 SQLTokenText 以及连接词 ConjunctionText,从而拼装成一个完整的 SQL 语句。 **注意到,这里获取 SQLTokenText 的方法是一个模板方法,需要 AbstractSQLBuilder 的子类进行实现:** +可以看到,如果 SQLRewriteContext 的 sqlTokens 为空,就直接返回保存在 SQLRewriteContext 中的最终 SQL;反之,会构建一个保存 SQL的StringBuilder,然后依次添加每个 SQLTokenText 以及连接词 ConjunctionText,从而拼装成一个完整的 SQL 语句。**注意到,这里获取 SQLTokenText 的方法是一个模板方法,需要 AbstractSQLBuilder 的子类进行实现:** ```plaintext //获取 SQLToken 文本 @@ -392,7 +392,7 @@ return sqlToken.toString(); ``` 对于输入的 SQLToken,这里有两个特殊的处理,即判断是否实现了 RoutingUnitAware 接口或 LogicAndActualTablesAware 接口。我们发现实现 RoutingUnitAware 接口的只有 ShardingInsertValuesToken;而实现 LogicAndActualTablesAware 的则有 IndexToken 和 TableToken 两个 SQLToken。 -这里以实现了 LogicAndActualTablesAware 的 TableToken 为例展开讨论。 **表名改写** 就是将逻辑表名改写为真实表名的过程,是一个典型的需要对 SQL 进行改写的场景。我们考虑最简单表名改写场景,如果逻辑 SQL 为: +这里以实现了 LogicAndActualTablesAware 的 TableToken 为例展开讨论。**表名改写** 就是将逻辑表名改写为真实表名的过程,是一个典型的需要对 SQL 进行改写的场景。我们考虑最简单表名改写场景,如果逻辑 SQL 为: ```sql SELECT user_name FROM user WHERE user_id = 1; @@ -526,7 +526,7 @@ private final List parameters; 在今天的内容中,我们可以明显感受到 **装饰器模式** 的强大作用。装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构,这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。 同时,我们注意到在 ShardingSphere 中,装饰器模式的作用对象是一个 **SQLRewriteContext** 上下文对象,这是一种值得学习的做法。在日常开发过程中,我们可以把需要根据不同场景进行不同处理的信息存储在一个上下文对象中,然后基于装饰器模式对这些信息进行装饰。两者的无缝集成,可以在很多应用场景下,完成基于子类实现方式所不能完成的功能,从而为对象动态添加一些额外的职责。 ### 小结与预告 -今天,我们花了一个课时的时间完整介绍了 ShardingSphere 中改写引擎的基本结构和各个核心类。 **改写引擎** 在设计上使用了装饰器模式,完成了从逻辑 SQL 到目标 SQL 的改写过程,我们也针对 **自增主键** 和 **表名改写** 这两个典型的应用场景,给出了对应的实现原理和源码分析。 +今天,我们花了一个课时的时间完整介绍了 ShardingSphere 中改写引擎的基本结构和各个核心类。**改写引擎** 在设计上使用了装饰器模式,完成了从逻辑 SQL 到目标 SQL 的改写过程,我们也针对 **自增主键** 和 **表名改写** 这两个典型的应用场景,给出了对应的实现原理和源码分析。 请注意,改写引擎在 ShardingSphere 中不仅仅只用于这些场景,在后面的课程“30 | 数据脱敏:如何基于改写引擎实现低侵入性数据脱敏方案?”中,我们还会看到它在数据脱敏等场景下的应用。 最后给你留一道思考题:ShardingSphere 中,如何通过装饰器模式对 SQL 改写的上下文进行装饰?欢迎你在留言区与大家讨论,我将逐一点评解答。 现在,我们已经针对输入的逻辑 SQL 通过改写引擎获取了目标 SQL,有了目标 SQL 接下来就可以执行 SQL 了,这就是下一课时中要开始介绍的 ShardingSphere 执行引擎要做的事情。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25421\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25421\350\256\262.md" index 6d87cc06b..361e86e03 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25421\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25421\350\256\262.md" @@ -1,12 +1,12 @@ # 21 执行引擎:分片环境下 SQL 执行的整体流程应该如何进行抽象? -从今天开始,我们将开始一个全新的主题,即 ShardingSphere 的执行引擎(ExecuteEngine)。 **一旦我们获取了** 从路由引擎和改写引擎中所生成的 SQL, **执行引擎** 就会完成这些SQL在具体数据库中的执行。 +从今天开始,我们将开始一个全新的主题,即 ShardingSphere 的执行引擎(ExecuteEngine)。**一旦我们获取了** 从路由引擎和改写引擎中所生成的 SQL,**执行引擎** 就会完成这些SQL在具体数据库中的执行。 执行引擎是 ShardingSphere 的核心模块,接下来我们将通过三个课时来对其进行全面介绍。今天,我们先讨论在分片环境下,ShardingSphere 对 SQL 执行的整体流程的抽象过程,后两个课时会向你讲解“如何把握 ShardingSphere 中的 Executor 执行模型”。 ### ShardingSphere 执行引擎总体结构 -在讲解具体的源代码之前,我们从《17 | 路由引擎:如何理解分片路由核心类 ShardingRouter 的运作机制?》中的 PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 这两个类出发, **看看在 ShardingSphere 中使用它们的入口** 。 +在讲解具体的源代码之前,我们从《17 | 路由引擎:如何理解分片路由核心类 ShardingRouter 的运作机制?》中的 PreparedQueryShardingEngine 和 SimpleQueryShardingEngine 这两个类出发,**看看在 ShardingSphere 中使用它们的入口** 。 我们在 **ShardingStatement** 类中找到了如下所示的一个 shard 方法,这里用到了 SimpleQueryShardingEngine: @@ -23,11 +23,11 @@ private void shard(final String sql) { 而在 **ShardingPreparedStatement** 中也存在一个类似的 shard 方法。 -从设计模式上讲,ShardingStatement 和 ShardingPreparedStatement 实际上就是很典型的 **外观类** ,它们把与 SQL 路由和执行的入口类都整合在一起。 +从设计模式上讲,ShardingStatement 和 ShardingPreparedStatement 实际上就是很典型的 **外观类**,它们把与 SQL 路由和执行的入口类都整合在一起。 -通过阅读[源码](https://github.com/tianyilan12/shardingsphere-demo),我们不难发现在 ShardingStatement 中存在一个 StatementExecutor;而在 ShardingPreparedStatement 中也存在 PreparedStatementExecutor 和 BatchPreparedStatementExecutor,这些类都以 Executor 结尾, **显然这就是我们要找的 SQL 执行引擎的入口类。** 我们发现上述三个 Executor 都位于 sharding-jdbc-core 工程中。 +通过阅读[源码](https://github.com/tianyilan12/shardingsphere-demo),我们不难发现在 ShardingStatement 中存在一个 StatementExecutor;而在 ShardingPreparedStatement 中也存在 PreparedStatementExecutor 和 BatchPreparedStatementExecutor,这些类都以 Executor 结尾,**显然这就是我们要找的 SQL 执行引擎的入口类。** 我们发现上述三个 Executor 都位于 sharding-jdbc-core 工程中。 -此外,还有一个与 sharding-core-route 和 sharding-core-rewrite 并列的 **sharding-core-execute 工程** ,从命名上看,这个工程应该也与执行引擎相关。果然,我们在这个工程中找到了 **ShardingExecuteEngine 类,这是分片执行引擎的入口类** 。 +此外,还有一个与 sharding-core-route 和 sharding-core-rewrite 并列的 **sharding-core-execute 工程**,从命名上看,这个工程应该也与执行引擎相关。果然,我们在这个工程中找到了 **ShardingExecuteEngine 类,这是分片执行引擎的入口类** 。 然后,我们又分别找到了 SQLExecuteTemplate 和 SQLExecutePrepareTemplate 类,这两个是典型的 **SQL 执行模板类** 。 @@ -76,7 +76,7 @@ executorService = MoreExecutors.listeningDecorator(getExecutorService(executorSi oreExecutors.addDelayedShutdownHook(executorService, 60, TimeUnit.SECONDS); ``` -明确了执行器 ExecutorService 之后,我们 **回到 ShardingExecuteEngine 类** ,该类以 groupExecute 方法为入口,这个方法参数比较多,也单独都列了一下: +明确了执行器 ExecutorService 之后,我们 **回到 ShardingExecuteEngine 类**,该类以 groupExecute 方法为入口,这个方法参数比较多,也单独都列了一下: ```plaintext /** @@ -99,7 +99,7 @@ public List groupExecute( } ``` -这里的分片执行组 ShardingExecuteGroup 对象实际上就是一个包含输入信息的列表,而上述 groupExecute 方法的输入是一个 ShardingExecuteGroup 的集合。通过判断输入参数 serial 是否为 true,上述代码流程分别转向了 **serialExecute 和 parallelExecute 这两个代码分支** ,接下来我来分别讲解一下这两个代码分支。 +这里的分片执行组 ShardingExecuteGroup 对象实际上就是一个包含输入信息的列表,而上述 groupExecute 方法的输入是一个 ShardingExecuteGroup 的集合。通过判断输入参数 serial 是否为 true,上述代码流程分别转向了 **serialExecute 和 parallelExecute 这两个代码分支**,接下来我来分别讲解一下这两个代码分支。 #### 2.SerialExecute @@ -202,7 +202,7 @@ private List getGroupResults(final Collection firstResults, final Coll 熟悉 Future 用法的同学对上述代码应该不会陌生,我们遍历 ListenableFuture,然后调动它的 get 方法同步等待返回结果,最后当所有的结果都获取到之后组装成一个结果列表并返回,这种写法在使用 Future 时非常常见。 -我们回过头来看,无论是 serialExecute 方法还是 parallelExecute 方法,都会从 ShardingExecuteGroup 中获取第一个 firstInputs 元素并进行执行,然后剩下的再进行同步或异步执行。ShardingSphere 这样使用线程的背后有其独特的设计思路。考虑到当前线程同样也是一种可用资源, **让第一个任务由当前线程进行执行就可以充分利用当前线程,从而最大化线程的利用率。** 至此,关于 ShardingExecuteEngine 类的介绍就告一段落。作为执行引擎,ShardingExecuteEngine 所做的事情就是提供一个多线程的执行环境。 **在系统设计上,这也是在日常开发过程中可以参考的一个技巧。我们可以设计并实现一个多线程执行环境,这个环境不需要完成具体的业务操作,而只需要负责执行传入的回调函数。ShardingSphere 中的ShardingExecuteEngine 就是提供了这样一种环境** ,同样的实现方式在其他诸如 Spring 等开源框架中也都可以看到。 +我们回过头来看,无论是 serialExecute 方法还是 parallelExecute 方法,都会从 ShardingExecuteGroup 中获取第一个 firstInputs 元素并进行执行,然后剩下的再进行同步或异步执行。ShardingSphere 这样使用线程的背后有其独特的设计思路。考虑到当前线程同样也是一种可用资源,**让第一个任务由当前线程进行执行就可以充分利用当前线程,从而最大化线程的利用率。** 至此,关于 ShardingExecuteEngine 类的介绍就告一段落。作为执行引擎,ShardingExecuteEngine 所做的事情就是提供一个多线程的执行环境。**在系统设计上,这也是在日常开发过程中可以参考的一个技巧。我们可以设计并实现一个多线程执行环境,这个环境不需要完成具体的业务操作,而只需要负责执行传入的回调函数。ShardingSphere 中的ShardingExecuteEngine 就是提供了这样一种环境**,同样的实现方式在其他诸如 Spring 等开源框架中也都可以看到。 接下来,就让我们来看一下 ShardingSphere 如何通过回调完成 SQL 的真正执行。 @@ -216,7 +216,7 @@ public interface ShardingGroupExecuteCallback { } ``` -该接口根据传入的泛型 inputs 集合和 shardingExecuteDataMap 完成真正的 SQL 执行操作。在 ShardingSphere 中,使用匿名方法实现 ShardingGroupExecuteCallback 接口的地方有很多,但显式实现这一接口的只有一个类,即 SQLExecuteCallback 类,这是一个 **抽象类** ,它的 execute 方法如下所示: +该接口根据传入的泛型 inputs 集合和 shardingExecuteDataMap 完成真正的 SQL 执行操作。在 ShardingSphere 中,使用匿名方法实现 ShardingGroupExecuteCallback 接口的地方有很多,但显式实现这一接口的只有一个类,即 SQLExecuteCallback 类,这是一个 **抽象类**,它的 execute 方法如下所示: ```java @Override @@ -280,7 +280,7 @@ return executeCallback(executeCallback); ### 模板类 SQLExecuteTemplate -在 ShardingSphere 执行引擎的底层组件中,还有一个类需要展开,这就是 **模板类 SQLExecuteTemplate** ,它是 ShardingExecuteEngine 的直接使用者。从命名上看,这是一个典型的模板工具类,定位上就像 Spring 中的 JdbcTemplate 一样。但凡这种模板工具类,其实现一般都比较简单,基本就是对底层对象的简单封装。 +在 ShardingSphere 执行引擎的底层组件中,还有一个类需要展开,这就是 **模板类 SQLExecuteTemplate**,它是 ShardingExecuteEngine 的直接使用者。从命名上看,这是一个典型的模板工具类,定位上就像 Spring 中的 JdbcTemplate 一样。但凡这种模板工具类,其实现一般都比较简单,基本就是对底层对象的简单封装。 SQLExecuteTemplate 也不例外,它要做的就是对 ShardingExecuteEngine 中的入口方法进行封装和处理。ShardingExecuteEngine 的核心方法就只有一个,即 executeGroup 方法: @@ -299,7 +299,7 @@ public List executeGroup(final Collection ShardingSphere 采用这样的设计实际上跟前面介绍的 ConnectionMode 有直接关系。 @@ -433,7 +433,7 @@ public static SQLExecuteCallback getPreparedSQLExecuteCallback(final Da ### 从源码解析到日常开发 -本课时关于两种 QueryResult 的设计思想,同样可以应用到日常开发中。当我们面对如何处理来自数据库或外部数据源的数据时,可以根据需要设计 **流式访问方式** 和 **内存访问方式** ,这两种访问方式在数据访问过程中都具有一定的代表性。 +本课时关于两种 QueryResult 的设计思想,同样可以应用到日常开发中。当我们面对如何处理来自数据库或外部数据源的数据时,可以根据需要设计 **流式访问方式** 和 **内存访问方式**,这两种访问方式在数据访问过程中都具有一定的代表性。 通常,我们会首先想到将所有访问到的数据存放在内存中,再进行二次处理,但这种处理方式会面临性能问题,流式访问方式性能更高,但需要我们挖掘适合的应用场景。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25423\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25423\350\256\262.md" index c73082bc4..d711e76bb 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25423\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25423\350\256\262.md" @@ -115,7 +115,7 @@ private void replayMethodForStatements() { 该方法实际上就是调用了基于反射的 replayMethodsInvocation 方法,然后这个replayMethodsInvocation 方法会针对 statementExecutor 中所有 Statement的 SQL 操作执行目标方法。 -最后,我们通过执行 statementExecutor.executeQuery() 方法获取 SQL 执行的结果,并用这个结果来创建 **归并引擎 MergeEngine** ,并通过归并引擎 MergeEngine 获取最终的执行结果。 +最后,我们通过执行 statementExecutor.executeQuery() 方法获取 SQL 执行的结果,并用这个结果来创建 **归并引擎 MergeEngine**,并通过归并引擎 MergeEngine 获取最终的执行结果。 > **归并引擎** 是 ShardingSphere 中与 SQL 解析引擎、路由引擎以及执行引擎并列的一个引擎,我们在下一课时中就会开始介绍这块内容,这里先不做具体展开。 @@ -178,7 +178,7 @@ public ResultSet executeQuery() throws SQLException { 这里我们没加注释,但也应该理解这一方法的执行流程,因为该方法的风格与 ShardingStatement 中的同名方法非常一致。 -关于 ShardingPreparedStatement 就没有太多可以介绍的内容了,我们接着来看它的父类 **AbstractShardingPreparedStatementAdapter 类** ,看到该类持有一个 SetParameterMethodInvocation 的列表,以及一个参数列表: +关于 ShardingPreparedStatement 就没有太多可以介绍的内容了,我们接着来看它的父类 **AbstractShardingPreparedStatementAdapter 类**,看到该类持有一个 SetParameterMethodInvocation 的列表,以及一个参数列表: ```java private final List setParameterMethodInvocations = new LinkedList<>(); @@ -219,7 +219,7 @@ protected final void replaySetParameter(final PreparedStatement preparedStatemen } ``` -关于 AbstractShardingPreparedStatementAdapter 还需要注意的是它的 **类层结构** ,如下图所示,可以看到 AbstractShardingPreparedStatementAdapter 继承了 AbstractUnsupportedOperationPreparedStatement 类;而 AbstractUnsupportedOperationPreparedStatement 却又继承了 AbstractStatementAdapter 类并实现了 PreparedStatement: +关于 AbstractShardingPreparedStatementAdapter 还需要注意的是它的 **类层结构**,如下图所示,可以看到 AbstractShardingPreparedStatementAdapter 继承了 AbstractUnsupportedOperationPreparedStatement 类;而 AbstractUnsupportedOperationPreparedStatement 却又继承了 AbstractStatementAdapter 类并实现了 PreparedStatement: ![Drawing 2.png](assets/Ciqc1F9MzNeACiagAACzQd-8eig186.png) @@ -424,7 +424,7 @@ List getUsers(final String sql) throws SQLException { } ``` -ShardingSphere 通过在准备阶段获取的连接模式,在执行阶段生成 **内存归并结果集** 或 **流式归并结果集** ,并将其传递至 **结果归并引擎** ,以进行下一步工作。 +ShardingSphere 通过在准备阶段获取的连接模式,在执行阶段生成 **内存归并结果集** 或 **流式归并结果集**,并将其传递至 **结果归并引擎**,以进行下一步工作。 ### 从源码解析到日常开发 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25424\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25424\350\256\262.md" index 628a2fc25..94890bcf1 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25424\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25424\350\256\262.md" @@ -21,17 +21,17 @@ result = getResultSet(mergeEngine); #### 1.归并的分类及其实现方案 -所谓 **归并** ,就是将从各个数据节点获取的多数据结果集,通过一定的策略组合成为一个结果集并正确的返回给请求客户端的过程。 +所谓 **归并**,就是将从各个数据节点获取的多数据结果集,通过一定的策略组合成为一个结果集并正确的返回给请求客户端的过程。 -**按照不同的 SQL 类型以及应用场景划分** ,归并的类型可以分为遍历、排序、分组、分页和聚合 5 种类型,这 5 种类型是组合而非互斥的关系。 +**按照不同的 SQL 类型以及应用场景划分**,归并的类型可以分为遍历、排序、分组、分页和聚合 5 种类型,这 5 种类型是组合而非互斥的关系。 其中遍历归并是最简单的归并,而排序归并是最常用地归并,在下文我会对两者分别详细介绍。 ![Lark20200903-185718.png](assets/CgqCHl9QzC6AA5U7AABkojINfPw834.png) -归并的五大类型 **按照归并实现的结构划分** ,ShardingSphere 中又存在流式归并、内存归并和装饰者归并这三种归并方案。 +归并的五大类型 **按照归并实现的结构划分**,ShardingSphere 中又存在流式归并、内存归并和装饰者归并这三种归并方案。 -- 所谓的 **流式归并** ,类似于 JDBC 中从 ResultSet 获取结果的处理方式,也就是说通过逐条获取的方式返回正确的单条数据; +- 所谓的 **流式归并**,类似于 JDBC 中从 ResultSet 获取结果的处理方式,也就是说通过逐条获取的方式返回正确的单条数据; - **内存归并** 的思路则不同,是将结果集的所有数据先存储在内存中,通过统一的计算之后,再将其封装成为逐条访问的数据结果集进行返回。 - 最后的 **装饰者归并** 是指,通过装饰器模式对所有的结果集进行归并,并进行统一的功能增强,类似于改写引擎中 SQLRewriteContextDecorator 对 SQLRewriteContext 进行装饰的过程。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25425\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25425\350\256\262.md" index 702774484..b6da185d8 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25425\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25425\350\256\262.md" @@ -225,7 +225,7 @@ public final class AccumulationAggregationUnit implements AggregationUnit { ### 追加的归并:聚合归并 -事实上,通过前面的分析,我们已经接触到了聚合归并相关的内容,我们也是站在 **分组归并** 的基础上讨论聚合归并。在这之前,我们需要明确聚合操作本身跟分组并没有关系,即除了分组的 SQL 之外,对不进行分组的 SQL 也可以使用聚合函数。另一方面,无论采用的是流式分组归并还是内存分组归并,对聚合函数的处理都是一致的。 **聚合归并** 可以理解为是在之前介绍的归并机制之上追加的一种归并能力。 +事实上,通过前面的分析,我们已经接触到了聚合归并相关的内容,我们也是站在 **分组归并** 的基础上讨论聚合归并。在这之前,我们需要明确聚合操作本身跟分组并没有关系,即除了分组的 SQL 之外,对不进行分组的 SQL 也可以使用聚合函数。另一方面,无论采用的是流式分组归并还是内存分组归并,对聚合函数的处理都是一致的。**聚合归并** 可以理解为是在之前介绍的归并机制之上追加的一种归并能力。 MAX、MIN、SUM、COUNT,以及 AVG 这 5 种 ShardingSphere 所支持的聚合函数可以分成三大类聚合的场景,MAX 和 MIN 用于比较场景,SUM 和 COUNT 用于累加的场景,而剩下的 AVG 则用于求平均值的场景。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25426\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25426\350\256\262.md" index 01decfca5..83f86440b 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25426\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25426\350\256\262.md" @@ -102,7 +102,7 @@ private MasterSlaveLoadBalanceAlgorithm createMasterSlaveLoadBalanceAlgorithm(fi 针对 MasterSlaveLoadBalanceAlgorithm 的 SPI 配置 -按照这里的配置信息,第一个获取的 SPI 实例应该是 RoundRobinMasterSlaveLoadBalanceAlgorithm,即 **轮询策略** ,它的 getDataSource 方法实现如下: +按照这里的配置信息,第一个获取的 SPI 实例应该是 RoundRobinMasterSlaveLoadBalanceAlgorithm,即 **轮询策略**,它的 getDataSource 方法实现如下: ```java @Override diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25427\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25427\350\256\262.md" index 6685e8d99..974603f47 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25427\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25427\350\256\262.md" @@ -237,7 +237,7 @@ Atomikos、Bitronix 等第三方工具的实现方式。我们会在下一课时 介绍完 XAShardingTransactionManager 之后,我们来看上图中 ShardingTransactionManager 接口的另一个实现类 SeataATShardingTransactionManager。因为基于不同技术体系和工作原理,所以 SeataATShardingTransactionManager 中的实现方法也完全不同,让我们来看一下。 -在介绍 SeataATShardingTransactionManager 之前,我们同样有必要对 Seata 本身做一些展开。与 XA 不同, **Seata 框架** 中一个分布式事务包含三种角色,除了 XA 中同样具备的 TransactionManager(TM)和 ResourceManager(RM) 之外,还存在一个事务协调器 TransactionCoordinator (TC),维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。 +在介绍 SeataATShardingTransactionManager 之前,我们同样有必要对 Seata 本身做一些展开。与 XA 不同,**Seata 框架** 中一个分布式事务包含三种角色,除了 XA 中同样具备的 TransactionManager(TM)和 ResourceManager(RM) 之外,还存在一个事务协调器 TransactionCoordinator (TC),维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。 其中,TM 是一个分布式事务的发起者和终结者,TC 负责维护分布式事务的运行状态,而 RM 则负责本地事务的运行。 diff --git "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25435\350\256\262.md" "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25435\350\256\262.md" index f78c7d59e..4ffcb87fe 100644 --- "a/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25435\350\256\262.md" +++ "b/docs/Middleware/ShardingSphere \346\240\270\345\277\203\345\216\237\347\220\206\347\262\276\350\256\262/\347\254\25435\350\256\262.md" @@ -10,13 +10,13 @@ #### 1.数据分片 -数据分片是 ShardingSphere 的基本功能。ShardingSphere 支持常规的基于垂直拆分和水平拆分的 **分库分表** 操作。在分库分表的基础上,ShardingSphere 也实现了基于数据库主从架构的 **读写分离机制** ,而且这种读写分离机制可以和数据分片完美地进行整合。 +数据分片是 ShardingSphere 的基本功能。ShardingSphere 支持常规的基于垂直拆分和水平拆分的 **分库分表** 操作。在分库分表的基础上,ShardingSphere 也实现了基于数据库主从架构的 **读写分离机制**,而且这种读写分离机制可以和数据分片完美地进行整合。 另一方面,作为一款具有高度可扩展性的开源框架,ShardingSphere 也预留了分片扩展点,开发人员可以基于需要实现分片策略的定制化开发。 #### 2.分布式事务 -分布式事务用于 **确保分布式环境下的数据一致性** ,这也是 ShardingSphere 区别于普通分库分表框架的关键功能,并且该功能使得分布式事务能够称为一种分布式数据库中间件。 +分布式事务用于 **确保分布式环境下的数据一致性**,这也是 ShardingSphere 区别于普通分库分表框架的关键功能,并且该功能使得分布式事务能够称为一种分布式数据库中间件。 ShardingSphere 对分布式事务的支持首先体现在 **抽象层面** 上。ShardingSphere 抽象了一组标准化的事务处理接口,并通过分片事务管理器 ShardingTransactionManager 进行统一管理。同样,在 **扩展性** 上,我们也可以根据需要实现自己的 ShardingTransactionManager 从而对分布式事务进行扩展。在事务类型上,ShardingSphere 也同时支持强一致性事务和柔性事务。 @@ -24,7 +24,7 @@ ShardingSphere 对分布式事务的支持首先体现在 **抽象层面** 上 #### 3.数据库治理 -如果你一直在学习我们的专栏,相信你已经知道使用 ShardingSphere 的主要手段就是利用它的配置体系。关于 **配置信息的管理** ,我们可以基于配置文件完成配置信息的维护,这在 ShardingSphere 中都得到了支持。 +如果你一直在学习我们的专栏,相信你已经知道使用 ShardingSphere 的主要手段就是利用它的配置体系。关于 **配置信息的管理**,我们可以基于配置文件完成配置信息的维护,这在 ShardingSphere 中都得到了支持。 更进一步,在ShardingSphere 中,它还提供了配置信息动态化管理机制,即可支持数据源、表与分片及读写分离策略的动态切换。而对于系统中当前正在运行的数据库实例,我们也需要进行动态的管理。在具体应用场景上,我们可以基于注册中心完成数据库实例管理、数据库熔断禁用等治理功能。 @@ -42,7 +42,7 @@ ShardingSphere 对这一数据脱敏过程实现了自动化和透明化,开 这个案例系统足够简单,可以让你从零开始就能理解和掌握其中的各项知识点;同时这个案例系统又足够完整,涉及的各个核心功能我们都提供了相关的配置项和示例代码,供大家在日常开发过程中进行参考。 -本专栏的 **最核心内容是 ShardingSphere 的源码解析** ,这部分内容占据了整个专栏 2/3 的篇幅,可以说是课程的精髓所在。 +本专栏的 **最核心内容是 ShardingSphere 的源码解析**,这部分内容占据了整个专栏 2/3 的篇幅,可以说是课程的精髓所在。 一方面,我们给出了微内核架构,以及分布式主键的设计理念和实现方法,更重要的是,我们对 ShardingSphere 中介绍的各项核心功能都从源码出发给出了详细的设计思想和实现机制。 @@ -72,7 +72,7 @@ ShardingSphere 对这一数据脱敏过程实现了自动化和透明化,开 #### 2.影子库压测 -5.X 版本引入的第二个功能是影子库压测,这个功能的背景来自如何对系统进行全链路压测。在数据库层面,为了保证生产数据的可靠性与完整性, **需要做好数据隔离,将压测的数据请求打入影子库,以防压测数据写入生产数据库,从而对真实数据造成污染** **。** +5.X 版本引入的第二个功能是影子库压测,这个功能的背景来自如何对系统进行全链路压测。在数据库层面,为了保证生产数据的可靠性与完整性,**需要做好数据隔离,将压测的数据请求打入影子库,以防压测数据写入生产数据库,从而对真实数据造成污染** **。** 在 ShardingSphere 中,我们可以通过数据路由功能将压测所需要执行的 SQL 路由到与之对应的数据源中。与数据脱敏一样,ShardingSphere 实现影子库压测的开发方式也是配置一个影子规则。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" index 79dedd5fb..f1f3d3fb8 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25400\350\256\262.md" @@ -37,7 +37,7 @@ ZooKeeper 可以实现分布式系统下的配置管理、域名服务、分布 记得一次用 ZooKeeper 实现一个分布式锁,在生产环境运行的时候出现了加锁错误的问题,具体表现在持有锁的 c1 客户端在未主动释放锁的情况下,另一个 c2 客户端也成功获取了锁,最终导致程序运行错误。这种在本地调试排查问题的时候没有任何异常,上线却出现了问题,而本地又找不到错误的情况,相信也是很多开发人员最苦恼的了。 -我第一时间搜索答案但未果,于时开始从底层实现角度去分析问题。最终发现,原来是因为运行客户端 c1 的 JVM 发生 GC,导致服务器没有检测到 c1 客户端的”心跳“,误认为客户端下线而自动删除了临时节点,从而产生了分布式锁失效的情况。 **定位了问题缘由,解决问题就是自然而然的事儿了。** +我第一时间搜索答案但未果,于时开始从底层实现角度去分析问题。最终发现,原来是因为运行客户端 c1 的 JVM 发生 GC,导致服务器没有检测到 c1 客户端的”心跳“,误认为客户端下线而自动删除了临时节点,从而产生了分布式锁失效的情况。**定位了问题缘由,解决问题就是自然而然的事儿了。** 从我的经历中你可以看出,BAT、京东、滴滴这些大型互联网公司对技术人员的要求更高,而它们的相关职位也基本占据了薪资金字塔的顶层。面对激烈的行业竞争,除了知识的广度,我们更应该注重知识的深度,知其然更知其所以然,才能有脱颖而出的机会。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" index 5fc87abf5..320e25057 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25401\350\256\262.md" @@ -114,7 +114,7 @@ ZooKeeper 中的数据模型是一种树形结构,非常像电脑中的文件 ![image](assets/CgqCHl6yL_6AOIONAAB3daUjikw147.png) -这样就实现了一个简单的悲观锁,不过这也有一个隐含的问题,就是当进程 a 因为异常中断导致 /locks 节点始终存在,其他线程因为无法再次创建节点而无法获取锁,这就产生了一个死锁问题。针对这种情况我们可以通过将节点设置为临时节点的方式避免。并通过在服务器端添加监听事件来通知其他进程重新获取锁。 **乐观锁** 乐观锁认为,进程对临界区资源的竞争不会总是出现,所以相对悲观锁而言。加锁方式没有那么激烈,不会全程的锁定资源,而是在数据进行提交更新的时候,对数据的冲突与否进行检测,如果发现冲突了,则拒绝操作。 +这样就实现了一个简单的悲观锁,不过这也有一个隐含的问题,就是当进程 a 因为异常中断导致 /locks 节点始终存在,其他线程因为无法再次创建节点而无法获取锁,这就产生了一个死锁问题。针对这种情况我们可以通过将节点设置为临时节点的方式避免。并通过在服务器端添加监听事件来通知其他进程重新获取锁。**乐观锁** 乐观锁认为,进程对临界区资源的竞争不会总是出现,所以相对悲观锁而言。加锁方式没有那么激烈,不会全程的锁定资源,而是在数据进行提交更新的时候,对数据的冲突与否进行检测,如果发现冲突了,则拒绝操作。 **乐观锁基本可以分为读取、校验、写入三个步骤。** CAS(Compare-And-Swap),即比较并替换,就是一个乐观锁的实现。CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25402\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25402\350\256\262.md" index 98ab51072..788b95579 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25402\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25402\350\256\262.md" @@ -275,7 +275,7 @@ private void processEvent(Object event) { ![image](assets/CgqCHl61INaAJeAEAAA8lZ8lpbE688.png) -我们使用 Watch 机制实现了一个分布式环境下的配置管理功能,通过对 ZooKeeper 服务器节点添加数据变更事件,实现当数据库配置项信息变更后,集群中的各个客户端能接收到该变更事件的通知,并获取最新的配置信息。 **要注意一点是,我们提到 Watch 具有一次性,所以当我们获得服务器通知后要再次添加 Watch 事件。** +我们使用 Watch 机制实现了一个分布式环境下的配置管理功能,通过对 ZooKeeper 服务器节点添加数据变更事件,实现当数据库配置项信息变更后,集群中的各个客户端能接收到该变更事件的通知,并获取最新的配置信息。**要注意一点是,我们提到 Watch 具有一次性,所以当我们获得服务器通知后要再次添加 Watch 事件。** ### 结束语 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" index 44ee3efec..4cda72a74 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25403\350\256\262.md" @@ -53,7 +53,7 @@ addauth digest user:passwd ![image](assets/CgqCHl67tw2AbgggAACW3WWz4D4066.png) -需要注意的一点是, **每个节点都有维护自身的 ACL 权限数据,即使是该节点的子节点也是有自己的 ACL 权限而不是直接继承其父节点的权限** 。如下中“172.168.11.1”服务器有“/Config”节点的读取权限,但是没有其子节点的“/Config/dataBase_Config1”权限。 +需要注意的一点是,**每个节点都有维护自身的 ACL 权限数据,即使是该节点的子节点也是有自己的 ACL 权限而不是直接继承其父节点的权限** 。如下中“172.168.11.1”服务器有“/Config”节点的读取权限,但是没有其子节点的“/Config/dataBase_Config1”权限。 ![image](assets/CgqCHl67txWALBicAABysKoJmFg484.png) diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" index 7a4516ed3..294cf0463 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25404\350\256\262.md" @@ -85,7 +85,7 @@ public void deserialize(INputArchive a_,String tag) throws { } ``` -到这里我们就介绍完了如何在 ZooKeeper 中使用 Jute 实现序列化,需要注意的是, **在实现了Record 接口后,具体的序列化和反序列化逻辑要我们自己在 serialize 和 deserialize 函数中完成** 。 +到这里我们就介绍完了如何在 ZooKeeper 中使用 Jute 实现序列化,需要注意的是,**在实现了Record 接口后,具体的序列化和反序列化逻辑要我们自己在 serialize 和 deserialize 函数中完成** 。 序列化和反序列化的实现逻辑编码方式相对固定,首先通过 startRecord 开启一段序列化操作,之后通过 writeLong、writeString 或 readLong、 readString 等方法执行序列化或反序列化。本例中只是实现了长整型和字符型的序列化和反序列化操作,除此之外 ZooKeeper 中的 Jute 框架还支持 整数类型(Int)、布尔类型(Bool)、双精度类型(Double)以及 Byte/Buffer 类型。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" index 31670b6e1..604c33995 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25406\350\256\262.md" @@ -85,7 +85,7 @@ public class ReplyHeader implements Record { #### 服务端请求体解析 -下面我们再看一下响应协议的请求体部分,服务端的请求体可以理解为对客户端所请求内容的封装,一个服务端的请求体包含了客户端所要查询的数据而对于不同的请求类型,在 ZooKeeper 的服务端也是采用了不同的结构进行处理的。与上面我们讲解客户端请求体的方法一样,我们还是通过会话的创建、数据节点的查询和修改这三种请求操作来介绍,看看 ZooKeeper 服务端是如何响应客户端请求的。 **响应会话创建** 对于客户端发起的一次会话连接操作,ZooKeeper 服务端在处理后,会返回给客户端一个 Response 响应。而在底层代码中 ZooKeeper 是通过 ConnectRespose 类来实现的。在该类中有四个属性,分别是 protocolVersion 请求协议的版本信息、timeOut 会话超时时间、sessionId 会话标识符以及 passwd 会话密码。 +下面我们再看一下响应协议的请求体部分,服务端的请求体可以理解为对客户端所请求内容的封装,一个服务端的请求体包含了客户端所要查询的数据而对于不同的请求类型,在 ZooKeeper 的服务端也是采用了不同的结构进行处理的。与上面我们讲解客户端请求体的方法一样,我们还是通过会话的创建、数据节点的查询和修改这三种请求操作来介绍,看看 ZooKeeper 服务端是如何响应客户端请求的。**响应会话创建** 对于客户端发起的一次会话连接操作,ZooKeeper 服务端在处理后,会返回给客户端一个 Response 响应。而在底层代码中 ZooKeeper 是通过 ConnectRespose 类来实现的。在该类中有四个属性,分别是 protocolVersion 请求协议的版本信息、timeOut 会话超时时间、sessionId 会话标识符以及 passwd 会话密码。 ```java public class ConnectResponse implements Record { diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" index 5cbcdaf7c..6461790ec 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25407\350\256\262.md" @@ -105,7 +105,7 @@ final public void setZooKeeperServer(ZooKeeperServer zks) { } ``` -在通过 ServerCnxnFactory 类制定了具体的 NIO 框架类后。ZooKeeper 首先会创建一个线程 Thread 类作为 ServerCnxnFactory 类的启动主线程。之后 ZooKeeper 服务再初始化具体的 NIO 类。这里请你注意的是,虽然初始化完相关的 NIO 类 ,比如已经设置好了服务端的对外端口,客户端也能通过诸如 2181 端口等访问到服务端,但是此时 ZooKeeper 服务器还是无法处理客户端的请求操作。 **这是因为 ZooKeeper 启动后,还需要从本地的快照数据文件和事务日志文件中恢复数据** 。这之后才真正完成了 ZooKeeper 服务的启动。 +在通过 ServerCnxnFactory 类制定了具体的 NIO 框架类后。ZooKeeper 首先会创建一个线程 Thread 类作为 ServerCnxnFactory 类的启动主线程。之后 ZooKeeper 服务再初始化具体的 NIO 类。这里请你注意的是,虽然初始化完相关的 NIO 类 ,比如已经设置好了服务端的对外端口,客户端也能通过诸如 2181 端口等访问到服务端,但是此时 ZooKeeper 服务器还是无法处理客户端的请求操作。**这是因为 ZooKeeper 启动后,还需要从本地的快照数据文件和事务日志文件中恢复数据** 。这之后才真正完成了 ZooKeeper 服务的启动。 #### 初始化请求处理链 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" index 1c1a32311..33ac74ab4 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25410\350\256\262.md" @@ -8,7 +8,7 @@ 在 ZooKeeper 客户端的底层实现中,ClientCnxn 类是其核心类,所有的客户端操作都是围绕这个类进行的。ClientCnxn 类主要负责维护客户端与服务端的网络连接和信息交互。 -在前面的课程中介绍过,向服务端发送创建数据节点或者添加 Watch 监控等操作时,都会先将请求信息封装成 Packet 对象。那么 Packet 是什么呢?其实 **Packet 可以看作是一个 ZooKeeper 定义的,用来进行网络通信的数据结构** ,其主要作用是封装了网络通信协议层的数据。而 Packet 内部的数据结构如下图所示: +在前面的课程中介绍过,向服务端发送创建数据节点或者添加 Watch 监控等操作时,都会先将请求信息封装成 Packet 对象。那么 Packet 是什么呢?其实 **Packet 可以看作是一个 ZooKeeper 定义的,用来进行网络通信的数据结构**,其主要作用是封装了网络通信协议层的数据。而 Packet 内部的数据结构如下图所示: ![image.png](assets/CgqCHl7aDQyAEkoJAAB9K_a8-pA768.png) diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" index ca7d34794..794c0411d 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25411\350\256\262.md" @@ -6,7 +6,7 @@ ### 会话管理策略 -通过前面的学习,我们知道在 ZooKeeper 中为了保证一个会话的存活状态,客户端需要向服务器周期性地发送心跳信息。而客户端所发送的心跳信息可以是一个 ping 请求,也可以是一个普通的业务请求。ZooKeeper 服务端接收请求后,会更新会话的过期时间,来保证会话的存活状态。从中也能看出, **在 ZooKeeper 的会话管理中,最主要的工作就是管理会话的过期时间。** +通过前面的学习,我们知道在 ZooKeeper 中为了保证一个会话的存活状态,客户端需要向服务器周期性地发送心跳信息。而客户端所发送的心跳信息可以是一个 ping 请求,也可以是一个普通的业务请求。ZooKeeper 服务端接收请求后,会更新会话的过期时间,来保证会话的存活状态。从中也能看出,**在 ZooKeeper 的会话管理中,最主要的工作就是管理会话的过期时间。** ZooKeeper 中采用了独特的会话管理方式来管理会话的过期时间,网络上也给这种方式起了一个比较形象的名字:“分桶策略”。我将结合下图给你讲解“分桶策略”的原理。如下图所示,在 ZooKeeper 中,会话将按照不同的时间间隔进行划分,超时时间相近的会话将被放在同一个间隔区间中,这种方式避免了 ZooKeeper 对每一个会话进行检查,而是采用分批次的方式管理会话。这就降低了会话管理的难度,因为每次小批量的处理会话过期也提高了会话处理的效率。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" index 1001a7db7..7f04721b5 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25412\350\256\262.md" @@ -60,7 +60,7 @@ PrepRequestProcessor 预处理器执行完工作后,就轮到 ProposalRequestP #### Commit 流程 -**请你注意,在完成 Proposal 流程后,ZooKeeper 服务器上的数据不会进行任何改变** ,成功通过 Proposal 流程只是说明 ZooKeeper 服务可以执行事务性的请求操作了,而要真正执行具体数据变更,需要在 Commit 流程中实现,这种实现方式很像是 MySQL 等数据库的操作方式。在 Commit 流程中,它的主要作用就是完成请求的执行。其底层实现是通过 CommitProcessor 实现的。如下面的代码所示,CommitProcessor 类的内部有一个 LinkedList 类型的 queuedRequests 队列,queuedRequests 队列的作用是,当 CommitProcessor 收到请求后,并不会立刻对该条请求进行处理,而是将其放在 queuedRequests 队列中。 +**请你注意,在完成 Proposal 流程后,ZooKeeper 服务器上的数据不会进行任何改变**,成功通过 Proposal 流程只是说明 ZooKeeper 服务可以执行事务性的请求操作了,而要真正执行具体数据变更,需要在 Commit 流程中实现,这种实现方式很像是 MySQL 等数据库的操作方式。在 Commit 流程中,它的主要作用就是完成请求的执行。其底层实现是通过 CommitProcessor 实现的。如下面的代码所示,CommitProcessor 类的内部有一个 LinkedList 类型的 queuedRequests 队列,queuedRequests 队列的作用是,当 CommitProcessor 收到请求后,并不会立刻对该条请求进行处理,而是将其放在 queuedRequests 队列中。 ```plaintext class CommitProcessor { diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25413\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25413\350\256\262.md" index 26891b1de..3435f09c8 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25413\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25413\350\256\262.md" @@ -59,7 +59,7 @@ client.start(); - **connectionString** :服务器地址列表,服务器地址列表参数的格式是 host1:port1,host2:port2。在指定服务器地址列表的时候可以是一个地址,也可以是多个地址。如果是多个地址,那么每个服务器地址列表用逗号分隔。 - **retryPolicy** :重试策略,当客户端异常退出或者与服务端失去连接的时候,可以通过设置客户端重新连接 ZooKeeper 服务端。而 Curator 提供了 一次重试、多次重试等不同种类的实现方式。在 Curator 内部,可以通过判断服务器返回的 keeperException 的状态代码来判断是否进行重试处理,如果返回的是 OK 表示一切操作都没有问题,而 SYSTEMERROR 表示系统或服务端错误。 -- **超时时间** :在 Curator 客户端创建过程中,有两个超时时间的设置。这也是平时你容易混淆的地方。一个是 sessionTimeoutMs 会话超时时间,用来设置该条会话在 ZooKeeper 服务端的失效时间。另一个是 connectionTimeoutMs 客户端创建会话的超时时间,用来限制客户端发起一个会话连接到接收 ZooKeeper 服务端应答的时间。 **sessionTimeoutMs 作用在服务端,而 connectionTimeoutMs 作用在客户端** ,请你在平时的开发中多注意。 +- **超时时间** :在 Curator 客户端创建过程中,有两个超时时间的设置。这也是平时你容易混淆的地方。一个是 sessionTimeoutMs 会话超时时间,用来设置该条会话在 ZooKeeper 服务端的失效时间。另一个是 connectionTimeoutMs 客户端创建会话的超时时间,用来限制客户端发起一个会话连接到接收 ZooKeeper 服务端应答的时间。**sessionTimeoutMs 作用在服务端,而 connectionTimeoutMs 作用在客户端**,请你在平时的开发中多注意。 在完成了客户端的创建和实例后,接下来我们就来看一看如何使用 Curator 对节点进行创建、删除、更新等基础操作。 @@ -113,7 +113,7 @@ ConnectionStateListener 来监控会话的连接状态,当连接状态改变 - **CONNECTED(已连接状态)** :当客户端发起的会话成功连接到服务端后,该条会话的状态变为 CONNECTED 已连接状态。 - **READONLY(只读状态)** :当一个客户端会话调用 CuratorFrameworkFactory.Builder.canBeReadOnly() 的时候,该会话会一直处于只读模式,直到重新设置该条会话的状态类型。 -- **SUSPENDED(会话连接挂起状态)** :当进行 Leader 选举和 lock 锁等操作时,需要先挂起客户端的连接。 **注意这里的会话挂起并不等于关闭会话,也不会触发诸如删除临时节点等操作。** - **RECONNECTED(重新连接状态)** :当已经与服务端成功连接的客户端断开后,尝试再次连接服务端后,该条会话的状态为 RECONNECTED,也就是重新连接。重新连接的会话会作为一条新会话在服务端运行,之前的临时节点等信息不会被保留。 +- **SUSPENDED(会话连接挂起状态)** :当进行 Leader 选举和 lock 锁等操作时,需要先挂起客户端的连接。**注意这里的会话挂起并不等于关闭会话,也不会触发诸如删除临时节点等操作。** - **RECONNECTED(重新连接状态)** :当已经与服务端成功连接的客户端断开后,尝试再次连接服务端后,该条会话的状态为 RECONNECTED,也就是重新连接。重新连接的会话会作为一条新会话在服务端运行,之前的临时节点等信息不会被保留。 - **LOST(会话丢失状态)** :这个比较好理解,当客户端与服务器端因为异常或超时,导致会话关闭时,该条会话的状态就变为 LOST。 在这里,我们以 Curator 捕捉到的会话关闭后重新发起的与服务器端的连接为例,介绍 Curator 是如何进行处理的。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25414\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25414\350\256\262.md" index a813d252a..46d41225b 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25414\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25414\350\256\262.md" @@ -6,7 +6,7 @@ ### Leader 的协调过程 -在分布式系统中有一个著名的 CAP 定理,是说一个分布式系统不能同时满足一致性、可用性,以及分区容错性。今天我们要讲的就是一致性。其实 ZooKeeper 中实现的一致性也不是强一致性,即集群中各个服务器上的数据每时每刻都是保持一致的特性。在 ZooKeeper 中,采用的是最终一致的特性, **即经过一段时间后,ZooKeeper 集群服务器上的数据最终保持一致的特性** 。 +在分布式系统中有一个著名的 CAP 定理,是说一个分布式系统不能同时满足一致性、可用性,以及分区容错性。今天我们要讲的就是一致性。其实 ZooKeeper 中实现的一致性也不是强一致性,即集群中各个服务器上的数据每时每刻都是保持一致的特性。在 ZooKeeper 中,采用的是最终一致的特性,**即经过一段时间后,ZooKeeper 集群服务器上的数据最终保持一致的特性** 。 在 ZooKeeper 集群中,Leader 服务器主要负责处理事物性的请求,而在接收到一个客户端的事务性请求操作时,Leader 服务器会先向集群中的各个机器针对该条会话发起投票询问。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25415\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25415\350\256\262.md" index 6efc94eae..8d40ef409 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25415\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25415\350\256\262.md" @@ -18,7 +18,7 @@ Leader 服务器的选举操作主要发生在两种情况下。第一种就是 #### 发起投票 -我们先来看一下发起投票的流程,在 ZooKeeper 服务器集群初始化启动的时候,集群中的每一台服务器都会将自己作为 Leader 服务器进行投票。 **也就是每次投票时,发送的服务器的 myid(服务器标识符)和 ZXID (集群投票信息标识符)等选票信息字段都指向本机服务器。** 而一个投票信息就是通过这两个字段组成的。以集群中三个服务器 Serverhost1、Serverhost2、Serverhost3 为例,三个服务器的投票内容分别是:Severhost1 的投票是(1,0)、Serverhost2 服务器的投票是(2,0)、Serverhost3 服务器的投票是(3,0)。 +我们先来看一下发起投票的流程,在 ZooKeeper 服务器集群初始化启动的时候,集群中的每一台服务器都会将自己作为 Leader 服务器进行投票。**也就是每次投票时,发送的服务器的 myid(服务器标识符)和 ZXID (集群投票信息标识符)等选票信息字段都指向本机服务器。** 而一个投票信息就是通过这两个字段组成的。以集群中三个服务器 Serverhost1、Serverhost2、Serverhost3 为例,三个服务器的投票内容分别是:Severhost1 的投票是(1,0)、Serverhost2 服务器的投票是(2,0)、Serverhost3 服务器的投票是(3,0)。 #### 接收投票 @@ -36,7 +36,7 @@ Leader 服务器的选举操作主要发生在两种情况下。第一种就是 ![1.png](assets/CgqCHl7zKRuARwOdAACqX-dZDEQ790.png) -当 ZooKeeper 集群选举出 Leader 服务器后,ZooKeeper 集群中的服务器就开始更新自己的角色信息, **除被选举成 Leader 的服务器之外,其他集群中的服务器角色变更为 Following。** +当 ZooKeeper 集群选举出 Leader 服务器后,ZooKeeper 集群中的服务器就开始更新自己的角色信息,**除被选举成 Leader 的服务器之外,其他集群中的服务器角色变更为 Following。** #### 服务运行时的 Leader 选举 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25416\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25416\350\256\262.md" index 3ac5d234a..7d3abb115 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25416\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25416\350\256\262.md" @@ -8,17 +8,17 @@ 事物性会话请求最常用的操作类型有节点的创建、删除、更新等操作。而查询数据节点等会话请求操作就是非事务性的,因为查询不会造成 ZooKeeper 集群中服务器上数据状态的变更 。 -我们之前介绍过,分布式环境下经常会出现 CAP 定义中的一致性问题。比如当一个 ZooKeeper 集群服务器中,Leader 节点处理了一个节点的创建会话操作后,该 Leader 服务器上就新增了一个数据节点。而如果不在 ZooKeeper 集群中进行数据同步,那么其他服务器上的数据则保持旧有的状态,新增加的节点在服务器上不存在。当 ZooKeeper 集群收到来自客户端的查询请求时,会出现该数据节点查询不到的情况, **这就是典型的集群中服务器数据不一致的情况** 。为了避免这种情况的发生,在进行事务性请求的操作后,ZooKeeper 集群中的服务器要进行数据同步,而主要的数据同步是从 Learnning 服务器同步 Leader 服务器上的数据。 +我们之前介绍过,分布式环境下经常会出现 CAP 定义中的一致性问题。比如当一个 ZooKeeper 集群服务器中,Leader 节点处理了一个节点的创建会话操作后,该 Leader 服务器上就新增了一个数据节点。而如果不在 ZooKeeper 集群中进行数据同步,那么其他服务器上的数据则保持旧有的状态,新增加的节点在服务器上不存在。当 ZooKeeper 集群收到来自客户端的查询请求时,会出现该数据节点查询不到的情况,**这就是典型的集群中服务器数据不一致的情况** 。为了避免这种情况的发生,在进行事务性请求的操作后,ZooKeeper 集群中的服务器要进行数据同步,而主要的数据同步是从 Learnning 服务器同步 Leader 服务器上的数据。 ### 同步方法 -在介绍了 ZooKeeper 集群服务器的同步作用后,接下来我们再学习一下 ZooKeeper 集群中数据同步的方法。 **我们主要通过三个方面来讲解 ZooKeeper 集群中的同步方法,分别是同步条件、同步过程、同步后的处理。** #### 同步条件 +在介绍了 ZooKeeper 集群服务器的同步作用后,接下来我们再学习一下 ZooKeeper 集群中数据同步的方法。**我们主要通过三个方面来讲解 ZooKeeper 集群中的同步方法,分别是同步条件、同步过程、同步后的处理。** #### 同步条件 -同步条件是指在 ZooKeeper 集群中何时触发数据同步的机制。与上一课时中 Leader 选举首先要判断集群中 Leader 服务器是否存在不同, **要想进行集群中的数据同步,首先需要 ZooKeeper 集群中存在用来进行数据同步的 Learning 服务器。** 也就是说,当 ZooKeeper 集群中选举出 Leader 节点后,除了被选举为 Leader 的服务器,其他服务器都作为 Learnning 服务器,并向 Leader 服务器注册。之后系统就进入到数据同步的过程中。 +同步条件是指在 ZooKeeper 集群中何时触发数据同步的机制。与上一课时中 Leader 选举首先要判断集群中 Leader 服务器是否存在不同,**要想进行集群中的数据同步,首先需要 ZooKeeper 集群中存在用来进行数据同步的 Learning 服务器。** 也就是说,当 ZooKeeper 集群中选举出 Leader 节点后,除了被选举为 Leader 的服务器,其他服务器都作为 Learnning 服务器,并向 Leader 服务器注册。之后系统就进入到数据同步的过程中。 #### 同步过程 -在数据同步的过程中,ZooKeeper 集群的主要工作就是将那些没有在 Learnning 服务器上执行过的事务性请求同步到 Learning 服务器上。 **这里请你注意,事务性的会话请求会被同步,而像数据节点的查询等非事务性请求则不在数据同步的操作范围内。** 而在具体实现数据同步的时候,ZooKeeper 集群又提供四种同步方式,如下图所示: +在数据同步的过程中,ZooKeeper 集群的主要工作就是将那些没有在 Learnning 服务器上执行过的事务性请求同步到 Learning 服务器上。**这里请你注意,事务性的会话请求会被同步,而像数据节点的查询等非事务性请求则不在数据同步的操作范围内。** 而在具体实现数据同步的时候,ZooKeeper 集群又提供四种同步方式,如下图所示: ![image](assets/CgqCHl7zLYaASBk3AAA-I033owc988.png) diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" index 98eb68c01..a922b76ea 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25418\350\256\262.md" @@ -6,7 +6,7 @@ ### 非事务性请求处理过程 -在 ZooKeeper 集群接收到来自客户端的请求后,会首先判断该会话请求的类型,如是否是事务性请求。 **所谓事务性请求,是指 ZooKeeper 服务器执行完该条会话请求后,是否会导致执行该条会话请求的服务器的数据或状态发生改变,进而导致与其他集群中的服务器出现数据不一致的情况** 。 +在 ZooKeeper 集群接收到来自客户端的请求后,会首先判断该会话请求的类型,如是否是事务性请求。**所谓事务性请求,是指 ZooKeeper 服务器执行完该条会话请求后,是否会导致执行该条会话请求的服务器的数据或状态发生改变,进而导致与其他集群中的服务器出现数据不一致的情况** 。 这里我们以客户端发起的数据节点查询请求为例,分析一下 ZooKeeper 在处理非事务性请求时的实现过程。 @@ -20,7 +20,7 @@ 简单介绍完 ZooKeeper 集群中 Follow 服务器在处理非事务性请求的过程后,接下来我们再从代码层面分析一下底层的逻辑实现是怎样的。 -从代码实现的角度讲,ZooKeeper 集群在接收到来自客户端的请求后,会将请求交给 Follow 服务器进行处理。而 Follow 服务器内部首先调用的是 FollowerZooKeeperServer 类, **该类的作用是封装 Follow 服务器的属性和行为,你可以把该类当作一台 Follow 服务器的代码抽象。** 如下图所示,该 FollowerZooKeeperServer 类继承了 LearnerZooKeeperServer 。在一个 FollowerZooKeeperServer 类内部,定义了一个核心的 ConcurrentLinkedQueue 类型的队列字段,用于存放接收到的会话请求。 +从代码实现的角度讲,ZooKeeper 集群在接收到来自客户端的请求后,会将请求交给 Follow 服务器进行处理。而 Follow 服务器内部首先调用的是 FollowerZooKeeperServer 类,**该类的作用是封装 Follow 服务器的属性和行为,你可以把该类当作一台 Follow 服务器的代码抽象。** 如下图所示,该 FollowerZooKeeperServer 类继承了 LearnerZooKeeperServer 。在一个 FollowerZooKeeperServer 类内部,定义了一个核心的 ConcurrentLinkedQueue 类型的队列字段,用于存放接收到的会话请求。 ![image](assets/Ciqc1F7_AqmAchKvAABHDmj-uIc721.png) @@ -41,7 +41,7 @@ protected void setupRequestProcessors() { ### 选举过程 -介绍完 Follow 服务器处理非事务性请求的过程后,接下来我们再学习一下 Follow 服务器的另一个主要的功能:在 Leader 服务器崩溃的时候,重新选举出 Leader 服务器。 **ZooKeeper 集群重新选举 Leader 的过程本质上只有 Follow 服务器参与工作** 。而在 ZooKeeper 集群重新选举 Leader 节点的过程中,如下图所示。主要可以分为 Leader 失效发现、重新选举 Leader 、Follow 服务器角色变更、集群同步这几个步骤。 +介绍完 Follow 服务器处理非事务性请求的过程后,接下来我们再学习一下 Follow 服务器的另一个主要的功能:在 Leader 服务器崩溃的时候,重新选举出 Leader 服务器。**ZooKeeper 集群重新选举 Leader 的过程本质上只有 Follow 服务器参与工作** 。而在 ZooKeeper 集群重新选举 Leader 节点的过程中,如下图所示。主要可以分为 Leader 失效发现、重新选举 Leader 、Follow 服务器角色变更、集群同步这几个步骤。 ![image](assets/CgqCHl7_ArmAD4KYAABMANi9AkA539.png) @@ -53,7 +53,7 @@ protected void setupRequestProcessors() { #### Leader 重新选举 -当 Follow 服务器向 Leader 服务器发送状态请求包后,如果没有得到 Leader 服务器的返回信息,这时, **如果是集群中个别的 Follow 服务器发现返回错误,并不会导致 ZooKeeper 集群立刻重新选举 Leader 服务器,而是将该 Follow 服务器的状态变更为 LOOKING 状态,并向网络中发起投票,当 ZooKeeper 集群中有更多的机器发起投票,最后当投票结果满足多数原则的情况下。ZooKeeper 会重新选举出 Leader 服务器。** +当 Follow 服务器向 Leader 服务器发送状态请求包后,如果没有得到 Leader 服务器的返回信息,这时,**如果是集群中个别的 Follow 服务器发现返回错误,并不会导致 ZooKeeper 集群立刻重新选举 Leader 服务器,而是将该 Follow 服务器的状态变更为 LOOKING 状态,并向网络中发起投票,当 ZooKeeper 集群中有更多的机器发起投票,最后当投票结果满足多数原则的情况下。ZooKeeper 会重新选举出 Leader 服务器。** #### Follow 角色变更 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" index 72a116e16..f91956c18 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25419\350\256\262.md" @@ -6,7 +6,7 @@ ### Observer 介绍 -在 ZooKeeper 集群服务运行的过程中,Observer 服务器与 Follow 服务器具有一个相同的功能,那就是负责处理来自客户端的诸如查询数据节点等非事务性的会话请求操作。但与 Follow 服务器不同的是, **Observer 不参与 Leader 服务器的选举工作,也不会被选举为 Leader 服务器** 。 +在 ZooKeeper 集群服务运行的过程中,Observer 服务器与 Follow 服务器具有一个相同的功能,那就是负责处理来自客户端的诸如查询数据节点等非事务性的会话请求操作。但与 Follow 服务器不同的是,**Observer 不参与 Leader 服务器的选举工作,也不会被选举为 Leader 服务器** 。 在前面的课程中,我们或多或少有涉及 Observer 服务器,当时我们把 Follow 服务器和 Observer 服务器统称为 Learner 服务器。你可能会觉得疑惑,Observer 服务器做的事情几乎和 Follow 服务器一样,那么为什么 ZooKeeper 还要创建一个 Observer 角色服务器呢? diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" index 527b691c3..b0c552f6c 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25420\350\256\262.md" @@ -8,7 +8,7 @@ ZooKeeper 服务提供了创建节点、添加 Watcher 监控机制、集群服 首先,我们介绍一下什么是内存数据。在专栏的基础篇中,主要讲解了通过 ZooKeeper 数据节点的特性,来实现一些像发布订阅这样的功能。而这些数据节点实际上就是 ZooKeeper 在服务运行过程中所操作的数据。 -我在基础篇中提到过,ZooKeeper 的数据模型可以看作一棵 **树形结构** ,而数据节点就是这棵树上的叶子节点。从数据存储的角度看,ZooKeeper 的数据模型是存储在内存中的。我们可以把 ZooKeeper 的数据模型看作是存储在内存中的数据库,而这个数据库不但存储数据的节点信息,还存储每个数据节点的 ACL 权限信息以及 stat 状态信息等。 +我在基础篇中提到过,ZooKeeper 的数据模型可以看作一棵 **树形结构**,而数据节点就是这棵树上的叶子节点。从数据存储的角度看,ZooKeeper 的数据模型是存储在内存中的。我们可以把 ZooKeeper 的数据模型看作是存储在内存中的数据库,而这个数据库不但存储数据的节点信息,还存储每个数据节点的 ACL 权限信息以及 stat 状态信息等。 而在底层实现中,ZooKeeper 数据模型是通过 DataTree 类来定义的。如下面的代码所示,DataTree 类定义了一个 ZooKeeper 数据的内存结构。DataTree 的内部定义类 nodes 节点类型、root 根节点信息、子节点的 WatchManager 监控信息等数据模型中的相关信息。可以说,一个 DataTree 类定义了 ZooKeeper 内存数据的逻辑结构。 @@ -130,6 +130,6 @@ public void save(DataTree dataTree, ### 总结 -通过本课时的学习,我们知道在 ZooKeeper 服务的运行过程中, **会涉及内存数据** 、 **事务日志** 、 **数据快照这三种数据文件** 。从存储位置上来说,事务日志和数据快照一样,都存储在本地磁盘上;而从业务角度来讲,内存数据就是我们创建数据节点、添加监控等请求时直接操作的数据。事务日志数据主要用于记录本地事务性会话操作,用于 ZooKeeper 集群服务器之间的数据同步。事务快照则是将内存数据持久化到本地磁盘。 +通过本课时的学习,我们知道在 ZooKeeper 服务的运行过程中,**会涉及内存数据** 、 **事务日志** 、 **数据快照这三种数据文件** 。从存储位置上来说,事务日志和数据快照一样,都存储在本地磁盘上;而从业务角度来讲,内存数据就是我们创建数据节点、添加监控等请求时直接操作的数据。事务日志数据主要用于记录本地事务性会话操作,用于 ZooKeeper 集群服务器之间的数据同步。事务快照则是将内存数据持久化到本地磁盘。 -这里要注意的一点是, **数据快照是每间隔一段时间才把内存数据存储到本地磁盘,因此数据并不会一直与内存数据保持一致** 。在单台 ZooKeeper 服务器运行过程中因为异常而关闭时,可能会出现数据丢失等情况。 +这里要注意的一点是,**数据快照是每间隔一段时间才把内存数据存储到本地磁盘,因此数据并不会一直与内存数据保持一致** 。在单台 ZooKeeper 服务器运行过程中因为异常而关闭时,可能会出现数据丢失等情况。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" index 70d4726ac..5a639360a 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25421\350\256\262.md" @@ -18,10 +18,10 @@ 当死锁发生时,系统资源会一直被某一个线程占用,从而导致其他线程无法访问到该资源,最终使整个系统的业务处理或运行性能受到影响,严重的甚至可能导致服务器无法对外提供服务。 -所以当我们在设计开发分布式系统的时候,要准备一些方案来面对可能会出现的死锁问题,当问题发生时,系统会根据我们预先设计的方案,避免死锁对整个系统的影响。 **常用的解决死锁问题的方法有超时方法和死锁检测** 。 +所以当我们在设计开发分布式系统的时候,要准备一些方案来面对可能会出现的死锁问题,当问题发生时,系统会根据我们预先设计的方案,避免死锁对整个系统的影响。**常用的解决死锁问题的方法有超时方法和死锁检测** 。 -- **超时方法** :在解决死锁问题时,超时方法可能是最简单的处理方式了。 **超时方式是在创建分布式线程的时候,对每个线程都设置一个超时时间** 。当该线程的超时时间到期后,无论该线程是否执行完毕,都要关闭该线程并释放该线程所占用的系统资源。之后其他线程就可以访问该线程释放的资源,这样就不会造成分布式死锁问题。但是这种设置超时时间的方法也有很多缺点,最主要的就是很难设置一个合适的超时时间。如果时间设置过短,可能造成线程未执行完相关的处理逻辑,就因为超时时间到期就被迫关闭,最终导致程序执行出错。 -- **死锁检测** :死锁检测是处理死锁问题的另一种方法,它解决了超时方法的缺陷。与超时方法相比,死锁检测方法主动检测发现线程死锁,在控制死锁问题上更加灵活准确。 **你可以把死锁检测理解为一个运行在各个服务器系统上的线程或方法,该方法专门用来探索发现应用服务上的线程是否发生了死锁** 。如果发生死锁,就会触发相应的预设处理方案。 +- **超时方法** :在解决死锁问题时,超时方法可能是最简单的处理方式了。**超时方式是在创建分布式线程的时候,对每个线程都设置一个超时时间** 。当该线程的超时时间到期后,无论该线程是否执行完毕,都要关闭该线程并释放该线程所占用的系统资源。之后其他线程就可以访问该线程释放的资源,这样就不会造成分布式死锁问题。但是这种设置超时时间的方法也有很多缺点,最主要的就是很难设置一个合适的超时时间。如果时间设置过短,可能造成线程未执行完相关的处理逻辑,就因为超时时间到期就被迫关闭,最终导致程序执行出错。 +- **死锁检测** :死锁检测是处理死锁问题的另一种方法,它解决了超时方法的缺陷。与超时方法相比,死锁检测方法主动检测发现线程死锁,在控制死锁问题上更加灵活准确。**你可以把死锁检测理解为一个运行在各个服务器系统上的线程或方法,该方法专门用来探索发现应用服务上的线程是否发生了死锁** 。如果发生死锁,就会触发相应的预设处理方案。 ### 锁的实现 @@ -29,7 +29,7 @@ #### 排他锁 -排他锁也叫作独占锁,从名字上就可以看出它的实现原理。当我们给某一个数据对象设置了排他锁后, **只有具有该锁的事务线程可以访问该条数据对象,直到该条事务主动释放锁** 。否则,在这期间其他事务不能对该数据对象进行任何操作。在第二课时我们已经学习了利用 ZooKeeper 实现排他锁,这里不再赘述。 +排他锁也叫作独占锁,从名字上就可以看出它的实现原理。当我们给某一个数据对象设置了排他锁后,**只有具有该锁的事务线程可以访问该条数据对象,直到该条事务主动释放锁** 。否则,在这期间其他事务不能对该数据对象进行任何操作。在第二课时我们已经学习了利用 ZooKeeper 实现排他锁,这里不再赘述。 #### 共享锁 @@ -39,7 +39,7 @@ #### 创建锁 -首先,我们通过在 ZooKeeper 服务器上创建数据节点的方式来创建一个共享锁。其实无论是共享锁还是排他锁,在锁的实现方式上都是一样的。唯一的区别在于, **共享锁为一个数据事务创建两个数据节点,来区分是写入操作还是读取操作** 。如下图所示,在 ZooKeeper 数据模型上的 Locks_shared 节点下创建临时顺序节点,临时顺序节点的名称中带有请求的操作类型分别是 R 读取操作、W 写入操作。 +首先,我们通过在 ZooKeeper 服务器上创建数据节点的方式来创建一个共享锁。其实无论是共享锁还是排他锁,在锁的实现方式上都是一样的。唯一的区别在于,**共享锁为一个数据事务创建两个数据节点,来区分是写入操作还是读取操作** 。如下图所示,在 ZooKeeper 数据模型上的 Locks_shared 节点下创建临时顺序节点,临时顺序节点的名称中带有请求的操作类型分别是 R 读取操作、W 写入操作。 ![image](assets/CgqCHl8Oc56AEMuZAAAsuQwHWCY999.png) diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" index 25178e574..8a44c0f09 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25422\350\256\262.md" @@ -6,11 +6,11 @@ ### 什么是 ID 生成器 -我们先来介绍一下什么是 ID 生成器。 **分布式 ID 生成器就是通过分布式的方式,实现自动生成分配 ID 编码的程序或服务** 。在日常开发中,Java 语言中的 UUID 就是生成一个 32 位的 ID 编码生成器。根据日常使用场景, **我们生成的 ID 编码一般具有唯一性、递增性、安全性、扩展性这几个特性** 。 +我们先来介绍一下什么是 ID 生成器。**分布式 ID 生成器就是通过分布式的方式,实现自动生成分配 ID 编码的程序或服务** 。在日常开发中,Java 语言中的 UUID 就是生成一个 32 位的 ID 编码生成器。根据日常使用场景,**我们生成的 ID 编码一般具有唯一性、递增性、安全性、扩展性这几个特性** 。 ![1.png](assets/CgqCHl8RWyyAP_5_AACcEDVnsPc537.png) -**唯一性** :ID 编码作为标记分布式系统重要资源的标识符,在整个分布式系统环境下,生成的 ID 编码应该具有全局唯一的特性。如果产生两个重复的 ID 编码,就无法通过 ID 编码准确找到对应的资源,这也是一个 ID 编码最基本的要求。 **递增性** :递增性也可以说是 ID 编码的有序特性,它指一般的 ID 编码具有一定的顺序规则。比如 MySQL 数据表主键 ID,一般是一个递增的整数数字,按逐条加一的方式顺序增大。我们现在学习的 ZooKeeper 系统的 zxID 也具有递增的特性,这样在投票阶段就可以根据 zxID 的有序特性,对投票信息进行比对。 **安全性** :有的业务场景对 ID 的安全性有很高的要求,但这里说的安全性是指,如果按照递增的方式生成 ID 编码,那么这种规律很容易被发现。比如淘宝的订单编码,如果被恶意的生成或使用,会严重影响系统的安全性,所以 ID 编码必须保证其安全性。 **扩展性** :该特性是指 ID 编码规则要有一定的扩展性,按照规则生成的编码资源应该满足业务的要求。还是拿淘宝订单编码为例,假设淘宝订单的 ID 生成规则是:随机产生 4 位有效的整数组成编码,那么最多可以生成 6561 个订单编码,这显然是无法满足淘宝系统需求的。所以在设计 ID 编码的时候,要充分考虑扩展的需要,比如编码规则能够生成足够多的 ID,从而满足业务的要求,或者能够通过不同的前缀区分不同的产品或业务线 。 +**唯一性** :ID 编码作为标记分布式系统重要资源的标识符,在整个分布式系统环境下,生成的 ID 编码应该具有全局唯一的特性。如果产生两个重复的 ID 编码,就无法通过 ID 编码准确找到对应的资源,这也是一个 ID 编码最基本的要求。**递增性** :递增性也可以说是 ID 编码的有序特性,它指一般的 ID 编码具有一定的顺序规则。比如 MySQL 数据表主键 ID,一般是一个递增的整数数字,按逐条加一的方式顺序增大。我们现在学习的 ZooKeeper 系统的 zxID 也具有递增的特性,这样在投票阶段就可以根据 zxID 的有序特性,对投票信息进行比对。**安全性** :有的业务场景对 ID 的安全性有很高的要求,但这里说的安全性是指,如果按照递增的方式生成 ID 编码,那么这种规律很容易被发现。比如淘宝的订单编码,如果被恶意的生成或使用,会严重影响系统的安全性,所以 ID 编码必须保证其安全性。**扩展性** :该特性是指 ID 编码规则要有一定的扩展性,按照规则生成的编码资源应该满足业务的要求。还是拿淘宝订单编码为例,假设淘宝订单的 ID 生成规则是:随机产生 4 位有效的整数组成编码,那么最多可以生成 6561 个订单编码,这显然是无法满足淘宝系统需求的。所以在设计 ID 编码的时候,要充分考虑扩展的需要,比如编码规则能够生成足够多的 ID,从而满足业务的要求,或者能够通过不同的前缀区分不同的产品或业务线 。 ### 生成策略 @@ -18,7 +18,7 @@ 介绍完 ID 编码在整个应用系统的重要作用和 ID 编码自身的特性后。接下来我们看看几种在日常开发中常见的生成策略。 -开发人员,尤其是 Java 程序员最为熟悉的编码生成方式就是 UUID。它是一种包含 16 个字节的数字编码。 **UUID 会根据运行应用的计算机网卡 MAC 地址、时间戳、命令空间等元素,通过一定的随机算法产生** 。正因为 UUID 算法元素的复杂性,保证了 UUID 在一定范围内的随机性。 +开发人员,尤其是 Java 程序员最为熟悉的编码生成方式就是 UUID。它是一种包含 16 个字节的数字编码。**UUID 会根据运行应用的计算机网卡 MAC 地址、时间戳、命令空间等元素,通过一定的随机算法产生** 。正因为 UUID 算法元素的复杂性,保证了 UUID 在一定范围内的随机性。 UUID 在本地应用中生成,速度比较快,不依赖于其他服务,网络的好坏对其没有任何影响。但从实现上来讲,使用 UUID 策略生成的代码耦合度大,不能作为单独的 ID 生成器使用。而且生成的编码不能满足递增的特性,没有任何有序性可言,在很多业务场景中都不合适。 @@ -40,7 +40,7 @@ TDDL 生成 ID 编码的大致过程如下图所示。首先,作为 ID 生成 上面介绍的几种策略,有的和底层编码耦合比较大,有的又局限在某一具体的使用场景下,并不满足作为分布式环境下一个公共 ID 生成器的要求。接下来我们就利用目前学到的 ZooKeeper 知识,动手实现一个真正的分布式 ID 生成器。 -首先,我们通过 ZooKeeper 自身的客户端和服务器运行模式,来实现一个分布式网络环境下的 ID 请求和分发过程。 **每个需要 ID 编码的业务服务器可以看作是 ZooKeeper 的客户端** 。ID 编码生成器可以作为 ZooKeeper 的服务端。客户端通过发送请求到 ZooKeeper 服务器,来获取编码信息,服务端接收到请求后,发送 ID 编码给客户端。 +首先,我们通过 ZooKeeper 自身的客户端和服务器运行模式,来实现一个分布式网络环境下的 ID 请求和分发过程。**每个需要 ID 编码的业务服务器可以看作是 ZooKeeper 的客户端** 。ID 编码生成器可以作为 ZooKeeper 的服务端。客户端通过发送请求到 ZooKeeper 服务器,来获取编码信息,服务端接收到请求后,发送 ID 编码给客户端。 ![Drawing 2.png](assets/CgqCHl8RTBGAB7QNAAAvwu3rspw007.png) @@ -48,7 +48,7 @@ TDDL 生成 ID 编码的大致过程如下图所示。首先,作为 ID 生成 通过上面的介绍,我们发现,使用 ZooKeeper 实现一个分布式环境下的公用 ID 编码生成器很容易。利用 ZooKeeper 中的顺序节点特性,很容易使我们创建的 ID 编码具有有序的特性。并且我们也可以通过客户端传递节点的名称,根据不同的业务编码区分不同的业务系统,从而使编码的扩展能力更强。 -虽然使用 ZooKeeper 的实现方式有这么多优点,但也会有一些潜在的问题。其中最主要的是, **在定义编码的规则上还是强烈依赖于程序员自身的能力和对业务的深入理解** 。很容易出现因为考虑不周,造成设置的规则在运行一段时间后,无法满足业务要求或者安全性不够等问题。为了解决这个问题,我们继续学习一个比较常用的编码算法——snowflake 算法。 +虽然使用 ZooKeeper 的实现方式有这么多优点,但也会有一些潜在的问题。其中最主要的是,**在定义编码的规则上还是强烈依赖于程序员自身的能力和对业务的深入理解** 。很容易出现因为考虑不周,造成设置的规则在运行一段时间后,无法满足业务要求或者安全性不够等问题。为了解决这个问题,我们继续学习一个比较常用的编码算法——snowflake 算法。 #### snowflake 算法 @@ -60,7 +60,7 @@ snowflake 算法是 Twitter 公司开源的一种用来生成分布式 ID 编码 在计算编码的过程中,首先获取机器的毫秒数,并存储为 41 位,之后查询机器的工作 ID,存储在后面的 10 位字节中。剩余的 12 字节就用来存储毫秒内的流水号和表示位符号值 0。 -从图中可以看出, **snowflake 算法最主要的实现手段就是对二进制数位的操作** 。从性能上说,这个算法理论上每秒可以生成 400 多万个 ID 编码,完全满足分布式环境下,对系统高并发的要求。因此,在平时的开发过程中,也尽量使用诸如 snowflake 这种业界普遍采用的分布式 ID 生成算法,避免自己闭门造车导致的性能或安全风险。 +从图中可以看出,**snowflake 算法最主要的实现手段就是对二进制数位的操作** 。从性能上说,这个算法理论上每秒可以生成 400 多万个 ID 编码,完全满足分布式环境下,对系统高并发的要求。因此,在平时的开发过程中,也尽量使用诸如 snowflake 这种业界普遍采用的分布式 ID 生成算法,避免自己闭门造车导致的性能或安全风险。 ### 总结 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" index 05fb9cec0..470b0f360 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25423\350\256\262.md" @@ -26,13 +26,13 @@ #### 原地址哈希法 -原地址哈希算法的核心思想是根据客户端的 IP 地址进行哈希计算,用计算结果进行取模后,根据最终结果选择服务器地址列表中的一台机器,处理该条会话请求。采用这种算法后,当同一 IP 的客户端再次访问服务端后,负载均衡服务器最终选举的还是上次处理该台机器会话请求的服务器, **也就是每次都会分配同一台服务器给客户端** 。 +原地址哈希算法的核心思想是根据客户端的 IP 地址进行哈希计算,用计算结果进行取模后,根据最终结果选择服务器地址列表中的一台机器,处理该条会话请求。采用这种算法后,当同一 IP 的客户端再次访问服务端后,负载均衡服务器最终选举的还是上次处理该台机器会话请求的服务器,**也就是每次都会分配同一台服务器给客户端** 。 #### 加权轮询法 在实际的生成环境中,一个分布式或集群系统中的机器可能部署在不同的网络环境中,每台机器的配置性能也有优劣之分。因此,它们处理和响应客户端请求的能力也各不相同。采用上面几种负载均衡算法,都不太合适,这会造成能力强的服务器在处理完业务后过早进入限制状态,而性能差或网络环境不好的服务器,一直忙于处理请求,造成任务积压。 -为了解决这个问题,我们可以采用加权轮询法,加权轮询的方式与轮询算法的方式很相似,唯一的不同在于选择机器的时候,不只是单纯按照顺序的方式选择, **还根据机器的配置和性能高低有所侧重** ,配置性能好的机器往往首先分配。 +为了解决这个问题,我们可以采用加权轮询法,加权轮询的方式与轮询算法的方式很相似,唯一的不同在于选择机器的时候,不只是单纯按照顺序的方式选择,**还根据机器的配置和性能高低有所侧重**,配置性能好的机器往往首先分配。 #### 加权随机法 @@ -44,13 +44,13 @@ ### 利用 ZooKeeper 实现 -介绍完负载均衡的常用算法后,接下来我们利用 ZooKeeper 来实现一个分布式系统下的负载均衡服务器。从上面介绍的几种负载均衡算法中不难看出。一个负载均衡服务器的底层实现, **关键在于找到网络集群中最适合处理该条会话请求的机器,并将该条会话请求分配给该台机器** 。因此探测和发现后台服务器的运行状态变得最为关键。 +介绍完负载均衡的常用算法后,接下来我们利用 ZooKeeper 来实现一个分布式系统下的负载均衡服务器。从上面介绍的几种负载均衡算法中不难看出。一个负载均衡服务器的底层实现,**关键在于找到网络集群中最适合处理该条会话请求的机器,并将该条会话请求分配给该台机器** 。因此探测和发现后台服务器的运行状态变得最为关键。 #### 状态收集 首先我们来实现网络中服务器运行状态的收集功能,利用 ZooKeeper 中的临时节点作为标记网络中服务器的状态点位。在网络中服务器上线运行的时候,通过在 ZooKeeper 服务器中创建临时节点,向 ZooKeeper 的服务列表进行注册,表示本台服务器已经上线可以正常工作。通过删除临时节点或者在与 ZooKeeper 服务器断开连接后,删除该临时节点。 -最后,通过统计临时节点的数量,来了解网络中服务器的运行情况。 **如下图所示,建立的 ZooKeeper 数据模型中 Severs 节点可以作为存储服务器列表的父节点** 。用于之后通过负载均衡算法在该列表中选择服务器。在它下面创建 servers_host1、servers_host2、servers_host3等临时节点来存储集群中的服务器运行状态信息。 +最后,通过统计临时节点的数量,来了解网络中服务器的运行情况。**如下图所示,建立的 ZooKeeper 数据模型中 Severs 节点可以作为存储服务器列表的父节点** 。用于之后通过负载均衡算法在该列表中选择服务器。在它下面创建 servers_host1、servers_host2、servers_host3等临时节点来存储集群中的服务器运行状态信息。 ![Drawing 0.png](assets/Ciqc1F8X5l-APWIjAAAsDI_4m_Q833.png) diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" index 0bd0097b8..36d962d48 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25424\350\256\262.md" @@ -6,7 +6,7 @@ #### Dubbo 实现过程 -Dubbo 是阿里巴巴开发的一套开源的技术框架, **是一款高性能、轻量级的开源 Java RPC 框架** 。它提供了三大核心能力: +Dubbo 是阿里巴巴开发的一套开源的技术框架,**是一款高性能、轻量级的开源 Java RPC 框架** 。它提供了三大核心能力: - 面向接口的远程方法调用 - 智能容错和负载均衡 @@ -20,7 +20,7 @@ Dubbo 是阿里巴巴开发的一套开源的技术框架, **是一款高性 #### ZooKeeper 注册中心 -通过上面的介绍,我们不难发现在整个 Dubbo 框架的实现过程中, **注册中心是其中最为关键的一点,它保证了整个 PRC 过程中服务对外的透明性** 。而 Dubbo 的注册中心也是通过 ZooKeeper 来实现的。 +通过上面的介绍,我们不难发现在整个 Dubbo 框架的实现过程中,**注册中心是其中最为关键的一点,它保证了整个 PRC 过程中服务对外的透明性** 。而 Dubbo 的注册中心也是通过 ZooKeeper 来实现的。 如下图所示,在整个 Dubbo 服务的启动过程中,服务提供者会在启动时向 /dubbo/com.foo.BarService/providers 目录写入自己的 URL 地址,这个操作可以看作是一个 ZooKeeper 客户端在 ZooKeeper 服务器的数据模型上创建一个数据节点。服务消费者在启动时订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址,并向 /dubbo/com.foo.BarService/consumers 目录写入自己的 URL 地址。该操作是通过 ZooKeeper 服务器在 /consumers 节点路径下创建一个子数据节点,然后再在请求会话中发起对 /providers 节点的 watch 监控。 @@ -36,7 +36,7 @@ Dubbo 是阿里巴巴开发的一套开源的技术框架, **是一款高性 在介绍 ZooKeeper 在 Kafka 中如何使用之前,我们先来简单地了解一下 Kafka 的一些关键概念,以便之后的学习。如下图所示,整个 Kafka 的系统架构主要由 Broker、Topic、Partition、Producer、Consumer、Consumer Group 这几个核心概念组成,下面我们来分别进行介绍。 -![4.png](assets/Ciqc1F8ah26APMkMAAH5xDJ2qz0508.png) **Broker** Kafka 也是一个分布式的系统架构,因此在整个系统中存在多台机器,它将每台机器定义为一个 Broker。 **Topic** Kafka 的主要功能是发送和接收消息,作为一个高效的消息管道,它存在于不同的系统中。Kafka 内部,将接收到的无论何种类型的消息统一定义为 Topic 类,可以将 Topic 看作是消息的容器。 **Partition** Partition 是分区的意思,与 Topic 概念相似,它也是存放消息的容器。不过 Partition 主要是物理上的分区,而 Topic 表示消息的逻辑分区。 **Producer** Producer 是消息的生产者,整个 Kafka 系统遵循的是生产者和消费者模式,消息会从生产者流通到接收者。 **Consumer 和 Consumer Group** +![4.png](assets/Ciqc1F8ah26APMkMAAH5xDJ2qz0508.png) **Broker** Kafka 也是一个分布式的系统架构,因此在整个系统中存在多台机器,它将每台机器定义为一个 Broker。**Topic** Kafka 的主要功能是发送和接收消息,作为一个高效的消息管道,它存在于不同的系统中。Kafka 内部,将接收到的无论何种类型的消息统一定义为 Topic 类,可以将 Topic 看作是消息的容器。**Partition** Partition 是分区的意思,与 Topic 概念相似,它也是存放消息的容器。不过 Partition 主要是物理上的分区,而 Topic 表示消息的逻辑分区。**Producer** Producer 是消息的生产者,整个 Kafka 系统遵循的是生产者和消费者模式,消息会从生产者流通到接收者。**Consumer 和 Consumer Group** Consumer 即消费者,是 Kafka 框架内部对信息对接收方的定义。Consumer Group 会将消费者分组,然后按照不同的种类进行管理。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" index a090c417b..4eaa32327 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25425\350\256\262.md" @@ -33,7 +33,7 @@ sever.3=127.0.0.1:4444:4445 #### 集群模式配置 -集群模式在配置上与伪集群模式基本相同。不同之处在于配置服务器地址列表的时候, **组成 ZooKeeper 集群的各个服务器 IP 地址列表分别指向每台服务在网络中的实际 IP 地址。** +集群模式在配置上与伪集群模式基本相同。不同之处在于配置服务器地址列表的时候,**组成 ZooKeeper 集群的各个服务器 IP 地址列表分别指向每台服务在网络中的实际 IP 地址。** ```plaintext tickTime=2000 @@ -86,7 +86,7 @@ vim /conf/zoo.cfg #### 多台服务器配置 按照上面介绍的方法,如果我们想搭建三台服务器规模的 ZooKeeper 集群服务,就需要重复上面的步骤三次,并分别在创建的三台 ZooKeeper 服务器进行配置。 -不过在实际生产环境中,我们需要的 ZooKeeper 规模可能远远大于三台,而且这种逐一部署的方式不但浪费时间,在配置过程中出错率也较高。因此,这里介绍另一种配置方式,通过 Docker Compose 的方式来部署 ZooKeeper 集群。 **Docker Compose 是用于定义和运行多容器 Docker 应用程序的工具** 。通过 Compose,你可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。如下面的代码所示,我们创建了一个名为 docker-compose.yml 的配置文件。 +不过在实际生产环境中,我们需要的 ZooKeeper 规模可能远远大于三台,而且这种逐一部署的方式不但浪费时间,在配置过程中出错率也较高。因此,这里介绍另一种配置方式,通过 Docker Compose 的方式来部署 ZooKeeper 集群。**Docker Compose 是用于定义和运行多容器 Docker 应用程序的工具** 。通过 Compose,你可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。如下面的代码所示,我们创建了一个名为 docker-compose.yml 的配置文件。 ```plaintext version: '3.6' diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" index 2b1ed605d..94fd4781a 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25428\350\256\262.md" @@ -22,7 +22,7 @@ 在协调服务器接收到来自集群中其他服务器的反馈信息后,会对信息进行统计。如果集群中的全部机器都能正确执行客户端发送的会话请求,那么协调者服务器就会再次向这些服务器发送提交命令。在集群服务器接收到协调服务器的提交指令后,会根据之前处理该条会话操作的日志记录在本地提交操作,并最终完成数据的修改。 -虽然二阶段提交可以有效地保证客户端会话在分布式集群中的事务性,但是 **该算法自身也有很多问题** ,主要可以归纳为以下几点:效率问题、单点故障、异常中断。 +虽然二阶段提交可以有效地保证客户端会话在分布式集群中的事务性,但是 **该算法自身也有很多问题**,主要可以归纳为以下几点:效率问题、单点故障、异常中断。 #### 性能问题 @@ -40,11 +40,11 @@ ### 三阶段提交 -三阶段提交(Three-phase commit)简称 3PC , 其实是在二阶段算法的基础上进行了优化和改进。如下图所示,在整个三阶段提交的过程中,相比二阶段提交, **增加了预提交阶段** 。 +三阶段提交(Three-phase commit)简称 3PC , 其实是在二阶段算法的基础上进行了优化和改进。如下图所示,在整个三阶段提交的过程中,相比二阶段提交,**增加了预提交阶段** 。 ![image](assets/Ciqc1F8tFZuAZgJHAADHKaM9oZI445.png) -#### 底层实现 **预提交阶段** 为了保证事务性操作的稳定性,同时避免二阶段提交中因为网络原因造成数据不一致等问题,完成提交准备阶段后,集群中的服务器已经为请求操作做好了准备,协调服务器会向参与的服务器发送预提交请求。集群服务器在接收到预提交请求后,在本地执行事务操作,并将执行结果存储到本地事务日志中,并对该条事务日志进行锁定处理。 **提交阶段** 在处理完预提交阶段后,集群服务器会返回执行结果到协调服务器,最终,协调服务器会根据返回的结果来判断是否继续执行操作。如果所有参与者服务器返回的都是可以执行事务操作,协调者服务器就会再次发送提交请求到参与者服务器。参与者服务器在接收到来自协调者服务器的提交请求后,在本地正式提交该条事务操作,并在完成事务操作后关闭该条会话处理线程、释放系统资源。当参与者服务器执行完相关的操作时,会再次向协调服务器发送执行结果信息 +#### 底层实现 **预提交阶段** 为了保证事务性操作的稳定性,同时避免二阶段提交中因为网络原因造成数据不一致等问题,完成提交准备阶段后,集群中的服务器已经为请求操作做好了准备,协调服务器会向参与的服务器发送预提交请求。集群服务器在接收到预提交请求后,在本地执行事务操作,并将执行结果存储到本地事务日志中,并对该条事务日志进行锁定处理。**提交阶段** 在处理完预提交阶段后,集群服务器会返回执行结果到协调服务器,最终,协调服务器会根据返回的结果来判断是否继续执行操作。如果所有参与者服务器返回的都是可以执行事务操作,协调者服务器就会再次发送提交请求到参与者服务器。参与者服务器在接收到来自协调者服务器的提交请求后,在本地正式提交该条事务操作,并在完成事务操作后关闭该条会话处理线程、释放系统资源。当参与者服务器执行完相关的操作时,会再次向协调服务器发送执行结果信息 协调者服务器在接收到返回的状态信息后会进行处理,如果全部参与者服务器都正确执行,并返回 yes 等状态信息,整个事务性会话请求在服务端的操作就结束了。如果在接收到的信息中,有参与者服务器没有正确执行,则协调者服务器会再次向参与者服务器发送 rollback 回滚事务操作请求,整个集群就退回到之前的状态,这样就避免了数据不一致的问题。 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" index 2fa586583..5aec0174c 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25429\350\256\262.md" @@ -16,9 +16,9 @@ ZooKeeper 最核心的作用就是保证分布式系统的数据一致性,而 #### 崩溃恢复 -在介绍完 ZAB 协议在架构层面的实现逻辑后,我们不难看出整个 ZooKeeper 集群处理客户端会话的核心点 **在一台 Leader 服务器上** 。所有的业务处理和数据同步操作都要靠 Leader 服务器完成。结合我们在“ 28 | 彻底掌握二阶段提交/三阶段提交算法原理” 中学习到的二阶段提交知识,会发现就目前介绍的 ZooKeeper 架构方式而言, **极易产生单点问题** ,即当集群中的 Leader 发生故障的时候,整个集群就会因为缺少 Leader 服务器而无法处理来自客户端的事务性的会话请求。因此,为了解决这个问题。在 ZAB 协议中也设置了处理该问题的崩溃恢复机制。 +在介绍完 ZAB 协议在架构层面的实现逻辑后,我们不难看出整个 ZooKeeper 集群处理客户端会话的核心点 **在一台 Leader 服务器上** 。所有的业务处理和数据同步操作都要靠 Leader 服务器完成。结合我们在“ 28 | 彻底掌握二阶段提交/三阶段提交算法原理” 中学习到的二阶段提交知识,会发现就目前介绍的 ZooKeeper 架构方式而言,**极易产生单点问题**,即当集群中的 Leader 发生故障的时候,整个集群就会因为缺少 Leader 服务器而无法处理来自客户端的事务性的会话请求。因此,为了解决这个问题。在 ZAB 协议中也设置了处理该问题的崩溃恢复机制。 -崩溃恢复机制是保证 ZooKeeper 集群服务高可用的关键。触发 ZooKeeper 集群执行崩溃恢复的事件是集群中的 Leader 节点服务器发生了异常而无法工作,于是 Follow 服务器会通过投票来决定是否选出新的 Leader 节点服务器。 **投票过程如下** :当崩溃恢复机制开始的时候,整个 ZooKeeper 集群的每台 Follow 服务器会发起投票,并同步给集群中的其他 Follow 服务器。在接收到来自集群中的其他 Follow 服务器的投票信息后,集群中的每个 Follow 服务器都会与自身的投票信息进行对比,如果判断新的投票信息更合适,则采用新的投票信息作为自己的投票信息。在集群中的投票信息还没有达到超过半数原则的情况下,再进行新一轮的投票,最终当整个 ZooKeeper 集群中的 Follow 服务器超过半数投出的结果相同的时候,就会产生新的 Leader 服务器。 +崩溃恢复机制是保证 ZooKeeper 集群服务高可用的关键。触发 ZooKeeper 集群执行崩溃恢复的事件是集群中的 Leader 节点服务器发生了异常而无法工作,于是 Follow 服务器会通过投票来决定是否选出新的 Leader 节点服务器。**投票过程如下** :当崩溃恢复机制开始的时候,整个 ZooKeeper 集群的每台 Follow 服务器会发起投票,并同步给集群中的其他 Follow 服务器。在接收到来自集群中的其他 Follow 服务器的投票信息后,集群中的每个 Follow 服务器都会与自身的投票信息进行对比,如果判断新的投票信息更合适,则采用新的投票信息作为自己的投票信息。在集群中的投票信息还没有达到超过半数原则的情况下,再进行新一轮的投票,最终当整个 ZooKeeper 集群中的 Follow 服务器超过半数投出的结果相同的时候,就会产生新的 Leader 服务器。 #### 选票结构 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" index 4fa81ef04..db0395fb7 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25430\350\256\262.md" @@ -20,7 +20,7 @@ Paxos 算法是基于消息传递的分布式一致性算法,很多大型的 - **决策者(Acceptor)** :参与决策,回应 Proposers 的提案。收到 Proposal 后可以接受提案,若 Proposal 获得超过半数 Acceptors 的许可,则称该 Proposal 被批准。 - **决策学习者** :不参与决策,从 Proposers/Acceptors 学习最新达成一致的提案(Value)。 -经过我们之前对 ZooKeeper 的学习,相信对 Paxos 算法的集群角色划分并不陌生。而与 ZAB 协议算法 **不同的是** ,在 Paxos 算法中,当处理来自客户端的事务性会话请求的过程时,首先会触发一个或多个服务器进程,就本次会话的处理发起提案。当该提案通过网络发送到集群中的其他角色服务器后,这些服务器会就该会话在本地的执行情况反馈给发起提案的服务器。发起提案的服务器会在接收到这些反馈信息后进行统计,当集群中超过半数的服务器认可该条事务性的客户端会话操作后,认为该客户端会话可以在本地执行操作。 +经过我们之前对 ZooKeeper 的学习,相信对 Paxos 算法的集群角色划分并不陌生。而与 ZAB 协议算法 **不同的是**,在 Paxos 算法中,当处理来自客户端的事务性会话请求的过程时,首先会触发一个或多个服务器进程,就本次会话的处理发起提案。当该提案通过网络发送到集群中的其他角色服务器后,这些服务器会就该会话在本地的执行情况反馈给发起提案的服务器。发起提案的服务器会在接收到这些反馈信息后进行统计,当集群中超过半数的服务器认可该条事务性的客户端会话操作后,认为该客户端会话可以在本地执行操作。 上面介绍的 Paxos 算法针对事务性会话的处理投票过程与 ZAB 协议十分相似,但不同的是,对于采用 ZAB 协议的 ZooKeeper 集群中发起投票的机器,所采用的是在集群中运行的一台 Leader 角色服务器。而 Paxos 算法则采用多副本的处理方式,即存在多个副本,每个副本分别包含提案者、决策者以及学习者。下图演示了三种角色的服务器之间的关系。 @@ -38,9 +38,9 @@ Paxos 算法是基于消息传递的分布式一致性算法,很多大型的 ### Paxos PK ZAB -经过上面的介绍我们对 Paxos 算法所能解决的问题,以及底层的实现原理都有了一个详细的了解。现在结合我们之前学习的 ZooKeeper 相关知识,来看看 Paxos 算法与 ZAB 算法的相同及不同之处。 **相同之处是** ,在执行事务行会话的处理中,两种算法最开始都需要一台服务器或者线程针对该会话,在集群中发起提案或是投票。只有当集群中的过半数服务器对该提案投票通过后,才能执行接下来的处理。 +经过上面的介绍我们对 Paxos 算法所能解决的问题,以及底层的实现原理都有了一个详细的了解。现在结合我们之前学习的 ZooKeeper 相关知识,来看看 Paxos 算法与 ZAB 算法的相同及不同之处。**相同之处是**,在执行事务行会话的处理中,两种算法最开始都需要一台服务器或者线程针对该会话,在集群中发起提案或是投票。只有当集群中的过半数服务器对该提案投票通过后,才能执行接下来的处理。 -而 Paxos 算法与 ZAB 协议 **不同的是** ,Paxos 算法的发起者可以是一个或多个。当集群中的 Acceptor 服务器中的大多数可以执行会话请求后,提议者服务器只负责发送提交指令,事务的执行实际发生在 Acceptor 服务器。这与 ZooKeeper 服务器上事务的执行发生在 Leader 服务器上不同。Paxos 算法在数据同步阶段,是多台 Acceptor 服务器作为数据源同步给集群中的多台 Learner 服务器,而 ZooKeeper 则是单台 Leader 服务器作为数据源同步给集群中的其他角色服务器。 +而 Paxos 算法与 ZAB 协议 **不同的是**,Paxos 算法的发起者可以是一个或多个。当集群中的 Acceptor 服务器中的大多数可以执行会话请求后,提议者服务器只负责发送提交指令,事务的执行实际发生在 Acceptor 服务器。这与 ZooKeeper 服务器上事务的执行发生在 Leader 服务器上不同。Paxos 算法在数据同步阶段,是多台 Acceptor 服务器作为数据源同步给集群中的多台 Learner 服务器,而 ZooKeeper 则是单台 Leader 服务器作为数据源同步给集群中的其他角色服务器。 ### 总结 diff --git "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" index caf38b3aa..fec8b3fd3 100644 --- "a/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" +++ "b/docs/Middleware/ZooKeeper \346\272\220\347\240\201\345\210\206\346\236\220\344\270\216\345\256\236\346\210\230/\347\254\25433\350\256\262.md" @@ -4,7 +4,7 @@ 通过本专栏的学习,可以帮助你掌握实现分布式一致性的各种算法,包括:二阶段提交 、三阶段提交、 ZooKeeper 的 ZAB 协议算法以及 Paxos 算法。掌握了这些算法的理论知识后,我们又进一步分析了,代码层面的底层架构设计思想和实现过程。实现一个分布式系统会面临很多的挑战,在明确问题的原因后,更应该灵活运用学到的知识,找到问题的最优解。 -在结束语中,我打算 **添加一个小彩蛋** ,向你介绍除了已经掌握的 ZooKeeper 框架的 ZAB 协议算法之外,解决分布式一致性问题的另一种方法:Raft 算法。 +在结束语中,我打算 **添加一个小彩蛋**,向你介绍除了已经掌握的 ZooKeeper 框架的 ZAB 协议算法之外,解决分布式一致性问题的另一种方法:Raft 算法。 ### 彩蛋:Raft 算法 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25400\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25400\350\256\262.md" index d8cc05d9e..b432d908b 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25400\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25400\350\256\262.md" @@ -8,7 +8,7 @@ ## 哪些人适合学消息队列? -**后端开发者** :消息队列几乎是每个后端程序员都会用到的中间件,无论你是开发微服务,实时计算,还是机器学习程序,都需要解决进程间通信的问题。 **渴望技术提升的开发者** :消息队列所涉及的高性能通信、海量数据存储、高并发这些底层的技术比较全面,并且功能简洁、结构清晰,容易入门但又同时具有足够的深度,非常适合用来深入分析和学习底层技术,帮助你实现从用“轮子”到造“轮子”的技术提升。 +**后端开发者** :消息队列几乎是每个后端程序员都会用到的中间件,无论你是开发微服务,实时计算,还是机器学习程序,都需要解决进程间通信的问题。**渴望技术提升的开发者** :消息队列所涉及的高性能通信、海量数据存储、高并发这些底层的技术比较全面,并且功能简洁、结构清晰,容易入门但又同时具有足够的深度,非常适合用来深入分析和学习底层技术,帮助你实现从用“轮子”到造“轮子”的技术提升。 ## 学习消息队列,有哪些门槛? @@ -25,7 +25,7 @@ **1. 英文的阅读能力** 因为整个技术圈大部分的技术类资料、开源软件的文档、代码的注释和论文都是用英文撰写的,如果你不满足于平时只看过时的二手资料,一定要努力提升自己,达到能独立、快速看懂英文技术文档的水平。 -这对于技术人,其实并不是非常难的事儿。大多数英文技术文档涉及的专业词汇不超过一百个,使用的语法和句式都比较简单,理解起来绝对不会比中学英语考试题中的阅读理解更难。所以,最重要的是不要对英语过于恐惧,并且不要怕麻烦,多读多练习,平时多进行英文搜索,你会发现自己快速阅读能力的提升。 **2. 掌握 Java 语言和其生态系统** 大部分服务端的开源软件,包括我们这个课程涉及的 RocketMQ、Kafka、Pulsar 等,都是使用 Java 语言开发的。虽然 Java 本身有很多让人诟病的地方,比如僵化的泛型系统,不确定的 GC 机制等,也不断有 Go、Scala 等这些新兴语言来挑战 Java 的江湖地位,但是 Java 强大的生态系统在短时间内还是难以替代的。所以,无论你现在使用的是什么编程语言,学一点 Java 总是一个不错的选择。 **3. 积极的学习态度** 最后,也是最重要的一点是,对待写代码这件事儿的,你的真实态度是什么? +这对于技术人,其实并不是非常难的事儿。大多数英文技术文档涉及的专业词汇不超过一百个,使用的语法和句式都比较简单,理解起来绝对不会比中学英语考试题中的阅读理解更难。所以,最重要的是不要对英语过于恐惧,并且不要怕麻烦,多读多练习,平时多进行英文搜索,你会发现自己快速阅读能力的提升。**2. 掌握 Java 语言和其生态系统** 大部分服务端的开源软件,包括我们这个课程涉及的 RocketMQ、Kafka、Pulsar 等,都是使用 Java 语言开发的。虽然 Java 本身有很多让人诟病的地方,比如僵化的泛型系统,不确定的 GC 机制等,也不断有 Go、Scala 等这些新兴语言来挑战 Java 的江湖地位,但是 Java 强大的生态系统在短时间内还是难以替代的。所以,无论你现在使用的是什么编程语言,学一点 Java 总是一个不错的选择。**3. 积极的学习态度** 最后,也是最重要的一点是,对待写代码这件事儿的,你的真实态度是什么? 你是不是会认真地思考每一个细节是否已经做到最优?有没有为使用到的每个集合,仔细考虑到底是用数组,还是链表,还是其他哪种数据结构更合适?你有多少次迫于项目进度的压力而交出“算了,虽然我知道这么做不好,但也能凑合用”的代码?你有没有过为自己的某个(哪怕是自认为)绝妙设计,而成就感满满,幸福好几天的时刻?你会不会因为沟通时别人提到了一个你不知道的技术名词感到焦虑和羞愧,然后赶紧偷偷学习补齐这个技术短板? diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25401\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25401\350\256\262.md" index 327d1d667..3f48ee4b8 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25401\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25401\350\256\262.md" @@ -51,7 +51,7 @@ 一个设计健壮的程序有自我保护的能力,也就是说,它应该可以在海量的请求下,还能在自身能力范围内尽可能多地处理请求,拒绝处理不了的请求并且保证自身运行正常。不幸的是,现实中很多程序并没有那么“健壮”,而直接拒绝请求返回错误对于用户来说也是不怎么好的体验。 -因此,我们需要设计一套足够健壮的架构来将后端的服务保护起来。 **我们的设计思路是,使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。** +因此,我们需要设计一套足够健壮的架构来将后端的服务保护起来。**我们的设计思路是,使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。** 加入消息队列后,整个秒杀流程变为: diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25402\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25402\350\256\262.md" index a21e0a995..c286f9344 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25402\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25402\350\256\262.md" @@ -48,7 +48,7 @@ RabbitMQ 的客户端支持的编程语言大概是所有消息队列中最多 最后一个问题是 RabbitMQ 使用的编程语言 Erlang,这个编程语言不仅是非常小众的语言,更麻烦的是,这个语言的学习曲线非常陡峭。大多数流行的编程语言,比如 Java、C/C++、Python 和 JavaScript,虽然语法、特性有很多的不同,但它们基本的体系结构都是一样的,你只精通一种语言,也很容易学习其他的语言,短时间内即使做不到精通,但至少能达到“会用”的水平。 -就像一个以英语为母语的人,学习法语、德语都很容易,但是你要是让他去学汉语,那基本上和学习其他这些语言不是一个难度级别的。很不幸的是,Erlang 就是编程语言中的“汉语”。所以如果你想基于 RabbitMQ 做一些扩展和二次开发什么的,建议你慎重考虑一下可持续维护的问题。 **2. RocketMQ** RocketMQ 是阿里巴巴在 2012 年开源的消息队列产品,后来捐赠给 Apache 软件基金会,2017 正式毕业,成为 Apache 的顶级项目。阿里内部也是使用 RocketMQ 作为支撑其业务的消息队列,经历过多次“双十一”考验,它的性能、稳定性和可靠性都是值得信赖的。作为优秀的国产消息队列,近年来越来越多的被国内众多大厂使用。 +就像一个以英语为母语的人,学习法语、德语都很容易,但是你要是让他去学汉语,那基本上和学习其他这些语言不是一个难度级别的。很不幸的是,Erlang 就是编程语言中的“汉语”。所以如果你想基于 RabbitMQ 做一些扩展和二次开发什么的,建议你慎重考虑一下可持续维护的问题。**2. RocketMQ** RocketMQ 是阿里巴巴在 2012 年开源的消息队列产品,后来捐赠给 Apache 软件基金会,2017 正式毕业,成为 Apache 的顶级项目。阿里内部也是使用 RocketMQ 作为支撑其业务的消息队列,经历过多次“双十一”考验,它的性能、稳定性和可靠性都是值得信赖的。作为优秀的国产消息队列,近年来越来越多的被国内众多大厂使用。 我在总结 RocketMQ 的特点时,发现很难找出 RocketMQ 有什么特别让我印象深刻的特点,也很难找到它有什么缺点。 @@ -56,17 +56,17 @@ RocketMQ 就像一个品学兼优的好学生,有着不错的性能,稳定 RocketMQ 有非常活跃的中文社区,大多数问题你都可以找到中文的答案,也许会成为你选择它的一个原因。另外,RocketMQ 使用 Java 语言开发,它的贡献者大多数都是中国人,源代码相对也比较容易读懂,你很容易对 RocketMQ 进行扩展或者二次开发。 -RocketMQ 对在线业务的响应时延做了很多的优化,大多数情况下可以做到毫秒级的响应, **如果你的应用场景很在意响应时延,那应该选择使用 RocketMQ。** RocketMQ 的性能比 RabbitMQ 要高一个数量级,每秒钟大概能处理几十万条消息。 +RocketMQ 对在线业务的响应时延做了很多的优化,大多数情况下可以做到毫秒级的响应,**如果你的应用场景很在意响应时延,那应该选择使用 RocketMQ。** RocketMQ 的性能比 RabbitMQ 要高一个数量级,每秒钟大概能处理几十万条消息。 -RocketMQ 的一个劣势是,作为国产的消息队列,相比国外的比较流行的同类产品,在国际上还没有那么流行,与周边生态系统的集成和兼容程度要略逊一筹。 **3. Kafka** 最后我们聊一聊 Kafka。Kafka 最早是由 LinkedIn 开发,目前也是 Apache 的顶级项目。Kafka 最初的设计目的是用于处理海量的日志。 +RocketMQ 的一个劣势是,作为国产的消息队列,相比国外的比较流行的同类产品,在国际上还没有那么流行,与周边生态系统的集成和兼容程度要略逊一筹。**3. Kafka** 最后我们聊一聊 Kafka。Kafka 最早是由 LinkedIn 开发,目前也是 Apache 的顶级项目。Kafka 最初的设计目的是用于处理海量的日志。 在早期的版本中,为了获得极致的性能,在设计方面做了很多的牺牲,比如不保证消息的可靠性,可能会丢失消息,也不支持集群,功能上也比较简陋,这些牺牲对于处理海量日志这个特定的场景都是可以接受的。这个时期的 Kafka 甚至不能称之为一个合格的消息队列。 -但是,请注意,重点一般都在后面。随后的几年 Kafka 逐步补齐了这些短板,你在网上搜到的很多消息队列的对比文章还在说 Kafka 不可靠,其实这种说法早已经过时了。当下的 Kafka 已经发展为一个非常成熟的消息队列产品,无论在数据可靠性、稳定性和功能特性等方面都可以满足绝大多数场景的需求。 **Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域,几乎所有的相关开源软件系统都会优先支持 Kafka。** Kafka 使用 Scala 和 Java 语言开发,设计上大量使用了批量和异步的思想,这种设计使得 Kafka 能做到超高的性能。Kafka 的性能,尤其是异步收发的性能,是三者中最好的,但与 RocketMQ 并没有量级上的差异,大约每秒钟可以处理几十万条消息。 +但是,请注意,重点一般都在后面。随后的几年 Kafka 逐步补齐了这些短板,你在网上搜到的很多消息队列的对比文章还在说 Kafka 不可靠,其实这种说法早已经过时了。当下的 Kafka 已经发展为一个非常成熟的消息队列产品,无论在数据可靠性、稳定性和功能特性等方面都可以满足绝大多数场景的需求。**Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域,几乎所有的相关开源软件系统都会优先支持 Kafka。** Kafka 使用 Scala 和 Java 语言开发,设计上大量使用了批量和异步的思想,这种设计使得 Kafka 能做到超高的性能。Kafka 的性能,尤其是异步收发的性能,是三者中最好的,但与 RocketMQ 并没有量级上的差异,大约每秒钟可以处理几十万条消息。 我曾经使用配置比较好的服务器对 Kafka 进行过压测,在有足够的客户端并发进行异步批量发送,并且开启压缩的情况下,Kafka 的极限处理能力可以超过每秒 2000 万条消息。 -但是 Kafka 这种异步批量的设计带来的问题是,它的同步收发消息的响应时延比较高,因为当客户端发送一条消息的时候,Kafka 并不会立即发送出去,而是要等一会儿攒一批再发送,在它的 Broker 中,很多地方都会使用这种“先攒一波再一起处理”的设计。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka 的时延反而会比较高。所以, **Kafka 不太适合在线业务场景。** +但是 Kafka 这种异步批量的设计带来的问题是,它的同步收发消息的响应时延比较高,因为当客户端发送一条消息的时候,Kafka 并不会立即发送出去,而是要等一会儿攒一批再发送,在它的 Broker 中,很多地方都会使用这种“先攒一波再一起处理”的设计。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka 的时延反而会比较高。所以,**Kafka 不太适合在线业务场景。** ## 第二梯队的消息队列 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25403\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25403\350\256\262.md" index 2900abf4a..6ca084fec 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25403\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25403\350\256\262.md" @@ -16,7 +16,7 @@ > 队列是先进先出(FIFO, First-In-First-Out)的线性表(Linear List)。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为 rear)进行插入操作,在前端(称为 front)进行删除操作。 -这个定义里面包含几个关键点,第一个是先进先出,这里面隐含着的一个要求是,在消息入队出队过程中,需要保证这些消息 **严格有序** ,按照什么顺序写进队列,必须按照同样的顺序从队列中读出来。不过,队列是没有“读”这个操作的,“读”就是出队,也就是从队列中“删除”这条消息。 +这个定义里面包含几个关键点,第一个是先进先出,这里面隐含着的一个要求是,在消息入队出队过程中,需要保证这些消息 **严格有序**,按照什么顺序写进队列,必须按照同样的顺序从队列中读出来。不过,队列是没有“读”这个操作的,“读”就是出队,也就是从队列中“删除”这条消息。 **早期的消息队列,就是按照“队列”的数据结构来设计的。** 我们一起看下这个图,生产者(Producer)发消息就是入队操作,消费者(Consumer)收消息就是出队也就是删除操作,服务端存放消息的容器自然就称为“队列”。 @@ -36,7 +36,7 @@ 在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。 -在消息领域的历史上很长的一段时间,队列模式和发布 - 订阅模式是并存的,有些消息队列同时支持这两种消息模型,比如 ActiveMQ。我们仔细对比一下这两种模型,生产者就是发布者,消费者就是订阅者,队列就是主题,并没有本质的区别。 **它们最大的区别其实就是,一份消息数据能不能被消费多次的问题。** +在消息领域的历史上很长的一段时间,队列模式和发布 - 订阅模式是并存的,有些消息队列同时支持这两种消息模型,比如 ActiveMQ。我们仔细对比一下这两种模型,生产者就是发布者,消费者就是订阅者,队列就是主题,并没有本质的区别。**它们最大的区别其实就是,一份消息数据能不能被消费多次的问题。** 实际上,在这种发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。也就是说,发布 - 订阅模型在功能层面上是可以兼容队列模型的。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25404\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25404\350\256\262.md" index c36dcc5e1..c0fcdf1d0 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25404\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25404\350\256\262.md" @@ -4,7 +4,7 @@ 一说起事务,你可能自然会联想到数据库。的确,我们日常使用事务的场景,绝大部分都是在操作数据库的时候。像 MySQL、Oracle 这些主流的关系型数据库,也都提供了完整的事务实现。那消息队列为什么也需要事务呢? -其实很多场景下,我们“发消息”这个过程,目的往往是通知另外一个系统或者模块去更新数据, **消息队列中的“事务”,主要解决的是消息生产者和消息消费者的数据一致性问题。** +其实很多场景下,我们“发消息”这个过程,目的往往是通知另外一个系统或者模块去更新数据,**消息队列中的“事务”,主要解决的是消息生产者和消息消费者的数据一致性问题。** 依然拿我们熟悉的电商来举个例子。一般来说,用户在电商 APP 上购物时,先把商品加到购物车里,然后几件商品一起下单,最后支付,完成购物流程,就可以愉快地等待收货了。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25405\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25405\350\256\262.md" index a2dc6e7f6..0791e6bdb 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25405\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25405\350\256\262.md" @@ -40,7 +40,7 @@ Consumer 实例的数量最好和分区数量一致,做到 Consumer 和分区 - **生产阶段** : 在这个阶段,从消息在 Producer 创建出来,经过网络传输发送到 Broker 端。 - **存储阶段** : 在这个阶段,消息在 Broker 端存储,如果是集群,消息会在这个阶段被复制到其他的副本上。 -- **消费阶段** : 在这个阶段,Consumer 从 Broker 上拉取消息,经过网络传输发送到 Consumer 上。 **1. 生产阶段** +- **消费阶段** : 在这个阶段,Consumer 从 Broker 上拉取消息,经过网络传输发送到 Consumer 上。**1. 生产阶段** 在生产阶段,消息队列通过最常用的请求确认机制,来保证消息的可靠传递:当你的代码调用发消息方法时,消息队列的客户端会把消息发送到 Broker,Broker 收到消息后,会给客户端返回一个确认响应,表明消息已经收到了。客户端收到响应后,完成了一次正常消息的发送。 @@ -73,11 +73,11 @@ producer.send(record, (metadata, exception) -> { }); ``` -**2. 存储阶段** 在存储阶段正常情况下,只要 Broker 在正常运行,就不会出现丢失消息的问题,但是如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。 **如果对消息的可靠性要求非常高,可以通过配置 Broker 参数来避免因为宕机丢消息。** 对于单个节点的 Broker,需要配置 Broker 参数,在收到消息后,将消息写入磁盘后再给 Producer 返回确认响应,这样即使发生宕机,由于消息已经被写入磁盘,就不会丢失消息,恢复后还可以继续消费。例如,在 RocketMQ 中,需要将刷盘方式 flushDiskType 配置为 SYNC_FLUSH 同步刷盘。 +**2. 存储阶段** 在存储阶段正常情况下,只要 Broker 在正常运行,就不会出现丢失消息的问题,但是如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。**如果对消息的可靠性要求非常高,可以通过配置 Broker 参数来避免因为宕机丢消息。** 对于单个节点的 Broker,需要配置 Broker 参数,在收到消息后,将消息写入磁盘后再给 Producer 返回确认响应,这样即使发生宕机,由于消息已经被写入磁盘,就不会丢失消息,恢复后还可以继续消费。例如,在 RocketMQ 中,需要将刷盘方式 flushDiskType 配置为 SYNC_FLUSH 同步刷盘。 -如果是 Broker 是由多个节点组成的集群,需要将 Broker 集群配置成:至少将消息发送到 2 个以上的节点,再给客户端回复发送确认响应。这样当某个 Broker 宕机时,其他的 Broker 可以替代宕机的 Broker,也不会发生消息丢失。后面我会专门安排一节课,来讲解在集群模式下,消息队列是如何通过消息复制来确保消息的可靠性的。 **3. 消费阶段** 消费阶段采用和生产阶段类似的确认机制来保证消息的可靠传递,客户端从 Broker 拉取消息后,执行用户的消费业务逻辑,成功后,才会给 Broker 发送消费确认响应。如果 Broker 没有收到消费确认响应,下次拉消息的时候还会返回同一条消息,确保消息不会在网络传输过程中丢失,也不会因为客户端在执行消费逻辑中出错导致丢失。 +如果是 Broker 是由多个节点组成的集群,需要将 Broker 集群配置成:至少将消息发送到 2 个以上的节点,再给客户端回复发送确认响应。这样当某个 Broker 宕机时,其他的 Broker 可以替代宕机的 Broker,也不会发生消息丢失。后面我会专门安排一节课,来讲解在集群模式下,消息队列是如何通过消息复制来确保消息的可靠性的。**3. 消费阶段** 消费阶段采用和生产阶段类似的确认机制来保证消息的可靠传递,客户端从 Broker 拉取消息后,执行用户的消费业务逻辑,成功后,才会给 Broker 发送消费确认响应。如果 Broker 没有收到消费确认响应,下次拉消息的时候还会返回同一条消息,确保消息不会在网络传输过程中丢失,也不会因为客户端在执行消费逻辑中出错导致丢失。 -你在编写消费代码时需要注意的是, **不要在收到消息后就立即发送消费确认,而是应该在执行完所有消费业务逻辑之后,再发送消费确认。** +你在编写消费代码时需要注意的是,**不要在收到消息后就立即发送消费确认,而是应该在执行完所有消费业务逻辑之后,再发送消费确认。** 同样,我们以用 Python 语言消费 RabbitMQ 消息为例,来看一下如何实现一段可靠的消费代码: diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25406\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25406\350\256\262.md" index 75cdd5e61..f0bf22c7f 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25406\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25406\350\256\262.md" @@ -30,11 +30,11 @@ Kafka 支持的“Exactly once”和我们刚刚提到的消息传递的服务 ## 用幂等性解决重复消息问题 -一般解决重复消息的办法是,在消费端,让我们消费消息的操作具备幂等性。 **幂等(Idempotence)** 本来是一个数学上的概念,它是这样定义的: +一般解决重复消息的办法是,在消费端,让我们消费消息的操作具备幂等性。**幂等(Idempotence)** 本来是一个数学上的概念,它是这样定义的: > 如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。 -这个概念被拓展到计算机领域,被用来描述一个操作、方法或者服务。一个幂等操作的特点是, **其任意多次执行所产生的影响均与一次执行的影响相同。** 一个幂等的方法,使用同样的参数,对它进行多次调用和一次调用,对系统产生的影响是一样的。所以,对于幂等的方法,不用担心重复执行会对系统造成任何改变。 +这个概念被拓展到计算机领域,被用来描述一个操作、方法或者服务。一个幂等操作的特点是,**其任意多次执行所产生的影响均与一次执行的影响相同。** 一个幂等的方法,使用同样的参数,对它进行多次调用和一次调用,对系统产生的影响是一样的。所以,对于幂等的方法,不用担心重复执行会对系统造成任何改变。 我们举个例子来说明一下。在不考虑并发的情况下,“将账户 X 的余额设置为 100 元”,执行一次后对系统的影响是,账户 X 的余额变成了 100 元。只要提供的参数 100 元不变,那即使再执行多少次,账户 X 的余额始终都是 100 元,不会变化,这个操作就是一个幂等的操作。 @@ -44,7 +44,7 @@ Kafka 支持的“Exactly once”和我们刚刚提到的消息传递的服务 从对系统的影响结果来说: **At least once + 幂等消费 = Exactly once。** -那么如何实现幂等操作呢?最好的方式就是, **从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。** 但是,不是所有的业务都能设计成天然幂等的,这里就需要一些方法和技巧来实现幂等。 +那么如何实现幂等操作呢?最好的方式就是,**从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。** 但是,不是所有的业务都能设计成天然幂等的,这里就需要一些方法和技巧来实现幂等。 下面我给你介绍几种常用的设计幂等操作的方法: @@ -54,11 +54,11 @@ Kafka 支持的“Exactly once”和我们刚刚提到的消息传递的服务 这样,我们消费消息的逻辑可以变为:“在转账流水表中增加一条转账记录,然后再根据转账记录,异步操作更新用户余额即可。”在转账流水表增加一条转账记录这个操作中,由于我们在这个表中预先定义了“账户 ID 转账单 ID”的唯一约束,对于同一个转账单同一个账户只能插入一条记录,后续重复的插入操作都会失败,这样就实现了一个幂等的操作。我们只要写一个 SQL,正确地实现它就可以了。 -基于这个思路,不光是可以使用关系型数据库,只要是支持类似“INSERT IF NOT EXIST”语义的存储类系统都可以用于实现幂等,比如,你可以用 Redis 的 SETNX 命令来替代数据库中的唯一约束,来实现幂等消费。 **2. 为更新的数据设置前置条件** 另外一种实现幂等的思路是,给数据变更设置一个前置条件,如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。这样,重复执行这个操作时,由于第一次更新数据的时候已经变更了前置条件中需要判断的数据,不满足前置条件,则不会重复执行更新数据操作。 +基于这个思路,不光是可以使用关系型数据库,只要是支持类似“INSERT IF NOT EXIST”语义的存储类系统都可以用于实现幂等,比如,你可以用 Redis 的 SETNX 命令来替代数据库中的唯一约束,来实现幂等消费。**2. 为更新的数据设置前置条件** 另外一种实现幂等的思路是,给数据变更设置一个前置条件,如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。这样,重复执行这个操作时,由于第一次更新数据的时候已经变更了前置条件中需要判断的数据,不满足前置条件,则不会重复执行更新数据操作。 比如,刚刚我们说过,“将账户 X 的余额增加 100 元”这个操作并不满足幂等性,我们可以把这个操作加上一个前置条件,变为:“如果账户 X 当前的余额为 500 元,将余额加 100 元”,这个操作就具备了幂等性。对应到消息队列中的使用时,可以在发消息时在消息体中带上当前的余额,在消费的时候进行判断数据库中,当前余额是否与消息中的余额相等,只有相等才执行变更操作。 -但是,如果我们要更新的数据不是数值,或者我们要做一个比较复杂的更新操作怎么办?用什么作为前置判断条件呢?更加通用的方法是,给你的数据增加一个版本号属性,每次更数据前,比较当前数据的版本号是否和消息中的版本号一致,如果不一致就拒绝更新数据,更新数据的同时将版本号 +1,一样可以实现幂等更新。 **3. 记录并检查操作** +但是,如果我们要更新的数据不是数值,或者我们要做一个比较复杂的更新操作怎么办?用什么作为前置判断条件呢?更加通用的方法是,给你的数据增加一个版本号属性,每次更数据前,比较当前数据的版本号是否和消息中的版本号一致,如果不一致就拒绝更新数据,更新数据的同时将版本号 +1,一样可以实现幂等更新。**3. 记录并检查操作** 如果上面提到的两种实现幂等方法都不能适用于你的场景,我们还有一种通用性最强,适用范围最广的实现幂等性方法:记录并检查操作,也称为“Token 机制或者 GUID(全局唯一 ID)机制”,实现的思路特别简单:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25407\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25407\350\256\262.md" index 64c45e6aa..9d6ec3424 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25407\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25407\350\256\262.md" @@ -14,9 +14,9 @@ 主要原因是,对于绝大多数使用消息队列的业务来说,消息队列本身的处理能力要远大于业务系统的处理能力。主流消息队列的单个节点,消息收发的性能可以达到每秒钟处理几万至几十万条消息的水平,还可以通过水平扩展 Broker 的实例数成倍地提升处理能力。 -而一般的业务系统需要处理的业务逻辑远比消息队列要复杂,单个节点每秒钟可以处理几百到几千次请求,已经可以算是性能非常好的了。所以,对于消息队列的性能优化,我们更关注的是, **在消息的收发两端,我们的业务代码怎么和消息队列配合,达到一个最佳的性能。** #### 1. 发送端性能优化 +而一般的业务系统需要处理的业务逻辑远比消息队列要复杂,单个节点每秒钟可以处理几百到几千次请求,已经可以算是性能非常好的了。所以,对于消息队列的性能优化,我们更关注的是,**在消息的收发两端,我们的业务代码怎么和消息队列配合,达到一个最佳的性能。** #### 1. 发送端性能优化 -发送端业务代码的处理性能,实际上和消息队列的关系不大,因为一般发送端都是先执行自己的业务逻辑,最后再发送消息。 **如果说,你的代码发送消息的性能上不去,你需要优先检查一下,是不是发消息之前的业务逻辑耗时太多导致的。** +发送端业务代码的处理性能,实际上和消息队列的关系不大,因为一般发送端都是先执行自己的业务逻辑,最后再发送消息。**如果说,你的代码发送消息的性能上不去,你需要优先检查一下,是不是发消息之前的业务逻辑耗时太多导致的。** 对于发送消息的业务逻辑,只需要注意设置合适的并发和批量大小,就可以达到很好的发送性能。为什么这么说呢? @@ -40,9 +40,9 @@ 要是消费速度一直比生产速度慢,时间长了,整个系统就会出现问题,要么,消息队列的存储被填满无法提供服务,要么消息丢失,这对于整个系统来说都是严重故障。 -所以,我们在设计系统的时候, **一定要保证消费端的消费性能要高于生产端的发送性能,这样的系统才能健康的持续运行。** +所以,我们在设计系统的时候,**一定要保证消费端的消费性能要高于生产端的发送性能,这样的系统才能健康的持续运行。** -消费端的性能优化除了优化消费业务逻辑以外,也可以通过水平扩容,增加消费端的并发数来提升总体的消费性能。特别需要注意的一点是, **在扩容 Consumer 的实例数量的同时,必须同步扩容主题中的分区(也叫队列)数量,确保 Consumer 的实例数和分区数量是相等的。** 如果 Consumer 的实例数量超过分区数量,这样的扩容实际上是没有效果的。原因我们之前讲过,因为对于消费者来说,在每个分区上实际上只能支持单线程消费。 +消费端的性能优化除了优化消费业务逻辑以外,也可以通过水平扩容,增加消费端的并发数来提升总体的消费性能。特别需要注意的一点是,**在扩容 Consumer 的实例数量的同时,必须同步扩容主题中的分区(也叫队列)数量,确保 Consumer 的实例数和分区数量是相等的。** 如果 Consumer 的实例数量超过分区数量,这样的扩容实际上是没有效果的。原因我们之前讲过,因为对于消费者来说,在每个分区上实际上只能支持单线程消费。 我见到过很多消费程序,他们是这样来解决消费慢的问题的: diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25408\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25408\350\256\262.md" index 0883d8bdb..bc58e5de3 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25408\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25408\350\256\262.md" @@ -103,7 +103,7 @@ public class RequestHandler { 先说消息生产这一端,假设我们有 3 个生产者实例:Produer0,Produer1 和 Producer2。 -这 3 个生产者是如何对应到 2 个 Broker 的,又是如何对应到 5 个队列的呢?这个很简单, **不用对应,随便发** 。每个生产者可以在 5 个队列中轮询发送,也可以随机选一个队列发送,或者只往某个队列发送,这些都可以。比如 Producer0 要发 5 条消息,可以都发到队列 Q0 里面,也可以 5 个队列每个队列发一条。 +这 3 个生产者是如何对应到 2 个 Broker 的,又是如何对应到 5 个队列的呢?这个很简单,**不用对应,随便发** 。每个生产者可以在 5 个队列中轮询发送,也可以随机选一个队列发送,或者只往某个队列发送,这些都可以。比如 Producer0 要发 5 条消息,可以都发到队列 Q0 里面,也可以 5 个队列每个队列发一条。 然后说消费端,很多同学没有搞清楚消费组、消费者和队列这几个概念的对应关系。 @@ -117,7 +117,7 @@ public class RequestHandler { 再强调一下,队列占用只是针对消费组内部来说的,对于其他的消费组来说是没有影响的。比如队列 Q2 被消费组 G1 的消费者 C1 占用了,对于消费组 G2 来说,是完全没有影响的,G2 也可以分配它的消费者来占用和消费队列 Q2。 -最后说一下消费位置,每个消费组内部维护自己的一组消费位置,每个队列对应一个消费位置。消费位置在服务端保存,并且, **消费位置和消费者是没有关系的** 。每个消费位置一般就是一个整数,记录这个消费组中,这个队列消费到哪个位置了,这个位置之前的消息都成功消费了,之后的消息都没有消费或者正在消费。 +最后说一下消费位置,每个消费组内部维护自己的一组消费位置,每个队列对应一个消费位置。消费位置在服务端保存,并且,**消费位置和消费者是没有关系的** 。每个消费位置一般就是一个整数,记录这个消费组中,这个队列消费到哪个位置了,这个位置之前的消息都成功消费了,之后的消息都没有消费或者正在消费。 我把咱们这个例子的消费位置整理成下面的表格,便于你理解。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25409\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25409\350\256\262.md" index cddefbe4c..8e1445828 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25409\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25409\350\256\262.md" @@ -10,7 +10,7 @@ ## 通过文档来了解开源项目 -学习源代码应该从哪儿入手呢? **最佳的方式就是先看它的文档。** +学习源代码应该从哪儿入手呢?**最佳的方式就是先看它的文档。** 通过看文档,你可以快速地掌握这个软件整体的结构,它有哪些功能特性,它涉及到的关键技术、实现原理和它的生态系统等等。在掌握了这些之后,你对它有个整体的了解,然后再去看它的源代码,就不会再有那种盲人摸象找不到头绪的感觉了。 @@ -60,12 +60,12 @@ 那程序的源代码是什么结构?那是一个网状结构,关系错综复杂,所以这种结构是非常不适合人类去阅读的。你如果是泛泛去读源代码,很容易迷失在这个代码织成的网里面。那怎么办? -我推荐大家阅读源码的方式是, **带着问题去读源码,最好是带着问题的答案去读源码。** 你每次读源码之前,确定一个具体的问题,比如: +我推荐大家阅读源码的方式是,**带着问题去读源码,最好是带着问题的答案去读源码。** 你每次读源码之前,确定一个具体的问题,比如: - RocketMQ 的消息是怎么写到文件里的? - Kafka 的 Coordinator 是怎么维护消费位置的? -类似这种非常细粒度的问题,粒度细到每个问题的答案就是一两个流程就可以回答,这样就可以了。如果说你就想学习一下源代码,或者说提不出这些问题怎么办呢?答案还是, **看文档。** +类似这种非常细粒度的问题,粒度细到每个问题的答案就是一两个流程就可以回答,这样就可以了。如果说你就想学习一下源代码,或者说提不出这些问题怎么办呢?答案还是,**看文档。** 确定问题后,先不要着急看源代码,而是应该先找一下是否有对应的实现文档,一般来说,核心功能都会有专门的文档来说明它的实现原理,比如在 Kafka 的文档中,[DESIGN](https://kafka.apache.org/documentation/#design)和[IMPLEMENTATION](https://kafka.apache.org/documentation/#implementation)两个章节中,介绍了 Kafka 很多功能的实现原理和细节。一些更细节的非核心的功能不一定有专门的文档来说明,但是我们可以去找一找是否有对应的 Improvement Proposal。(Kafka 的所有 Improvement Proposals 在[这里](https://cwiki.apache.org/confluence/display/KAFKA/Kafka+Improvement+Proposals)。) diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25410\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25410\350\256\262.md" index cc28f46ce..b4666cb4a 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25410\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25410\350\256\262.md" @@ -49,7 +49,7 @@ Transfer(accountFrom, accountTo, amount) { 这是不是已经到了这台服务器所能承受的极限了呢?其实远远没有,如果我们监测一下服务器的各项指标,会发现无论是 CPU、内存,还是网卡流量或者是磁盘的 IO 都空闲的很,那我们 Transfer 服务中的那 10,000 个线程在干什么呢?对,绝大部分线程都在等待 Add 服务返回结果。 -也就是说, **采用同步实现的方式,整个服务器的所有线程大部分时间都没有在工作,而是都在等待。** +也就是说,**采用同步实现的方式,整个服务器的所有线程大部分时间都没有在工作,而是都在等待。** 如果我们能减少或者避免这种无意义的等待,就可以大幅提升服务的吞吐能力,从而提升服务的总体性能。 @@ -92,7 +92,7 @@ OnAllDone(OnComplete()) { ![img](assets/38ab8de8fbfaf4cd4b34fbd9ddd3360d.jpg) -你会发现,异步化实现后,整个流程的时序和同步实现是完全一样的, **区别只是在线程模型上由同步顺序调用改为了异步调用和回调的机制。** +你会发现,异步化实现后,整个流程的时序和同步实现是完全一样的,**区别只是在线程模型上由同步顺序调用改为了异步调用和回调的机制。** 接下来我们分析一下异步实现的性能,由于流程的时序和同步实现是一样,在低请求数量的场景下,平均响应时延一样是 100ms。在超高请求数量场景下,异步的实现不再需要线程等待执行结果,只需要个位数量的线程,即可实现同步场景大量线程一样的吞吐量。 @@ -186,7 +186,7 @@ public class Client { ## 小结 -简单的说,异步思想就是, **当我们要执行一项比较耗时的操作时,不去等待操作结束,而是给这个操作一个命令:“当操作完成后,接下来去执行什么。”** +简单的说,异步思想就是,**当我们要执行一项比较耗时的操作时,不去等待操作结束,而是给这个操作一个命令:“当操作完成后,接下来去执行什么。”** 使用异步编程模型,虽然并不能加快程序本身的速度,但可以减少或者避免线程等待,只用很少的线程就可以达到超高的吞吐能力。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25411\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25411\350\256\262.md" index 51151a5a3..ae974898c 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25411\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25411\350\256\262.md" @@ -16,13 +16,13 @@ IO 密集型系统大部分时间都在执行 IO 操作,这个 IO 操作主要 发送数据的过程比较简单,我们直接往这个通道里面来写入数据就可以了。用户代码在发送时写入的数据会暂存在缓存中,然后操作系统会通过网卡,把发送缓存中的数据传输到对端的服务器上。 -只要这个缓存不满,或者说,我们发送数据的速度没有超过网卡传输速度的上限,那这个发送数据的操作耗时,只不过是一次内存写入的时间,这个时间是非常快的。所以, **发送数据的时候同步发送就可以了,没有必要异步。** 比较麻烦的是接收数据。对于数据的接收方来说,它并不知道什么时候会收到数据。那我们能直接想到的方法就是,用一个线程阻塞在那儿等着数据,当有数据到来的时候,操作系统会先把数据写入接收缓存,然后给接收数据的线程发一个通知,线程收到通知后结束等待,开始读取数据。处理完这一批数据后,继续阻塞等待下一批数据到来,这样周而复始地处理收到的数据。 +只要这个缓存不满,或者说,我们发送数据的速度没有超过网卡传输速度的上限,那这个发送数据的操作耗时,只不过是一次内存写入的时间,这个时间是非常快的。所以,**发送数据的时候同步发送就可以了,没有必要异步。** 比较麻烦的是接收数据。对于数据的接收方来说,它并不知道什么时候会收到数据。那我们能直接想到的方法就是,用一个线程阻塞在那儿等着数据,当有数据到来的时候,操作系统会先把数据写入接收缓存,然后给接收数据的线程发一个通知,线程收到通知后结束等待,开始读取数据。处理完这一批数据后,继续阻塞等待下一批数据到来,这样周而复始地处理收到的数据。 ![img](assets/4c94c5e1e437ac087ef3b50acf8dceb2.jpg) 这就是同步网络 IO 的模型。同步网络 IO 模型在处理少量连接的时候,是没有问题的。但是如果要同时处理非常多的连接,同步的网络 IO 模型就有点儿力不从心了。 因为,每个连接都需要阻塞一个线程来等待数据,大量的连接数就会需要相同数量的数据接收线程。当这些 TCP 连接都在进行数据收发的时候,会导致什么情况呢?对,会有大量的线程来抢占 CPU 时间,造成频繁的 CPU 上下文切换,导致 CPU 的负载升高,整个系统的性能就会比较慢。 -所以,我们需要使用异步的模型来解决网络 IO 问题。怎么解决呢? **你可以先抛开你知道的各种语言的异步类库和各种异步的网络 IO 框架,想一想,对于业务开发者来说,一个好的异步网络框架,它的 API 应该是什么样的呢?** +所以,我们需要使用异步的模型来解决网络 IO 问题。怎么解决呢?**你可以先抛开你知道的各种语言的异步类库和各种异步的网络 IO 框架,想一想,对于业务开发者来说,一个好的异步网络框架,它的 API 应该是什么样的呢?** 我们希望达到的效果,无非就是,只用少量的线程就能处理大量的连接,有数据到来的时候能第一时间处理就可以了。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25412\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25412\350\256\262.md" index 928bdba79..2b6bfbc96 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25412\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25412\350\256\262.md" @@ -12,7 +12,7 @@ 那对于我们编写的程序来说,它需要通过网络传输的数据是什么形式的呢?是结构化的数据,比如,一条命令、一段文本或者是一条消息。对应到我们写的代码中,这些结构化的数据是什么?这些都可以用一个类(Class)或者一个结构体(Struct)来表示。 -那显然, **要想使用网络框架的 API 来传输结构化的数据,必须得先实现结构化的数据与字节流之间的双向转换。** 这种将结构化数据转换成字节流的过程,我们称为序列化,反过来转换,就是反序列化。 +那显然,**要想使用网络框架的 API 来传输结构化的数据,必须得先实现结构化的数据与字节流之间的双向转换。** 这种将结构化数据转换成字节流的过程,我们称为序列化,反过来转换,就是反序列化。 序列化的用途除了用于在网络上传输数据以外,另外的一个重要用途是,将结构化数据保存在文件中,因为在文件内保存数据的形式也是二进制序列,和网络传输过程中的数据是一样的,所以序列化同样适用于将结构化数据保存在文件中。 @@ -37,7 +37,7 @@ 1. 序列化和反序列化的速度越快越好; 1. 序列化后的信息密度越大越好,也就是说,同样的一个结构化数据,序列化之后占用的存储空间越小越好; -当然, **不会存在一种序列化实现在这四个方面都是最优的** ,否则我们就没必要来纠结到底选择哪种实现了。因为,大多数情况下,易于阅读和信息密度是矛盾的,实现的复杂度和性能也是互相矛盾的。所以,我们需要根据所实现的业务,来选择合适的序列化实现。 +当然,**不会存在一种序列化实现在这四个方面都是最优的**,否则我们就没必要来纠结到底选择哪种实现了。因为,大多数情况下,易于阅读和信息密度是矛盾的,实现的复杂度和性能也是互相矛盾的。所以,我们需要根据所实现的业务,来选择合适的序列化实现。 像 JSON、XML 这些序列化方法,可读性最好,但信息密度也最低。像 Kryo、Hessian 这些通用的二进制序列化实现,适用范围广,使用简单,性能比 JSON、XML 要好一些,但是肯定不如专用的序列化实现。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25414\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25414\350\256\262.md" index 106869e9c..802a29f3f 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25414\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25414\350\256\262.md" @@ -35,7 +35,7 @@ 答案是,不一定。我们刚刚回收了 2 个 Short,正好是 4 个字节,但是,创建一个 Int 对象需要连续 4 个字节的内存空间,2 段 2 个字节的内存,并不一定就等于一段连续的 4 字节内存。如果这两段 2 字节的空闲内存不连续,我们就无法创建 Int 对象,这就是内存碎片问题。 -所以, **垃圾回收完成后,还需要进行内存碎片整理,将不连续的空闲内存移动到一起,以便空出足够的连续内存空间供后续使用。** 和垃圾回收算法一样,内存碎片整理也有很多非常复杂的实现方法,但由于整理过程中需要移动内存中的数据,也都不可避免地需要暂停进程。 +所以,**垃圾回收完成后,还需要进行内存碎片整理,将不连续的空闲内存移动到一起,以便空出足够的连续内存空间供后续使用。** 和垃圾回收算法一样,内存碎片整理也有很多非常复杂的实现方法,但由于整理过程中需要移动内存中的数据,也都不可避免地需要暂停进程。 虽然自动内存管理机制有效地解决了内存泄漏问题,带来的代价是执行垃圾回收时会暂停进程,如果暂停的时间过长,程序看起来就像“卡死了”一样。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25415\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25415\350\256\262.md" index 96b4fb8b7..5db798ae3 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25415\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25415\350\256\262.md" @@ -24,7 +24,7 @@ Apache Kafka 是一个高性能的消息队列,在众多消息队列产品中 在 Kafka 的服务端,也就是 Broker 这一端,又是如何处理这一批一批的消息呢? -在服务端,Kafka 不会把一批消息再还原成多条消息,再一条一条地处理,这样太慢了。Kafka 这块儿处理的非常聪明,每批消息都会被当做一个“批消息”来处理。也就是说,在 Broker 整个处理流程中,无论是写入磁盘、从磁盘读出来、还是复制到其他副本这些流程中, **批消息都不会被解开,一直是作为一条“批消息”来进行处理的。** +在服务端,Kafka 不会把一批消息再还原成多条消息,再一条一条地处理,这样太慢了。Kafka 这块儿处理的非常聪明,每批消息都会被当做一个“批消息”来处理。也就是说,在 Broker 整个处理流程中,无论是写入磁盘、从磁盘读出来、还是复制到其他副本这些流程中,**批消息都不会被解开,一直是作为一条“批消息”来进行处理的。** 在消费时,消息同样是以批为单位进行传递的,Consumer 从 Broker 拉到一批消息后,在客户端把批消息解开,再一条一条交给用户代码处理。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25416\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25416\350\256\262.md" index b4ce45979..9bb01da3b 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25416\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25416\350\256\262.md" @@ -8,7 +8,7 @@ 但是,磁盘它有一个致命的问题,就是读写速度很慢。它有多慢呢?一般来说 SSD(固态硬盘)每秒钟可以读写几千次,如果说我们的程序在处理业务请求的时候直接来读写磁盘,假设处理每次请求需要读写 3~5 次,即使每次请求的数据量不大,你的程序最多每秒也就能处理 1000 次左右的请求。 -而内存的随机读写速度是磁盘的 10 万倍!所以, **使用内存作为缓存来加速应用程序的访问速度,是几乎所有高性能系统都会采用的方法。** +而内存的随机读写速度是磁盘的 10 万倍!所以,**使用内存作为缓存来加速应用程序的访问速度,是几乎所有高性能系统都会采用的方法。** 缓存的思想很简单,就是把低速存储的数据,复制一份副本放到高速的存储中,用来加速数据的访问。缓存使用起来也非常简单,很多同学在做一些业务系统的时候,在一些执行比较慢的方法上加上一个 @Cacheable 的注解,就可以使用缓存来提升它的访问性能了。 @@ -24,7 +24,7 @@ 我们可以看到,在数据写到 PageCache 中后,它并不是同时就写到磁盘上了,这中间是有一个延迟的。操作系统可以保证,即使是应用程序意外退出了,操作系统也会把这部分数据同步到磁盘上。但是,如果服务器突然掉电了,这部分数据就丢失了。 -你需要知道, **读写缓存的这种设计,它天然就是不可靠的,是一种牺牲数据一致性换取性能的设计。** 当然,应用程序可以调用 sync 等系统调用,强制操作系统立即把缓存数据同步到磁盘文件中去,但是这个同步的过程是很慢的,也就失去了缓存的意义。 +你需要知道,**读写缓存的这种设计,它天然就是不可靠的,是一种牺牲数据一致性换取性能的设计。** 当然,应用程序可以调用 sync 等系统调用,强制操作系统立即把缓存数据同步到磁盘文件中去,但是这个同步的过程是很慢的,也就失去了缓存的意义。 另外,写缓存的实现是非常复杂的。应用程序不停地更新 PageCache 中的数据,操作系统需要记录哪些数据有变化,同时还要在另外一个线程中,把缓存中变化的数据更新到磁盘文件中。在提供并发读写的同时来异步更新数据,这个过程中要保证数据的一致性,并且有非常好的性能,实现这些真不是一件容易的事儿。 @@ -88,7 +88,7 @@ 另外一个选择,就是使用通用的置换算法。一个最经典也是最实用的算法就是 LRU 算法,也叫最近最少使用算法。这个算法它的思想是,最近刚刚被访问的数据,它在将来被访问的可能性也很大,而很久都没被访问过的数据,未来再被访问的几率也不大。 -基于这个思想, **LRU 的算法原理非常简单,它总是把最长时间未被访问的数据置换出去。** 你别看这个 LRU 算法这么简单,它的效果是非常非常好的。 +基于这个思想,**LRU 的算法原理非常简单,它总是把最长时间未被访问的数据置换出去。** 你别看这个 LRU 算法这么简单,它的效果是非常非常好的。 Kafka 使用的 PageCache,是由 Linux 内核实现的,它的置换算法的就是一种 LRU 的变种算法 :LRU 2Q。我在设计 JMQ 的缓存策略时,也是采用一种改进的 LRU 算法。LRU 淘汰最近最少使用的页,JMQ 根据消息这种流数据存储的特点,在淘汰时增加了一个考量维度:页面位置与尾部的距离。因为越是靠近尾部的数据,被访问的概率越大。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25417\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25417\350\256\262.md" index b1f035bfb..35861e1b0 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25417\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25417\350\256\262.md" @@ -63,7 +63,7 @@ public void visitShareResWithLock() { 使用锁的时候,你需要注意几个问题: -第一个,也是最重要的问题就是, **使用完锁,一定要释放它** 。比较容易出现状况的地方是,很多语言都有异常机制,当抛出异常的时候,不再执行后面的代码。如果在访问共享资源时抛出异常,那后面释放锁的代码就不会被执行,这样,锁就一直无法释放,形成死锁。所以,你要考虑到代码可能走到的所有正常和异常的分支,确保所有情况下,锁都能被释放。 +第一个,也是最重要的问题就是,**使用完锁,一定要释放它** 。比较容易出现状况的地方是,很多语言都有异常机制,当抛出异常的时候,不再执行后面的代码。如果在访问共享资源时抛出异常,那后面释放锁的代码就不会被执行,这样,锁就一直无法释放,形成死锁。所以,你要考虑到代码可能走到的所有正常和异常的分支,确保所有情况下,锁都能被释放。 有些语言提供了 try-with 的机制,不需要显式地获取和释放锁,可以简化编程,有效避免这种问题,推荐你使用。 @@ -100,7 +100,7 @@ public void visitShareResWithLock() { 在这段代码中,当前的线程获取到了锁 lock,然后在持有这把锁的情况下,再次去尝试获取这把锁,这样会导致死锁吗? -答案是,不一定。 **会不会死锁取决于,你获取的这把锁它是不是可重入锁。** 如果是可重入锁,那就没有问题,否则就会死锁。 +答案是,不一定。**会不会死锁取决于,你获取的这把锁它是不是可重入锁。** 如果是可重入锁,那就没有问题,否则就会死锁。 大部分编程语言都提供了可重入锁,如果没有特别的要求,你要尽量使用可重入锁。有的同学可能会问,“既然已经获取到锁了,我干嘛还要再次获取同一把锁呢?” diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25418\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25418\350\256\262.md" index f0f7f8114..15dc0f187 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25418\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25418\350\256\262.md" @@ -56,7 +56,7 @@ FAA 原语的语义是,先获取变量 p 当前的值 value,然后给变量 上面的这两段伪代码,如果我们用编程语言来实现,肯定是无法保证原子性的。而原语的特殊之处就是,它们都是由计算机硬件,具体说就是 CPU 提供的实现,可以保证操作的原子性。 -我们知道, **原子操作具有不可分割性,也就不存在并发的问题** 。所以在某些情况下,原语可以用来替代锁,实现一些即安全又高效的并发操作。 +我们知道,**原子操作具有不可分割性,也就不存在并发的问题** 。所以在某些情况下,原语可以用来替代锁,实现一些即安全又高效的并发操作。 CAS 和 FAA 在各种编程语言中,都有相应的实现,可以来直接使用,无论你是使用哪种编程语言,它们底层的实现是一样的,效果也是一样的。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25419\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25419\350\256\262.md" index b1ccdfe4f..be22fe45b 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25419\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25419\350\256\262.md" @@ -8,7 +8,7 @@ 我们的测试服务器的网卡就是普通的万兆网卡,极限带宽也就是 10Gb/s,压测时候的实际网络流量大概在 7Gb/s 左右。这里面,最重要的原因就是,我在测试的时候开启了 Kafka 的压缩功能。可以看到,对于 Kafka 来说,使用数据压缩,提升了大概几十倍的吞吐量。当然,在实际生产时,不太可能达到这么高的压缩率,但是合理地使用数据压缩,仍然可以做到提升数倍的吞吐量。 -所以, **数据压缩不仅能节省存储空间,还可以用于提升网络传输性能。** 这种使用压缩来提升系统性能的方法,不仅限于在消息队列中使用,我们日常开发的应用程序也可以使用。比如,我们的程序要传输大量的数据,或者要在磁盘、数据库中存储比较大的数据,这些情况下,都可以考虑使用数据压缩来提升性能,还能节省网络带宽和存储空间。 +所以,**数据压缩不仅能节省存储空间,还可以用于提升网络传输性能。** 这种使用压缩来提升系统性能的方法,不仅限于在消息队列中使用,我们日常开发的应用程序也可以使用。比如,我们的程序要传输大量的数据,或者要在磁盘、数据库中存储比较大的数据,这些情况下,都可以考虑使用数据压缩来提升性能,还能节省网络带宽和存储空间。 那如何在你的程序中使用压缩?应该选择什么样的压缩算法更适合我们的系统呢?这节课,我带你一起学习一下,使用数据压缩来提升系统性能的方法。 @@ -59,7 +59,7 @@ 大部分的压缩算法,他们的区别主要是,对数据进行编码的算法,压缩的流程和压缩包的结构大致一样的。而在压缩过程中,你最需要了解的就是如何选择合适的压缩分段大小。 -在压缩时,给定的被压缩数据它必须有确定的长度,或者说,是有头有尾的,不能是一个无限的数据流, **如果要对流数据进行压缩,那必须把流数据划分成多个帧,一帧一帧的分段压缩。** +在压缩时,给定的被压缩数据它必须有确定的长度,或者说,是有头有尾的,不能是一个无限的数据流,**如果要对流数据进行压缩,那必须把流数据划分成多个帧,一帧一帧的分段压缩。** 主要原因是,压缩算法在开始压缩之前,一般都需要对被压缩数据从头到尾进行一次扫描,扫描的目的是确定如何对数据进行划分和编码,一般的原则是重复次数多、占用空间大的内容,使用尽量短的编码,这样压缩率会更高。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25422\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25422\350\256\262.md" index 8489dd2f6..5fa4e9d16 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25422\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25422\350\256\262.md" @@ -32,7 +32,7 @@ 假设我们的集群采用“一主二从三副本”的模式,如果只要消息写入到两个副本就算是写入成功了,那这三个节点最多允许宕机一个节点,否则就没法提供服务了。如果说我们把要求写入的副本数量降到 1,只要消息写入到主节点就算成功了,那三个节点中,可以允许宕机两个节点,系统依然可以提供服务,这个可用性就更好一些。但是,有可能出现一种情况:主节点有一部分消息还没来得复制到任何一个从节点上,主节点就宕机了,这时候就会丢消息,数据一致性又没有办法保证了。 -以上我讲的这些内容,还没有涉及到任何复制或者选举的方法和算法,都是最朴素,最基本的原理。你可以看出,这里面是有很多天然的矛盾,所以, **目前并没有一种完美的实现方案能够兼顾高性能、高可用和一致性。** 不同的消息队列选择了不同的复制实现方式,这些实现方式都有各自的优缺点,在高性能、高可用和一致性方面提供的能力也是各有高低。接下来我们一起来看一下 RocketMQ 和 Kafka 分别是如何来实现复制的。 +以上我讲的这些内容,还没有涉及到任何复制或者选举的方法和算法,都是最朴素,最基本的原理。你可以看出,这里面是有很多天然的矛盾,所以,**目前并没有一种完美的实现方案能够兼顾高性能、高可用和一致性。** 不同的消息队列选择了不同的复制实现方式,这些实现方式都有各自的优缺点,在高性能、高可用和一致性方面提供的能力也是各有高低。接下来我们一起来看一下 RocketMQ 和 Kafka 分别是如何来实现复制的。 ## RocketMQ 如何实现复制? @@ -48,7 +48,7 @@ RocketMQ 提供了两种复制方式,一种是异步复制,消息先发送 在 RocketMQ 中,Broker 的主从关系是通过配置固定的,不支持动态切换。如果主节点宕机,生产者就不能再生产消息了,消费者可以自动切换到从节点继续进行消费。这时候,即使有一些消息没有来得及复制到从节点上,这些消息依然躺在主节点的磁盘上,除非是主节点的磁盘坏了,否则等主节点重新恢复服务的时候,这些消息依然可以继续复制到从节点上,也可以继续消费,不会丢消息,消息的顺序也是没有问题的。 -从设计上来讲, **RocketMQ 的这种主从复制方式,牺牲了可用性,换取了比较好的性能和数据一致性。** +从设计上来讲,**RocketMQ 的这种主从复制方式,牺牲了可用性,换取了比较好的性能和数据一致性。** 那 RocketMQ 又是如何解决可用性的问题的呢?一对儿主从节点可用性不行,多来几对儿主从节点不就解决了?RocketMQ 支持把一个主题分布到多对主从节点上去,每对主从节点中承担主题中的一部分队列,如果某个主节点宕机了,会自动切换到其他主节点上继续发消息,这样既解决了可用性的问题,还可以通过水平扩容来提升 Topic 总体的性能。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25424\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25424\350\256\262.md" index 17badbf65..29040696a 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25424\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25424\350\256\262.md" @@ -139,7 +139,7 @@ Kafka 在每个 Broker 中都维护了一份和 ZooKeeper 中一样的元数据 最后我们对这节课的内容做一个总结。 -首先,我们简单的介绍了 ZooKeeper,它是一个分布式的协调服务,它的核心服务是一个高可用、高可靠的一致性存储,在此基础上,提供了包括读写元数据、节点监控、选举、节点间通信和分布式锁等很多功能, **这些功能可以极大方便我们快速开发一个分布式的集群系统。** +首先,我们简单的介绍了 ZooKeeper,它是一个分布式的协调服务,它的核心服务是一个高可用、高可靠的一致性存储,在此基础上,提供了包括读写元数据、节点监控、选举、节点间通信和分布式锁等很多功能,**这些功能可以极大方便我们快速开发一个分布式的集群系统。** 但是,ZooKeeper 也并不是完美的,在使用的时候你需要注意几个问题: diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25427\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25427\350\256\262.md" index 8748e2eda..db6e96234 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25427\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25427\350\256\262.md" @@ -14,7 +14,7 @@ Pulsar 也是一个开源的分布式消息队列产品,最早是由 Yahoo 开 在集群模式下,为了避免单点故障导致丢消息,Broker 在保存消息的时候,必须也把消息复制到其他的 Broker 上。当某个 Broker 节点故障的时候,并不是集群中任意一个节点都能替代这个故障的节点,只有那些“和这个故障节点拥有相同数据的节点”才能替代这个故障的节点。原因就是,每一个 Broker 存储的消息数据是不一样的,或者说,每个节点上都存储了状态(数据)。这种节点称为“有状态的节点(Stateful Node)”。 -Pulsar 与其他消息队列在架构上,最大的不同在于,它的 Broker 是无状态的(Stateless)。也就是说, **在 Pulsar 的 Broker 中既不保存元数据,也不存储消息** 。那 Pulsar 的消息存储在哪儿呢?我们来看一下 Pulsar 的架构是什么样的。 +Pulsar 与其他消息队列在架构上,最大的不同在于,它的 Broker 是无状态的(Stateless)。也就是说,**在 Pulsar 的 Broker 中既不保存元数据,也不存储消息** 。那 Pulsar 的消息存储在哪儿呢?我们来看一下 Pulsar 的架构是什么样的。 ![img](assets/c6d87dbd3ef911f3581b8e51681d3339.png) @@ -50,7 +50,7 @@ Pulsar 的客户端要读写某个主题分区上的数据之前,依然要在 比如说,所有的大数据系统,包括 Map Reduce 这种传统的批量计算,和现在比较流行的 Spark、Flink 这种流计算,它们都采用的存储计算分离设计。数据保存在 HDFS 中,也就是说 HDFS 负责存储,而负责计算的节点,无论是用 YARN 调度还是 Kubernetes 调度,都只负责“读取 - 计算 - 写入”这样一种通用的计算逻辑,不保存任何数据。 -更普遍的, **我们每天都在开发的各种 Web 应用和微服务应用,绝大多数也采用的是存储计算分离的设计** 。数据保存在数据库中,微服务节点只负责响应请求,执行业务逻辑。也就是说,数据库负责存储,微服务节点负责计算。 +更普遍的,**我们每天都在开发的各种 Web 应用和微服务应用,绝大多数也采用的是存储计算分离的设计** 。数据保存在数据库中,微服务节点只负责响应请求,执行业务逻辑。也就是说,数据库负责存储,微服务节点负责计算。 那存储计算分离有什么优点呢?我们分两方面来看。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25428\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25428\350\256\262.md" index 59ca837ec..9b3a4de75 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25428\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25428\350\256\262.md" @@ -41,7 +41,7 @@ 结果,账户余额错误地变成了 200 元。 -同学,请把我的 100 块钱还给我!通过这个题,我们可以总结出来, **一个操作是否幂等,还跟调用顺序有关系** ,在线性调用情况下,具备幂等性的操作,在并发调用时,就不一定具备幂等性了。如果你在设计系统的时候,没有注意到这个细节,那系统就有可能出现我们上面这个例子中的错误,在生产系中,这是非常危险的。 +同学,请把我的 100 块钱还给我!通过这个题,我们可以总结出来,**一个操作是否幂等,还跟调用顺序有关系**,在线性调用情况下,具备幂等性的操作,在并发调用时,就不一定具备幂等性了。如果你在设计系统的时候,没有注意到这个细节,那系统就有可能出现我们上面这个例子中的错误,在生产系中,这是非常危险的。 ## 2. Kafka 和 RocketMQ 如何通过选举来产生新的 Leader? @@ -51,11 +51,11 @@ 在 Kafka 中 Controller 本身也是通过 ZooKeeper 选举产生的。接下来我要讲的,Kafka Controller 利用 ZooKeeper 选举的方法,你一定要记住并学会,因为这种方法非常简单实用,并且适用性非常广泛,在设计很多分布式系统中都可以用到。 -这种选举方法严格来说也不是真正的“选举”,而是一种抢占模式。实现也很简单,每个 Broker 在启动后,都会尝试在 ZooKeeper 中创建同一个临时节点:/controller,并把自身的信息写入到这个节点中。由于 ZooKeeper 它是一个 **可以保证数据一致性的分布式存储** ,所以,集群中只会有一个 Broker 抢到这个临时节点,那它就是 Leader 节点。其他没抢到 Leader 的节点,会 Watch 这个临时节点,如果当前的 Leader 节点宕机,所有其他节点都会收到通知,它们会开始新一轮的抢 Leader 游戏。 +这种选举方法严格来说也不是真正的“选举”,而是一种抢占模式。实现也很简单,每个 Broker 在启动后,都会尝试在 ZooKeeper 中创建同一个临时节点:/controller,并把自身的信息写入到这个节点中。由于 ZooKeeper 它是一个 **可以保证数据一致性的分布式存储**,所以,集群中只会有一个 Broker 抢到这个临时节点,那它就是 Leader 节点。其他没抢到 Leader 的节点,会 Watch 这个临时节点,如果当前的 Leader 节点宕机,所有其他节点都会收到通知,它们会开始新一轮的抢 Leader 游戏。 这就好比有个玉玺,也就是皇帝用的那个上面雕着龙纹的大印章,谁都可以抢这个玉玺,谁抢到谁做皇帝,其他没抢到的人也不甘心,时刻盯着这个玉玺,一旦现在这个皇帝驾崩了,所有人一哄而上,再“抢”出一个新皇帝。这个算法虽然不怎么优雅,但胜在简单直接,并且快速公平,是非常不错的选举方法。 -但是这个算法它依赖一个“玉玺”,也就是一个 **可以保证数据一致性的分布式存储** ,这个分布式存储不一定非得是 ZooKeeper,可以是 Redis,可以是 MySQL,也可以是 HDFS,只要是可以保证数据一致性的分布式存储,都可以充当这个“玉玺”,所以这个选举方法的适用场景也是非常广泛的。 +但是这个算法它依赖一个“玉玺”,也就是一个 **可以保证数据一致性的分布式存储**,这个分布式存储不一定非得是 ZooKeeper,可以是 Redis,可以是 MySQL,也可以是 HDFS,只要是可以保证数据一致性的分布式存储,都可以充当这个“玉玺”,所以这个选举方法的适用场景也是非常广泛的。 再来说 RocketMQ/Dledger 的选举,在 Dledger 中的 Leader 真的是通过投票选举出来的,所以它不需要借助于任何外部的系统,仅靠集群的节点间投票来达成一致,选举出 Leader。一般这种自我选举的算法,为了保证数据一致性、避免集群分裂,算法设计的都非常非常复杂,我们不太可能自己来实现这样一个选举算法,所以我在这里不展开讲。Dledger 采用的是[Raft 一致性算法](https://raft.github.io/),感兴趣的同学可以读一下这篇[经典的论文](https://raft.github.io/raft.pdf)。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25429\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25429\350\256\262.md" index c071fb018..fefde23c1 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25429\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25429\350\256\262.md" @@ -94,7 +94,7 @@ object SocketWindowIpCount { 这样就完成了一个流计算任务的定义。可以看到,定义一个计算任务的代码还是非常简单的,如果我们要自己写一个分布式的统计程序来实现一样的功能,代码量和复杂度肯定要远远超过上面这段代码。 -总结下来,无论是使用 Flink、Spark 还是其他的流计算框架,定义一个流计算的任务基本上都可以分为:定义输入、定义计算逻辑和定义输出三部分,通俗地说,也就是: **数据从哪儿来,怎么计算,结果写到哪儿去** ,这三件事儿。 +总结下来,无论是使用 Flink、Spark 还是其他的流计算框架,定义一个流计算的任务基本上都可以分为:定义输入、定义计算逻辑和定义输出三部分,通俗地说,也就是: **数据从哪儿来,怎么计算,结果写到哪儿去**,这三件事儿。 我把这个例子的代码上传到了 GitHub 上,你可以在[这里](https://github.com/liyue2008/IpCount)下载,关于如何设置环境、编译并运行这个例子,我在代码中的 README 中都给出了说明,你可以下载查看。 @@ -113,7 +113,7 @@ object SocketWindowIpCount { 4> 18:40:20 192.168.1.4 26 ``` -对于流计算的初学者,特别不好理解的一点是,我们上面编写的这段代码, **它只是“用来定义计算任务的代码”,而不是“真正处理数据的代码”。** 对于普通的应用程序,源代码编译之后,计算机就直接执行了,这个比较好理解。而在 Flink 中,当这个计算任务在 Flink 集群的计算节点中运行的时候,真正处理数据的代码并不是我们上面写的那段代码,而是 Flink 在解析了计算任务之后,动态生成的代码。 +对于流计算的初学者,特别不好理解的一点是,我们上面编写的这段代码,**它只是“用来定义计算任务的代码”,而不是“真正处理数据的代码”。** 对于普通的应用程序,源代码编译之后,计算机就直接执行了,这个比较好理解。而在 Flink 中,当这个计算任务在 Flink 集群的计算节点中运行的时候,真正处理数据的代码并不是我们上面写的那段代码,而是 Flink 在解析了计算任务之后,动态生成的代码。 这个有点儿类似于我们在查询 MySQL 的时候执行的 SQL,我们提交一个 SQL 查询后,MySQL 在执行查询遍历数据库中每条数据时,并不是对每条数据执行一遍 SQL,真正执行的其实是 MySQL 自己的代码。SQL 只是告诉 MySQL 我们要如何来查询数据,同样,我们编写的这段定义计算任务的代码,只是告诉 Flink 我们要如何来处理数据而已。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25431\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25431\350\256\262.md" index 178861328..2b4a01c23 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25431\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25431\350\256\262.md" @@ -4,7 +4,7 @@ 接下来的四节课,我们会一起实现一个 RPC 框架。你可能会问,为什么不实现一个消息队列,而要实现一个 RPC 框架呢?原因很简单,我们课程的目的是希望你能够学以致用举一反三,而不只是照猫画虎。在之前的课程中,我们一直在讲解消息队列的原理和实现消息队列的各种技术,那我们在实践篇如果再实现一个消息队列,不过是把之前课程中的内容重复实现一遍,意义不大。 -消息队列和 RPC 框架是我们最常用的两种通信方式,虽然这两种中间系统的功能不一样,但是, **实现这两种中间件系统的过程中,有很多相似之处** ,比如,它们都是分布式系统,都需要解决应用间通信的问题,都需要解决序列化的问题等等。 +消息队列和 RPC 框架是我们最常用的两种通信方式,虽然这两种中间系统的功能不一样,但是,**实现这两种中间件系统的过程中,有很多相似之处**,比如,它们都是分布式系统,都需要解决应用间通信的问题,都需要解决序列化的问题等等。 实现 RPC 框架用到的大部分底层技术,是和消息队列一样的,也都是我们在之前的课程中讲过的。所以,我们花四节课的时间来实现一个 RPC 框架,既可以检验你对进阶篇中学习到的底层技术掌握的是不是扎实,又可以学到 RPC 框架的实现原理,买一送一,很超值。 @@ -66,7 +66,7 @@ public class HelloServiceImpl implements HelloService { ![img](assets/946841b09cab0b11ce349a5a1eeea0ea.jpg) -在上面的这个调用流程中,我们忽略了一个问题,那就是客户端是如何找到服务端地址的呢?在 RPC 框架中, **这部分的实现原理其实和消息队列的实现是完全一样的** ,都是通过一个 NamingService 来解决的。 +在上面的这个调用流程中,我们忽略了一个问题,那就是客户端是如何找到服务端地址的呢?在 RPC 框架中,**这部分的实现原理其实和消息队列的实现是完全一样的**,都是通过一个 NamingService 来解决的。 在 RPC 框架中,这个 NamingService 一般称为注册中心。服务端的业务代码在向 RPC 框架中注册服务之后,RPC 框架就会把这个服务的名称和地址发布到注册中心上。客户端的桩在调用服务端之前,会向注册中心请求服务端的地址,请求的参数就是服务名称,也就是我们上面例子中的方法签名 HelloService#hello,注册中心会返回提供这个服务的地址,然后客户端再去请求服务端。 @@ -74,7 +74,7 @@ public class HelloServiceImpl implements HelloService { 我们可以再回顾一下上面那张调用的流程图,如果需要实现跨语言的调用,也就是说,图中的客户端进程和服务端进程是由两种不同的编程语言开发的。其实,只要客户端发出去的请求能被服务端正确解析,同样,服务端返回的响应,客户端也能正确解析,其他的步骤完全不用做任何改变,不就可以实现跨语言调用了吗? -在客户端和服务端,收发请求响应的工作都是 RPC 框架来实现的,所以, **只要 RPC 框架保证在不同的编程语言中,使用相同的序列化协议,就可以实现跨语言的通信。** 另外,为了在不同的语言中能描述相同的服务定义,也就是我们上面例子中的 HelloService 接口,跨语言的 RPC 框架还需要提供一套描述服务的语言,称为 IDL(Interface description language)。所有的服务都需要用 IDL 定义,再由 RPC 框架转换为特定编程语言的接口或者抽象类。这样,就可以实现跨语言调用了。 +在客户端和服务端,收发请求响应的工作都是 RPC 框架来实现的,所以,**只要 RPC 框架保证在不同的编程语言中,使用相同的序列化协议,就可以实现跨语言的通信。** 另外,为了在不同的语言中能描述相同的服务定义,也就是我们上面例子中的 HelloService 接口,跨语言的 RPC 框架还需要提供一套描述服务的语言,称为 IDL(Interface description language)。所有的服务都需要用 IDL 定义,再由 RPC 框架转换为特定编程语言的接口或者抽象类。这样,就可以实现跨语言调用了。 讲到这里,RPC 框架的基本实现原理就很清楚了,可以看到,实现一个简单的 RPC 框架并不是很难,这里面用到的绝大部分技术,包括:高性能网络传输、序列化和反序列化、服务路由的发现方法等,都是我们在学习消息队列实现原理过程中讲过的知识。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25434\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25434\350\256\262.md" index 4670052c8..b93d0e410 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25434\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25434\350\256\262.md" @@ -53,7 +53,7 @@ public interface NameService { 我们这个例子中注册中心的实现类是 LocalFileNameService,它的实现比较简单,就是去读写一个本地文件,实现注册服务 registerService 方法时,把服务提供者保存到本地文件中;实现查找服务 lookupService 时,就是去本地文件中读出所有的服务提供者,找到对应的服务提供者,然后返回。 -这里面有一点需要注意的是,由于这个本地文件它是一个共享资源,它会被 RPC 框架所有的客户端和服务端并发读写。所以,这时你要怎么做呢?对, **必须要加锁!** +这里面有一点需要注意的是,由于这个本地文件它是一个共享资源,它会被 RPC 框架所有的客户端和服务端并发读写。所以,这时你要怎么做呢?对,**必须要加锁!** 由于我们这个文件可能被多个进程读写,所以这里不能使用我们之前讲过的,编程语言提供的那些锁,原因是这些锁只能在进程内起作用,它锁不住其他进程。我们这里面必须使用由操作系统提供的文件锁。这个锁的使用和其他的锁并没有什么区别,同样是在访问共享文件之前先获取锁,访问共享资源结束后必须释放锁。具体的代码你可以去查看 LocalFileNameService 这个实现类。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25435\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25435\350\256\262.md" index 17d225953..aa00f8457 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25435\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25435\350\256\262.md" @@ -46,7 +46,7 @@ 比如,你要在一个学校门口开个网吧,到底能不能赚钱需要事先进行调研,看看学生的流量够不够撑起你这个网吧。然后,你就蹲在学校门口数人头,每过来一个学生你就数一下,数一下一天中每个小时会有多少个学生经过,这是流计算。你还可以放个摄像头,让它自动把路过的每个人都拍下来,然后晚上回家再慢慢数这些照片,这就是批计算。简单地说,流计算就是实时统计计算,批计算则是事后统计计算,这两种方式都可以统计出每小时的人流量。 -那这两种方式哪种更好呢?还是那句话, **看具体的使用场景和需求** 。流计算的优势就是实时统计,每到整点的时候,上一个小时的人流量就已经数出来了。在 T+0 的时刻就能第一时间得到统计结果,批计算相对就要慢一些,它最早在 T+0 时刻才开始进行统计,什么时候出结果取决于统计的耗时。 +那这两种方式哪种更好呢?还是那句话,**看具体的使用场景和需求** 。流计算的优势就是实时统计,每到整点的时候,上一个小时的人流量就已经数出来了。在 T+0 的时刻就能第一时间得到统计结果,批计算相对就要慢一些,它最早在 T+0 时刻才开始进行统计,什么时候出结果取决于统计的耗时。 但是,流计算也有它的一些不足,比如说,你在数人头的时候突然来了个美女,你多看了几眼,漏数了一些人怎么办?没办法,明天再来重新数吧。也就是说,对于流计算的故障恢复还是一个比较难解决的问题。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25436\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25436\350\256\262.md" index 123d9a091..af053e063 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25436\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25436\350\256\262.md" @@ -8,7 +8,7 @@ 基础篇中我们给大家讲解消息队列的原理和一些使用方法,重点是让大家学会使用消息队列。 -你在进阶篇中,我们课程设计的重点是讲解实现消息队列必备的技术知识,通过分析源码讲解消息队列的实现原理。 **希望你通过进阶篇的学习能够掌握到设计、实现消息队列所必备的知识和技术,这些知识和技术也是设计所有高性能、高可靠的分布式系统都需要具备的。** +你在进阶篇中,我们课程设计的重点是讲解实现消息队列必备的技术知识,通过分析源码讲解消息队列的实现原理。**希望你通过进阶篇的学习能够掌握到设计、实现消息队列所必备的知识和技术,这些知识和技术也是设计所有高性能、高可靠的分布式系统都需要具备的。** 进阶篇的上半部分,我们每一节课一个专题,来讲解设计实现一个高性能消息队列,必备的技术和知识。这里面每节课中讲解的技术点,不仅可以用来设计消息队列,同学们在设计日常的应用系统中也一定会用得到。 @@ -70,7 +70,7 @@ - 第三是,这个处理的全流程是近乎无锁的设计,避免了线程因为等待锁导致的阻塞; - 第四是,我们把回复响应这个需要等待资源的操作,也异步放到其他的线程中去执行。 -你看,一个看起来很简单的接收请求写入数据并回响应的流程,需要涉及的技术包括: **异步的设计、缓存设计、锁的正确使用、线程协调、序列化和内存管理** ,等等。你需要对这些技术都有深入的理解,并合理地使用,才能在确保逻辑正确、数据准确的前提下,做到极致的性能。这也是为什么我们在课程的进阶篇中,用这么多节课来逐一讲解这些“看起来和消息队列没什么关系”的知识点和技术。 +你看,一个看起来很简单的接收请求写入数据并回响应的流程,需要涉及的技术包括: **异步的设计、缓存设计、锁的正确使用、线程协调、序列化和内存管理**,等等。你需要对这些技术都有深入的理解,并合理地使用,才能在确保逻辑正确、数据准确的前提下,做到极致的性能。这也是为什么我们在课程的进阶篇中,用这么多节课来逐一讲解这些“看起来和消息队列没什么关系”的知识点和技术。 我也希望同学们在学习这些知识点的时候,不仅仅只是记住了,能说出来,用于回答面试问题,还要能真正理解这些知识点和技术背后深刻的思想,并使用在日常的设计和开发过程中。 @@ -266,13 +266,13 @@ go run hutong.go 耗时: 4.962786896s ``` -在这段程序里面, **我没有对程序做任何特殊的性能优化,只是使用了我们之前四节课中讲到的,上面列出来的那些知识点,完成了一个基本的实现。** 在这段程序中,我们首先定义了 RequestResponse 这个结构体,代表请求或响应,它包括序号和内容两个字段。readFrom 方法的功能是,接收数据,反序列化成 RequestResponse。 +在这段程序里面,**我没有对程序做任何特殊的性能优化,只是使用了我们之前四节课中讲到的,上面列出来的那些知识点,完成了一个基本的实现。** 在这段程序中,我们首先定义了 RequestResponse 这个结构体,代表请求或响应,它包括序号和内容两个字段。readFrom 方法的功能是,接收数据,反序列化成 RequestResponse。 协议的设计是这样的:首先用 4 个字节来标明这个请求的长度,然后用 4 个字节来保存序号,最后变长的部分就是大爷说的话。这里面用到了使用前置长度的方式来进行断句,这种断句的方式我在之前的课程中专门讲到过。 这里面我们使用了专有的序列化方法,原因我在之前的课程中重点讲过,专有的序列化方法具备最好的性能,序列化出来的字节数也更少,而我们这个作业比拼的就是性能,所以在这个作业中采用这种序列化方式是最合适的选择。 -zhangDaYeListen 和 liDaYeListen 这两个方法,它们的实现是差不多的,就是接收对方发出的请求,然后给出正确的响应。zhangDaYeSay 和 liDaYeSay 这两个方法的实现也是差不多的,当俩大爷遇见后,就开始不停地说各自的请求, **并不等待对方的响应** ,连续说 10 万次。 +zhangDaYeListen 和 liDaYeListen 这两个方法,它们的实现是差不多的,就是接收对方发出的请求,然后给出正确的响应。zhangDaYeSay 和 liDaYeSay 这两个方法的实现也是差不多的,当俩大爷遇见后,就开始不停地说各自的请求,**并不等待对方的响应**,连续说 10 万次。 这 4 个方法,分别在 4 个不同的协程中并行运行,两收两发,实现了全双工的通信。在这个地方,不少同学还是摆脱不了“一问一答,再问再答”这种人类交流的自然方式对思维的影响,写出来的依然是单工通信的程序,单工通信的性能是远远不如双工通信的,所以,要想做到比较好的网络传输性能,双工通信的方式才是最佳的选择。 diff --git "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25437\350\256\262.md" "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25437\350\256\262.md" index 3d1637d59..aa1860997 100644 --- "a/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25437\350\256\262.md" +++ "b/docs/Middleware/\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276/\347\254\25437\350\256\262.md" @@ -12,7 +12,7 @@ 缓解技术焦虑的“导航”是什么?如果你能跳出来,看清整个技术体系全貌,知道你自己的技术栈在这个技术体系中的位置,了解自己的长处和短板,也就不再焦虑了。 -我们可以把整个技术体系理解为一个超大的倒立的锥形体,上大下小。这个锥形, **越靠上越偏重于应用,或者说偏重于业务,越靠下,越偏重于基础技术和理论。** 整个技术知识结构是这样的模式,组成这个技术模型的每个技术点也呈现这样的状态。比如消息队列,就是整个技术体系中的一小块,它也是一个倒立的锥形。 +我们可以把整个技术体系理解为一个超大的倒立的锥形体,上大下小。这个锥形,**越靠上越偏重于应用,或者说偏重于业务,越靠下,越偏重于基础技术和理论。** 整个技术知识结构是这样的模式,组成这个技术模型的每个技术点也呈现这样的状态。比如消息队列,就是整个技术体系中的一小块,它也是一个倒立的锥形。 ![img](assets/34ca9b5814eec2bd611bccd5b679d460.jpg) @@ -20,7 +20,7 @@ 这个锥形越往下层,涉及到的技术就越少。比如说,消息队列的实现原理,我们这一整门课也就差不多讲全了。它用到的底层技术,就是异步、并发、锁等。直到这个锥形的尖尖,就一个数据结构,也是所有消息队列的理论基础:“队列”这个数据结构。 -在回到宏观层面来看这个大锥形,虽然它越来越大,但是,新增的部分都在哪儿?都在上面是不是?也就是说,这个大锥形它上面的大饼越摊越大,但是底下的部分,其实变化很少。虽然计算机相关的科学也只有几十年的历史,但是, **近二十几年,基础理论方面几乎没有任何突破性的进展** ,也即是说这个大锥形的尖尖,二十年没变过。我十几年前大学本科学的课程,和现在在校大学生学的课程相比,基本没什么变化,还是编译原理、图论这些课。 +在回到宏观层面来看这个大锥形,虽然它越来越大,但是,新增的部分都在哪儿?都在上面是不是?也就是说,这个大锥形它上面的大饼越摊越大,但是底下的部分,其实变化很少。虽然计算机相关的科学也只有几十年的历史,但是,**近二十几年,基础理论方面几乎没有任何突破性的进展**,也即是说这个大锥形的尖尖,二十年没变过。我十几年前大学本科学的课程,和现在在校大学生学的课程相比,基本没什么变化,还是编译原理、图论这些课。 看清了技术体系的整体,再来看你自身这个个体。对于整个技术体系这个超大的锥形体,我们每个人能掌握的,也就是你个人的技术栈,也就只有其中很小的一部分。 @@ -32,7 +32,7 @@ 把你个人的技术栈放到大锥形体中,应该像一个头向下倒立的鱿鱼。我们都知道,鱿鱼脑袋又大又尖,须子又多又长。把鱿鱼倒过来,它脑袋要尽量塞满这个大锥形的底部,也就是说,底层的大部分基础知识你要掌握。 -向上延伸的很多触手,代表整个技术体系的最上层的众多领域中,其中的几个领域你也是要掌握的。并且,自上而下,最好不要有断层,上层你掌握的技术不能只是浮于表面,而是要足够的深入, **深入到与你掌握的底层技术连通起来** ,代表你的知识体系是贯通的。 +向上延伸的很多触手,代表整个技术体系的最上层的众多领域中,其中的几个领域你也是要掌握的。并且,自上而下,最好不要有断层,上层你掌握的技术不能只是浮于表面,而是要足够的深入,**深入到与你掌握的底层技术连通起来**,代表你的知识体系是贯通的。 举个例子,比如你写了一段代码,往数据库中写了一条数据。你编写的程序,它在运行时是怎么存储和传输这条数据的?数据是如何从你的程序传递给数据库的?数据在数据库中是如何处理并存储的?数据库又是怎么把数据保存到磁盘上的?数据在磁盘上是以什么形式保存的?如果你可以回答出这些问题,那代表在这方面你的知识体系自上而下已经打通了。 diff --git "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25401\350\256\262.md" "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25401\350\256\262.md" index bf011b14a..e873cc5b3 100644 --- "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25401\350\256\262.md" +++ "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25401\350\256\262.md" @@ -24,7 +24,7 @@ 从当时查看服务打印的日记可以看出三个问题。 -**1. 服务 A:RPC 远程调用大量超时** 我们配置服务 B 每个接口的超时时间都是 3 秒。服务 B 提供的接口的实现都是缓存级别的操作,3 秒的超时时间,理论上除了网络问题,调用不可能会超过这个值。 **2. 服务 B:Jedis 读操作超时,服务 B 几个节点的日记全是 Jedis 读超时(Read time out!)** 服务 B 每个节点配置了 200 个最小连接数的 Jedis 连接池,这是根据 Netty 工作线程数配置的,即读写操作就算 200 个线程并发执行,也能为每个线程分配一个 Jedis 连接。 **3. 服务 A:文件句柄数达到上限** +**1. 服务 A:RPC 远程调用大量超时** 我们配置服务 B 每个接口的超时时间都是 3 秒。服务 B 提供的接口的实现都是缓存级别的操作,3 秒的超时时间,理论上除了网络问题,调用不可能会超过这个值。**2. 服务 B:Jedis 读操作超时,服务 B 几个节点的日记全是 Jedis 读超时(Read time out!)** 服务 B 每个节点配置了 200 个最小连接数的 Jedis 连接池,这是根据 Netty 工作线程数配置的,即读写操作就算 200 个线程并发执行,也能为每个线程分配一个 Jedis 连接。**3. 服务 A:文件句柄数达到上限** SocketChannel 套接字会占用一个文件句柄,有多少个客户端连接就占用多少个文件句柄。我们在服务的启动脚本上为每个进程配置 102400 的最大文件打开数,理论上当时的并发量并不可能会达到这个数值。服务 A 底层用的是自研的基于 Netty 实现的 HTTP 服务框架,没有限制最大连接数。 diff --git "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25409\350\256\262.md" "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25409\350\256\262.md" index 8d55046ae..781c4a536 100644 --- "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25409\350\256\262.md" +++ "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25409\350\256\262.md" @@ -114,7 +114,7 @@ public interface ProcessorSlotEntryCallback { } ``` -**第三种情况:捕获到 BlockException 异常** ,BlockException 异常只在需要拒绝请求时抛出。 +**第三种情况:捕获到 BlockException 异常**,BlockException 异常只在需要拒绝请求时抛出。 当捕获到 BlockException 异常时,将异常记录到调用链路上下文的当前 Entry(StatisticSlot 的 exit 方法会用到),然后调用 DefaultNode#increaseBlockQps 方法记录当前请求被拒绝,将当前时间窗口的 block qps 这项指标数据的值加 1。如果调用来源不为空,让调用来源的 StatisticsNode 也记录当前请求被拒绝;如果流量类型为 IN,则让用于统计所有资源指标数据的 ClusterNode 也记录当前请求被拒绝。这部分的源码如下: @@ -137,7 +137,7 @@ public interface ProcessorSlotEntryCallback { throw e; ``` -StatisticSlot 捕获 BlockException 异常只是为了收集被拒绝的请求,BlockException 异常还是会往上抛出。抛出异常的目的是为了拦住请求,让入口处能够执行到 catch 代码块完成请求被拒绝后的服务降级处理。 **第四种情况:捕获到其它异常。** 其它异常并非指业务异常,因为此时业务代码还未执行,而业务代码抛出的异常是通过调用 Tracer#trace 方法记录的。 +StatisticSlot 捕获 BlockException 异常只是为了收集被拒绝的请求,BlockException 异常还是会往上抛出。抛出异常的目的是为了拦住请求,让入口处能够执行到 catch 代码块完成请求被拒绝后的服务降级处理。**第四种情况:捕获到其它异常。** 其它异常并非指业务异常,因为此时业务代码还未执行,而业务代码抛出的异常是通过调用 Tracer#trace 方法记录的。 当捕获到非 BlockException 异常时,除 PriorityWaitException 异常外,其它类型的异常都同样处理。让 DefaultNode 记录当前请求异常,将当前时间窗口的 exception qps 这项指标数据的值加 1。调用来源的 StatisticsNode、用于统计所有资源指标数据的 ClusterNode 也记录下这个异常。这部分源码如下: diff --git "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25411\350\256\262.md" "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25411\350\256\262.md" index 48f36d9fa..686370b0e 100644 --- "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25411\350\256\262.md" +++ "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25411\350\256\262.md" @@ -172,11 +172,11 @@ RateLimiterController 实现的 canPass 方法源码如下: } ``` -**1. 计算队列中连续的两个请求的通过时间的间隔时长** 假设阈值 QPS 为 200,那么连续的两个请求的通过时间间隔为 5 毫秒,每 5 毫秒通过一个请求就是匀速的速率,即每 5 毫秒允许通过一个请求。 **2. 计算当前请求期望的通过时间** 请求通过的间隔时间加上最近一个请求通过的时间就是当前请求预期通过的时间。 **3. 期望通过时间少于当前时间则当前请求可通过并且可以立即通过** 理想的情况是每个请求在队列中排队通过,那么每个请求都在固定的不重叠的时间通过。但在多核 CPU 的硬件条件下可能出现多个请求并行通过,这就是为什么说实际通过的 QPS 会超过限流阈值的 QPS。 +**1. 计算队列中连续的两个请求的通过时间的间隔时长** 假设阈值 QPS 为 200,那么连续的两个请求的通过时间间隔为 5 毫秒,每 5 毫秒通过一个请求就是匀速的速率,即每 5 毫秒允许通过一个请求。**2. 计算当前请求期望的通过时间** 请求通过的间隔时间加上最近一个请求通过的时间就是当前请求预期通过的时间。**3. 期望通过时间少于当前时间则当前请求可通过并且可以立即通过** 理想的情况是每个请求在队列中排队通过,那么每个请求都在固定的不重叠的时间通过。但在多核 CPU 的硬件条件下可能出现多个请求并行通过,这就是为什么说实际通过的 QPS 会超过限流阈值的 QPS。 -源码中给的注释:这里可能存在争论,但没关系。因并行导致超出的请求数不会超阈值太多,所以影响不大。 **4. 预期通过时间如果超过当前时间那就休眠等待** ,需要等待的时间等于预期通过时间减去当前时间,如果等待时间超过队列允许的最大等待时间,则直接拒绝该请求。 **5. 如果当前请求更新 latestPassedTime 为自己的预期通过时间后** ,需要等待的时间少于限定的最大等待时间,说明排队有效,否则自己退出队列并回退一个间隔时间。 +源码中给的注释:这里可能存在争论,但没关系。因并行导致超出的请求数不会超阈值太多,所以影响不大。**4. 预期通过时间如果超过当前时间那就休眠等待**,需要等待的时间等于预期通过时间减去当前时间,如果等待时间超过队列允许的最大等待时间,则直接拒绝该请求。**5. 如果当前请求更新 latestPassedTime 为自己的预期通过时间后**,需要等待的时间少于限定的最大等待时间,说明排队有效,否则自己退出队列并回退一个间隔时间。 -此时 latestPassedTime 就是当前请求的预期通过时间,后续的请求将排在该请求的后面。这就是虚拟队列的核心实现,按预期通过时间排队。 **6. 如果等待时间超过队列允许的最大排队时间则回退一个间隔时间,并拒绝当前请求。** 回退一个间隔时间相当于将数组中一个元素移除后,将此元素后面的所有元素都向前移动一个位置。此处与数组移动不同的是,该操作不会减少已经在等待的请求的等待时间。 **7. 休眠等待** +此时 latestPassedTime 就是当前请求的预期通过时间,后续的请求将排在该请求的后面。这就是虚拟队列的核心实现,按预期通过时间排队。**6. 如果等待时间超过队列允许的最大排队时间则回退一个间隔时间,并拒绝当前请求。** 回退一个间隔时间相当于将数组中一个元素移除后,将此元素后面的所有元素都向前移动一个位置。此处与数组移动不同的是,该操作不会减少已经在等待的请求的等待时间。**7. 休眠等待** 匀速流控适合用于请求突发性增长后剧降的场景。例如用在有定时任务调用的接口,在定时任务执行时请求量一下子飙高,但随后又没有请求的情况,这个时候我们不希望一下子让所有请求都通过,避免把系统压垮,但也不想直接拒绝超出阈值的请求,这种场景下使用匀速流控可以将突增的请求排队到低峰时执行,起到“削峰填谷”的效果。 diff --git "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25417\350\256\262.md" "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25417\350\256\262.md" index 7bff79cd3..71ae4b5d2 100644 --- "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25417\350\256\262.md" +++ "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25417\350\256\262.md" @@ -216,7 +216,7 @@ feign: enabled: true ``` -**3. 熔断降级规则配置** 可基于动态数据源实现,也可直接调用 DegradeRuleManager 的 loadRules API 硬编码实现,可参考上一篇。 **4. 给 @FeignClient 注解配置异常回调** 给接口上的 @FeignClient 注解配置 fallback 属性,实现请求被拒绝后的处理。 +**3. 熔断降级规则配置** 可基于动态数据源实现,也可直接调用 DegradeRuleManager 的 loadRules API 硬编码实现,可参考上一篇。**4. 给 @FeignClient 注解配置异常回调** 给接口上的 @FeignClient 注解配置 fallback 属性,实现请求被拒绝后的处理。 ```java @FeignClient( diff --git "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25420\350\256\262.md" "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25420\350\256\262.md" index 1a526191d..122eee48f 100644 --- "a/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25420\350\256\262.md" +++ "b/docs/Middleware/\346\267\261\345\205\245\347\220\206\350\247\243 Sentinel/\347\254\25420\350\256\262.md" @@ -65,7 +65,7 @@ WarmUpController 冷启动限流效果的实现并不控制每个请求的通过 注:1.23 版本是 JMH 目前最新的版本。 -#### **注解方式使用** 在运行时,注解配置被用于解析生成 BenchmarkListEntry 配置类实例。一个方法对应一个 @Benchmark 注解,一个 @Benchmark 注解对应一个基准测试方法。注释在类上的注解,或者注释在类的字段上的注解,则是类中所有基准测试方法共用的配置。 **@Benchmark** @Benchmark 注解用于声明一个 public 方法为基准测试方法,如下代码所示 +#### **注解方式使用** 在运行时,注解配置被用于解析生成 BenchmarkListEntry 配置类实例。一个方法对应一个 @Benchmark 注解,一个 @Benchmark 注解对应一个基准测试方法。注释在类上的注解,或者注释在类的字段上的注解,则是类中所有基准测试方法共用的配置。**@Benchmark** @Benchmark 注解用于声明一个 public 方法为基准测试方法,如下代码所示 ```java public class MyTestBenchmark { @@ -189,7 +189,7 @@ public class MyTestBenchmark { } ``` -如果 @Measurement 注解指定 time 为 1s,基准测试方法的执行耗时为 1s,那么如果只使用单个线程,一次测量只会执行一次基准测试方法,如果使用 10 个线程,一次测量就能执行 10 次基准测试方法。 **公共注解** 假设我们需要在 MyTestBenchmark 类中创建两个基准测试方法,一个是 testFunction1,另一个是 testFunction2,这两个方法分别调用不同的支付接口,用于对比两个接口的性能。那么我们可以将除 @Benchmark 注解外的其它注解都声明到类上,让两个基准测试方法都使用同样的配置,代码如下。 +如果 @Measurement 注解指定 time 为 1s,基准测试方法的执行耗时为 1s,那么如果只使用单个线程,一次测量只会执行一次基准测试方法,如果使用 10 个线程,一次测量就能执行 10 次基准测试方法。**公共注解** 假设我们需要在 MyTestBenchmark 类中创建两个基准测试方法,一个是 testFunction1,另一个是 testFunction2,这两个方法分别调用不同的支付接口,用于对比两个接口的性能。那么我们可以将除 @Benchmark 注解外的其它注解都声明到类上,让两个基准测试方法都使用同样的配置,代码如下。 ```java @BenchmarkMode(Mode.AverageTime) @@ -404,7 +404,7 @@ Options 在前面已经介绍过了,由于本例中 JsonBenchmark 这个类已 > [https://github.com/artyushov/idea-jmh-plugin](https://github.com/artyushov/idea-jmh-plugin) -安装:在 IDEA 中搜索 JMH Plugin,安装后重启即可使用。 **1. 只执行单个 Benchmark 方法** 在方法名称所在行,IDEA 会有一个▶️执行符号,右键点击运行即可。如果写的是单元测试方法,IDEA 会提示你选择执行单元测试还是基准测试。 **2. 执行一个类中的所有 Benchmark 方法** +安装:在 IDEA 中搜索 JMH Plugin,安装后重启即可使用。**1. 只执行单个 Benchmark 方法** 在方法名称所在行,IDEA 会有一个▶️执行符号,右键点击运行即可。如果写的是单元测试方法,IDEA 会提示你选择执行单元测试还是基准测试。**2. 执行一个类中的所有 Benchmark 方法** 在类名所在行,IDEA 会有一个`▶️`执行符号,右键点击运行,该类下的所有被 @Benchmark 注解注释的方法都会执行。如果写的是单元测试方法,IDEA 会提示你选择执行单元测试还是基准测试。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25400\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25400\350\256\262.md" index 53c1f7635..2c7f279df 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25400\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25400\350\256\262.md" @@ -17,15 +17,15 @@ 3. Netty 的内存池、对象池是如何设计的? 4. 针对 Netty 你有哪些印象比较深刻的系统调优案例? -这些问题看似简单,但如果你对 Netty 掌握不够深入,回答时就很容易“翻车”。我面试过很多求职者,虽然他们都有一定的 Netty 使用经验,但当深入探讨技术细节及如何解决项目中的实际问题时,就会发现大部分人只是简单使用,并没有深入掌握 Netty 的技术原理。 **如果你可以学好 Netty,掌握底层原理,一定会成为你求职面试的加分项。** **而且通过 Netty 的学习,还可以锻炼你的编程思维,对 Java 其他的知识体系起到融会贯通的作用。** 当年我刚踏入工作,领到的第一个任务是数据采集和上报。我尝试了各种解决方案最后都被主管否掉了,他说“不用那么麻烦,直接使用 Netty 就好了”。于是我一边学习一边完成工作,工作之余还会挤出时间研究 Netty 源码。 +这些问题看似简单,但如果你对 Netty 掌握不够深入,回答时就很容易“翻车”。我面试过很多求职者,虽然他们都有一定的 Netty 使用经验,但当深入探讨技术细节及如何解决项目中的实际问题时,就会发现大部分人只是简单使用,并没有深入掌握 Netty 的技术原理。**如果你可以学好 Netty,掌握底层原理,一定会成为你求职面试的加分项。** **而且通过 Netty 的学习,还可以锻炼你的编程思维,对 Java 其他的知识体系起到融会贯通的作用。** 当年我刚踏入工作,领到的第一个任务是数据采集和上报。我尝试了各种解决方案最后都被主管否掉了,他说“不用那么麻烦,直接使用 Netty 就好了”。于是我一边学习一边完成工作,工作之余还会挤出时间研究 Netty 源码。 -回想起研究源码的那段日子,虽然很辛苦,但仿佛为我 **打开了一扇 Java 新世界的大门** ,当我理解领悟 Netty 的设计原理之后,对 I/O 模型 、内存管理、线程模型、数据结构等当时理解起来有一定难度的知识,仿佛一瞬间“顿悟”了。而且在我日后再去学习 RocketMQ、Nginx、Redis 等优秀框架时,也明显感觉更加便捷、高效了。 +回想起研究源码的那段日子,虽然很辛苦,但仿佛为我 **打开了一扇 Java 新世界的大门**,当我理解领悟 Netty 的设计原理之后,对 I/O 模型 、内存管理、线程模型、数据结构等当时理解起来有一定难度的知识,仿佛一瞬间“顿悟”了。而且在我日后再去学习 RocketMQ、Nginx、Redis 等优秀框架时,也明显感觉更加便捷、高效了。 -因此,如果你想提升自己的技术水平并找到一份满意的工作,学习掌握 Netty 就非常重要。事实上,在平时的开发工作中, **Netty 的易用性和可靠性也极大程度上降低了开发者的心智负担。** 我在学生时代,写过不少网络应用,现在看来,非常冗长。当我熟练掌握 Netty 后,一切问题迎刃而解。Netty 对 Java NIO 进行了高级封装,简化了网络应用的开发过程,我们不再需要花费大量精力关注 Selector、SocketChannel、ServerSocketChannel 等繁杂的 API。 +因此,如果你想提升自己的技术水平并找到一份满意的工作,学习掌握 Netty 就非常重要。事实上,在平时的开发工作中,**Netty 的易用性和可靠性也极大程度上降低了开发者的心智负担。** 我在学生时代,写过不少网络应用,现在看来,非常冗长。当我熟练掌握 Netty 后,一切问题迎刃而解。Netty 对 Java NIO 进行了高级封装,简化了网络应用的开发过程,我们不再需要花费大量精力关注 Selector、SocketChannel、ServerSocketChannel 等繁杂的 API。 当我自己写网络应用时,拆包/粘包、数据编解码、TCP 断线重连等一系列问题都需要考虑到,而现在 Netty 给我们提供了现成的解决方案。此外遇到问题还可以在社区讨论,Netty 的迭代周期短修复问题快,其可靠性和健壮性被越来越多的公司所认可和采纳。 -不客气地说, **正是因为有 Netty 的存在,网络编程领域 Java 才得以与 C++ 并肩而立** 。 +不客气地说,**正是因为有 Netty 的存在,网络编程领域 Java 才得以与 C++ 并肩而立** 。 由以上几点出发,我想和你一起学习 Netty,希望在工作和求职的过程中能够为你提供帮助,也可以为你打开学习思路。 @@ -57,7 +57,7 @@ 3. 想系统学习 Netty 服务端开发,并希望通过实战来加深理解; 4. 正在从事网络、分布式服务框架等方向的工作,期望自己成为该领域的专家。 -那么这个课程就是为你量身定做的,课程中我会 **结合高频的面试题** ,从源码出发剖析 Netty 的核心技术原理,同时将这么多年使我受益匪浅的一些 **编程思想** 和 **实战经验** 分享给你,帮助你在工作中学以致用,避免踩坑。 +那么这个课程就是为你量身定做的,课程中我会 **结合高频的面试题**,从源码出发剖析 Netty 的核心技术原理,同时将这么多年使我受益匪浅的一些 **编程思想** 和 **实战经验** 分享给你,帮助你在工作中学以致用,避免踩坑。 在这里我也总结归纳出一份 Netty 核心知识点的思维导图,希望可以帮助你梳理本专栏的整体知识脉络。我会由浅入深地带你建立起完整的 Netty 知识体系,夯实你的 Netty 基础知识、Netty 进阶技能、实战开发经验。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25401\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25401\350\256\262.md" index 65349b00c..f8e6eeb8e 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25401\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25401\350\256\262.md" @@ -26,7 +26,7 @@ Netty 是一款用于高效开发网络应用的 NIO 网络框架,它大大简 I/O 请求可以分为两个阶段,分别为调用阶段和执行阶段。 -- 第一个阶段为 **I/O 调用阶段** ,即用户进程向内核发起系统调用。 +- 第一个阶段为 **I/O 调用阶段**,即用户进程向内核发起系统调用。 - 第二个阶段为 **I/O 执行阶段** 。此时,内核等待 I/O 请求处理完成返回。该阶段分为两个过程:首先等待数据就绪,并写入内核缓冲区;随后将内核缓冲区数据拷贝至用户态缓冲区。 为了方便大家理解,可以看一下这张图: @@ -39,7 +39,7 @@ I/O 请求可以分为两个阶段,分别为调用阶段和执行阶段。 ![1.png](assets/CgqCHl-OnUKAeEELAAEnHU3FHGA343.png){style="width:100%;max-width:600px"} -如上图所表现的那样,应用进程向内核发起 I/O 请求,发起调用的线程一直等待内核返回结果。一次完整的 I/O 请求称为 BIO( Blocking IO,阻塞 I/O),所以 BIO 在实现异步操作时, **只能使用多线程模型** ,一个请求对应一个线程。但是,线程的资源是有限且宝贵的,创建过多的线程会增加线程切换的开销。 +如上图所表现的那样,应用进程向内核发起 I/O 请求,发起调用的线程一直等待内核返回结果。一次完整的 I/O 请求称为 BIO( Blocking IO,阻塞 I/O),所以 BIO 在实现异步操作时,**只能使用多线程模型**,一个请求对应一个线程。但是,线程的资源是有限且宝贵的,创建过多的线程会增加线程切换的开销。 #### 2. 同步非阻塞 I/O (NIO) @@ -47,13 +47,13 @@ I/O 请求可以分为两个阶段,分别为调用阶段和执行阶段。 在刚介绍完 BIO 的网络模型之后, NIO 自然就很好理解了。 -如上图所示,应用进程向内核发起 I/O 请求后不再会同步等待结果,而是会立即返回,通过轮询的方式获取请求结果。 NIO 相比 BIO 虽然大幅提升了性能,但是轮询过程中大量的系统调用导致上下文切换开销很大。所以, **单独使用非阻塞 I/O 时效率并不高** ,而且随着并发量的提升,非阻塞 I/O 会存在严重的性能浪费。 +如上图所示,应用进程向内核发起 I/O 请求后不再会同步等待结果,而是会立即返回,通过轮询的方式获取请求结果。 NIO 相比 BIO 虽然大幅提升了性能,但是轮询过程中大量的系统调用导致上下文切换开销很大。所以,**单独使用非阻塞 I/O 时效率并不高**,而且随着并发量的提升,非阻塞 I/O 会存在严重的性能浪费。 #### 3. I/O 多路复用 ![3.png](assets/CgqCHl-OnV2ADXBhAAFUZ6oiz6U529.png){style="width:100%;max-width:600px"} -多路复用实现了 **一个线程处理多个 I/O 句柄的操作** 。多路指的是多个 **数据通道** ,复用指的是使用一个或多个固定线程来处理每一个 Socket。 select、 poll、 epoll 都是 I/O 多路复用的具体实现,线程一次 select 调用可以获取内核态中多个数据通道的数据状态。多路复用解决了同步阻塞 I/O 和同步非阻塞 I/O 的问题,是一种非常高效的 I/O 模型。 +多路复用实现了 **一个线程处理多个 I/O 句柄的操作** 。多路指的是多个 **数据通道**,复用指的是使用一个或多个固定线程来处理每一个 Socket。 select、 poll、 epoll 都是 I/O 多路复用的具体实现,线程一次 select 调用可以获取内核态中多个数据通道的数据状态。多路复用解决了同步阻塞 I/O 和同步非阻塞 I/O 的问题,是一种非常高效的 I/O 模型。 #### 4. 信号驱动 I/O @@ -71,7 +71,7 @@ I/O 请求可以分为两个阶段,分别为调用阶段和执行阶段。 了解了上述五种 I/O,我们再来看 Netty 如何实现自己的 I/O 模型。 Netty 的 I/O 模型是基于非阻塞 I/O 实现的,底层依赖的是 JDK NIO 框架的多路复用器 Selector。一个多路复用器 Selector 可以同时轮询多个 Channel,采用 epoll 模式后,只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。 -在 I/O 多路复用的场景下,当有数据处于就绪状态后,需要一个事件分发器( Event Dispather),它负责将读写事件分发给对应的读写事件处理器( Event Handler)。事件分发器有两种设计模式: Reactor 和 Proactor, **Reactor 采用同步 I/O, Proactor 采用异步 I/O** 。 +在 I/O 多路复用的场景下,当有数据处于就绪状态后,需要一个事件分发器( Event Dispather),它负责将读写事件分发给对应的读写事件处理器( Event Handler)。事件分发器有两种设计模式: Reactor 和 Proactor,**Reactor 采用同步 I/O, Proactor 采用异步 I/O** 。 Reactor 实现相对简单,适合处理耗时短的场景,对于耗时长的 I/O 操作容易造成阻塞。 Proactor 性能更高,但是实现逻辑非常复杂,目前主流的事件驱动模型还是依赖 select 或 epoll 来实现。 @@ -136,7 +136,7 @@ Netty 官方提供 3.x、 4.x 的稳定版本,之前一直处于测试阶段 4. GC 更加友好,增加池化缓存, 4.1 版本开始 jemalloc 成为默认内存分配方式。 5. 内存泄漏检测功能。 - **通用工具类** : io.netty.util.concurrent 包中提供了较多异步编程的数据结构。 -- **更加严谨的线程模型控制** ,降低用户编写 ChannelHandler 的心智,不必过于担心线程安全问题。 +- **更加严谨的线程模型控制**,降低用户编写 ChannelHandler 的心智,不必过于担心线程安全问题。 可见 Netty 4.x 带来了很多提升,性能、健壮性都变得更加强大了。 Netty 精益求精的设计精神值得每个人学习。当然,其中还有更多细节变化,感兴趣的同学可以参考以下网址:[https://netty.io/wiki/new-and-noteworthy-in-4.0.html](https://netty.io/wiki/new-and-noteworthy-in-4.0.html)。如果你现在对这些概念还不是很清晰,也不必担心,专栏后续的内容中我都会具体讲解。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25402\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25402\350\256\262.md" index 907f43bce..609be78f5 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25402\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25402\350\256\262.md" @@ -8,7 +8,7 @@ ## Netty 整体结构 -Netty 是一个设计非常用心的 **网络基础组件** , Netty 官网给出了有关 Netty 的整体功能模块结构,却没有其他更多的解释。从图中,我们可以清晰地看出 Netty 结构一共分为三个模块: +Netty 是一个设计非常用心的 **网络基础组件**, Netty 官网给出了有关 Netty 的整体功能模块结构,却没有其他更多的解释。从图中,我们可以清晰地看出 Netty 结构一共分为三个模块: ![Drawing 0.png](assets/CgqCHl-NO7eATPMMAAH8t8KvehQ985.png){style="width:100%;max-width:600px"} @@ -24,7 +24,7 @@ Core 核心层是 Netty 最精华的内容,它提供了底层网络通信的 传输服务层提供了网络传输能力的定义和实现方法。它支持 Socket、 HTTP 隧道、虚拟机管道等传输方式。 Netty 对 TCP、 UDP 等数据传输做了抽象和封装,用户可以更聚焦在业务逻辑实现上,而不必关系底层数据传输的细节。 -Netty 的模块设计具备较高的 **通用性和可扩展性** ,它不仅是一个优秀的网络框架,还可以作为网络编程的工具箱。 Netty 的设计理念非常优雅,值得我们学习借鉴。 +Netty 的模块设计具备较高的 **通用性和可扩展性**,它不仅是一个优秀的网络框架,还可以作为网络编程的工具箱。 Netty 的设计理念非常优雅,值得我们学习借鉴。 现在,我们对 Netty 的整体结构已经有了一个大概的印象,下面我们一起看下 Netty 的逻辑架构,学习下 Netty 是如何做功能分解的。 @@ -44,7 +44,7 @@ Netty 的模块设计具备较高的 **通用性和可扩展性** ,它不仅 Bootstrap 是“引导”的意思,它主要负责整个 Netty 程序的启动、初始化、服务器连接等过程,它相当于一条主线,串联了 Netty 的其他核心组件。 -如下图所示, Netty 中的引导器共分为两种类型:一个为 **用于客户端引导的 Bootstrap** ,另一个为 **用于服务端引导的 ServerBootStrap** ,它们都继承自抽象类 AbstractBootstrap。 +如下图所示, Netty 中的引导器共分为两种类型:一个为 **用于客户端引导的 Bootstrap**,另一个为 **用于服务端引导的 ServerBootStrap**,它们都继承自抽象类 AbstractBootstrap。 ![Drawing 2.png](assets/Ciqc1F-NO9yAeCsoAAHf2YCqjsQ005.png){style="width:100%;max-width:400px"} @@ -127,7 +127,7 @@ EventLoopGroup 是 Netty 的核心处理引擎,那么 EventLoopGroup 和之前 #### ChannelPipeline -ChannelPipeline 是 Netty 的核心编排组件, **负责组装各种 ChannelHandler** ,实际数据的编解码以及加工处理操作都是由 ChannelHandler 完成的。 ChannelPipeline 可以理解为 **ChannelHandler 的实例列表** ——内部通过双向链表将不同的 ChannelHandler 链接在一起。当 I/O 读写事件触发时, ChannelPipeline 会依次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。 +ChannelPipeline 是 Netty 的核心编排组件,**负责组装各种 ChannelHandler**,实际数据的编解码以及加工处理操作都是由 ChannelHandler 完成的。 ChannelPipeline 可以理解为 **ChannelHandler 的实例列表** ——内部通过双向链表将不同的 ChannelHandler 链接在一起。当 I/O 读写事件触发时, ChannelPipeline 会依次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。 ChannelPipeline 是线程安全的,因为每一个新的 Channel 都会对应绑定一个新的 ChannelPipeline。一个 ChannelPipeline 关联一个 EventLoop,一个 EventLoop 仅会绑定一个线程。 @@ -139,7 +139,7 @@ ChannelPipeline、 ChannelHandler 都是高度可定制的组件。开发者可 ![Drawing 7.png](assets/CgqCHl-NPKaASxvgAAFHMPYQFhM940.png) -客户端和服务端都有各自的 ChannelPipeline。以客户端为例,数据从客户端发向服务端,该过程称为 **出站** ,反之则称为 **入站** 。数据入站会由一系列 InBoundHandler 处理,然后再以相反方向的 OutBoundHandler 处理后完成出站。我们经常使用的编码 Encoder 是出站操作,解码 Decoder 是入站操作。服务端接收到客户端数据后,需要先经过 Decoder 入站处理后,再通过 Encoder 出站通知客户端。所以客户端和服务端一次完整的请求应答过程可以分为三个步骤:客户端出站(请求数据)、服务端入站(解析数据并执行业务逻辑)、服务端出站(响应结果)。 +客户端和服务端都有各自的 ChannelPipeline。以客户端为例,数据从客户端发向服务端,该过程称为 **出站**,反之则称为 **入站** 。数据入站会由一系列 InBoundHandler 处理,然后再以相反方向的 OutBoundHandler 处理后完成出站。我们经常使用的编码 Encoder 是出站操作,解码 Decoder 是入站操作。服务端接收到客户端数据后,需要先经过 Decoder 入站处理后,再通过 Encoder 出站通知客户端。所以客户端和服务端一次完整的请求应答过程可以分为三个步骤:客户端出站(请求数据)、服务端入站(解析数据并执行业务逻辑)、服务端出站(响应结果)。 #### ChannelHandler & ChannelHandlerContext @@ -182,7 +182,7 @@ Netty 源码分为多个模块,模块之间职责划分非常清楚。如同 - 通用工具类:比如定时器工具 TimerTask、时间轮 HashedWheelTimer 等。 - 自定义并发包:比如异步模型 **Future & Promise**、相比 JDK 增强的 FastThreadLocal 等。 -在 **netty-buffer 模块中** Netty 自己实现了的一个更加完备的 **ByteBuf 工具类** ,用于网络通信中的数据载体。由于人性化的 Buffer API 设计,它已经成为 Java ByteBuffer 的完美替代品。 ByteBuf 的动态性设计不仅解决了 ByteBuffer 长度固定造成的内存浪费问题,而且更安全地更改了 Buffer 的容量。此外 Netty 针对 ByteBuf 做了很多优化,例如缓存池化、减少数据拷贝的 **CompositeByteBuf** 等。 +在 **netty-buffer 模块中** Netty 自己实现了的一个更加完备的 **ByteBuf 工具类**,用于网络通信中的数据载体。由于人性化的 Buffer API 设计,它已经成为 Java ByteBuffer 的完美替代品。 ByteBuf 的动态性设计不仅解决了 ByteBuffer 长度固定造成的内存浪费问题,而且更安全地更改了 Buffer 的容量。此外 Netty 针对 ByteBuf 做了很多优化,例如缓存池化、减少数据拷贝的 **CompositeByteBuf** 等。 **netty-resover** 模块主要提供了一些有关 **基础设施** 的解析工具,包括 IP Address、 Hostname、 DNS 等。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25403\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25403\350\256\262.md" index 619257cec..0d8b700f1 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25403\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25403\350\256\262.md" @@ -2,7 +2,7 @@ 你好,我是若地。上节课我们介绍了 Netty 中核心组件的作用以及组件协作的方式方法。从这节课开始,我们将对 Netty 的每个核心组件依次进行深入剖析解读。我会结合相应的代码示例讲解,帮助你快速上手 Netty。 -我们在使用 Netty 编写网络应用程序的时候,一定会从 **引导器 Bootstrap** 开始入手。 Bootstrap 作为整个 Netty 客户端和服务端的 **程序入口** ,可以把 Netty 的核心组件像搭积木一样组装在一起。本节课我会从 Netty 的引导器 **Bootstrap** 出发,带你学习如何使用 Netty 进行最基本的程序开发。 +我们在使用 Netty 编写网络应用程序的时候,一定会从 **引导器 Bootstrap** 开始入手。 Bootstrap 作为整个 Netty 客户端和服务端的 **程序入口**,可以把 Netty 的核心组件像搭积木一样组装在一起。本节课我会从 Netty 的引导器 **Bootstrap** 出发,带你学习如何使用 Netty 进行最基本的程序开发。 ## 从一个简单的 HTTP 服务器开始 @@ -14,7 +14,7 @@ HTTP 服务器是我们平时最常用的工具之一。同传统 Web 容器 Tom 2. 从浏览器或者终端发起 HTTP 请求。 3. 成功得到服务端的响应结果。 -Netty 的模块化设计非常优雅,客户端或者服务端的启动方式基本是固定的。作为开发者来说,只要照葫芦画瓢即可轻松上手。大多数场景下,你只需要实现与业务逻辑相关的一系列 ChannelHandler,再加上 Netty 已经预置了 HTTP 相关的编解码器就可以快速完成服务端框架的搭建。所以,我们只需要两个类就可以完成一个最简单的 HTTP 服务器,它们分别为 **服务器启动类** 和 **业务逻辑处理类** ,结合完整的代码实现我将对它们分别进行讲解。 +Netty 的模块化设计非常优雅,客户端或者服务端的启动方式基本是固定的。作为开发者来说,只要照葫芦画瓢即可轻松上手。大多数场景下,你只需要实现与业务逻辑相关的一系列 ChannelHandler,再加上 Netty 已经预置了 HTTP 相关的编解码器就可以快速完成服务端框架的搭建。所以,我们只需要两个类就可以完成一个最简单的 HTTP 服务器,它们分别为 **服务器启动类** 和 **业务逻辑处理类**,结合完整的代码实现我将对它们分别进行讲解。 ### 服务端启动类 @@ -165,7 +165,7 @@ b.childHandler(new ChannelInitializer() { }) ``` -ServerBootstrap 的 childHandler() 方法需要注册一个 ChannelHandler。 **ChannelInitializer** 是实现了 ChannelHandler **接口的匿名类** ,通过实例化 ChannelInitializer 作为 ServerBootstrap 的参数。 +ServerBootstrap 的 childHandler() 方法需要注册一个 ChannelHandler。**ChannelInitializer** 是实现了 ChannelHandler **接口的匿名类**,通过实例化 ChannelInitializer 作为 ServerBootstrap 的参数。 Channel 初始化时都会绑定一个 Pipeline,它主要用于服务编排。 Pipeline 管理了多个 ChannelHandler。 I/O 事件依次在 ChannelHandler 中传播, ChannelHandler 负责业务逻辑处理。上述 HTTP 服务器示例中使用链式的方式加载了多个 ChannelHandler,包含 **HTTP 编解码处理器、HTTPContent 压缩处理器、HTTP 消息聚合处理器、自定义业务逻辑处理器** 。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25404\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25404\350\256\262.md" index d0660ff7f..4e0c23f9b 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25404\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25404\350\256\262.md" @@ -50,7 +50,7 @@ Netty 推荐使用主从多线程模型,这样就可以轻松达到成千上 ### EventLoop 是什么 -EventLoop 这个概念其实并不是 Netty 独有的,它是一种 **事件等待和处理的程序模型** ,可以解决多线程资源消耗高的问题。例如 Node.js 就采用了 EventLoop 的运行机制,不仅占用资源低,而且能够支撑了大规模的流量访问。 +EventLoop 这个概念其实并不是 Netty 独有的,它是一种 **事件等待和处理的程序模型**,可以解决多线程资源消耗高的问题。例如 Node.js 就采用了 EventLoop 的运行机制,不仅占用资源低,而且能够支撑了大规模的流量访问。 下图展示了 EventLoop 通用的运行模式。每当事件发生时,应用程序都会将产生的事件放入事件队列当中,然后 EventLoop 会轮询从队列中取出事件执行或者将事件分发给相应的事件监听者执行。事件执行的方式通常分为 **立即执行、延后执行、定期执行** 几种。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25405\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25405\350\256\262.md" index a856fa74c..5c378c97f 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25405\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25405\350\256\262.md" @@ -49,7 +49,7 @@ TailContext 只实现了 ChannelInboundHandler 接口。它会在 ChannelInbound ## ChannelHandler 接口设计 -在学习 ChannelPipeline 事件传播机制之前,我们需要了解 I/O 事件的生命周期。整个 ChannelHandler 是围绕 I/O 事件的生命周期所设计的,例如建立连接、读数据、写数据、连接销毁等。 ChannelHandler 有两个重要的 **子接口** : **ChannelInboundHandler** 和 **ChannelOutboundHandler** ,分别拦截 **入站和出站的各种 I/O 事件** 。 +在学习 ChannelPipeline 事件传播机制之前,我们需要了解 I/O 事件的生命周期。整个 ChannelHandler 是围绕 I/O 事件的生命周期所设计的,例如建立连接、读数据、写数据、连接销毁等。 ChannelHandler 有两个重要的 **子接口** : **ChannelInboundHandler** 和 **ChannelOutboundHandler**,分别拦截 **入站和出站的各种 I/O 事件** 。 ### 1. ChannelInboundHandler 的事件回调方法与触发时机 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25406\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25406\350\256\262.md" index 571e2255f..8cdae4e53 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25406\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25406\350\256\262.md" @@ -10,7 +10,7 @@ TCP 传输协议是面向流的,没有数据包界限。客户端向服务端 ### MTU 最大传输单元和 MSS 最大分段大小 -**MTU(Maxitum Transmission Unit)** 是链路层一次最大传输数据的大小。 MTU 一般来说大小为 1500 byte。 **MSS(Maximum Segement Size)** 是指 TCP 最大报文段长度,它是传输层一次发送最大数据的大小。如下图所示, MTU 和 MSS 一般的计算关系为: MSS = MTU - IP 首部 - TCP 首部,如果 MSS + TCP 首部 + IP 首部 > MTU,那么数据包将会被拆分为多个发送。这就是拆包现象。 +**MTU(Maxitum Transmission Unit)** 是链路层一次最大传输数据的大小。 MTU 一般来说大小为 1500 byte。**MSS(Maximum Segement Size)** 是指 TCP 最大报文段长度,它是传输层一次发送最大数据的大小。如下图所示, MTU 和 MSS 一般的计算关系为: MSS = MTU - IP 首部 - TCP 首部,如果 MSS + TCP 首部 + IP 首部 > MTU,那么数据包将会被拆分为多个发送。这就是拆包现象。 ![Drawing 1.png](assets/CgqCHl-iZjqAVNpwAAC-5hm9AJA479.png) @@ -22,7 +22,7 @@ TCP 传输协议是面向流的,没有数据包界限。客户端向服务端 ### Nagle 算法 -**Nagle 算法** 于 1984 年被福特航空和通信公司定义为 TCP/IP 拥塞控制方法。它主要用于解决频繁发送小数据包而带来的网络拥塞问题。试想如果每次需要发送的数据只有 1 字节,加上 20 个字节 IP Header 和 20 个字节 TCP Header,每次发送的数据包大小为 41 字节,但是只有 1 字节是有效信息,这就造成了非常大的浪费。 Nagle 算法可以理解为 **批量发送** ,也是我们平时编程中经常用到的优化思路,它是在数据未得到确认之前先写入缓冲区,等待数据确认或者缓冲区积攒到一定大小再把数据包发送出去。 +**Nagle 算法** 于 1984 年被福特航空和通信公司定义为 TCP/IP 拥塞控制方法。它主要用于解决频繁发送小数据包而带来的网络拥塞问题。试想如果每次需要发送的数据只有 1 字节,加上 20 个字节 IP Header 和 20 个字节 TCP Header,每次发送的数据包大小为 41 字节,但是只有 1 字节是有效信息,这就造成了非常大的浪费。 Nagle 算法可以理解为 **批量发送**,也是我们平时编程中经常用到的优化思路,它是在数据未得到确认之前先写入缓冲区,等待数据确认或者缓冲区积攒到一定大小再把数据包发送出去。 Linux 在默认情况下是开启 Nagle 算法的,在大量小数据包的场景下可以有效地降低网络开销。但如果你的业务场景每次发送的数据都需要获得及时响应,那么 Nagle 算法就不能满足你的需求了,因为 Nagle 算法会有一定的数据延迟。你可以通过 Linux 提供的 TCP_NODELAY 参数禁用 Nagle 算法。 Netty 中为了使数据传输延迟最小化,就默认禁用了 Nagle 算法,这一点与 Linux 操作系统的默认行为是相反的。 @@ -62,7 +62,7 @@ Linux 在默认情况下是开启 Nagle 算法的,在大量小数据包的场 ### 特定分隔符 -既然接收方无法区分消息的边界,那么我们可以在每次发送报文的尾部加上 **特定分隔符** ,接收方就可以根据特殊分隔符进行消息拆分。以下报文根据特定分隔符 \\n 按行解析,即可得到 AB、 CDEF、 GHIJ、 K、 LM 五条原始报文。 +既然接收方无法区分消息的边界,那么我们可以在每次发送报文的尾部加上 **特定分隔符**,接收方就可以根据特殊分隔符进行消息拆分。以下报文根据特定分隔符 \\n 按行解析,即可得到 AB、 CDEF、 GHIJ、 K、 LM 五条原始报文。 ```bash +-------------------------+ diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25407\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25407\350\256\262.md" index 0ae0837ca..3438dd11f 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25407\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25407\350\256\262.md" @@ -6,11 +6,11 @@ 所谓协议,就是通信双方事先商量好的接口暗语,在 TCP 网络编程中,发送方和接收方的数据包格式都是二进制,发送方将对象转化成二进制流发送给接收方,接收方获得二进制数据后需要知道如何解析成对象,所以协议是 **双方能够正常通信的基础** 。 -目前市面上已经有不少通用的协议,例如 HTTP、 HTTPS、 JSON-RPC、 FTP、 IMAP、 Protobuf 等。 **通用协议** 兼容性好,易于维护,各种异构系统之间可以实现无缝对接。如果在满足业务场景以及性能需求的前提下,推荐采用通用协议的方案。相比通用协议,自定义协议主要有以下优点。 +目前市面上已经有不少通用的协议,例如 HTTP、 HTTPS、 JSON-RPC、 FTP、 IMAP、 Protobuf 等。**通用协议** 兼容性好,易于维护,各种异构系统之间可以实现无缝对接。如果在满足业务场景以及性能需求的前提下,推荐采用通用协议的方案。相比通用协议,自定义协议主要有以下优点。 - **极致性能** :通用的通信协议考虑了很多兼容性的因素,必然在性能方面有所损失。 - **扩展性** :自定义的协议相比通用协议更好扩展,可以更好地满足自己的业务需求。 -- **安全性** :通用协议是公开的,很多漏洞已经很多被黑客攻破。自定义协议更加 **安全** ,因为黑客需要先破解你的协议内容。 +- **安全性** :通用协议是公开的,很多漏洞已经很多被黑客攻破。自定义协议更加 **安全**,因为黑客需要先破解你的协议内容。 那么如何设计自定义的通信协议呢?这个答案见仁见智,但是设计通信协议有经验方法可循。结合实战经验我们一起看下一个完备的网络协议需要具备哪些基本要素。 @@ -36,7 +36,7 @@ ### 6. 请求数据 -请求数据通常为序列化之后得到的 **二进制流** ,每种请求数据的内容是不一样的。 +请求数据通常为序列化之后得到的 **二进制流**,每种请求数据的内容是不一样的。 ### 7. 状态 @@ -62,7 +62,7 @@ 在学习完如何设计协议之后,我们又该如何在 Netty 中实现自定义的通信协议呢?其实 Netty 作为一个非常优秀的网络通信框架,已经为我们提供了非常丰富的编解码抽象基类,帮助我们更方便地基于这些抽象基类扩展实现自定义协议。 -首先我们看下 Netty 中编解码器是如何分类的。 **Netty 常用编码器类型:** +首先我们看下 Netty 中编解码器是如何分类的。**Netty 常用编码器类型:** - MessageToByteEncoder 对象编码成字节流; - MessageToMessageEncoder 一种消息类型编码成另外一种消息类型。 @@ -72,7 +72,7 @@ - ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象; - MessageToMessageDecoder 将一种消息类型解码为另外一种消息类型。 -编解码器可以分为 **一次解码器** 和 **二次解码器** ,一次解码器用于解决 TCP 拆包 / 粘包问题,按协议解析后得到的字节数据。如果你需要对解析后的字节数据做对象模型的转换,这时候便需要用到二次解码器,同理编码器的过程是反过来的。 +编解码器可以分为 **一次解码器** 和 **二次解码器**,一次解码器用于解决 TCP 拆包 / 粘包问题,按协议解析后得到的字节数据。如果你需要对解析后的字节数据做对象模型的转换,这时候便需要用到二次解码器,同理编码器的过程是反过来的。 - 一次编解码器: MessageToByteEncoder/ByteToMessageDecoder。 - 二次编解码器: MessageToMessageEncoder/MessageToMessageDecoder。 @@ -145,7 +145,7 @@ public class StringToByteEncoder extends MessageToByteEncoder { #### MessageToMessageEncoder -MessageToMessageEncoder 与 MessageToByteEncoder 类似,同样只需要实现 encode 方法。与 MessageToByteEncoder 不同的是, MessageToMessageEncoder 是将一种格式的消息转换为另外一种格式的消息。其中第二个 Message 所指的可以是任意一个对象,如果该对象是 ByteBuf 类型,那么基本上和 MessageToByteEncoder 的实现原理是一致的。此外 MessageToByteEncoder 的输出结果是对象列表,编码后的结果属于 **中间对象** ,最终仍然会转化成 ByteBuf 进行传输。 +MessageToMessageEncoder 与 MessageToByteEncoder 类似,同样只需要实现 encode 方法。与 MessageToByteEncoder 不同的是, MessageToMessageEncoder 是将一种格式的消息转换为另外一种格式的消息。其中第二个 Message 所指的可以是任意一个对象,如果该对象是 ByteBuf 类型,那么基本上和 MessageToByteEncoder 的实现原理是一致的。此外 MessageToByteEncoder 的输出结果是对象列表,编码后的结果属于 **中间对象**,最终仍然会转化成 ByteBuf 进行传输。 MessageToMessageEncoder 常用的 **实现子类** 有 StringEncoder、 LineEncoder、 Base64Encoder 等。以 StringEncoder 为例看下 MessageToMessageEncoder 的用法。源码示例如下:将 CharSequence 类型( String、 StringBuilder、 StringBuffer 等)转换成 ByteBuf 类型,结合 StringDecoder 可以直接实现 String 类型数据的编解码。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25409\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25409\350\256\262.md" index 0bfb73b60..c09165186 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25409\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25409\350\256\262.md" @@ -97,7 +97,7 @@ public final ChannelFuture writeAndFlush(Object msg) { } ``` -继续跟进 tail.writeAndFlush 的源码,最终会定位到 AbstractChannelHandlerContext 中的 write 方法。该方法是 writeAndFlush 的 **核心逻辑** ,具体见以下源码。 +继续跟进 tail.writeAndFlush 的源码,最终会定位到 AbstractChannelHandlerContext 中的 write 方法。该方法是 writeAndFlush 的 **核心逻辑**,具体见以下源码。 ```java private void write(Object msg, boolean flush, ChannelPromise promise) { diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25410\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25410\350\256\262.md" index 6074e11b7..1eb175b0e 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25410\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25410\350\256\262.md" @@ -4,7 +4,7 @@ ## 为什么需要堆外内存 -在 Java 中对象都是在堆内分配的,通常我们说的 **JVM 内存** 也就指的 **堆内内存** , **堆内内存** 完全被 **JVM 虚拟机** 所管理, JVM 有自己的垃圾回收算法,对于使用者来说不必关心对象的内存如何回收。 **堆外内存** 与堆内内存相对应,对于整个机器内存而言,除 **堆内内存以外部分即为堆外内存** ,如下图所示。堆外内存不受 JVM 虚拟机管理,直接由操作系统管理。 +在 Java 中对象都是在堆内分配的,通常我们说的 **JVM 内存** 也就指的 **堆内内存**,**堆内内存** 完全被 **JVM 虚拟机** 所管理, JVM 有自己的垃圾回收算法,对于使用者来说不必关心对象的内存如何回收。**堆外内存** 与堆内内存相对应,对于整个机器内存而言,除 **堆内内存以外部分即为堆外内存**,如下图所示。堆外内存不受 JVM 虚拟机管理,直接由操作系统管理。 ![图片1.png](assets/CgqCHl-06zuAdxB_AAKnPyI9NhA898.png){style="max-width:600px;width:100%"} @@ -61,7 +61,7 @@ DirectByteBuffer(int cap) { 从 DirectByteBuffer 的构造函数中可以看出,真正分配堆外内存的逻辑还是通过 unsafe.allocateMemory(size),接下来我们一起认识下 Unsafe 这个神秘的工具类。 -Unsafe 是一个非常不安全的类,它用于执行内存访问、分配、修改等 **敏感操作** ,可以越过 JVM 限制的枷锁。 Unsafe 最初并不是为开发者设计的,使用它时虽然可以获取对底层资源的控制权,但也失去了安全性的保证,所以使用 Unsafe 一定要慎重。 Netty 中依赖了 Unsafe 工具类,是因为 Netty 需要与底层 Socket 进行交互, Unsafe 在提升 Netty 的性能方面起到了一定的帮助。 +Unsafe 是一个非常不安全的类,它用于执行内存访问、分配、修改等 **敏感操作**,可以越过 JVM 限制的枷锁。 Unsafe 最初并不是为开发者设计的,使用它时虽然可以获取对底层资源的控制权,但也失去了安全性的保证,所以使用 Unsafe 一定要慎重。 Netty 中依赖了 Unsafe 工具类,是因为 Netty 需要与底层 Socket 进行交互, Unsafe 在提升 Netty 的性能方面起到了一定的帮助。 在 Java 中是不能直接使用 Unsafe 的,但是我们可以通过反射获取 Unsafe 实例,使用方式如下所示。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25411\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25411\350\256\262.md" index cd0ba0799..ccca9bd2e 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25411\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25411\350\256\262.md" @@ -37,15 +37,15 @@ ByteBuffer 作为网络通信中高频使用的数据载体,显然不能够满 ![Netty11 ( 2 ).png](assets/CgqCHl-3uraAAhvwAASZGuNRMtA960.png) -从图中可以看出, ByteBuf 包含三个指针: **读指针 readerIndex** 、 **写指针 writeIndex** 、 **最大容量 maxCapacity** ,根据指针的位置又可以将 ByteBuf 内部结构可以分为四个部分: +从图中可以看出, ByteBuf 包含三个指针: **读指针 readerIndex** 、 **写指针 writeIndex** 、 **最大容量 maxCapacity**,根据指针的位置又可以将 ByteBuf 内部结构可以分为四个部分: -第一部分是 **废弃字节** ,表示已经丢弃的无效字节数据。 +第一部分是 **废弃字节**,表示已经丢弃的无效字节数据。 -第二部分是 **可读字节** ,表示 ByteBuf 中可以被读取的字节内容,可以通过 writeIndex - readerIndex 计算得出。从 ByteBuf 读取 N 个字节, readerIndex 就会自增 N, readerIndex 不会大于 writeIndex,当 readerIndex == writeIndex 时,表示 ByteBuf 已经不可读。 +第二部分是 **可读字节**,表示 ByteBuf 中可以被读取的字节内容,可以通过 writeIndex - readerIndex 计算得出。从 ByteBuf 读取 N 个字节, readerIndex 就会自增 N, readerIndex 不会大于 writeIndex,当 readerIndex == writeIndex 时,表示 ByteBuf 已经不可读。 -第三部分是 **可写字节** ,向 ByteBuf 中写入数据都会存储到可写字节区域。向 ByteBuf 写入 N 字节数据, writeIndex 就会自增 N,当 writeIndex 超过 capacity,表示 ByteBuf 容量不足,需要扩容。 +第三部分是 **可写字节**,向 ByteBuf 中写入数据都会存储到可写字节区域。向 ByteBuf 写入 N 字节数据, writeIndex 就会自增 N,当 writeIndex 超过 capacity,表示 ByteBuf 容量不足,需要扩容。 -第四部分是 **可扩容字节** ,表示 ByteBuf 最多还可以扩容多少字节,当 writeIndex 超过 capacity 时,会触发 ByteBuf 扩容,最多扩容到 maxCapacity 为止,超过 maxCapacity 再写入就会出错。 +第四部分是 **可扩容字节**,表示 ByteBuf 最多还可以扩容多少字节,当 writeIndex 超过 capacity 时,会触发 ByteBuf 扩容,最多扩容到 maxCapacity 为止,超过 maxCapacity 再写入就会出错。 由此可见, Netty 重新设计的 ByteBuf 有效地区分了可读、可写以及可扩容数据,解决了 ByteBuffer 无法扩容以及读写模式切换烦琐的缺陷。接下来,我们一起学习下 ByteBuf 的核心 API,你可以把它当作 ByteBuffer 的替代品单独使用。 @@ -68,7 +68,7 @@ assert buffer.refCnt() == 0; ## ByteBuf 分类 -ByteBuf 有多种实现类,每种都有不同的特性,下图是 ByteBuf 的家族图谱,可以划分为三个不同的维度: **Heap/Direct** 、 **Pooled/Unpooled** 和 **Unsafe/非 Unsafe** ,我逐一介绍这三个维度的不同特性。 +ByteBuf 有多种实现类,每种都有不同的特性,下图是 ByteBuf 的家族图谱,可以划分为三个不同的维度: **Heap/Direct** 、 **Pooled/Unpooled** 和 **Unsafe/非 Unsafe**,我逐一介绍这三个维度的不同特性。 ![image](assets/Ciqc1F-3h3WAMF4CAAe4IOav4SA876.png) diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25412\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25412\350\256\262.md" index 740125ce2..bc2476ce2 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25412\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25412\350\256\262.md" @@ -51,11 +51,11 @@ DMA 是从一整块内存中按需分配,对于分配出的内存会记录元 ![图片3.png](assets/Ciqc1F--Oq2AL3z_AALiP5oG4Kg709.png) -**第二种是循环首次适应算法(next fit)** ,该算法是由首次适应算法的变种,循环首次适应算法不再是每次从链表的开始进行查找,而是从上次找到的空闲分区的下⼀个空闲分区开始查找。如下图所示,P1 请求在内存块 A 完成分配,然后再为 P2 分配内存时,是直接继续向下寻找可用分区,最终在 B 内存块中完成分配。该算法相比⾸次适应算法空闲分区的分布更加均匀,而且查找的效率有所提升,但是正因为如此会造成空闲分区链中大的空闲分区会越来越少。 +**第二种是循环首次适应算法(next fit)**,该算法是由首次适应算法的变种,循环首次适应算法不再是每次从链表的开始进行查找,而是从上次找到的空闲分区的下⼀个空闲分区开始查找。如下图所示,P1 请求在内存块 A 完成分配,然后再为 P2 分配内存时,是直接继续向下寻找可用分区,最终在 B 内存块中完成分配。该算法相比⾸次适应算法空闲分区的分布更加均匀,而且查找的效率有所提升,但是正因为如此会造成空闲分区链中大的空闲分区会越来越少。 ![图片4.png](assets/Ciqc1F--OrmAQbSMAALYfPT8Bo0517.png) -**第三种是最佳适应算法(best fit)** ,空闲分区链以空闲分区大小递增的顺序将空闲分区以双向链表的形式连接在一起,每次从空闲分区链的开头进行查找,这样第一个满足分配条件的空间分区就是最优解。如下图所示,在 A 内存块分配完 P1 请求后,空闲分区链重新按分区大小进行排序,再为 P2 请求查找满足条件的空闲分区。该算法的空间利用率更高,但同样也会留下很多较难利用的小空闲分区,由于每次分配完需要重新排序,所以会有造成性能损耗。 +**第三种是最佳适应算法(best fit)**,空闲分区链以空闲分区大小递增的顺序将空闲分区以双向链表的形式连接在一起,每次从空闲分区链的开头进行查找,这样第一个满足分配条件的空间分区就是最优解。如下图所示,在 A 内存块分配完 P1 请求后,空闲分区链重新按分区大小进行排序,再为 P2 请求查找满足条件的空闲分区。该算法的空间利用率更高,但同样也会留下很多较难利用的小空闲分区,由于每次分配完需要重新排序,所以会有造成性能损耗。 ![图片5.png](assets/Ciqc1F--OsOAIwduAAKS6MtXII4882.png) @@ -90,7 +90,7 @@ Linux 内核使用的就是 Slab 算法,因为内核需要频繁地分配小 在 Slab 算法中维护着大小不同的 Slab 集合,在最顶层是 cache_chain,cache_chain 中维护着一组 kmem_cache 引用,kmem_cache 负责管理一块固定大小的对象池。通常会提前分配一块内存,然后将这块内存划分为大小相同的 slot,不会对内存块再进行合并,同时使用位图 bitmap 记录每个 slot 的使用情况。 -kmem_cache 中包含三个 Slab 链表: **完全分配使用 slab_full** 、 **部分分配使用 slab_partial** 和 **完全空闲 slabs_empty** ,这三个链表负责内存的分配和释放。每个链表中维护的 Slab 都是一个或多个连续 Page,每个 Slab 被分配多个对象进行存储。Slab 算法是基于对象进行内存管理的,它把相同类型的对象分为一类。当分配内存时,从 Slab 链表中划分相应的内存单元;当释放内存时,Slab 算法并不会丢弃已经分配的对象,而是将它保存在缓存中,当下次再为对象分配内存时,直接会使用最近释放的内存块。 +kmem_cache 中包含三个 Slab 链表: **完全分配使用 slab_full** 、 **部分分配使用 slab_partial** 和 **完全空闲 slabs_empty**,这三个链表负责内存的分配和释放。每个链表中维护的 Slab 都是一个或多个连续 Page,每个 Slab 被分配多个对象进行存储。Slab 算法是基于对象进行内存管理的,它把相同类型的对象分为一类。当分配内存时,从 Slab 链表中划分相应的内存单元;当释放内存时,Slab 算法并不会丢弃已经分配的对象,而是将它保存在缓存中,当下次再为对象分配内存时,直接会使用最近释放的内存块。 单个 Slab 可以在不同的链表之间移动,例如当一个 Slab 被分配完,就会从 slab_partial 移动到 slabs_full,当一个 Slab 中有对象被释放后,就会从 slab_full 再次回到 slab_partial,所有对象都被释放完的话,就会从 slab_partial 移动到 slab_empty。 @@ -104,17 +104,17 @@ kmem_cache 中包含三个 Slab 链表: **完全分配使用 slab_full** 、 * 上图中涉及 jemalloc 的几个核心概念,例如 arena、bin、chunk、run、region、tcache 等,我们下面逐一进行介绍。 -**arena 是 jemalloc 最重要的部分** ,内存由一定数量的 arenas 负责管理。每个用户线程都会被绑定到一个 arena 上,线程采用 round-robin 轮询的方式选择可用的 arena 进行内存分配,为了减少线程之间的锁竞争,默认每个 CPU 会分配 4 个 arena。 +**arena 是 jemalloc 最重要的部分**,内存由一定数量的 arenas 负责管理。每个用户线程都会被绑定到一个 arena 上,线程采用 round-robin 轮询的方式选择可用的 arena 进行内存分配,为了减少线程之间的锁竞争,默认每个 CPU 会分配 4 个 arena。 -**bin 用于管理不同档位的内存单元** ,每个 bin 管理的内存大小是按分类依次递增。因为 jemalloc 中小内存的分配是基于 Slab 算法完成的,所以会产生不同类别的内存块。 +**bin 用于管理不同档位的内存单元**,每个 bin 管理的内存大小是按分类依次递增。因为 jemalloc 中小内存的分配是基于 Slab 算法完成的,所以会产生不同类别的内存块。 -**chunk 是负责管理用户内存块的数据结构** ,chunk 以 Page 为单位管理内存,默认大小是 4M,即 1024 个连续 Page。每个 chunk 可被用于多次小内存的申请,但是在大内存分配的场景下只能分配一次。 +**chunk 是负责管理用户内存块的数据结构**,chunk 以 Page 为单位管理内存,默认大小是 4M,即 1024 个连续 Page。每个 chunk 可被用于多次小内存的申请,但是在大内存分配的场景下只能分配一次。 -**run 实际上是 chunk 中的一块内存区域** ,每个 bin 管理相同类型的 run,最终通过操作 run 完成内存分配。run 结构具体的大小由不同的 bin 决定,例如 8 字节的 bin 对应的 run 只有一个 Page,可以从中选取 8 字节的块进行分配。 +**run 实际上是 chunk 中的一块内存区域**,每个 bin 管理相同类型的 run,最终通过操作 run 完成内存分配。run 结构具体的大小由不同的 bin 决定,例如 8 字节的 bin 对应的 run 只有一个 Page,可以从中选取 8 字节的块进行分配。 -**region 是每个 run 中的对应的若干个小内存块** ,每个 run 会将划分为若干个等长的 region,每次内存分配也是按照 region 进行分发。 +**region 是每个 run 中的对应的若干个小内存块**,每个 run 会将划分为若干个等长的 region,每次内存分配也是按照 region 进行分发。 -**tcache 是每个线程私有的缓存** ,用于 small 和 large 场景下的内存分配,每个 tcahe 会对应一个 arena,tcache 本身也会有一个 bin 数组,称为tbin。与 arena 中 bin 不同的是,它不会有 run 的概念。tcache 每次从 arena 申请一批内存,在分配内存时首先在 tcache 查找,从而避免锁竞争,如果分配失败才会通过 run 执行内存分配。 +**tcache 是每个线程私有的缓存**,用于 small 和 large 场景下的内存分配,每个 tcahe 会对应一个 arena,tcache 本身也会有一个 bin 数组,称为tbin。与 arena 中 bin 不同的是,它不会有 run 的概念。tcache 每次从 arena 申请一批内存,在分配内存时首先在 tcache 查找,从而避免锁竞争,如果分配失败才会通过 run 执行内存分配。 jemalloc 的几个核心的概念介绍完了,我们再重新梳理下它们之间的关系: diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25415\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25415\350\256\262.md" index 55a1e2093..dcdf8c759 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25415\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25415\350\256\262.md" @@ -328,7 +328,7 @@ private void pushLater(DefaultHandle item, Thread thread) { } ``` -pushLater 的实现过程可以总结为两个步骤: **获取 WeakOrderQueue** , **添加对象到 WeakOrderQueue 中** 。 +pushLater 的实现过程可以总结为两个步骤: **获取 WeakOrderQueue**,**添加对象到 WeakOrderQueue 中** 。 - 首先看下如何获取 **WeakOrderQueue** 对象。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25416\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25416\350\256\262.md" index 632535b1e..a1a84bd98 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25416\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25416\350\256\262.md" @@ -10,7 +10,7 @@ ![Drawing 0.png](assets/Ciqc1F_Qbz2AD4uMAARnlgeSFc4993.png){style="width:100%;max-width:800px"} -从上图中可以看出,从数据读取到发送一共经历了 **四次数据拷贝** ,具体流程如下: +从上图中可以看出,从数据读取到发送一共经历了 **四次数据拷贝**,具体流程如下: 1. 当用户进程发起 **read()** 调用后,上下文从用户态切换至内核态。**DMA** 引擎从文件中读取数据,并存储到内核态缓冲区,这里是 **第一次数据拷贝**。 2. 请求的数据从内核态缓冲区拷贝到用户态缓冲区,然后返回给用户进程。第二次数据拷贝的过程同时,会导致上下文从内核态再次切换到用户态。 @@ -81,7 +81,7 @@ Netty 在进行 I/O 操作时都是使用的堆外内存,可以避免数据从 ### CompositeByteBuf -CompositeByteBuf 是 Netty 中实现零拷贝机制非常重要的一个数据结构,CompositeByteBuf 可以理解为一个虚拟的 Buffer 对象,它是由多个 ByteBuf 组合而成,但是在 CompositeByteBuf 内部保存着每个 ByteBuf 的引用关系,从逻辑上构成一个整体。比较常见的像 HTTP 协议数据可以分为 **头部信息 header** 和 **消息体数据 body** ,分别存在两个不同的 ByteBuf 中,通常我们需要将两个 ByteBuf 合并成一个完整的协议数据进行发送,可以使用如下方式完成: +CompositeByteBuf 是 Netty 中实现零拷贝机制非常重要的一个数据结构,CompositeByteBuf 可以理解为一个虚拟的 Buffer 对象,它是由多个 ByteBuf 组合而成,但是在 CompositeByteBuf 内部保存着每个 ByteBuf 的引用关系,从逻辑上构成一个整体。比较常见的像 HTTP 协议数据可以分为 **头部信息 header** 和 **消息体数据 body**,分别存在两个不同的 ByteBuf 中,通常我们需要将两个 ByteBuf 合并成一个完整的协议数据进行发送,可以使用如下方式完成: ```java ByteBuf httpBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes()); diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25421\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25421\350\256\262.md" index 4543b398b..6b2277c55 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25421\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25421\350\256\262.md" @@ -242,12 +242,12 @@ public HashedWheelTimer( HashedWheelTimer 的构造函数清晰地列举出了几个核心属性: -- **threadFactory** ,线程池,但是只创建了一个线程; -- **tickDuration** ,时针每次 tick 的时间,相当于时针间隔多久走到下一个 slot; -- **unit** ,表示 tickDuration 的时间单位; -- **ticksPerWheel** ,时间轮上一共有多少个 slot,默认 512 个。分配的 slot 越多,占用的内存空间就越大; -- **leakDetection** ,是否开启内存泄漏检测; -- **maxPendingTimeouts** ,最大允许等待任务数。 +- **threadFactory**,线程池,但是只创建了一个线程; +- **tickDuration**,时针每次 tick 的时间,相当于时针间隔多久走到下一个 slot; +- **unit**,表示 tickDuration 的时间单位; +- **ticksPerWheel**,时间轮上一共有多少个 slot,默认 512 个。分配的 slot 越多,占用的内存空间就越大; +- **leakDetection**,是否开启内存泄漏检测; +- **maxPendingTimeouts**,最大允许等待任务数。 下面我们看下 HashedWheelTimer 是如何创建出来的,我们直接跟进 createWheel() 方法的源码: @@ -553,9 +553,9 @@ public Set stop() { 到此为止,HashedWheelTimer 的实现原理我们已经分析完了。再来回顾一下 HashedWheelTimer 的几个核心成员。 -- **HashedWheelTimeout** ,任务的封装类,包含任务的到期时间 deadline、需要经历的圈数 remainingRounds 等属性。 -- **HashedWheelBucket** ,相当于时间轮的每个 slot,内部采用双向链表保存了当前需要执行的 HashedWheelTimeout 列表。 -- **Worker** ,HashedWheelTimer 的核心工作引擎,负责处理定时任务。 +- **HashedWheelTimeout**,任务的封装类,包含任务的到期时间 deadline、需要经历的圈数 remainingRounds 等属性。 +- **HashedWheelBucket**,相当于时间轮的每个 slot,内部采用双向链表保存了当前需要执行的 HashedWheelTimeout 列表。 +- **Worker**,HashedWheelTimer 的核心工作引擎,负责处理定时任务。 ## 时间轮进阶应用 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25422\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25422\350\256\262.md" index 09032142a..1cb84af48 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25422\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25422\350\256\262.md" @@ -42,8 +42,8 @@ 说完阻塞队列,我们再来看下非阻塞队列。非阻塞队列不需要通过加锁的方式对线程阻塞,并发性能更好。JDK 中常用的非阻塞队列有以下几种: -- **ConcurrentLinkedQueue** ,它是一个采用双向链表实现的无界并发非阻塞队列,它属于 LinkedQueue 的安全版本。ConcurrentLinkedQueue 内部采用 CAS 操作保证线程安全,这是非阻塞队列实现的基础,相比 ArrayBlockingQueue、LinkedBlockingQueue 具备较高的性能。 -- **ConcurrentLinkedDeque** ,也是一种采用双向链表结构的无界并发非阻塞队列。与 ConcurrentLinkedQueue 不同的是,ConcurrentLinkedDeque 属于双端队列,它同时支持 FIFO 和 FILO 两种模式,可以从队列的头部插入和删除数据,也可以从队列尾部插入和删除数据,适用于多生产者和多消费者的场景。 +- **ConcurrentLinkedQueue**,它是一个采用双向链表实现的无界并发非阻塞队列,它属于 LinkedQueue 的安全版本。ConcurrentLinkedQueue 内部采用 CAS 操作保证线程安全,这是非阻塞队列实现的基础,相比 ArrayBlockingQueue、LinkedBlockingQueue 具备较高的性能。 +- **ConcurrentLinkedDeque**,也是一种采用双向链表结构的无界并发非阻塞队列。与 ConcurrentLinkedQueue 不同的是,ConcurrentLinkedDeque 属于双端队列,它同时支持 FIFO 和 FILO 两种模式,可以从队列的头部插入和删除数据,也可以从队列尾部插入和删除数据,适用于多生产者和多消费者的场景。 至此,常见的队列类型我们已经介绍完了。我们在平时开发中使用频率最高的是 BlockingQueue。实现一个阻塞队列需要具备哪些基本功能呢?下面看 BlockingQueue 的接口,如下图所示。 diff --git "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25428\350\256\262.md" "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25428\350\256\262.md" index 3c0190663..fba4268a3 100644 --- "a/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25428\350\256\262.md" +++ "b/docs/Network/Netty \346\240\270\345\277\203\345\216\237\347\220\206\345\211\226\346\236\220\344\270\216 RPC \345\256\236\350\267\265/\347\254\25428\350\256\262.md" @@ -18,7 +18,7 @@ Netty 提供了 ServerBootstrap 引导类作为程序启动入口,ServerBootst ### ByteBuf -ByteBuf 是必须要掌握的核心工具类,并且能够理解 ByteBuf 的内部构造。ByteBuf 包含三个指针: **读指针 readerIndex** 、 **写指针 writeIndex** 、 **最大容量 maxCapacity** ,根据指针的位置又可以将 ByteBuf 内部结构可以分为四个部分:废弃字节、可读字节、可写字节和可扩容字节。如下图所示。 +ByteBuf 是必须要掌握的核心工具类,并且能够理解 ByteBuf 的内部构造。ByteBuf 包含三个指针: **读指针 readerIndex** 、 **写指针 writeIndex** 、 **最大容量 maxCapacity**,根据指针的位置又可以将 ByteBuf 内部结构可以分为四个部分:废弃字节、可读字节、可写字节和可扩容字节。如下图所示。 ![Lark20210120-170502.png](assets/Ciqc1GAH8qKAQ69-AAL5JCNOFek288.png){style="width:100%;max-width:800px"} diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25400\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25400\350\256\262.md" index 69bf4bc09..c4a523809 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25400\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25400\350\256\262.md" @@ -12,9 +12,9 @@ 至于语言方面,咱常年出入各大技术论坛,什么场子没趟过。一个两天的线下培训,咱都能扛过来。每篇10分钟,总共36篇,那不才是6个小时嘛,肯定没问题。 -但是,写了之后我发现, **自己会是一回事儿,能讲给别人是另一回事儿** ,而能讲给“看不见的陌生人”听,是这世上最难的事儿。 +但是,写了之后我发现,**自己会是一回事儿,能讲给别人是另一回事儿**,而能讲给“看不见的陌生人”听,是这世上最难的事儿。 -我知道,很多技术人都有这样一个“毛病”,就是觉得掌握技术本身是最重要的,其他什么产品、市场、销售,都没技术含量。这种思维导致很多技术比较牛的人会以自我为中心,仅站在自己的角度思考问题。所以, **常常是自己讲得很爽,完全不管听的人是不是真的接受了** 。写专栏的时候,这绝对是个大忌。 +我知道,很多技术人都有这样一个“毛病”,就是觉得掌握技术本身是最重要的,其他什么产品、市场、销售,都没技术含量。这种思维导致很多技术比较牛的人会以自我为中心,仅站在自己的角度思考问题。所以,**常常是自己讲得很爽,完全不管听的人是不是真的接受了** 。写专栏的时候,这绝对是个大忌。 除此之外,这种思维对职业发展的影响也是很大的。单打独斗,一个人搞定一个软件的时代已经过去了。学会和别人合作,才是现代社会的生存法则,而良好的合作源于沟通。 @@ -24,13 +24,13 @@ 这个抽象的“你”,看起来只有一个,其实却是看不到、摸不着的许许多多的人。所以,这个其实是最难的。协议专栏上线10天,就有10000多人订阅,而订阅专栏的用户里,只有少数人会留言。所以,对于很多读者的真实情况,我都无从得知,你可能每天都听但是没有留言的习惯,也可能买了之后觉得我讲得不好,骂一句“这钱白花了”,然后再也不听。 -所以, **如何把控内容,写给广大未知受众,是我写这个专栏面临的最大挑战** 。而这里面,文章的深度、广度,音频的语调、语气,每一个细节都非常重要。 +所以,**如何把控内容,写给广大未知受众,是我写这个专栏面临的最大挑战** 。而这里面,文章的深度、广度,音频的语调、语气,每一个细节都非常重要。 ## 专栏文章是怎么写的? 经过大纲和前几篇文稿的打磨,我对“极客时间”和专栏创作也有了更深的了解。我私下和很多人交流过一个问题,那就是,咱们平时聊一个话题的时候,有很多话可以说。但是真正去写一篇文章的时候,好像又没有什么可讲的,尤其是那些看起来很基础的内容。 -我在写专栏的过程中,仔细思考过这样一个问题:很多人对某一领域或者行业研究得很深入,也有自己长期的实践,但是 **有多少人可以从感性认识上升到理性认知的高度呢?** 现在技术变化这么快,我们每个人的精力都是有限的,不少人学习新知识的方式就是看看书,看看博客、技术文章,或者听同事讲一下,了解个大概就觉得可以直接上手去做了。我也是这样的。可是一旦到写专栏的时候, **基础掌握不扎实的问题一下子全都“暴露”出来了。** 落到文字上的东西一定要是严谨的。所以,在写到很多细节的时候,我查了大量的资料,找到权威的书籍、官方文档、RFC里面的具体描述,有时候我甚至要做个实验,或者打开代码再看一下,才放心下笔。 +我在写专栏的过程中,仔细思考过这样一个问题:很多人对某一领域或者行业研究得很深入,也有自己长期的实践,但是 **有多少人可以从感性认识上升到理性认知的高度呢?** 现在技术变化这么快,我们每个人的精力都是有限的,不少人学习新知识的方式就是看看书,看看博客、技术文章,或者听同事讲一下,了解个大概就觉得可以直接上手去做了。我也是这样的。可是一旦到写专栏的时候,**基础掌握不扎实的问题一下子全都“暴露”出来了。** 落到文字上的东西一定要是严谨的。所以,在写到很多细节的时候,我查了大量的资料,找到权威的书籍、官方文档、RFC里面的具体描述,有时候我甚至要做个实验,或者打开代码再看一下,才放心下笔。 尽管我对自己写文章有很多“完美倾向”的要求,但是这其实依旧是站在我自己的角度去看的。读者究竟想要看什么内容呢? @@ -60,7 +60,7 @@ 录音频这件事对我的改变非常大。我说话、演讲的时候变得更加严谨了。我会下意识地不去重复已经说过的话。一旦想重复,也闭嘴不发音,等想好了下一句再说。后面,我的录音也越来越顺利,一开始要录五六遍才能成功,后面基本一遍就过了。 -创作专栏的过程还有许多事情,都是我很难得的记忆。我很佩服“极客时间”的编辑做专栏时的专业和认真。我也很庆幸,我没有固执地按照自己认为正确的方向和方式来做,而是尊重了他们的专业。很显然, **他们没有我懂技术,但是他们比我更懂“你”。** 专栏结束后,我回看这半年的准备和努力,我发现, **无论对自己的领域多么熟悉,写这个专栏都让我又上升了一个新高度** 。 +创作专栏的过程还有许多事情,都是我很难得的记忆。我很佩服“极客时间”的编辑做专栏时的专业和认真。我也很庆幸,我没有固执地按照自己认为正确的方向和方式来做,而是尊重了他们的专业。很显然,**他们没有我懂技术,但是他们比我更懂“你”。** 专栏结束后,我回看这半年的准备和努力,我发现,**无论对自己的领域多么熟悉,写这个专栏都让我又上升了一个新高度** 。 我知道很多技术人都喜欢分享,而写文章又是最容易实现的方式。写文章的时候,可以检验你对基础知识的掌握是否扎实,是不是有换位思考能力,能不能从感性认识上升到理性认知。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25401\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25401\350\256\262.md" index e87120ece..87159d086 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25401\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25401\350\256\262.md" @@ -14,7 +14,7 @@ public class HelloWorld { } ``` -如果你是程序员,一定看得懂上面这一段文字。这是每一个程序员向计算机世界说“你好,世界”的方式。但是,你不一定知道,这段文字也是一种协议,是人类和计算机沟通的协议, **只有通过这种协议,计算机才知道我们想让它做什么。** +如果你是程序员,一定看得懂上面这一段文字。这是每一个程序员向计算机世界说“你好,世界”的方式。但是,你不一定知道,这段文字也是一种协议,是人类和计算机沟通的协议,**只有通过这种协议,计算机才知道我们想让它做什么。** ## 协议三要素 @@ -24,13 +24,13 @@ public class HelloWorld { 但是可以看得出,计算机语言作为程序员控制一台计算机工作的协议,具备了协议的三要素。 -- **语法** ,就是这一段内容要符合一定的规则和格式。例如,括号要成对,结束要使用分号等。 -- **语义** ,就是这一段内容要代表某种意义。例如数字减去数字是有意义的,数字减去文本一般来说就没有意义。 -- **顺序** ,就是先干啥,后干啥。例如,可以先加上某个数值,然后再减去某个数值。 +- **语法**,就是这一段内容要符合一定的规则和格式。例如,括号要成对,结束要使用分号等。 +- **语义**,就是这一段内容要代表某种意义。例如数字减去数字是有意义的,数字减去文本一般来说就没有意义。 +- **顺序**,就是先干啥,后干啥。例如,可以先加上某个数值,然后再减去某个数值。 会了计算机语言,你就能够教给一台计算机完成你的工作了。恭喜你,入门了! -但是,要想打造互联网世界的通天塔,只教给一台机器做什么是不够的,你需要学会教给一大片机器做什么。这就需要网络协议。 **只有通过网络协议,才能使一大片机器互相协作、共同完成一件事。** 这个时候,你可能会问,网络协议长啥样,这么神奇,能干成啥事?我先拿一个简单的例子,让你尝尝鲜,然后再讲一个大事。 +但是,要想打造互联网世界的通天塔,只教给一台机器做什么是不够的,你需要学会教给一大片机器做什么。这就需要网络协议。**只有通过网络协议,才能使一大片机器互相协作、共同完成一件事。** 这个时候,你可能会问,网络协议长啥样,这么神奇,能干成啥事?我先拿一个简单的例子,让你尝尝鲜,然后再讲一个大事。 当你想要买一个商品,常规的做法就是打开浏览器,输入购物网站的地址。浏览器就会给你显示一个缤纷多彩的页面。 @@ -50,7 +50,7 @@ Content-Language: zh-CN 这符合协议的三要素吗?我带你来看一下。 -首先,符合语法,也就是说,只有按照上面那个格式来,浏览器才认。例如,上来是 **状态** ,然后是 **首部** ,然后是 **内容** 。 +首先,符合语法,也就是说,只有按照上面那个格式来,浏览器才认。例如,上来是 **状态**,然后是 **首部**,然后是 **内容** 。 第二,符合语义,就是要按照约定的意思来。例如,状态200,表述的意思是网页成功返回。如果不成功,就是我们常见的“404”。 @@ -72,7 +72,7 @@ Content-Language: zh-CN ![img](assets/d8a65ca347ad26acc9f1de49b10320c6.png) -DNS、HTTP、HTTPS所在的层我们称为 **应用层** 。经过应用层封装后,浏览器会将应用层的包交给下一层去完成,通过socket编程来实现。下一层是 **传输层** 。传输层有两种协议,一种是无连接的协议 **UDP** ,一种是面向连接的协议 **TCP** 。对于支付来讲,往往使用TCP协议。所谓的面向连接就是,TCP会保证这个包能够到达目的地。如果不能到达,就会重新发送,直至到达。 +DNS、HTTP、HTTPS所在的层我们称为 **应用层** 。经过应用层封装后,浏览器会将应用层的包交给下一层去完成,通过socket编程来实现。下一层是 **传输层** 。传输层有两种协议,一种是无连接的协议 **UDP**,一种是面向连接的协议 **TCP** 。对于支付来讲,往往使用TCP协议。所谓的面向连接就是,TCP会保证这个包能够到达目的地。如果不能到达,就会重新发送,直至到达。 TCP协议里面会有两个端口,一个是浏览器监听的端口,一个是电商的服务器监听的端口。操作系统往往通过端口来判断,它得到的包应该给哪个进程。 @@ -100,7 +100,7 @@ TCP协议里面会有两个端口,一个是浏览器监听的端口,一个 ![img](assets/f7ea602aec91c67b35e710fb72a975e2.png) -城关往往是知道这些“知识”的,因为城关和临近的城关也会经常沟通。到哪里应该怎么走,这种沟通的协议称为 **路由协议** ,常用的有 **OSPF** 和 **BGP** 。 +城关往往是知道这些“知识”的,因为城关和临近的城关也会经常沟通。到哪里应该怎么走,这种沟通的协议称为 **路由协议**,常用的有 **OSPF** 和 **BGP** 。 ![img](assets/b25ad7afba7b79331d95875dd0f451d4.png) @@ -114,7 +114,7 @@ TCP协议里面会有两个端口,一个是浏览器监听的端口,一个 因为一旦出了国门,西行路上千难万险,如果在这个过程中,网络包走丢了,例如进了大沙漠,或者被强盗抢劫杀害怎么办呢?因而到了要报个平安。 -如果过一段时间还是没到,发送端的TCP层会重新发送这个包,还是上面的过程,直到有一天收到平安到达的回复。 **这个重试绝非你的浏览器重新将下单这个动作重新请求一次** 。对于浏览器来讲,就发送了一次下单请求,TCP层不断自己闷头重试。除非TCP这一层出了问题,例如连接断了,才轮到浏览器的应用层重新发送下单请求。 +如果过一段时间还是没到,发送端的TCP层会重新发送这个包,还是上面的过程,直到有一天收到平安到达的回复。**这个重试绝非你的浏览器重新将下单这个动作重新请求一次** 。对于浏览器来讲,就发送了一次下单请求,TCP层不断自己闷头重试。除非TCP这一层出了问题,例如连接断了,才轮到浏览器的应用层重新发送下单请求。 当网络包平安到达TCP层之后,TCP头中有目标端口号,通过这个端口号,可以找到电商网站的进程正在监听这个端口号,假设一个Tomcat,将这个包发给电商网站。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25402\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25402\350\256\262.md" index 68e91f9e9..3a6866422 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25402\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25402\350\256\262.md" @@ -10,7 +10,7 @@ 因为教科书或者老师往往会打一个十分不恰当的比喻:为什么网络要分层呀?因为不同的层次之间有不同的沟通方式,这个叫作协议。例如,一家公司也是分“层次”的,分总经理、经理、组长、员工。总经理之间有他们的沟通方式,经理和经理之间也有沟通方式,同理组长和员工。有没有听过类似的比喻? -那么 **第一个问题** 来了。请问经理在握手的时候,员工在干什么?很多人听过TCP建立连接的 **三次握手协议** ,也会把它当知识点背诵。同理问你,TCP在进行三次握手的时候,IP层和MAC层对应都有什么操作呢? +那么 **第一个问题** 来了。请问经理在握手的时候,员工在干什么?很多人听过TCP建立连接的 **三次握手协议**,也会把它当知识点背诵。同理问你,TCP在进行三次握手的时候,IP层和MAC层对应都有什么操作呢? 除了上面这个不恰当的比喻,教科书还会列出每个层次所包含的协议,然后开始逐层地去讲这些协议。但是这些协议之间的关系呢?却很少有教科书会讲。 @@ -34,7 +34,7 @@ 理解计算机网络中的概念,一个很好的角度是,想象网络包就是一段Buffer,或者一块内存,是有格式的。同时,想象自己是一个处理网络包的程序,而且这个程序可以跑在电脑上,可以跑在服务器上,可以跑在交换机上,也可以跑在路由器上。你想象自己有很多的网口,从某个口拿进一个网络包来,用自己的程序处理一下,再从另一个网口发送出去。 -当然网络包的格式很复杂,这个程序也很复杂。 **复杂的程序都要分层,这是程序设计的要求。** 比如,复杂的电商还会分数据库层、缓存层、Compose层、Controller层和接入层,每一层专注做本层的事情。 +当然网络包的格式很复杂,这个程序也很复杂。**复杂的程序都要分层,这是程序设计的要求。** 比如,复杂的电商还会分数据库层、缓存层、Compose层、Controller层和接入层,每一层专注做本层的事情。 ## 程序是如何工作的? @@ -68,13 +68,13 @@ 知道了这个过程之后,我们再来看一下原来困惑的问题。 -首先是分层的比喻。 **所有不能表示出层层封装含义的比喻,都是不恰当的。** 总经理握手,不需要员工在吧,总经理之间谈什么,不需要员工参与吧,但是网络世界不是这样的。正确的应该是,总经理之间沟通的时候,经理将总经理放在自己兜里,然后组长把经理放自己兜里,员工把组长放自己兜里,像套娃娃一样。那员工直接沟通,不带上总经理,就不恰当了。 +首先是分层的比喻。**所有不能表示出层层封装含义的比喻,都是不恰当的。** 总经理握手,不需要员工在吧,总经理之间谈什么,不需要员工参与吧,但是网络世界不是这样的。正确的应该是,总经理之间沟通的时候,经理将总经理放在自己兜里,然后组长把经理放自己兜里,员工把组长放自己兜里,像套娃娃一样。那员工直接沟通,不带上总经理,就不恰当了。 现实生活中,往往是员工说一句,组长补充两句,然后经理补充两句,最后总经理再补充两句。但是在网络世界,应该是总经理说话,经理补充两句,组长补充两句,员工再补充两句。 那TCP在三次握手的时候,IP层和MAC层在做什么呢?当然是TCP发送每一个消息,都会带着IP层和MAC层了。因为,TCP每发送一个消息,IP层和MAC层的所有机制都要运行一遍。而你只看到TCP三次握手了,其实,IP层和MAC层为此也忙活好久了。 -这里要记住一点: **只要是在网络上跑的包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。** 所以, **对TCP协议来说,三次握手也好,重试也好,只要想发出去包,就要有IP层和MAC层,不然是发不出去的。** +这里要记住一点: **只要是在网络上跑的包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。** 所以,**对TCP协议来说,三次握手也好,重试也好,只要想发出去包,就要有IP层和MAC层,不然是发不出去的。** 经常有人会问这样一个问题,我都知道那台机器的IP地址了,直接发给他消息呗,要MAC地址干啥?这里的关键就是,没有MAC地址消息是发不出去的。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25403\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25403\350\256\262.md" index 85d78ea2a..c637de46a 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25403\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25403\350\256\262.md" @@ -50,11 +50,11 @@ IP地址是一个网卡在网络世界的通讯地址,相当于我们现实世 ## 无类型域间选路(CIDR) -于是有了一个折中的方式叫作 **无类型域间选路** ,简称 **CIDR** 。这种方式打破了原来设计的几类地址的做法,将32位的IP地址一分为二,前面是 **网络号** ,后面是 **主机号** 。从哪里分呢?你如果注意观察的话可以看到,10.100.122.2/24,这个IP地址中有一个斜杠,斜杠后面有个数字24。这种地址表示形式,就是CIDR。后面24的意思是,32位中,前24位是网络号,后8位是主机号。 +于是有了一个折中的方式叫作 **无类型域间选路**,简称 **CIDR** 。这种方式打破了原来设计的几类地址的做法,将32位的IP地址一分为二,前面是 **网络号**,后面是 **主机号** 。从哪里分呢?你如果注意观察的话可以看到,10.100.122.2/24,这个IP地址中有一个斜杠,斜杠后面有个数字24。这种地址表示形式,就是CIDR。后面24的意思是,32位中,前24位是网络号,后8位是主机号。 -伴随着CIDR存在的,一个是 **广播地址** ,10.100.122.255。如果发送这个地址,所有10.100.122网络里面的机器都可以收到。另一个是 **子网掩码** ,255.255.255.0。 +伴随着CIDR存在的,一个是 **广播地址**,10.100.122.255。如果发送这个地址,所有10.100.122网络里面的机器都可以收到。另一个是 **子网掩码**,255.255.255.0。 -## 将子网掩码和IP地址进行AND计算。前面三个255,转成二进制都是1。1和任何数值取AND,都是原来数值,因而前三个数不变,为10.100.122。后面一个0,转换成二进制是0,0和任何数值取AND,都是0,因而最后一个数变为0,合起来就是10.100.122.0。这就是 **网络号** 。 **将子网掩码和IP地址按位计算AND,就可得到网络号。** 公有IP地址和私有IP地址 +## 将子网掩码和IP地址进行AND计算。前面三个255,转成二进制都是1。1和任何数值取AND,都是原来数值,因而前三个数不变,为10.100.122。后面一个0,转换成二进制是0,0和任何数值取AND,都是0,因而最后一个数变为0,合起来就是10.100.122.0。这就是 **网络号** 。**将子网掩码和IP地址按位计算AND,就可得到网络号。** 公有IP地址和私有IP地址 在日常的工作中,几乎不用划分A类、B类或者C类,所以时间长了,很多人就忘记了这个分类,而只记得CIDR。但是有一点还是要注意的,就是公有IP地址和私有IP地址。 @@ -88,15 +88,15 @@ IP地址是一个网卡在网络世界的通讯地址,相当于我们现实世 在IP地址的后面有个scope,对于eth0这张网卡来讲,是global,说明这张网卡是可以对外的,可以接收来自各个地方的包。对于lo来讲,是host,说明这张网卡仅仅可以供本机相互通信。 -lo全称是 **loopback** ,又称 **环回接口** ,往往会被分配到127.0.0.1这个地址。这个地址用于本机通信,经过内核处理后直接返回,不会在任何网络中出现。 +lo全称是 **loopback**,又称 **环回接口**,往往会被分配到127.0.0.1这个地址。这个地址用于本机通信,经过内核处理后直接返回,不会在任何网络中出现。 ## MAC地址 -在IP地址的上一行是link/ether fa:16:3e:c7:79:75 brd ff:ff:ff:ff:ff:ff,这个被称为 **MAC地址** ,是一个网卡的物理地址,用十六进制,6个byte表示。 +在IP地址的上一行是link/ether fa:16:3e:c7:79:75 brd ff:ff:ff:ff:ff:ff,这个被称为 **MAC地址**,是一个网卡的物理地址,用十六进制,6个byte表示。 MAC地址是一个很容易让人“误解”的地址。因为MAC地址号称全局唯一,不会有两个网卡有相同的MAC地址,而且网卡自生产出来,就带着这个地址。很多人看到这里就会想,既然这样,整个互联网的通信,全部用MAC地址好了,只要知道了对方的MAC地址,就可以把信息传过去。 -这样当然是不行的。 **一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能。** 而有门牌号码属性的IP地址,才是有远程定位功能的。 +这样当然是不行的。**一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能。** 而有门牌号码属性的IP地址,才是有远程定位功能的。 例如,你去杭州市网商路599号B楼6层找刘超,你在路上问路,可能被问的人不知道B楼是哪个,但是可以给你指网商路怎么去。但是如果你问一个人,你知道这个身份证号的人在哪里吗?可想而知,没有人知道。 @@ -108,13 +108,13 @@ MAC地址是有一定定位功能的,只不过范围非常有限。你可以 ## 网络设备的状态标识 -解析完了MAC地址,我们再来看 \是干什么的?这个叫作 **net_device flags** , **网络设备的状态标识** 。 +解析完了MAC地址,我们再来看 \是干什么的?这个叫作 **net_device flags**,**网络设备的状态标识** 。 UP表示网卡处于启动的状态;BROADCAST表示这个网卡有广播地址,可以发送广播包;MULTICAST表示网卡可以发送多播包;LOWER_UP表示L1是启动的,也即网线插着呢。MTU1500是指什么意思呢?是哪一层的概念呢?最大传输单元MTU为1500,这是以太网的默认值。 上一节,我们讲过网络包是层层封装的。MTU是二层MAC层的概念。MAC层有MAC的头,以太网规定连MAC头带正文合起来,不允许超过1500个字节。正文里面有IP的头、TCP的头、HTTP的头。如果放不下,就需要分片来传输。 -qdisc pfifo_fast是什么意思呢?qdisc全称是 **queueing discipline** ,中文叫 **排队规则** 。内核如果需要通过某个网络接口发送数据包,它都需要按照为这个接口配置的qdisc(排队规则)把数据包加入队列。 +qdisc pfifo_fast是什么意思呢?qdisc全称是 **queueing discipline**,中文叫 **排队规则** 。内核如果需要通过某个网络接口发送数据包,它都需要按照为这个接口配置的qdisc(排队规则)把数据包加入队列。 最简单的qdisc是pfifo,它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列。pfifo_fast稍微复杂一些,它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25404\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25404\350\256\262.md" index fb6a6e336..53c78ddfe 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25404\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25404\350\256\262.md" @@ -17,18 +17,18 @@ sudo ip link set up eth1 不会出现任何现象,就是包发不出去呗。为什么发不出去呢?我来举例说明。 192.168.1.6就在你这台机器的旁边,甚至是在同一个交换机上,而你把机器的地址设为了16.158.23.6。在这台机器上,你企图去ping192.168.1.6,你觉得只要将包发出去,同一个交换机的另一台机器马上就能收到,对不对? 可是Linux系统不是这样的,它没你想得那么智能。你用肉眼看到那台机器就在旁边,它则需要根据自己的逻辑进行处理。 -还记得我们在第二节说过的原则吗? **只要是在网络上跑的包,都是完整的,可以有下层没上层,绝对不可能有上层没下层。** 所以,你看着它有自己的源IP地址16.158.23.6,也有目标IP地址192.168.1.6,但是包发不出去,这是因为MAC层还没填。 +还记得我们在第二节说过的原则吗?**只要是在网络上跑的包,都是完整的,可以有下层没上层,绝对不可能有上层没下层。** 所以,你看着它有自己的源IP地址16.158.23.6,也有目标IP地址192.168.1.6,但是包发不出去,这是因为MAC层还没填。 自己的MAC地址自己知道,这个容易。但是目标MAC填什么呢?是不是填192.168.1.6这台机器的MAC地址呢? -当然不是。Linux首先会判断,要去的这个地址和我是一个网段的吗,或者和我的一个网卡是同一网段的吗?只有是一个网段的,它才会发送ARP请求,获取MAC地址。如果发现不是呢? **Linux默认的逻辑是,如果这是一个跨网段的调用,它便不会直接将包发送到网络上,而是企图将包发送到网关。** 如果你配置了网关的话,Linux会获取网关的MAC地址,然后将包发出去。对于192.168.1.6这台机器来讲,虽然路过它家门的这个包,目标IP是它,但是无奈MAC地址不是它的,所以它的网卡是不会把包收进去的。 +当然不是。Linux首先会判断,要去的这个地址和我是一个网段的吗,或者和我的一个网卡是同一网段的吗?只有是一个网段的,它才会发送ARP请求,获取MAC地址。如果发现不是呢?**Linux默认的逻辑是,如果这是一个跨网段的调用,它便不会直接将包发送到网络上,而是企图将包发送到网关。** 如果你配置了网关的话,Linux会获取网关的MAC地址,然后将包发出去。对于192.168.1.6这台机器来讲,虽然路过它家门的这个包,目标IP是它,但是无奈MAC地址不是它的,所以它的网卡是不会把包收进去的。 如果没有配置网关呢?那包压根就发不出去。 如果将网关配置为192.168.1.6呢?不可能,Linux不会让你配置成功的,因为网关要和当前的网络至少一个网卡是同一个网段的,怎么可能16.158.23.6的网关是192.168.1.6呢? -所以,当你需要手动配置一台机器的网络IP时,一定要好好问问你的网络管理员。如果在机房里面,要去网络管理员那里申请,让他给你分配一段正确的IP地址。当然,真正配置的时候,一定不是直接用命令配置的,而是放在一个配置文件里面。 **不同系统的配置文件格式不同,但是无非就是CIDR、子网掩码、广播地址和网关地址** 。 +所以,当你需要手动配置一台机器的网络IP时,一定要好好问问你的网络管理员。如果在机房里面,要去网络管理员那里申请,让他给你分配一段正确的IP地址。当然,真正配置的时候,一定不是直接用命令配置的,而是放在一个配置文件里面。**不同系统的配置文件格式不同,但是无非就是CIDR、子网掩码、广播地址和网关地址** 。 动态主机配置协议(DHCP) -------------- 原来配置IP有这么多门道儿啊。你可能会问了,配置了IP之后一般不能变的,配置一个服务端的机器还可以,但是如果是客户端的机器呢?我抱着一台笔记本电脑在公司里走来走去,或者白天来晚上走,每次使用都要配置IP地址,那可怎么办?还有人事、行政等非技术人员,如果公司所有的电脑都需要IT人员配置,肯定忙不过来啊。 -因此,我们需要有一个自动配置的协议,也就是称 **动态主机配置协议(Dynamic Host Configuration Protocol)** ,简称 **DHCP** 。 +因此,我们需要有一个自动配置的协议,也就是称 **动态主机配置协议(Dynamic Host Configuration Protocol)**,简称 **DHCP** 。 有了这个协议,网络管理员就轻松多了。他只需要配置一段共享的IP地址。每一台新接入的机器都通过DHCP协议,来这个共享的IP地址里申请,然后自动配置好就可以了。等人走了,或者用完了,还回去,这样其他的机器也能用。 -所以说, **如果是数据中心里面的服务器,IP一旦配置好,基本不会变,这就相当于买房自己装修。DHCP的方式就相当于租房。你不用装修,都是帮你配置好的。你暂时用一下,用完退租就可以了。** 解析DHCP的工作方式 +所以说,**如果是数据中心里面的服务器,IP一旦配置好,基本不会变,这就相当于买房自己装修。DHCP的方式就相当于租房。你不用装修,都是帮你配置好的。你暂时用一下,用完退租就可以了。** 解析DHCP的工作方式 ----------- 当一台机器新加入一个网络的时候,肯定一脸懵,啥情况都不知道,只知道自己的MAC地址。怎么办?先吼一句,我来啦,有人吗?这时候的沟通基本靠“吼”。这一步,我们称为 **DHCP Discover。** 新来的机器使用IP地址0.0.0.0发送了一个广播包,目的IP地址为255.255.255.255。广播包封装了UDP,UDP封装了BOOTP。其实DHCP是BOOTP的增强版,但是如果你去抓包的话,很可能看到的名称还是BOOTP协议。 在这个广播包里面,新人大声喊:我是新来的(Boot request),我的MAC地址是这个,我还没有IP,谁能给租给我个IP地址! @@ -58,7 +58,7 @@ IP地址的收回和续租 所以管理员希望的不仅仅是自动分配IP地址,还要自动安装系统。装好系统之后自动分配IP地址,直接启动就能用了,这样当然最好了! 这事儿其实仔细一想,还是挺有难度的。安装操作系统,应该有个光盘吧。数据中心里不能用光盘吧,想了一个办法就是,可以将光盘里面要安装的操作系统放在一个服务器上,让客户端去下载。但是客户端放在哪里呢?它怎么知道去哪个服务器上下载呢?客户端总得安装在一个操作系统上呀,可是这个客户端本来就是用来安装操作系统的呀? 其实,这个过程和操作系统启动的过程有点儿像。首先,启动BIOS。这是一个特别小的小系统,只能干特别小的一件事情。其实就是读取硬盘的MBR启动扇区,将GRUB启动起来;然后将权力交给GRUB,GRUB加载内核、加载作为根文件系统的initramfs文件;然后将权力交给内核;最后内核启动,初始化整个操作系统。 -那我们安装操作系统的过程,只能插在BIOS启动之后了。因为没安装系统之前,连启动扇区都没有。因而这个过程叫做 **预启动执行环境(Pre-boot Execution Environment)** ,简称 **PXE。** PXE协议分为客户端和服务器端,由于还没有操作系统,只能先把客户端放在BIOS里面。当计算机启动时,BIOS把PXE客户端调入内存里面,就可以连接到服务端做一些操作了。 +那我们安装操作系统的过程,只能插在BIOS启动之后了。因为没安装系统之前,连启动扇区都没有。因而这个过程叫做 **预启动执行环境(Pre-boot Execution Environment)**,简称 **PXE。** PXE协议分为客户端和服务器端,由于还没有操作系统,只能先把客户端放在BIOS里面。当计算机启动时,BIOS把PXE客户端调入内存里面,就可以连接到服务端做一些操作了。 首先,PXE客户端自己也需要有个IP地址。因为PXE的客户端启动起来,就可以发送一个DHCP的请求,让DHCP Server给它分配一个地址。PXE客户端有了自己的地址,那它怎么知道PXE服务器在哪里呢?对于其他的协议,都好办,要么人告诉他。例如,告诉浏览器要访问的IP地址,或者在配置中告诉它;例如,微服务之间的相互调用。 但是PXE客户端启动的时候,啥都没有。好在DHCP Server除了分配IP地址以外,还可以做一些其他的事情。这里有一个DHCP Server的一个样例配置: diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25405\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25405\350\256\262.md" index 608cf91d4..c5e027fc1 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25405\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25405\350\256\262.md" @@ -24,7 +24,7 @@ 这里我想问你一个问题,两台电脑之间的网络包,包含MAC层吗?当然包含,要完整。IP层要封装了MAC层才能将包放入物理层。 -到此为止,两台电脑已经构成了一个最小的 **局域网** ,也即 **LAN。** 可以玩联机局域网游戏啦! +到此为止,两台电脑已经构成了一个最小的 **局域网**,也即 **LAN。** 可以玩联机局域网游戏啦! 等到第三个哥们也买了一台电脑,怎么把三台电脑连在一起呢? @@ -38,7 +38,7 @@ 1. 大家都在发,会不会产生混乱?有没有谁先发、谁后发的规则? 1. 如果发送的时候出现了错误,怎么办? -这几个问题,都是第二层,数据链路层,也即MAC层要解决的问题。 **MAC** 的全称是 **Medium Access Control** ,即 **媒体访问控制。控制什么呢?其实就是控制在往媒体上发数据的时候,谁先发、谁后发的问题。防止发生混乱。这解决的是第二个问题。这个问题中的规则,学名叫多路访问** 。有很多算法可以解决这个问题。就像车管所管束马路上跑的车,能想的办法都想过了。 +这几个问题,都是第二层,数据链路层,也即MAC层要解决的问题。**MAC** 的全称是 **Medium Access Control**,即 **媒体访问控制。控制什么呢?其实就是控制在往媒体上发数据的时候,谁先发、谁后发的问题。防止发生混乱。这解决的是第二个问题。这个问题中的规则,学名叫多路访问** 。有很多算法可以解决这个问题。就像车管所管束马路上跑的车,能想的办法都想过了。 比如接下来这三种方式: @@ -52,15 +52,15 @@ ![img](assets/cef93d665ca863fef40f7f854d5d33ed.jpg) -接下来是 **类型** ,大部分的类型是IP数据包,然后IP里面包含TCP、UDP,以及HTTP等,这都是里层封装的事情。 +接下来是 **类型**,大部分的类型是IP数据包,然后IP里面包含TCP、UDP,以及HTTP等,这都是里层封装的事情。 有了这个目标MAC地址,数据包在链路上广播,MAC的网卡才能发现,这个包是给它的。MAC的网卡把包收进来,然后打开IP包,发现IP地址也是自己的,再打开TCP包,发现端口是自己,也就是80,而nginx就是监听80。 于是将请求提交给nginx,nginx返回一个网页。然后将网页需要发回请求的机器。然后层层封装,最后到MAC层。因为来的时候有源MAC地址,返回的时候,源MAC就变成了目标MAC,再返给请求的机器。 -对于以太网,第二层的最后面是 **CRC** ,也就是 **循环冗余检测** 。通过XOR异或的算法,来计算整个包是否在发送的过程中出现了错误,主要解决第三个问题。 +对于以太网,第二层的最后面是 **CRC**,也就是 **循环冗余检测** 。通过XOR异或的算法,来计算整个包是否在发送的过程中出现了错误,主要解决第三个问题。 -这里还有一个没有解决的问题,当源机器知道目标机器的时候,可以将目标地址放入包里面,如果不知道呢?一个广播的网络里面接入了N台机器,我怎么知道每个MAC地址是谁呢?这就是 **ARP协议** ,也就是已知IP地址,求MAC地址的协议。 +这里还有一个没有解决的问题,当源机器知道目标机器的时候,可以将目标地址放入包里面,如果不知道呢?一个广播的网络里面接入了N台机器,我怎么知道每个MAC地址是谁呢?这就是 **ARP协议**,也就是已知IP地址,求MAC地址的协议。 ![img](assets/17ac2f46ef531e2b4380300f10267e3d.jpg) @@ -90,11 +90,11 @@ 一台MAC1电脑将一个包发送给另一台MAC2电脑,当这个包到达交换机的时候,一开始交换机也不知道MAC2的电脑在哪个口,所以没办法,它只能将包转发给除了来的那个口之外的其他所有的口。但是,这个时候,交换机会干一件非常聪明的事情,就是交换机会记住,MAC1是来自一个明确的口。以后有包的目的地址是MAC1的,直接发送到这个口就可以了。 -当交换机作为一个关卡一样,过了一段时间之后,就有了整个网络的一个结构了,这个时候,基本上不用广播了,全部可以准确转发。当然,每个机器的IP地址会变,所在的口也会变,因而交换机上的学习的结果,我们称为 **转发表** ,是有一个过期时间的。 +当交换机作为一个关卡一样,过了一段时间之后,就有了整个网络的一个结构了,这个时候,基本上不用广播了,全部可以准确转发。当然,每个机器的IP地址会变,所在的口也会变,因而交换机上的学习的结果,我们称为 **转发表**,是有一个过期时间的。 有了交换机,一般来说,你接个几十台、上百台机器打游戏,应该没啥问题。你可以组个战队了。能上网了,就可以玩网游了。 -这里,给你推荐一个课程,极客时间新上线了 **《从0开始学游戏开发》** ,由原网易游戏引擎架构师、资深底层技术专家蔡能老师,手把手带你梳理游戏开发的流程和细节,为你剖析热门游戏的成功之道。帮助普通程序员成为游戏开发工程师,步入游戏开发之路。 **你可以点击文末的图片进入课程。** +这里,给你推荐一个课程,极客时间新上线了 **《从0开始学游戏开发》**,由原网易游戏引擎架构师、资深底层技术专家蔡能老师,手把手带你梳理游戏开发的流程和细节,为你剖析热门游戏的成功之道。帮助普通程序员成为游戏开发工程师,步入游戏开发之路。**你可以点击文末的图片进入课程。** ## 小结 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25406\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25406\350\256\262.md" index 256fab4d6..c2e8470dd 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25406\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25406\350\256\262.md" @@ -42,7 +42,7 @@ ## STP协议中那些难以理解的概念 -在数据结构中,有一个方法叫作 **最小生成树** 。有环的我们常称为 **图** 。将图中的环破了,就生成了 **树** 。在计算机网络中,生成树的算法叫作 **STP** ,全称 **Spanning Tree Protocol** 。 +在数据结构中,有一个方法叫作 **最小生成树** 。有环的我们常称为 **图** 。将图中的环破了,就生成了 **树** 。在计算机网络中,生成树的算法叫作 **STP**,全称 **Spanning Tree Protocol** 。 STP协议比较复杂,一开始很难看懂,但是其实这是一场血雨腥风的武林比武或者华山论剑,最终决出五岳盟主的方式。 @@ -50,10 +50,10 @@ STP协议比较复杂,一开始很难看懂,但是其实这是一场血雨 在STP协议里面有很多概念,译名就非常拗口,但是我一作比喻,你很容易就明白了。 -- **Root Bridge** ,也就是 **根交换机** 。这个比较容易理解,可以比喻为“掌门”交换机,是某棵树的老大,是掌门,最大的大哥。 -- **Designated Bridges** ,有的翻译为 **指定交换机** 。这个比较难理解,可以想像成一个“小弟”,对于树来说,就是一棵树的树枝。所谓“指定”的意思是,我拜谁做大哥,其他交换机通过这个交换机到达根交换机,也就相当于拜他做了大哥。这里注意是树枝,不是叶子,因为叶子往往是主机。 -- **Bridge Protocol Data Units (BPDU)** , **网桥协议数据单元** 。可以比喻为“相互比较实力”的协议。行走江湖,比的就是武功,拼的就是实力。当两个交换机碰见的时候,也就是相连的时候,就需要互相比一比内力了。BPDU只有掌门能发,已经隶属于某个掌门的交换机只能传达掌门的指示。 -- **Priority Vector** , **优先级向量** 。可以比喻为实力 (值越小越牛)。实力是啥?就是一组ID数目,\[Root Bridge ID, Root Path Cost, Bridge ID, and Port ID\]。为什么这样设计呢?这是因为要看怎么来比实力。先看Root Bridge ID。拿出老大的ID看看,发现掌门一样,那就是师兄弟;再比Root Path Cost,也即我距离我的老大的距离,也就是拿和掌门关系比,看同一个门派内谁和老大关系铁;最后比Bridge ID,比我自己的ID,拿自己的本事比。 +- **Root Bridge**,也就是 **根交换机** 。这个比较容易理解,可以比喻为“掌门”交换机,是某棵树的老大,是掌门,最大的大哥。 +- **Designated Bridges**,有的翻译为 **指定交换机** 。这个比较难理解,可以想像成一个“小弟”,对于树来说,就是一棵树的树枝。所谓“指定”的意思是,我拜谁做大哥,其他交换机通过这个交换机到达根交换机,也就相当于拜他做了大哥。这里注意是树枝,不是叶子,因为叶子往往是主机。 +- **Bridge Protocol Data Units (BPDU)**,**网桥协议数据单元** 。可以比喻为“相互比较实力”的协议。行走江湖,比的就是武功,拼的就是实力。当两个交换机碰见的时候,也就是相连的时候,就需要互相比一比内力了。BPDU只有掌门能发,已经隶属于某个掌门的交换机只能传达掌门的指示。 +- **Priority Vector**,**优先级向量** 。可以比喻为实力 (值越小越牛)。实力是啥?就是一组ID数目,\[Root Bridge ID, Root Path Cost, Bridge ID, and Port ID\]。为什么这样设计呢?这是因为要看怎么来比实力。先看Root Bridge ID。拿出老大的ID看看,发现掌门一样,那就是师兄弟;再比Root Path Cost,也即我距离我的老大的距离,也就是拿和掌门关系比,看同一个门派内谁和老大关系铁;最后比Bridge ID,比我自己的ID,拿自己的本事比。 ## STP的工作过程是怎样的? @@ -115,7 +115,7 @@ STP协议比较复杂,一开始很难看懂,但是其实这是一场血雨 有两种分的方法,一个是 **物理隔离** 。每个部门设一个单独的会议室,对应到网络方面,就是每个部门有单独的交换机,配置单独的子网,这样部门之间的沟通就需要路由器了。路由器咱们还没讲到,以后再说。这样的问题在于,有的部门人多,有的部门人少。人少的部门慢慢人会变多,人多的部门也可能人越变越少。如果每个部门有单独的交换机,口多了浪费,少了又不够用。 -另外一种方式是 **虚拟隔离** ,就是用我们常说的 **VLAN** ,或者叫 **虚拟局域网** 。使用VLAN,一个交换机上会连属于多个局域网的机器,那交换机怎么区分哪个机器属于哪个局域网呢? +另外一种方式是 **虚拟隔离**,就是用我们常说的 **VLAN**,或者叫 **虚拟局域网** 。使用VLAN,一个交换机上会连属于多个局域网的机器,那交换机怎么区分哪个机器属于哪个局域网呢? ![img](assets/2ede82f511ccac2570c17a62ffc749ed.jpg) diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25407\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25407\350\256\262.md" index c454efeba..3c9821313 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25407\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25407\350\256\262.md" @@ -6,7 +6,7 @@ 一般情况下,你会想到ping一下。那你知道ping是如何工作的吗? -ping是基于ICMP协议工作的。 **ICMP** 全称 **Internet Control Message Protocol** ,就是 **互联网控制报文协议** 。这里面的关键词是“控制”,那具体是怎么控制的呢? +ping是基于ICMP协议工作的。**ICMP** 全称 **Internet Control Message Protocol**,就是 **互联网控制报文协议** 。这里面的关键词是“控制”,那具体是怎么控制的呢? 网络包在异常复杂的网络环境中传输时,常常会遇到各种各样的问题。当遇到问题的时候,总不能“死个不明不白”,要传出消息来,报告情况,这样才可以调整传输策略。这就相当于我们经常看到的电视剧里,古代行军的时候,为将为帅者需要通过侦察兵、哨探或传令兵等人肉的方式来掌握情况,控制整个战局。 @@ -14,7 +14,7 @@ ICMP报文是封装在IP包里面的。因为传输指令的时候,肯定需 ![img](assets/23aecf653d60dd94b7c5c6dc21ca21ff.jpg) -ICMP报文有很多的类型,不同的类型有不同的代码。 **最常用的类型是主动请求为8,主动请求的应答为0** 。 +ICMP报文有很多的类型,不同的类型有不同的代码。**最常用的类型是主动请求为8,主动请求的应答为0** 。 ## 查询报文类型 @@ -22,7 +22,7 @@ ICMP报文有很多的类型,不同的类型有不同的代码。 **最常用 这种是主帅发起的,主动查看敌情,对应ICMP的 **查询报文类型** 。例如,常用的 **ping就是查询报文,是一种主动请求,并且获得主动应答的ICMP协议。** 所以,ping发的包也是符合ICMP协议格式的,只不过它在后面增加了自己的格式。 -对ping的主动请求,进行网络抓包,称为 **ICMP ECHO REQUEST。同理主动请求的回复,称为ICMP ECHO REPLY** 。比起原生的ICMP,这里面多了两个字段,一个是 **标识符** 。这个很好理解,你派出去两队侦查兵,一队是侦查战况的,一队是去查找水源的,要有个标识才能区分。另一个是 **序号** ,你派出去的侦查兵,都要编个号。如果派出去10个,回来10个,就说明前方战况不错;如果派出去10个,回来2个,说明情况可能不妙。 +对ping的主动请求,进行网络抓包,称为 **ICMP ECHO REQUEST。同理主动请求的回复,称为ICMP ECHO REPLY** 。比起原生的ICMP,这里面多了两个字段,一个是 **标识符** 。这个很好理解,你派出去两队侦查兵,一队是侦查战况的,一队是去查找水源的,要有个标识才能区分。另一个是 **序号**,你派出去的侦查兵,都要编个号。如果派出去10个,回来10个,就说明前方战况不错;如果派出去10个,回来2个,说明情况可能不妙。 在选项数据中,ping还会存放发送请求的时间值,来计算往返时间,说明路程的长短。 @@ -32,7 +32,7 @@ ICMP报文有很多的类型,不同的类型有不同的代码。 **最常用 主帅骑马走着走着,突然来了一匹快马,上面的小兵气喘吁吁的:报告主公,不好啦!张将军遭遇埋伏,全军覆没啦!这种是异常情况发起的,来报告发生了不好的事情,对应ICMP的 **差错报文类型** 。 -我举几个ICMP差错报文的例子: **终点不可达为3,源抑制为4,超时为11,重定向为5** 。这些都是什么意思呢?我给你具体解释一下。 **第一种是终点不可达** 。小兵:报告主公,您让把粮草送到张将军那里,结果没有送到。 +我举几个ICMP差错报文的例子: **终点不可达为3,源抑制为4,超时为11,重定向为5** 。这些都是什么意思呢?我给你具体解释一下。**第一种是终点不可达** 。小兵:报告主公,您让把粮草送到张将军那里,结果没有送到。 如果你是主公,你肯定会问,为啥送不到?具体的原因在代码中表示就是,网络不可达代码为0,主机不可达代码为1,协议不可达代码为2,端口不可达代码为3,需要进行分片但设置了不分片位代码为4。 @@ -44,7 +44,7 @@ ICMP报文有很多的类型,不同的类型有不同的代码。 **最常用 - 端口不可达:主公,找到地方,找到人,对了口号,事儿没对上,我去送粮草,人家说他们在等救兵。 - 需要进行分片但设置了不分片位:主公,走到一半,山路狭窄,想换小车,但是您的将令,严禁换小车,就没办法送到了。 -**第二种是源站抑制** ,也就是让源站放慢发送速度。小兵:报告主公,您粮草送的太多了吃不完。 **第三种是时间超时** ,也就是超过网络包的生存时间还是没到。小兵:报告主公,送粮草的人,自己把粮草吃完了,还没找到地方,已经饿死啦。 **第四种是路由重定向** ,也就是让下次发给另一个路由器。小兵:报告主公,上次送粮草的人本来只要走一站地铁,非得从五环绕,下次别这样了啊。 +**第二种是源站抑制**,也就是让源站放慢发送速度。小兵:报告主公,您粮草送的太多了吃不完。**第三种是时间超时**,也就是超过网络包的生存时间还是没到。小兵:报告主公,送粮草的人,自己把粮草吃完了,还没找到地方,已经饿死啦。**第四种是路由重定向**,也就是让下次发给另一个路由器。小兵:报告主公,上次送粮草的人本来只要走一站地铁,非得从五环绕,下次别这样了啊。 差错报文的结构相对复杂一些。除了前面还是IP,ICMP的前8字节不变,后面则跟上出错的那个IP包的IP头和IP正文的前8个字节。 @@ -61,7 +61,7 @@ ICMP报文有很多的类型,不同的类型有不同的代码。 **最常用 假定主机A的IP地址是192.168.1.1,主机B的IP地址是192.168.1.2,它们都在同一个子网。那当你在主机A上运行“ping 192.168.1.2”后,会发生什么呢? -ping命令执行的时候,源主机首先会构建一个ICMP请求数据包,ICMP数据包内包含多个字段。最重要的是两个,第一个是 **类型字段** ,对于请求数据包而言该字段为 8;另外一个是 **顺序号** ,主要用于区分连续ping的时候发出的多个数据包。每发出一个请求数据包,顺序号会自动加1。为了能够计算往返时间RTT,它会在报文的数据部分插入发送时间。 +ping命令执行的时候,源主机首先会构建一个ICMP请求数据包,ICMP数据包内包含多个字段。最重要的是两个,第一个是 **类型字段**,对于请求数据包而言该字段为 8;另外一个是 **顺序号**,主要用于区分连续ping的时候发出的多个数据包。每发出一个请求数据包,顺序号会自动加1。为了能够计算往返时间RTT,它会在报文的数据部分插入发送时间。 然后,由ICMP协议将这个数据包连同地址192.168.1.2一起交给IP层。IP层将以192.168.1.2作为目的地址,本机IP地址作为源地址,加上一些其他控制信息,构建一个IP数据包。 @@ -85,7 +85,7 @@ ping命令执行的时候,源主机首先会构建一个ICMP请求数据包, 那其他的类型呢?是不是只有真正遇到错误的时候,才能收到呢?那也不是,有一个程序Traceroute,是个“大骗子”。它会使用ICMP的规则,故意制造一些能够产生错误的场景。 -所以, **Traceroute的第一个作用就是故意设置特殊的TTL,来追踪去往目的地时沿途经过的路由器** 。Traceroute的参数指向某个目的IP地址,它会发送一个UDP的数据包。将TTL设置成1,也就是说一旦遇到一个路由器或者一个关卡,就表示它“牺牲”了。 +所以,**Traceroute的第一个作用就是故意设置特殊的TTL,来追踪去往目的地时沿途经过的路由器** 。Traceroute的参数指向某个目的IP地址,它会发送一个UDP的数据包。将TTL设置成1,也就是说一旦遇到一个路由器或者一个关卡,就表示它“牺牲”了。 如果中间的路由器不止一个,当然碰到第一个就“牺牲”。于是,返回一个ICMP包,也就是网络差错包,类型是时间超时。那大军前行就带一顿饭,试一试走多远会被饿死,然后找个哨探回来报告,那我就知道大军只带一顿饭能走多远了。 @@ -93,7 +93,7 @@ ping命令执行的时候,源主机首先会构建一个ICMP请求数据包, 怎么知道UDP有没有到达目的主机呢?Traceroute程序会发送一份UDP数据报给目的主机,但它会选择一个不可能的值作为UDP端口号(大于30000)。当该数据报到达时,将使目的主机的 UDP模块产生一份“端口不可达”错误ICMP报文。如果数据报没有到达,则可能是超时。 -这就相当于故意派人去西天如来那里去请一本《道德经》,结果人家信佛不信道,消息就会被打出来。被打的消息传回来,你就知道西天是能够到达的。为什么不去取《心经》呢?因为UDP是无连接的。也就是说这人一派出去,你就得不到任何音信。你无法区别到底是半路走丢了,还是真的信佛遁入空门了,只有让人家打出来,你才会得到消息。 **Traceroute还有一个作用是故意设置不分片,从而确定路径的MTU。** 要做的工作首先是发送分组,并设置“不分片”标志。发送的第一个分组的长度正好与出口MTU相等。如果中间遇到窄的关口会被卡住,会发送ICMP网络差错包,类型为“需要进行分片但设置了不分片位”。其实,这是人家故意的好吧,每次收到ICMP“不能分片”差错时就减小分组的长度,直到到达目标主机。 +这就相当于故意派人去西天如来那里去请一本《道德经》,结果人家信佛不信道,消息就会被打出来。被打的消息传回来,你就知道西天是能够到达的。为什么不去取《心经》呢?因为UDP是无连接的。也就是说这人一派出去,你就得不到任何音信。你无法区别到底是半路走丢了,还是真的信佛遁入空门了,只有让人家打出来,你才会得到消息。**Traceroute还有一个作用是故意设置不分片,从而确定路径的MTU。** 要做的工作首先是发送分组,并设置“不分片”标志。发送的第一个分组的长度正好与出口MTU相等。如果中间遇到窄的关口会被卡住,会发送ICMP网络差错包,类型为“需要进行分片但设置了不分片位”。其实,这是人家故意的好吧,每次收到ICMP“不能分片”差错时就减小分组的长度,直到到达目标主机。 ## 小结 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25408\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25408\350\256\262.md" index 2c992f068..b213893ad 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25408\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25408\350\256\262.md" @@ -8,7 +8,7 @@ 这个时候,你要在宿舍上网,有两个办法: -第一个办法,让你们宿舍长再买一个网卡。这个时候,你们宿舍长的电脑里就有两张网卡。一张网卡的线插到你们宿舍的交换机上,另一张网卡的线插到校园网的网口。而且,这张新的网卡的IP地址要按照学校网管部门分配的配置,不然上不了网。 **这种情况下,如果你们宿舍的人要上网,就需要一直开着宿舍长的电脑。** 第二个办法,你们共同出钱买个家庭路由器(反正当时我们买不起)。家庭路由器会有内网网口和外网网口。把外网网口的线插到校园网的网口上,将这个外网网口配置成和网管部的一样。内网网口连上你们宿舍的所有的电脑。 **这种情况下,如果你们宿舍的人要上网,就需要一直开着路由器。** 这两种方法其实是一样的。只不过第一种方式,让你的宿舍长的电脑,变成一个有多个口的路由器而已。而你买的家庭路由器,里面也跑着程序,和你宿舍长电脑里的功能一样,只不过是一个嵌入式的系统。 +第一个办法,让你们宿舍长再买一个网卡。这个时候,你们宿舍长的电脑里就有两张网卡。一张网卡的线插到你们宿舍的交换机上,另一张网卡的线插到校园网的网口。而且,这张新的网卡的IP地址要按照学校网管部门分配的配置,不然上不了网。**这种情况下,如果你们宿舍的人要上网,就需要一直开着宿舍长的电脑。** 第二个办法,你们共同出钱买个家庭路由器(反正当时我们买不起)。家庭路由器会有内网网口和外网网口。把外网网口的线插到校园网的网口上,将这个外网网口配置成和网管部的一样。内网网口连上你们宿舍的所有的电脑。**这种情况下,如果你们宿舍的人要上网,就需要一直开着路由器。** 这两种方法其实是一样的。只不过第一种方式,让你的宿舍长的电脑,变成一个有多个口的路由器而已。而你买的家庭路由器,里面也跑着程序,和你宿舍长电脑里的功能一样,只不过是一个嵌入式的系统。 当你的宿舍长能够上网之后,接下来,就是其他人的电脑怎么上网的问题。这就需要配置你们的 **网卡。当然DHCP是可以默认配置的。在进行网卡配置的时候,除了IP地址,还需要配置一个Gateway** 的东西,这个就是 **网关** 。 @@ -20,9 +20,9 @@ 在MAC头里面,先是目标MAC地址,然后是源MAC地址,然后有一个协议类型,用来说明里面是IP协议。IP头里面的版本号,目前主流的还是IPv4,服务类型TOS在第三节讲ip addr命令的时候讲过,TTL在第7节讲ICMP协议的时候讲过。另外,还有8位标识协议。这里到了下一层的协议,也就是,是TCP还是UDP。最重要的就是源IP和目标IP。先是源IP地址,然后是目标IP地址。 -在任何一台机器上,当要访问另一个IP地址的时候,都会先判断,这个目标IP地址,和当前机器的IP地址,是否在同一个网段。怎么判断同一个网段呢?需要CIDR和子网掩码,这个在第三节的时候也讲过了。 **如果是同一个网段** ,例如,你访问你旁边的兄弟的电脑,那就没网关什么事情,直接将源地址和目标地址放入IP头中,然后通过ARP获得MAC地址,将源MAC和目的MAC放入MAC头中,发出去就可以了。 **如果不是同一网段** ,例如,你要访问你们校园网里面的BBS,该怎么办?这就需要发往默认网关Gateway。Gateway的地址一定是和源IP地址是一个网段的。往往不是第一个,就是第二个。例如192.168.1.0/24这个网段,Gateway往往会是192.168.1.1/24或者192.168.1.2/24。 +在任何一台机器上,当要访问另一个IP地址的时候,都会先判断,这个目标IP地址,和当前机器的IP地址,是否在同一个网段。怎么判断同一个网段呢?需要CIDR和子网掩码,这个在第三节的时候也讲过了。**如果是同一个网段**,例如,你访问你旁边的兄弟的电脑,那就没网关什么事情,直接将源地址和目标地址放入IP头中,然后通过ARP获得MAC地址,将源MAC和目的MAC放入MAC头中,发出去就可以了。**如果不是同一网段**,例如,你要访问你们校园网里面的BBS,该怎么办?这就需要发往默认网关Gateway。Gateway的地址一定是和源IP地址是一个网段的。往往不是第一个,就是第二个。例如192.168.1.0/24这个网段,Gateway往往会是192.168.1.1/24或者192.168.1.2/24。 -如何发往默认网关呢?网关不是和源IP地址是一个网段的么?这个过程就和发往同一个网段的其他机器是一样的:将源地址和目标IP地址放入IP头中,通过ARP获得网关的MAC地址,将源MAC和网关的MAC放入MAC头中,发送出去。网关所在的端口,例如192.168.1.1/24将网络包收进来,然后接下来怎么做,就完全看网关的了。 **网关往往是一个路由器,是一个三层转发的设备。** 啥叫三层设备?前面也说过了,就是把MAC头和IP头都取下来,然后根据里面的内容,看看接下来把包往哪里转发的设备。 +如何发往默认网关呢?网关不是和源IP地址是一个网段的么?这个过程就和发往同一个网段的其他机器是一样的:将源地址和目标IP地址放入IP头中,通过ARP获得网关的MAC地址,将源MAC和网关的MAC放入MAC头中,发送出去。网关所在的端口,例如192.168.1.1/24将网络包收进来,然后接下来怎么做,就完全看网关的了。**网关往往是一个路由器,是一个三层转发的设备。** 啥叫三层设备?前面也说过了,就是把MAC头和IP头都取下来,然后根据里面的内容,看看接下来把包往哪里转发的设备。 在你的宿舍里面,网关就是你宿舍长的电脑。一个路由器往往有多个网口,如果是一台服务器做这个事情,则就有多个网卡,其中一个网卡是和源IP同网段的。 @@ -30,7 +30,7 @@ ## 静态路由是什么? -这个时候,问题来了,该选择哪一只手?IP头和MAC头加什么内容,哪些变、哪些不变呢?这个问题比较复杂,大致可以分为两类,一个是 **静态路由** ,一个是 **动态路由** 。动态路由下一节我们详细地讲。这一节我们先说静态路由。 **静态路由,其实就是在路由器上,配置一条一条规则。** 这些规则包括:想访问BBS站(它肯定有个网段),从2号口出去,下一跳是IP2;想访问教学视频站(它也有个自己的网段),从3号口出去,下一跳是IP3,然后保存在路由器里。 +这个时候,问题来了,该选择哪一只手?IP头和MAC头加什么内容,哪些变、哪些不变呢?这个问题比较复杂,大致可以分为两类,一个是 **静态路由**,一个是 **动态路由** 。动态路由下一节我们详细地讲。这一节我们先说静态路由。**静态路由,其实就是在路由器上,配置一条一条规则。** 这些规则包括:想访问BBS站(它肯定有个网段),从2号口出去,下一跳是IP2;想访问教学视频站(它也有个自己的网段),从3号口出去,下一跳是IP3,然后保存在路由器里。 每当要选择从哪只手抛出去的时候,就一条一条的匹配规则,找到符合的规则,就按规则中设置的那样,从某个口抛出去,找下一跳IPX。 @@ -77,7 +77,7 @@ 包到达服务器B,MAC地址匹配,将包收进来。 -通过这个过程可以看出,每到一个新的局域网,MAC都是要变的,但是IP地址都不变。在IP头里面,不会保存任何网关的IP地址。 **所谓的下一跳是,某个IP要将这个IP地址转换为MAC放入MAC头。** +通过这个过程可以看出,每到一个新的局域网,MAC都是要变的,但是IP地址都不变。在IP头里面,不会保存任何网关的IP地址。**所谓的下一跳是,某个IP要将这个IP地址转换为MAC放入MAC头。** 之所以将这种模式比喻称为欧洲十国游,是因为在整个过程中,IP头里面的地址都是不变的。IP地址在三个局域网都可见,在三个局域网之间的网段都不会冲突。在三个网段之间传输包,IP头不改变。这就像在欧洲各国之间旅游,一个签证就能搞定。 ![img](assets/016120f1bf46100812f1d1ccec1e517f.jpg) @@ -130,7 +130,7 @@ 从服务器B接收的包可以看出,源IP为服务器A的国际身份,因而发送返回包的时候,也发给这个国际身份,由路由器A做NAT,转换为国内身份。 -从这个过程可以看出,IP地址也会变。这个过程用英文说就是 **Network Address Translation** ,简称 **NAT** 。 +从这个过程可以看出,IP地址也会变。这个过程用英文说就是 **Network Address Translation**,简称 **NAT** 。 其实这第二种方式我们经常见,现在大家每家都有家用路由器,家里的网段都是192.168.1.x,所以你肯定访问不了你邻居家的这个私网的IP地址的。所以,当我们家里的包发出去的时候,都被家用路由器NAT成为了运营商的地址了。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25409\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25409\350\256\262.md" index 28ba11182..6eee643e9 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25409\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25409\350\256\262.md" @@ -131,7 +131,7 @@ default via 183.134.188.1 dev eth2 每个路由器根据新收集的信息,计算和其他路由器的距离,比如自己的一个邻居距离目标路由器的距离是M,而自己距离邻居是x,则自己距离目标路由器是x+M。 -这个算法比较简单,但是还是有问题。 **第一个问题就是好消息传得快,坏消息传得慢。** 如果有个路由器加入了这个网络,它的邻居就能很快发现它,然后将消息广播出去。要不了多久,整个网络就都知道了。但是一旦一个路由器挂了,挂的消息是没有广播的。当每个路由器发现原来的道路到不了这个路由器的时候,感觉不到它已经挂了,而是试图通过其他的路径访问,直到试过了所有的路径,才发现这个路由器是真的挂了。 +这个算法比较简单,但是还是有问题。**第一个问题就是好消息传得快,坏消息传得慢。** 如果有个路由器加入了这个网络,它的邻居就能很快发现它,然后将消息广播出去。要不了多久,整个网络就都知道了。但是一旦一个路由器挂了,挂的消息是没有广播的。当每个路由器发现原来的道路到不了这个路由器的时候,感觉不到它已经挂了,而是试图通过其他的路径访问,直到试过了所有的路径,才发现这个路由器是真的挂了。 我再举个例子。 @@ -139,7 +139,7 @@ default via 183.134.188.1 dev eth2 原来的网络包括两个节点,B和C。A加入了网络,它的邻居B很快就发现A启动起来了。于是它将自己和A的距离设为1,同样C也发现A起来了,将自己和A的距离设置为2。但是如果A挂掉,情况就不妙了。B本来和A是邻居,发现连不上A了,但是C还是能够连上,只不过距离远了点,是2,于是将自己的距离设置为3。殊不知C的距离2其实是基于原来自己的距离为1计算出来的。C发现自己也连不上A,并且发现B设置为3,于是自己改成距离4。依次类推,数越来越大,直到超过一个阈值,我们才能判定A真的挂了。 -这个道理有点像有人走丢了。当你突然发现找不到这个人了。于是你去学校问,是不是在他姨家呀?找到他姨家,他姨说,是不是在他舅舅家呀?他舅舅说,是不是在他姥姥家呀?他姥姥说,是不是在学校呀?总归要问一圈,或者是超过一定的时间,大家才会认为这个人的确走丢了。如果这个人其实只是去见了一个谁都不认识的网友去了,当这个人回来的时候,只要他随便见到其中的一个亲戚,这个亲戚就会拉着他到他的家长那里,说你赶紧回家,你妈都找你一天了。 **这种算法的第二个问题是,每次发送的时候,要发送整个全局路由表。** 网络大了,谁也受不了,所以最早的路由协议RIP就是这个算法。它适用于小型网络(小于15跳)。当网络规模都小的时候,没有问题。现在一个数据中心内部路由器数目就很多,因而不适用了。 +这个道理有点像有人走丢了。当你突然发现找不到这个人了。于是你去学校问,是不是在他姨家呀?找到他姨家,他姨说,是不是在他舅舅家呀?他舅舅说,是不是在他姥姥家呀?他姥姥说,是不是在学校呀?总归要问一圈,或者是超过一定的时间,大家才会认为这个人的确走丢了。如果这个人其实只是去见了一个谁都不认识的网友去了,当这个人回来的时候,只要他随便见到其中的一个亲戚,这个亲戚就会拉着他到他的家长那里,说你赶紧回家,你妈都找你一天了。**这种算法的第二个问题是,每次发送的时候,要发送整个全局路由表。** 网络大了,谁也受不了,所以最早的路由协议RIP就是这个算法。它适用于小型网络(小于15跳)。当网络规模都小的时候,没有问题。现在一个数据中心内部路由器数目就很多,因而不适用了。 所以上面的两个问题,限制了距离矢量路由的网络规模。 @@ -153,7 +153,7 @@ default via 183.134.188.1 dev eth2 ## 动态路由协议 -### 1.基于链路状态路由算法的OSPF **OSPF** ( **Open Shortest Path First** , **开放式最短路径优先** )就是这样一个基于链路状态路由协议,广泛应用在数据中心中的协议。由于主要用在数据中心内部,用于路由决策,因而称为 **内部网关协议** ( **Interior Gateway Protocol** ,简称 **IGP** ) +### 1.基于链路状态路由算法的OSPF **OSPF** ( **Open Shortest Path First**,**开放式最短路径优先** )就是这样一个基于链路状态路由协议,广泛应用在数据中心中的协议。由于主要用在数据中心内部,用于路由决策,因而称为 **内部网关协议** ( **Interior Gateway Protocol**,简称 **IGP** ) 内部网关协议的重点就是找到最短的路径。在一个组织内部,路径最短往往最优。当然有时候OSPF可以发现多个最短的路径,可以在这多个路径中进行负载均衡,这常常被称为 **等价路由** 。 @@ -165,7 +165,7 @@ default via 183.134.188.1 dev eth2 ### 2.基于距离矢量路由算法的BGP -但是外网的路由协议,也即国家之间的,又有所不同。我们称为 **外网路由协议** ( **Border Gateway Protocol** ,简称 **BGP** )。 +但是外网的路由协议,也即国家之间的,又有所不同。我们称为 **外网路由协议** ( **Border Gateway Protocol**,简称 **BGP** )。 在一个国家内部,有路当然选近的走。但是国家之间,不光远近的问题,还有政策的问题。例如,唐僧去西天取经,有的路近。但是路过的国家看不惯僧人,见了僧人就抓。例如灭法国,连光头都要抓。这样的情况即便路近,也最好绕远点走。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25410\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25410\350\256\262.md" index 2d760b2f8..b67701a14 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25410\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25410\350\256\262.md" @@ -8,7 +8,7 @@ 什么叫面向连接,什么叫无连接呢?在互通之前,面向连接的协议会先建立连接。例如,TCP会三次握手,而UDP不会。为什么要建立连接呢?你TCP三次握手,我UDP也可以发三个包玩玩,有什么区别吗? -**所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。** 例如, **TCP提供可靠交付** 。通过TCP连接传输的数据,无差错、不丢失、不重复、并且按序到达。我们都知道IP包是没有任何可靠性保证的,一旦发出去,就像西天取经,走丢了、被妖怪吃了,都只能随它去。但是TCP号称能做到那个连接维护的程序做的事情,这个下两节我会详细描述。而 **UDP继承了IP包的特性,不保证不丢失,不保证按顺序到达。** 再如, **TCP是面向字节流的** 。发送的时候发的是一个流,没头没尾。IP包可不是一个流,而是一个个的IP包。之所以变成了流,这也是TCP自己的状态维护做的事情。而 **UDP继承了IP的特性,基于数据报的,一个一个地发,一个一个地收。** 还有 **TCP是可以有拥塞控制的** 。它意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。 **UDP就不会,应用让我发,我就发,管它洪水滔天。** 因而 **TCP其实是一个有状态服务** ,通俗地讲就是有脑子的,里面精确地记着发送了没有,接收到没有,发送到哪个了,应该接收哪个了,错一点儿都不行。而 **UDP则是无状态服务。** 通俗地说是没脑子的,天真无邪的,发出去就发出去了。 +**所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。** 例如,**TCP提供可靠交付** 。通过TCP连接传输的数据,无差错、不丢失、不重复、并且按序到达。我们都知道IP包是没有任何可靠性保证的,一旦发出去,就像西天取经,走丢了、被妖怪吃了,都只能随它去。但是TCP号称能做到那个连接维护的程序做的事情,这个下两节我会详细描述。而 **UDP继承了IP包的特性,不保证不丢失,不保证按顺序到达。** 再如,**TCP是面向字节流的** 。发送的时候发的是一个流,没头没尾。IP包可不是一个流,而是一个个的IP包。之所以变成了流,这也是TCP自己的状态维护做的事情。而 **UDP继承了IP的特性,基于数据报的,一个一个地发,一个一个地收。** 还有 **TCP是可以有拥塞控制的** 。它意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。**UDP就不会,应用让我发,我就发,管它洪水滔天。** 因而 **TCP其实是一个有状态服务**,通俗地讲就是有脑子的,里面精确地记着发送了没有,接收到没有,发送到哪个了,应该接收哪个了,错一点儿都不行。而 **UDP则是无状态服务。** 通俗地说是没脑子的,天真无邪的,发出去就发出去了。 我们可以这样比喻,如果MAC层定义了本地局域网的传输行为,IP层定义了整个网络端到端的传输行为,这两层基本定义了这样的基因:网络传输是以包为单位的,二层叫帧,网络层叫包,传输层叫段。我们笼统地称为包。包单独传输,自行选路,在不同的设备封装解封装,不保证到达。基于这个基因,生下来的孩子UDP完全继承了这些特性,几乎没有自己的思想。 @@ -30,21 +30,21 @@ UDP就像小孩子一样,有以下这些特点: -第一, **沟通简单** ,不需要一肚子花花肠子(大量的数据结构、处理逻辑、包头字段)。前提是它相信网络世界是美好的,秉承性善论,相信网络通路默认就是很容易送达的,不容易被丢弃的。 +第一,**沟通简单**,不需要一肚子花花肠子(大量的数据结构、处理逻辑、包头字段)。前提是它相信网络世界是美好的,秉承性善论,相信网络通路默认就是很容易送达的,不容易被丢弃的。 -第二, **轻信他人** 。它不会建立连接,虽然有端口号,但是监听在这个地方,谁都可以传给他数据,他也可以传给任何人数据,甚至可以同时传给多个人数据。 +第二,**轻信他人** 。它不会建立连接,虽然有端口号,但是监听在这个地方,谁都可以传给他数据,他也可以传给任何人数据,甚至可以同时传给多个人数据。 -第三, **愣头青,做事不懂权变** 。不知道什么时候该坚持,什么时候该退让。它不会根据网络的情况进行发包的拥塞控制,无论网络丢包丢成啥样了,它该怎么发还怎么发。 +第三,**愣头青,做事不懂权变** 。不知道什么时候该坚持,什么时候该退让。它不会根据网络的情况进行发包的拥塞控制,无论网络丢包丢成啥样了,它该怎么发还怎么发。 ## UDP的三大使用场景 基于UDP这种“小孩子”的特点,我们可以考虑在以下的场景中使用。 -第一, **需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用** 。这很好理解,就像如果你是领导,你会让你们组刚毕业的小朋友去做一些没有那么难的项目,打一些没有那么难的客户,或者做一些失败了也能忍受的实验性项目。 +第一,**需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用** 。这很好理解,就像如果你是领导,你会让你们组刚毕业的小朋友去做一些没有那么难的项目,打一些没有那么难的客户,或者做一些失败了也能忍受的实验性项目。 我们在第四节讲的DHCP就是基于UDP协议的。一般的获取IP地址都是内网请求,而且一次获取不到IP又没事,过一会儿还有机会。我们讲过PXE可以在启动的时候自动安装操作系统,操作系统镜像的下载使用的TFTP,这个也是基于UDP协议的。在还没有操作系统的时候,客户端拥有的资源很少,不适合维护一个复杂的状态机,而是因为是内网,一般也没啥问题。 -第二, **不需要一对一沟通,建立连接,而是可以广播的应用** 。咱们小时候人都很简单,大家在班级里面,谁成绩好,谁写作好,应该表扬谁惩罚谁,谁得几个小红花都是当着全班的面讲的,公平公正公开。长大了人心复杂了,薪水、奖金要背靠背,和员工一对一沟通。 +第二,**不需要一对一沟通,建立连接,而是可以广播的应用** 。咱们小时候人都很简单,大家在班级里面,谁成绩好,谁写作好,应该表扬谁惩罚谁,谁得几个小红花都是当着全班的面讲的,公平公正公开。长大了人心复杂了,薪水、奖金要背靠背,和员工一对一沟通。 UDP的不面向连接的功能,可以使得可以承载广播或者多播的协议。DHCP就是一种广播的形式,就是基于UDP协议的,而广播包的格式前面说过了。 @@ -52,7 +52,7 @@ UDP的不面向连接的功能,可以使得可以承载广播或者多播的 在后面云中网络部分,有一个协议VXLAN,也是需要用到组播,也是基于UDP协议的。 -第三, **需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候** 。记得曾国藩建立湘军的时候,专门招出生牛犊不怕虎的新兵,而不用那些“老油条”的八旗兵,就是因为八旗兵经历的事情多,遇到敌军不敢舍死忘生。 +第三,**需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候** 。记得曾国藩建立湘军的时候,专门招出生牛犊不怕虎的新兵,而不用那些“老油条”的八旗兵,就是因为八旗兵经历的事情多,遇到敌军不敢舍死忘生。 同理,UDP简单、处理速度快,不像TCP那样,操这么多的心,各种重传啊,保证顺序啊,前面的不收到,后面的没法处理啊。不然等这些事情做完了,时延早就上去了。而TCP在网络不好出现丢包的时候,拥塞控制策略会主动的退缩,降低发送速度,这就相当于本来环境就差,还自断臂膀,用户本来就卡,这下更卡了。 @@ -70,7 +70,7 @@ UDP的不面向连接的功能,可以使得可以承载广播或者多播的 原来访问网页和手机APP都是基于HTTP协议的。HTTP协议是基于TCP的,建立连接都需要多次交互,对于时延比较大的目前主流的移动互联网来讲,建立一次连接需要的时间会比较长,然而既然是移动中,TCP可能还会断了重连,也是很耗时的。而且目前的HTTP协议,往往采取多个数据通道共享一个连接的情况,这样本来为了加快传输速度,但是TCP的严格顺序策略使得哪怕共享通道,前一个不来,后一个和前一个即便没关系,也要等着,时延也会加大。 -而 **QUIC** (全称 **Quick UDP Internet Connections** , **快速UDP互联网连接** )是Google提出的一种基于UDP改进的通信协议,其目的是降低网络通信的延迟,提供更好的用户互动体验。 +而 **QUIC** (全称 **Quick UDP Internet Connections**,**快速UDP互联网连接** )是Google提出的一种基于UDP改进的通信协议,其目的是降低网络通信的延迟,提供更好的用户互动体验。 QUIC在应用层上,会自己实现快速连接建立、减少重传时延,自适应拥塞控制,是应用层“城会玩”的代表。这一节主要是讲UDP,QUIC我们放到应用层去讲。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25411\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25411\350\256\262.md" index de3684734..9831a7a6e 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25411\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25411\350\256\262.md" @@ -122,7 +122,7 @@ A收到“B说知道了”,就进入FIN_WAIT_2的状态,如果这个时候B A直接跑路还有一个问题是,A的端口就直接空出来了,但是B不知道,B原来发过的很多包很可能还在路上,如果A的端口被一个新的应用占用了,这个新的应用会收到上个连接中B发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来B发送的所有的包都死翘翘,再空出端口来。 -等待的时间设为2MSL, **MSL** 是 **Maximum Segment Lifetime** , **报文最大生存时间** ,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL域,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。协议规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。 +等待的时间设为2MSL,**MSL** 是 **Maximum Segment Lifetime**,**报文最大生存时间**,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL域,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。协议规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。 还有一个异常情况就是,B超过了2MSL的时间,依然没有收到它发的FIN的ACK,怎么办呢?按照TCP的原理,B当然还会重发FIN,这个时候A再收到这个包之后,A就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送RST,B就知道A早就跑了。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25412\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25412\350\256\262.md" index ffa5a91eb..c78a5b416 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25412\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25412\350\256\262.md" @@ -1,6 +1,6 @@ # 12 讲TCP协议(下):西行必定多妖孽,恒心智慧消磨难 -我们前面说到玄奘西行,要出网关。既然出了网关,那就是在公网上传输数据,公网往往是不可靠的,因而需要很多的机制去保证传输的可靠性,这里面需要恒心,也即各种 **重传的策略** ,还需要有智慧,也就是说,这里面包含着 **大量的算法** 。 +我们前面说到玄奘西行,要出网关。既然出了网关,那就是在公网上传输数据,公网往往是不可靠的,因而需要很多的机制去保证传输的可靠性,这里面需要恒心,也即各种 **重传的策略**,还需要有智慧,也就是说,这里面包含着 **大量的算法** 。 ## 如何做个靠谱的人? @@ -88,11 +88,11 @@ AdvertisedWindow其实是MaxRcvBuffer减去A。 假设4的确认到了,不幸的是,5的ACK丢了,6、7的数据包丢了,这该怎么办呢? -一种方法就是 **超时重试** ,也即对每一个发送了,但是没有ACK的包,都有设一个定时器,超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?这个时间不宜过短,时间必须大于往返时间RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。 +一种方法就是 **超时重试**,也即对每一个发送了,但是没有ACK的包,都有设一个定时器,超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?这个时间不宜过短,时间必须大于往返时间RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。 估计往返时间,需要TCP通过采样RTT的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断的变化。除了采样RTT,还要采样RTT的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为 **自适应重传算法** ( **Adaptive Retransmission Algorithm** )。 -如果过一段时间,5、6、7都超时了,就会重新发送。接收方发现5原来接收过,于是丢弃5;6收到了,发送ACK,要求下一个是7,7不幸又丢了。当7再次超时的时候,有需要重传的时候,TCP的策略是 **超时间隔加倍** 。 **每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍** 。 **两次超时,就说明网络环境差,不宜频繁反复发送。** 超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢? +如果过一段时间,5、6、7都超时了,就会重新发送。接收方发现5原来接收过,于是丢弃5;6收到了,发送ACK,要求下一个是7,7不幸又丢了。当7再次超时的时候,有需要重传的时候,TCP的策略是 **超时间隔加倍** 。**每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍** 。**两次超时,就说明网络环境差,不宜频繁反复发送。** 超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢? 有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。 @@ -160,7 +160,7 @@ AdvertisedWindow其实是MaxRcvBuffer减去A。 这个时候,我们可以想其他的办法,例如这个四个设备本来每秒处理一个包,但是我们在这些设备上加缓存,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加时延,这个缓存的包,4s肯定到达不了接收端了,如果时延达到一定程度,就会超时重传,也是我们不想看到的。 -于是TCP的拥塞控制主要来避免两种现象, **包丢失** 和 **超时重传** 。一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢? +于是TCP的拥塞控制主要来避免两种现象,**包丢失** 和 **超时重传** 。一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢? 如果我们通过漏斗往瓶子里灌水,我们就知道,不能一桶水一下子倒进去,肯定会溅出来,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动。 @@ -178,7 +178,7 @@ AdvertisedWindow其实是MaxRcvBuffer减去A。 ![img](assets/1910bc1a0048d4de7b2128eb0f5dbcd2-1584286484075.jpg) -就像前面说的一样,正是这种知进退,使得时延很重要的情况下,反而降低了速度。但是如果你仔细想一下,TCP的拥塞控制主要来避免的两个现象都是有问题的。 **第一个问题** 是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。 **第二个问题** 是TCP的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实TCP只要填满管道就可以了,不应该接着填,直到连缓存也填满。 +就像前面说的一样,正是这种知进退,使得时延很重要的情况下,反而降低了速度。但是如果你仔细想一下,TCP的拥塞控制主要来避免的两个现象都是有问题的。**第一个问题** 是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。**第二个问题** 是TCP的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实TCP只要填满管道就可以了,不应该接着填,直到连缓存也填满。 为了优化这两个问题,后来有了 **TCP BBR拥塞算法** 。它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25413\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25413\350\256\262.md" index 81b736a64..a19368848 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25413\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25413\350\256\262.md" @@ -24,7 +24,7 @@ TCP的服务端要先监听一个端口,一般是先调用bind函数,给这 在服务端等待的时候,客户端可以通过connect函数发起连接。先在参数中指明要连接的IP地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept就会返回另一个Socket。 -这是一个经常考的知识点,就是监听的Socket和真正用来传数据的Socket是两个,一个叫作 **监听Socket** ,一个叫作 **已连接Socket** 。 +这是一个经常考的知识点,就是监听的Socket和真正用来传数据的Socket是两个,一个叫作 **监听Socket**,一个叫作 **已连接Socket** 。 连接建立成功之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西一样。 @@ -38,7 +38,7 @@ TCP的服务端要先监听一个端口,一般是先调用bind函数,给这 这个数组中的内容是一个指针,指向内核中所有打开的文件的列表。既然是一个文件,就会有一个inode,只不过Socket对应的inode不像真正的文件系统一样,保存在硬盘上的,而是在内存中的。在这个inode中,指向了Socket在内核中的Socket结构。 -在这个结构里面,主要的是两个队列,一个是 **发送队列** ,一个是 **接收队列** 。在这两个队列里面保存的是一个缓存sk_buff。这个缓存里面能够看到完整的包的结构。看到这个,是不是能和前面讲过的收发包的场景联系起来了? +在这个结构里面,主要的是两个队列,一个是 **发送队列**,一个是 **接收队列** 。在这两个队列里面保存的是一个缓存sk_buff。这个缓存里面能够看到完整的包的结构。看到这个,是不是能和前面讲过的收发包的场景联系起来了? 整个数据结构我也画了一张图。 @@ -60,7 +60,7 @@ TCP的服务端要先监听一个端口,一般是先调用bind函数,给这 那作为老板你就要想了,我最多能接多少项目呢?当然是越多越好。 -我们先来算一下理论值,也就是 **最大连接数** ,系统会用一个四元组来标识一个TCP连接。 +我们先来算一下理论值,也就是 **最大连接数**,系统会用一个四元组来标识一个TCP连接。 ```bash {本机IP, 本机端口, 对端IP, 对端端口} @@ -68,7 +68,7 @@ TCP的服务端要先监听一个端口,一般是先调用bind函数,给这 服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,服务端端TCP连接四元组中只有对端IP, 也就是客户端的IP和对端的端口,也即客户端的端口是可变的,因此,最大TCP连接数=客户端IP数×客户端端口数。对IPv4,客户端的IP数最多为2的32次方,客户端的端口数最多为2的16次方,也就是服务端单机最大TCP连接数,约为2的48次方。 -当然,服务端最大并发TCP连接数远不能达到理论上限。首先主要是 **文件描述符限制** ,按照上面的原理,Socket都是文件,所以首先要通过ulimit配置文件描述符的数目;另一个限制是 **内存** ,按上面的数据结构,每个TCP连接都要占用一定内存,操作系统是有限的。 +当然,服务端最大并发TCP连接数远不能达到理论上限。首先主要是 **文件描述符限制**,按照上面的原理,Socket都是文件,所以首先要通过ulimit配置文件描述符的数目;另一个限制是 **内存**,按上面的数据结构,每个TCP连接都要占用一定内存,操作系统是有限的。 所以,作为老板,在资源有限的情况下,要想接更多的项目,就需要降低每个项目消耗的资源数目。 @@ -98,7 +98,7 @@ TCP的服务端要先监听一个端口,一般是先调用bind函数,给这 ![img](assets/ab6e0ecfee5e21f7a563999a94bd8bd7.jpg) 新的线程也可以通过已连接Socket处理请求,从而达到并发处理的目的。 -上面基于进程或者线程模型的,其实还是有问题的。新到来一个TCP连接,就需要分配一个进程或者线程。一台机器无法创建很多进程或者线程。有个 **C10K** ,它的意思是一台机器要维护1万个连接,就要创建1万个进程或者线程,那么操作系统是无法承受的。如果维持1亿用户在线需要10万台服务器,成本也太高了。 +上面基于进程或者线程模型的,其实还是有问题的。新到来一个TCP连接,就需要分配一个进程或者线程。一台机器无法创建很多进程或者线程。有个 **C10K**,它的意思是一台机器要维护1万个连接,就要创建1万个进程或者线程,那么操作系统是无法承受的。如果维持1亿用户在线需要10万台服务器,成本也太高了。 其实C10K问题就是,你接项目接的太多了,如果每个项目都成立单独的项目组,就要招聘10万人,你肯定养不起,那怎么办呢? @@ -106,7 +106,7 @@ TCP的服务端要先监听一个端口,一般是先调用bind函数,给这 当然,一个项目组可以看多个项目了。这个时候,每个项目组都应该有个项目进度墙,将自己组看的项目列在那里,然后每天通过项目墙看每个项目的进度,一旦某个项目有了进展,就派人去盯一下。 -由于Socket是文件描述符,因而某个线程盯的所有的Socket,都放在一个文件描述符集合fd_set中,这就是 **项目进度墙** ,然后调用select函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在fd_set对应的位都设为1,表示Socket可读或者可写,从而可以进行读写操作,然后再调用select,接着盯着下一轮的变化。。 +由于Socket是文件描述符,因而某个线程盯的所有的Socket,都放在一个文件描述符集合fd_set中,这就是 **项目进度墙**,然后调用select函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在fd_set对应的位都设为1,表示Socket可读或者可写,从而可以进行读写操作,然后再调用select,接着盯着下一轮的变化。。 ### 方式四:一个项目组支撑多个项目(IO多路复用,从“派人盯着”到“有事通知”) @@ -122,7 +122,7 @@ TCP的服务端要先监听一个端口,一般是先调用bind函数,给这 当epoll_ctl添加一个Socket的时候,其实是加入这个红黑树,同时红黑树里面的节点指向一个结构,将这个结构挂在被监听的Socket的事件列表中。当一个Socket来了一个事件的时候,可以从这个列表中得到epoll对象,并调用call back通知它。 -这种通知方式使得监听的Socket数据增加的时候,效率不会大幅度降低,能够同时监听的Socket的数目也非常的多了。上限就为系统定义的、进程打开的最大文件描述符个数。因而, **epoll被称为解决C10K问题的利器** 。 +这种通知方式使得监听的Socket数据增加的时候,效率不会大幅度降低,能够同时监听的Socket的数目也非常的多了。上限就为系统定义的、进程打开的最大文件描述符个数。因而,**epoll被称为解决C10K问题的利器** 。 ## 小结 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25414\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25414\350\256\262.md" index c72c5ce5e..9d319db51 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25414\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25414\350\256\262.md" @@ -1,6 +1,6 @@ # 14 讲HTTP协议:看个新闻原来这么麻烦 -前面讲述完 **传输层** ,接下来开始讲 **应用层** 的协议。从哪里开始讲呢,就从咱们最常用的HTTP协议开始。 +前面讲述完 **传输层**,接下来开始讲 **应用层** 的协议。从哪里开始讲呢,就从咱们最常用的HTTP协议开始。 HTTP协议,几乎是每个人上网用的第一个协议,同时也是很容易被人忽略的协议。 @@ -26,7 +26,7 @@ HTTP协议,几乎是每个人上网用的第一个协议,同时也是很容 ![img](assets/10ff27d1032bf32393195f23ef2f9874.jpg) -HTTP的报文大概分为三大部分。第一部分是 **请求行** ,第二部分是请求的 **首部** ,第三部分才是请求的 **正文实体** 。 +HTTP的报文大概分为三大部分。第一部分是 **请求行**,第二部分是请求的 **首部**,第三部分才是请求的 **正文实体** 。 ### 第一部分:请求行 @@ -42,7 +42,7 @@ HTTP的报文大概分为三大部分。第一部分是 **请求行** ,第二 再如,在云计算里,如果我们的服务器端,要提供一个基于HTTP协议的创建云主机的API,也会用到POST方法。这个时候往往需要将“我要创建多大的云主机?多少CPU多少内存?多大硬盘?”这些信息放在JSON字符串里面,通过POST的方法告诉服务器端。 -还有一种类型叫 **PUT** ,就是向指定资源位置上传最新内容。但是,HTTP的服务器往往是不允许上传文件的,所以PUT和POST就都变成了要传给服务器东西的方法。 +还有一种类型叫 **PUT**,就是向指定资源位置上传最新内容。但是,HTTP的服务器往往是不允许上传文件的,所以PUT和POST就都变成了要传给服务器东西的方法。 在实际使用过程中,这两者还会有稍许的区别。POST往往是用来创建一个资源的,而PUT往往是用来修改一个资源的。 @@ -54,9 +54,9 @@ HTTP的报文大概分为三大部分。第一部分是 **请求行** ,第二 请求行下面就是我们的首部字段。首部是key value,通过冒号分隔。这里面,往往保存了一些非常重要的字段。 -例如, **Accept-Charset** ,表示 **客户端可以接受的字符集** 。防止传过来的是另外的字符集,从而导致出现乱码。 +例如,**Accept-Charset**,表示 **客户端可以接受的字符集** 。防止传过来的是另外的字符集,从而导致出现乱码。 -再如, **Content-Type** 是指 **正文的格式** 。例如,我们进行POST的请求,如果正文是JSON,那么我们就应该将这个值设置为JSON。 +再如,**Content-Type** 是指 **正文的格式** 。例如,我们进行POST的请求,如果正文是JSON,那么我们就应该将这个值设置为JSON。 这里需要重点说一下的就是 **缓存** 。为啥要使用缓存呢?那是因为一个非常大的页面有很多东西。 @@ -70,9 +70,9 @@ HTTP的报文大概分为三大部分。第一部分是 **请求行** ,第二 其中DNS、CDN我在后面的章节会讲。和这一节关系比较大的就是Nginx这一层,它如何处理HTTP协议呢?对于静态资源,有Vanish缓存层。当缓存过期的时候,才会访问真正的Tomcat应用集群。 -在HTTP头里面, **Cache-control** 是用来 **控制缓存** 的。当客户端发送的请求中包含max-age指令时,如果判定缓存层中,资源的缓存时间数值比指定时间的数值小,那么客户端可以接受缓存的资源;当指定max-age值为0,那么缓存层通常需要将请求转发给应用集群。 +在HTTP头里面,**Cache-control** 是用来 **控制缓存** 的。当客户端发送的请求中包含max-age指令时,如果判定缓存层中,资源的缓存时间数值比指定时间的数值小,那么客户端可以接受缓存的资源;当指定max-age值为0,那么缓存层通常需要将请求转发给应用集群。 -另外, **If-Modified-Since** 也是一个关于缓存的。也就是说,如果服务器的资源在某个时间之后更新了,那么客户端就应该下载最新的资源;如果没有更新,服务端会返回“304 Not Modified”的响应,那客户端就不用下载了,也会节省带宽。 +另外,**If-Modified-Since** 也是一个关于缓存的。也就是说,如果服务器的资源在某个时间之后更新了,那么客户端就应该下载最新的资源;如果没有更新,服务端会返回“304 Not Modified”的响应,那客户端就不用下载了,也会节省带宽。 到此为止,我们仅仅是拼凑起了HTTP请求的报文格式,接下来,浏览器会把它交给下一层传输层。怎么交给传输层呢?其实也无非是用Socket这些东西,只不过用的浏览器里,这些程序不需要你自己写,有人已经帮你写好了。 @@ -104,9 +104,9 @@ HTTP的返回报文也是有一定格式的。这也是基于HTTP 1.1的。 接下来是返回首部的 **key value** 。 -这里面, **Retry-After** 表示,告诉客户端应该在多长时间以后再次尝试一下。“503错误”是说“服务暂时不再和这个值配合使用”。 +这里面,**Retry-After** 表示,告诉客户端应该在多长时间以后再次尝试一下。“503错误”是说“服务暂时不再和这个值配合使用”。 -在返回的头部里面也会有 **Content-Type** ,表示返回的是HTML,还是JSON。 +在返回的头部里面也会有 **Content-Type**,表示返回的是HTML,还是JSON。 构造好了返回的HTTP报文,接下来就是把这个报文发送出去。还是交给Socket去发送,还是交给TCP层,让TCP层将返回的HTML,也分成一个个小的段,并且保证每个段都可靠到达。 @@ -128,7 +128,7 @@ HTTP 1.1在应用层以纯文本的形式进行通信。每次通信都要带完 另外,HTTP 2.0协议将一个TCP的连接中,切分成多个流,每个流都有自己的ID,而且流可以是客户端发往服务端,也可以是服务端发往客户端。它其实只是一个虚拟的通道。流是有优先级的。 -HTTP 2.0还将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。常见的帧有 **Header帧** ,用于传输Header内容,并且会开启一个新的流。再就是 **Data帧** ,用来传输正文实体。多个Data帧属于同一个流。 +HTTP 2.0还将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。常见的帧有 **Header帧**,用于传输Header内容,并且会开启一个新的流。再就是 **Data帧**,用来传输正文实体。多个Data帧属于同一个流。 通过这两种机制,HTTP 2.0的客户端可以将多个请求分到不同的流中,然后将请求内容拆成帧,进行二进制传输。这些帧可以打散乱序发送, 然后根据每个帧首部的流标识符重新组装,并且可以根据优先级,决定优先处理哪个流的数据。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25415\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25415\350\256\262.md" index 1329a36b2..b0eb23168 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25415\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25415\350\256\262.md" @@ -4,7 +4,7 @@ 你发送一个请求,说我要点个外卖,但是这个网络包被截获了,于是在服务器回复你之前,黑客先假装自己就是外卖网站,然后给你回复一个假的消息说:“好啊好啊,来来来,银行卡号、密码拿来。”如果这时候你真把银行卡密码发给它,那你就真的上套了。 -那怎么解决这个问题呢?当然一般的思路就是 **加密** 。加密分为两种方式一种是 **对称加密** ,一种是 **非对称加密** 。 +那怎么解决这个问题呢?当然一般的思路就是 **加密** 。加密分为两种方式一种是 **对称加密**,一种是 **非对称加密** 。 在对称加密算法中,加密和解密使用的密钥是相同的。也就是说,加密和解密使用的是同一个密钥。因此,对称加密算法要保证安全性的话,密钥要做好保密。只能让使用的人知道,不能对外公开。 @@ -58,7 +58,7 @@ openssl rsa -in cliu8siteprivate.key -pubout -outcliu8sitepublic.pem 这个时候就需要权威部门的介入了,就像每个人都可以打印自己的简历,说自己是谁,但是有公安局盖章的,就只有户口本,这个才能证明你是你。这个由权威部门颁发的称为 **证书** ( **Certificate** )。 -证书里面有什么呢?当然应该有 **公钥** ,这是最重要的;还有证书的 **所有者** ,就像户口本上有你的姓名和身份证号,说明这个户口本是你的;另外还有证书的 **发布机构** 和证书的 **有效期** ,这个有点像身份证上的机构是哪个区公安局,有效期到多少年。 +证书里面有什么呢?当然应该有 **公钥**,这是最重要的;还有证书的 **所有者**,就像户口本上有你的姓名和身份证号,说明这个户口本是你的;另外还有证书的 **发布机构** 和证书的 **有效期**,这个有点像身份证上的机构是哪个区公安局,有效期到多少年。 这个证书是怎么生成的呢?会不会有人假冒权威机构颁发证书呢?就像有假身份证、假户口本一样。生成证书需要发起一个证书请求,然后将这个请求发给一个权威机构去认证,这个权威机构我们称为 **CA** ( **Certificate Authority** )。 @@ -92,9 +92,9 @@ openssl x509 -in cliu8sitecertificate.pem -noout -text 你有没有发现,又有新问题了。要想验证证书,需要CA的公钥,问题是,你怎么确定CA的公钥就是对的呢? -所以,CA的公钥也需要更牛的CA给它签名,然后形成CA的证书。要想知道某个CA的证书是否可靠,要看CA的上级证书的公钥,能不能解开这个CA的签名。就像你不相信区公安局,可以打电话问市公安局,让市公安局确认区公安局的合法性。这样层层上去,直到全球皆知的几个著名大CA,称为 **root CA** ,做最后的背书。通过这种 **层层授信背书** 的方式,从而保证了非对称加密模式的正常运转。 +所以,CA的公钥也需要更牛的CA给它签名,然后形成CA的证书。要想知道某个CA的证书是否可靠,要看CA的上级证书的公钥,能不能解开这个CA的签名。就像你不相信区公安局,可以打电话问市公安局,让市公安局确认区公安局的合法性。这样层层上去,直到全球皆知的几个著名大CA,称为 **root CA**,做最后的背书。通过这种 **层层授信背书** 的方式,从而保证了非对称加密模式的正常运转。 -除此之外,还有一种证书,称为 **Self-Signed Certificate** ,就是自己给自己签名。这个给人一种“我就是我,你爱信不信”的感觉。这里我就不多说了。 +除此之外,还有一种证书,称为 **Self-Signed Certificate**,就是自己给自己签名。这个给人一种“我就是我,你爱信不信”的感觉。这里我就不多说了。 ## HTTPS的工作模式 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25416\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25416\350\256\262.md" index b091a69c0..4dd50d903 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25416\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25416\350\256\262.md" @@ -26,7 +26,7 @@ 是不是不算不知道,一算吓一跳?这个数据量实在是太大,根本没办法存储和传输。如果这样存储,你的硬盘很快就满了;如果这样传输,那多少带宽也不够用啊! -## 怎么办呢?人们想到了 **编码** ,就是看如何用尽量少的Bit数保存视频,使播放的时候画面看起来仍然很精美。 **编码是一个压缩的过程。** 视频和图片的压缩过程有什么特点? +## 怎么办呢?人们想到了 **编码**,就是看如何用尽量少的Bit数保存视频,使播放的时候画面看起来仍然很精美。**编码是一个压缩的过程。** 视频和图片的压缩过程有什么特点? 之所以能够对视频流中的图片进行压缩,因为视频和图片有这样一些特点。 @@ -44,7 +44,7 @@ 能不能形成一定的标准呢?要不然开发视频播放的人得累死了。当然能,我这里就给你介绍,视频编码的两大流派。 - 流派一:ITU(International Telecommunications Union)的VCEG(Video Coding Experts Group),这个称为 **国际电联下的VCEG** 。既然是电信,可想而知,他们最初做视频编码,主要侧重传输。名词系列二,就是这个组织制定的标准。 -- 流派二:ISO(International Standards Organization)的MPEG(Moving Picture Experts Group),这个是 **ISO旗下的MPEG** ,本来是做视频存储的。例如,编码后保存在VCD和DVD中。当然后来也慢慢侧重视频传输了。名词系列三,就是这个组织制定的标准。 +- 流派二:ISO(International Standards Organization)的MPEG(Moving Picture Experts Group),这个是 **ISO旗下的MPEG**,本来是做视频存储的。例如,编码后保存在VCD和DVD中。当然后来也慢慢侧重视频传输了。名词系列三,就是这个组织制定的标准。 后来,ITU-T(国际电信联盟电信标准化部门,ITU Telecommunication Standardization Sector)与MPEG联合制定了H.264/MPEG-4 AVC,这才是我们这一节要重点关注的。 @@ -58,11 +58,11 @@ 网络协议将 **编码** 好的视频流,从主播端推送到服务器,在服务器上有个运行了同样协议的服务端来接收这些网络包,从而得到里面的视频流,这个过程称为 **接流** 。 -服务端接到视频流之后,可以对视频流进行一定的处理,例如 **转码** ,也即从一个编码格式,转成另一种格式。因为观众使用的客户端千差万别,要保证他们都能看到直播。 **流处理** 完毕之后,就可以等待观众的客户端来请求这些视频流。观众的客户端请求的过程称为 **拉流** 。 +服务端接到视频流之后,可以对视频流进行一定的处理,例如 **转码**,也即从一个编码格式,转成另一种格式。因为观众使用的客户端千差万别,要保证他们都能看到直播。**流处理** 完毕之后,就可以等待观众的客户端来请求这些视频流。观众的客户端请求的过程称为 **拉流** 。 -如果有非常多的观众,同时看一个视频直播,那都从一个服务器上 **拉流** ,压力太大了,因而需要一个视频的 **分发** 网络,将视频预先加载到就近的边缘节点,这样大部分观众看的视频,是从边缘节点拉取的,就能降低服务器的压力。 +如果有非常多的观众,同时看一个视频直播,那都从一个服务器上 **拉流**,压力太大了,因而需要一个视频的 **分发** 网络,将视频预先加载到就近的边缘节点,这样大部分观众看的视频,是从边缘节点拉取的,就能降低服务器的压力。 -当观众的客户端将视频流拉下来之后,就需要进行 **解码** ,也即通过上述过程的逆过程,将一串串看不懂的二进制,再转变成一帧帧生动的图片,在客户端 **播放** 出来,这样你就能看到美女帅哥啦。 +当观众的客户端将视频流拉下来之后,就需要进行 **解码**,也即通过上述过程的逆过程,将一串串看不懂的二进制,再转变成一帧帧生动的图片,在客户端 **播放** 出来,这样你就能看到美女帅哥啦。 整个直播过程,可以用这个的图来描述。 @@ -74,9 +74,9 @@ 虽然我们说视频是一张张图片的序列,但是如果每张图片都完整,就太大了,因而会将视频序列分成三种帧。 -- **I帧** ,也称关键帧。里面是完整的图片,只需要本帧数据,就可以完成解码。 -- **P帧** ,前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面,叠加上和本帧定义的差别,生成最终画面。 -- **B帧** ,双向预测内插编码帧。B帧记录的是本帧与前后帧的差别。要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的数据与本帧数据的叠加,取得最终的画面。 +- **I帧**,也称关键帧。里面是完整的图片,只需要本帧数据,就可以完成解码。 +- **P帧**,前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面,叠加上和本帧定义的差别,生成最终画面。 +- **B帧**,双向预测内插编码帧。B帧记录的是本帧与前后帧的差别。要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的数据与本帧数据的叠加,取得最终的画面。 可以看出,I帧最完整,B帧压缩率最高,而压缩后帧的序列,应该是在IBBP的间隔出现的。这就是 **通过时序进行编码** 。 @@ -84,7 +84,7 @@ 在一帧中,分成多个片,每个片中分成多个宏块,每个宏块分成多个子块,这样将一张大的图分解成一个个小块,可以方便进行 **空间上的编码** 。 -尽管时空非常立体的组成了一个序列,但是总归还是要压缩成一个二进制流。这个流是有结构的,是一个个的 **网络提取层单元** ( **NALU** , **Network Abstraction Layer Unit** )。变成这种格式就是为了传输,因为网络上的传输,默认的是一个个的包,因而这里也就分成了一个个的单元。 +尽管时空非常立体的组成了一个序列,但是总归还是要压缩成一个二进制流。这个流是有结构的,是一个个的 **网络提取层单元** ( **NALU**,**Network Abstraction Layer Unit** )。变成这种格式就是为了传输,因为网络上的传输,默认的是一个个的包,因而这里也就分成了一个个的单元。 ![img](assets/42dcd0705e3b1bad05d59fd9d6707d60.jpg) @@ -101,17 +101,17 @@ 如果类型是帧,则Payload中才是正的视频数据,当然也是一帧一帧存放的,前面说了,一帧的内容还是挺多的,因而每一个NALU里面保存的是一片。对于每一片,到底是I帧,还是P帧,还是B帧,在片结构里面也有个Header,这里面有个类型,然后是片的内容。 -这样,整个格式就出来了, **一个视频,可以拆分成一系列的帧,每一帧拆分成一系列的片,每一片都放在一个NALU里面,NALU之间都是通过特殊的起始标识符分隔,在每一个I帧的第一片前面,要插入单独保存SPS和PPS的NALU,最终形成一个长长的NALU序列** 。 +这样,整个格式就出来了,**一个视频,可以拆分成一系列的帧,每一帧拆分成一系列的片,每一片都放在一个NALU里面,NALU之间都是通过特殊的起始标识符分隔,在每一个I帧的第一片前面,要插入单独保存SPS和PPS的NALU,最终形成一个长长的NALU序列** 。 ### 推流:如何把数据流打包传输到对端? -那这个格式是不是就能够直接在网上传输到对端,开始直播了呢?其实还不是,还需要将这个二进制的流打包成网络包进行发送,这里我们使用 **RTMP协议** 。这就进入了第二个过程, **推流** 。 +那这个格式是不是就能够直接在网上传输到对端,开始直播了呢?其实还不是,还需要将这个二进制的流打包成网络包进行发送,这里我们使用 **RTMP协议** 。这就进入了第二个过程,**推流** 。 RTMP是基于TCP的,因而肯定需要双方建立一个TCP的连接。在有TCP的连接的基础上,还需要建立一个RTMP的连接,也即在程序里面,你需要调用RTMP类库的Connect函数,显示创建一个连接。 RTMP为什么需要建立一个单独的连接呢? -因为它们需要商量一些事情,保证以后的传输能正常进行。主要就是两个事情,一个是 **版本号** ,如果客户端、服务器的版本号不一致,则不能工作。另一个就是 **时间戳** ,视频播放中,时间是很重要的,后面的数据流互通的时候,经常要带上时间戳的差值,因而一开始双方就要知道对方的时间戳。 +因为它们需要商量一些事情,保证以后的传输能正常进行。主要就是两个事情,一个是 **版本号**,如果客户端、服务器的版本号不一致,则不能工作。另一个就是 **时间戳**,视频播放中,时间是很重要的,后面的数据流互通的时候,经常要带上时间戳的差值,因而一开始双方就要知道对方的时间戳。 未来沟通这些事情,需要发送六条消息:客户端发送C0、C1、 C2,服务器发送S0、 S1、 S2。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25417\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25417\350\256\262.md" index 4e2379b2b..7f6f722fd 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25417\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25417\350\256\262.md" @@ -4,7 +4,7 @@ 当然,最简单的方式就是通过 **HTTP** 进行下载。但是相信你有过这样的体验,通过浏览器下载的时候,只要文件稍微大点,下载的速度就奇慢无比。 -还有种下载文件的方式,就是通过 **FTP** ,也即 **文件传输协议** 。FTP采用两个TCP连接来传输一个文件。 +还有种下载文件的方式,就是通过 **FTP**,也即 **文件传输协议** 。FTP采用两个TCP连接来传输一个文件。 - **控制连接** :服务器以被动的方式,打开众所周知用于FTP的端口21,客户端则主动发起连接。该连接将命令从客户端传给服务器,并传回服务器的应答。常用的命令有:list——获取文件目录;reter——取一个文件;store——存一个文件。 - **数据连接** :每当一个文件在客户端与服务器之间传输时,就创建一个数据连接。 @@ -19,9 +19,9 @@ ## P2P是什么? -但是无论是HTTP的方式,还是FTP的方式,都有一个比较大的缺点,就是 **难以解决单一服务器的带宽压力** , 因为它们使用的都是传统的客户端服务器的方式。 +但是无论是HTTP的方式,还是FTP的方式,都有一个比较大的缺点,就是 **难以解决单一服务器的带宽压力**, 因为它们使用的都是传统的客户端服务器的方式。 -后来,一种创新的、称为P2P的方式流行起来。 **P2P** 就是 **peer-to-peer** 。资源开始并不集中地存储在某些设备上,而是分散地存储在多台设备上。这些设备我们姑且称为peer。 +后来,一种创新的、称为P2P的方式流行起来。**P2P** 就是 **peer-to-peer** 。资源开始并不集中地存储在某些设备上,而是分散地存储在多台设备上。这些设备我们姑且称为peer。 想要下载一个文件的时候,你只要得到那些已经存在了文件的peer,并和这些peer之间,建立点对点的连接,而不需要到中心服务器上,就可以就近下载文件。一旦下载了文件,你也就成为peer中的一员,你旁边的那些机器,也可能会选择从你这里下载文件,所以当你使用P2P软件的时候,例如BitTorrent,往往能够看到,既有下载流量,也有上传的流量,也即你自己也加入了这个P2P的网络,自己从别人那里下载,同时也提供给其他人下载。可以想象,这种方式,参与的人越多,下载速度越快,一切完美。 @@ -54,11 +54,11 @@ 有一种著名的DHT协议,叫 **Kademlia协议** 。这个和区块链的概念一样,很抽象,我来详细讲一下这个协议。 -任何一个BitTorrent启动之后,它都有两个角色。一个是 **peer** ,监听一个TCP端口,用来上传和下载文件,这个角色表明,我这里有某个文件。另一个角色 **DHT node** ,监听一个UDP的端口,通过这个角色,这个节点加入了一个DHT的网络。 +任何一个BitTorrent启动之后,它都有两个角色。一个是 **peer**,监听一个TCP端口,用来上传和下载文件,这个角色表明,我这里有某个文件。另一个角色 **DHT node**,监听一个UDP的端口,通过这个角色,这个节点加入了一个DHT的网络。 ![img](assets/8ece62f3f99cb3fe7ee0274a1ad79fcf.jpg) -在DHT网络里面,每一个DHT node都有一个ID。这个ID是一个很长的串。每个DHT node都有责任掌握一些知识,也就是 **文件索引** ,也即它应该知道某些文件是保存在哪些节点上。它只需要有这些知识就可以了,而它自己本身不一定就是保存这个文件的节点。 +在DHT网络里面,每一个DHT node都有一个ID。这个ID是一个很长的串。每个DHT node都有责任掌握一些知识,也就是 **文件索引**,也即它应该知道某些文件是保存在哪些节点上。它只需要有这些知识就可以了,而它自己本身不一定就是保存这个文件的节点。 ## 哈希值 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25418\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25418\350\256\262.md" index b27e464de..983e9e41d 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25418\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25418\350\256\262.md" @@ -8,7 +8,7 @@ 在网络世界,也是这样的。你肯定记得住网站的名称,但是很难记住网站的IP地址,因而也需要一个地址簿,就是 **DNS服务器** 。 -由此可见,DNS在日常生活中多么重要。每个人上网,都需要访问它,但是同时,这对它来讲也是非常大的挑战。一旦它出了故障,整个互联网都将瘫痪。另外,上网的人分布在全世界各地,如果大家都去同一个地方访问某一台服务器,时延将会非常大。因而, **DNS服务器,一定要设置成高可用、高并发和分布式的** 。 +由此可见,DNS在日常生活中多么重要。每个人上网,都需要访问它,但是同时,这对它来讲也是非常大的挑战。一旦它出了故障,整个互联网都将瘫痪。另外,上网的人分布在全世界各地,如果大家都去同一个地方访问某一台服务器,时延将会非常大。因而,**DNS服务器,一定要设置成高可用、高并发和分布式的** 。 于是,就有了这样 **树状的层次结构** 。 @@ -69,14 +69,14 @@ DNS首先可以做 **内部负载均衡** 。 对于不需要做全局负载均衡的简单应用来讲,yourcompany.com的权威DNS服务器可以直接将 object.yourcompany.com这个域名解析为一个或者多个IP地址,然后客户端可以通过多个IP地址,进行简单的轮询,实现简单的负载均衡。 -但是对于复杂的应用,尤其是跨地域跨运营商的大型应用,则需要更加复杂的全局负载均衡机制,因而需要专门的设备或者服务器来做这件事情,这就是 **全局负载均衡器** ( **GSLB** , **Global Server Load Balance** )。 +但是对于复杂的应用,尤其是跨地域跨运营商的大型应用,则需要更加复杂的全局负载均衡机制,因而需要专门的设备或者服务器来做这件事情,这就是 **全局负载均衡器** ( **GSLB**,**Global Server Load Balance** )。 在yourcompany.com的DNS服务器中,一般是通过配置CNAME的方式,给 object.yourcompany.com起一个别名,例如 object.vip.yourcomany.com,然后告诉本地DNS服务器,让它请求GSLB解析这个域名,GSLB就可以在解析这个域名的过程中,通过自己的策略实现负载均衡。 图中画了两层的GSLB,是因为分运营商和地域。我们希望不同运营商的客户,可以访问相同运营商机房中的资源,这样不跨运营商访问,有利于提高吞吐量,减少时延。 1. 第一层GSLB,通过查看请求它的本地DNS服务器所在的运营商,就知道用户所在的运营商。假设是移动,通过CNAME的方式,通过另一个别名 object.yd.yourcompany.com,告诉本地DNS服务器去请求第二层的GSLB。 -1. 第二层GSLB,通过查看请求它的本地DNS服务器所在的地址,就知道用户所在的地理位置,然后将距离用户位置比较近的Region里面,六个 **内部负载均衡** ( **SLB** ,S **erver Load Balancer** )的地址,返回给本地DNS服务器。 +1. 第二层GSLB,通过查看请求它的本地DNS服务器所在的地址,就知道用户所在的地理位置,然后将距离用户位置比较近的Region里面,六个 **内部负载均衡** ( **SLB**,S **erver Load Balancer** )的地址,返回给本地DNS服务器。 1. 本地DNS服务器将结果返回给本地DNS解析器。 1. 本地DNS解析器将结果缓存后,返回给客户端。 1. 客户端开始访问属于相同运营商的距离较近的Region 1中的对象存储,当然客户端得到了六个IP地址,它可以通过负载均衡的方式,随机或者轮询选择一个可用区进行访问。对象存储一般会有三个备份,从而可以实现对存储读写的负载均衡。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25419\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25419\350\256\262.md" index 0af3a43f4..3f0392263 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25419\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25419\350\256\262.md" @@ -36,7 +36,7 @@ ### 3.出口NAT问题 -前面讲述网关的时候,我们知道,出口的时候,很多机房都会配置 **NAT** ,也即 **网络地址转换** ,使得从这个网关出去的包,都换成新的IP地址,当然请求返回的时候,在这个网关,再将IP地址转换回去,所以对于访问来说是没有任何问题。 +前面讲述网关的时候,我们知道,出口的时候,很多机房都会配置 **NAT**,也即 **网络地址转换**,使得从这个网关出去的包,都换成新的IP地址,当然请求返回的时候,在这个网关,再将IP地址转换回去,所以对于访问来说是没有任何问题。 但是一旦做了网络地址的转换,权威的DNS服务器,就没办法通过这个地址,来判断客户到底是来自哪个运营商,而且极有可能因为转换过后的地址,误判运营商,导致跨运营商的访问。 @@ -54,7 +54,7 @@ ## HTTPDNS的工作模式 -既然DNS解析中有这么多问题,那怎么办呢?难不成退回到直接用IP地址?这样显然不合适,所以就有了 **HTTPDNS** 。 **HTTPNDS其实就是,不走传统的DNS解析,而是自己搭建基于HTTP协议的DNS服务器集群,分布在多个地点和多个运营商。当客户端需要DNS解析的时候,直接通过HTTP协议进行请求这个服务器集群,得到就近的地址。** 这就相当于每家基于HTTP协议,自己实现自己的域名解析,自己做一个自己的地址簿,而不使用统一的地址簿。但是默认的域名解析都是走DNS的,因而使用HTTPDNS需要绕过默认的DNS路径,就不能使用默认的客户端。使用HTTPDNS的,往往是手机应用,需要在手机端嵌入支持HTTPDNS的客户端SDK。 +既然DNS解析中有这么多问题,那怎么办呢?难不成退回到直接用IP地址?这样显然不合适,所以就有了 **HTTPDNS** 。**HTTPNDS其实就是,不走传统的DNS解析,而是自己搭建基于HTTP协议的DNS服务器集群,分布在多个地点和多个运营商。当客户端需要DNS解析的时候,直接通过HTTP协议进行请求这个服务器集群,得到就近的地址。** 这就相当于每家基于HTTP协议,自己实现自己的域名解析,自己做一个自己的地址簿,而不使用统一的地址簿。但是默认的域名解析都是走DNS的,因而使用HTTPDNS需要绕过默认的DNS路径,就不能使用默认的客户端。使用HTTPDNS的,往往是手机应用,需要在手机端嵌入支持HTTPDNS的客户端SDK。 通过自己的HTTPDNS服务器和自己的SDK,实现了从依赖本地导游,到自己上网查询做旅游攻略,进行自由行,爱怎么玩怎么玩。这样就能够避免依赖导游,而导游又不专业,你还不能把他怎么样的尴尬。 @@ -100,9 +100,9 @@ HTTPDNS的缓存设计策略也是咱们做应用架构中常用的缓存设计 SDK中的缓存会严格按照缓存过期时间,如果缓存没有命中,或者已经过期,而且客户端不允许使用过期的记录,则会发起一次解析,保障记录是更新的。 -解析可以 **同步进行** ,也就是直接调用HTTPDNS的接口,返回最新的记录,更新缓存;也可以 **异步进行** ,添加一个解析任务到后台,由后台任务调用HTTPDNS的接口。 **同步更新** 的 **优点** 是实时性好,缺点是如果有多个请求都发现过期的时候,同时会请求HTTPDNS多次,其实是一种浪费。 +解析可以 **同步进行**,也就是直接调用HTTPDNS的接口,返回最新的记录,更新缓存;也可以 **异步进行**,添加一个解析任务到后台,由后台任务调用HTTPDNS的接口。**同步更新** 的 **优点** 是实时性好,缺点是如果有多个请求都发现过期的时候,同时会请求HTTPDNS多次,其实是一种浪费。 -同步更新的方式对应到应用架构中缓存的 **Cache-Aside机制** ,也即先读缓存,不命中读数据库,同时将结果写入缓存。 +同步更新的方式对应到应用架构中缓存的 **Cache-Aside机制**,也即先读缓存,不命中读数据库,同时将结果写入缓存。 ![img](assets/a9ae8782b23c73bcc0c824dcf9fc370b.jpg) **异步更新** 的 **优点** 是,可以将多个请求都发现过期的情况,合并为一个对于HTTPDNS的请求任务,只执行一次,减少HTTPDNS的压力。同时可以在即将过期的时候,就创建一个任务进行预加载,防止过期之后再刷新,称为 **预加载** 。 @@ -110,7 +110,7 @@ SDK中的缓存会严格按照缓存过期时间,如果缓存没有命中, ![img](assets/e35240b0992c260602c5cff53299bf44.jpg) -异步更新的机制对应到应用架构中缓存的 **Refresh-Ahead机制** ,即业务仅仅访问缓存,当过期的时候定期刷新。在著名的应用缓存Guava Cache中,有个RefreshAfterWrite机制,对于并发情况下,多个缓存访问不命中从而引发并发回源的情况,可以采取只有一个请求回源的模式。在应用架构的缓存中,也常常用 **数据预热** 或者 **预加载** 的机制。 +异步更新的机制对应到应用架构中缓存的 **Refresh-Ahead机制**,即业务仅仅访问缓存,当过期的时候定期刷新。在著名的应用缓存Guava Cache中,有个RefreshAfterWrite机制,对于并发情况下,多个缓存访问不命中从而引发并发回源的情况,可以采取只有一个请求回源的模式。在应用架构的缓存中,也常常用 **数据预热** 或者 **预加载** 的机制。 ![img](assets/962250440c7e0bc39e510d7a9d075acd.jpg) @@ -118,13 +118,13 @@ SDK中的缓存会严格按照缓存过期时间,如果缓存没有命中, 由于客户端嵌入了SDK,因而就不会因为本地DNS的各种缓存、转发、NAT,让权威DNS服务器误会客户端所在的位置和运营商,而可以拿到第一手资料。 -在 **客户端** ,可以知道手机是哪个国家、哪个运营商、哪个省,甚至哪个市,HTTPDNS服务端可以根据这些信息,选择最佳的服务节点返回。 +在 **客户端**,可以知道手机是哪个国家、哪个运营商、哪个省,甚至哪个市,HTTPDNS服务端可以根据这些信息,选择最佳的服务节点返回。 如果有多个节点,还会考虑错误率、请求时间、服务器压力、网络状况等,进行综合选择,而非仅仅考虑地理位置。当有一个节点宕机或者性能下降的时候,可以尽快进行切换。 要做到这一点,需要客户端使用HTTPDNS返回的IP访问业务应用。客户端的SDK会收集网络请求数据,如错误率、请求时间等网络请求质量数据,并发送到统计后台,进行分析、聚合,以此查看不同的IP的服务质量。 -在 **服务端** ,应用可以通过调用HTTPDNS的管理接口,配置不同服务质量的优先级、权重。HTTPDNS会根据这些策略综合地理位置和线路状况算出一个排序,优先访问当前那些优质的、时延低的IP地址。 +在 **服务端**,应用可以通过调用HTTPDNS的管理接口,配置不同服务质量的优先级、权重。HTTPDNS会根据这些策略综合地理位置和线路状况算出一个排序,优先访问当前那些优质的、时延低的IP地址。 HTTPDNS通过智能调度之后返回的结果,也会缓存在客户端。为了不让缓存使得调度失真,客户端可以根据不同的移动网络运营商WIFI的SSID来分维度缓存。不同的运营商或者WIFI解析出来的结果会不同。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25420\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25420\350\256\262.md" index eec38c517..8200fe290 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25420\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25420\350\256\262.md" @@ -8,7 +8,7 @@ 电商网站根据统计大概知道,北京、上海、广州、深圳、杭州等地,每天能够卖出去多少书籍、卫生纸、包、电器等存放期比较长的物品。这些物品用不着从中心仓库发出,所以平时就可以将它们分布在各地仓库里,客户一下单,就近的仓库发出,第二天就可以收到了。 -这样,用户体验大大提高。当然,这里面也有个难点就是,生鲜这类东西保质期太短,如果提前都备好货,但是没有人下单,那肯定就坏了。这个问题,我后文再说。 **我们先说,我们的网站访问可以借鉴“就近配送”这个思路。** 全球有这么多的数据中心,无论在哪里上网,临近不远的地方基本上都有数据中心。是不是可以在这些数据中心里部署几台机器,形成一个缓存的集群来缓存部分数据,那么用户访问数据的时候,就可以就近访问了呢? +这样,用户体验大大提高。当然,这里面也有个难点就是,生鲜这类东西保质期太短,如果提前都备好货,但是没有人下单,那肯定就坏了。这个问题,我后文再说。**我们先说,我们的网站访问可以借鉴“就近配送”这个思路。** 全球有这么多的数据中心,无论在哪里上网,临近不远的地方基本上都有数据中心。是不是可以在这些数据中心里部署几台机器,形成一个缓存的集群来缓存部分数据,那么用户访问数据的时候,就可以就近访问了呢? 当然是可以的。这些分布在各个地方的各个数据中心的节点,就称为 **边缘节点** 。 @@ -18,9 +18,9 @@ 这就是 **CDN的分发系统的架构** 。CDN系统的缓存,也是一层一层的,能不访问后端真正的源,就不打扰它。这也是电商网站物流系统的思路,北京局找不到,找华北局,华北局找不到,再找北方局。 -有了这个分发系统之后,接下来就是, **客户端如何找到相应的边缘节点进行访问呢?** 还记得我们讲过的基于DNS的全局负载均衡吗?这个负载均衡主要用来选择一个就近的同样运营商的服务器进行访问。你会发现,CDN分发网络也是一个分布在多个区域、多个运营商的分布式系统,也可以用相同的思路选择最合适的边缘节点。 +有了这个分发系统之后,接下来就是,**客户端如何找到相应的边缘节点进行访问呢?** 还记得我们讲过的基于DNS的全局负载均衡吗?这个负载均衡主要用来选择一个就近的同样运营商的服务器进行访问。你会发现,CDN分发网络也是一个分布在多个区域、多个运营商的分布式系统,也可以用相同的思路选择最合适的边缘节点。 -![img](assets/a94d543020d85c8feb9cd665eb4a3502.jpg) **在没有CDN的情况下** ,用户向浏览器输入www.web.com这个域名,客户端访问本地DNS服务器的时候,如果本地DNS服务器有缓存,则返回网站的地址;如果没有,递归查询到网站的权威DNS服务器,这个权威DNS服务器是负责web.com的,它会返回网站的IP地址。本地DNS服务器缓存下IP地址,将IP地址返回,然后客户端直接访问这个IP地址,就访问到了这个网站。 +![img](assets/a94d543020d85c8feb9cd665eb4a3502.jpg) **在没有CDN的情况下**,用户向浏览器输入www.web.com这个域名,客户端访问本地DNS服务器的时候,如果本地DNS服务器有缓存,则返回网站的地址;如果没有,递归查询到网站的权威DNS服务器,这个权威DNS服务器是负责web.com的,它会返回网站的IP地址。本地DNS服务器缓存下IP地址,将IP地址返回,然后客户端直接访问这个IP地址,就访问到了这个网站。 然而 **有了CDN之后,情况发生了变化** 。在web.com这个权威DNS服务器上,会设置一个CNAME别名,指向另外一个域名 [www.web.cdn.com](http://www.web.cdn.com/),返回给本地DNS服务器。 @@ -45,15 +45,15 @@ 但是静态内容中,有一种特殊的内容,也大量使用了CDN,这个就是前面讲过的\[流媒体\]。 -CDN支持 **流媒体协议** ,例如前面讲过的RTMP协议。在很多情况下,这相当于一个代理,从上一级缓存读取内容,转发给用户。由于流媒体往往是连续的,因而可以进行预先缓存的策略,也可以预先推送到用户的客户端。 +CDN支持 **流媒体协议**,例如前面讲过的RTMP协议。在很多情况下,这相当于一个代理,从上一级缓存读取内容,转发给用户。由于流媒体往往是连续的,因而可以进行预先缓存的策略,也可以预先推送到用户的客户端。 -对于静态页面来讲,内容的分发往往采取 **拉取** 的方式,也即当发现未命中的时候,再去上一级进行拉取。但是,流媒体数据量大,如果出现 **回源** ,压力会比较大,所以往往采取主动 **推送** 的模式,将热点数据主动推送到边缘节点。 +对于静态页面来讲,内容的分发往往采取 **拉取** 的方式,也即当发现未命中的时候,再去上一级进行拉取。但是,流媒体数据量大,如果出现 **回源**,压力会比较大,所以往往采取主动 **推送** 的模式,将热点数据主动推送到边缘节点。 -对于流媒体来讲,很多CDN还提供 **预处理服务** ,也即文件在分发之前,经过一定的处理。例如将视频转换为不同的码流,以适应不同的网络带宽的用户需求;再如对视频进行分片,降低存储压力,也使得客户端可以选择使用不同的码率加载不同的分片。这就是我们常见的,“我要看超清、标清、流畅等”。 +对于流媒体来讲,很多CDN还提供 **预处理服务**,也即文件在分发之前,经过一定的处理。例如将视频转换为不同的码流,以适应不同的网络带宽的用户需求;再如对视频进行分片,降低存储压力,也使得客户端可以选择使用不同的码率加载不同的分片。这就是我们常见的,“我要看超清、标清、流畅等”。 对于流媒体CDN来讲,有个关键的问题是 **防盗链** 问题。因为视频是要花大价钱买版权的,为了挣点钱,收点广告费,如果流媒体被其他的网站盗走,在人家的网站播放,那损失可就大了。 -最常用也最简单的方法就是 **HTTP头的refer字段** , 当浏览器发送请求的时候,一般会带上referer,告诉服务器是从哪个页面链接过来的,服务器基于此可以获得一些信息用于处理。如果refer信息不是来自本站,就阻止访问或者跳到其它链接。 **refer的机制相对比较容易破解,所以还需要配合其他的机制。** 一种常用的机制是 **时间戳防盗链** 。使用CDN的管理员可以在配置界面上,和CDN厂商约定一个加密字符串。 +最常用也最简单的方法就是 **HTTP头的refer字段**, 当浏览器发送请求的时候,一般会带上referer,告诉服务器是从哪个页面链接过来的,服务器基于此可以获得一些信息用于处理。如果refer信息不是来自本站,就阻止访问或者跳到其它链接。**refer的机制相对比较容易破解,所以还需要配合其他的机制。** 一种常用的机制是 **时间戳防盗链** 。使用CDN的管理员可以在配置界面上,和CDN厂商约定一个加密字符串。 客户端取出当前的时间戳,要访问的资源及其路径,连同加密字符串进行签名算法得到一个字符串,然后生成一个下载链接,带上这个签名字符串和截止时间戳去访问CDN。 @@ -61,8 +61,8 @@ CDN支持 **流媒体协议** ,例如前面讲过的RTMP协议。在很多情 然而比如在电商仓库中,我在前面提过,有关生鲜的缓存就是非常麻烦的事情,这对应着就是动态的数据,比较难以缓存。怎么办呢?现在也有 **动态CDN,主要有两种模式** 。 -- 一种为 **生鲜超市模式** ,也即 **边缘计算的模式** 。既然数据是动态生成的,所以数据的逻辑计算和存储,也相应的放在边缘的节点。其中定时从源数据那里同步存储的数据,然后在边缘进行计算得到结果。就像对生鲜的烹饪是动态的,没办法事先做好缓存,因而将生鲜超市放在你家旁边,既能够送货上门,也能够现场烹饪,也是边缘计算的一种体现。 -- 另一种是 **冷链运输模式** ,也即 **路径优化的模式** 。数据不是在边缘计算生成的,而是在源站生成的,但是数据的下发则可以通过CDN的网络,对路径进行优化。因为CDN节点较多,能够找到离源站很近的边缘节点,也能找到离用户很近的边缘节点。中间的链路完全由CDN来规划,选择一个更加可靠的路径,使用类似专线的方式进行访问。 +- 一种为 **生鲜超市模式**,也即 **边缘计算的模式** 。既然数据是动态生成的,所以数据的逻辑计算和存储,也相应的放在边缘的节点。其中定时从源数据那里同步存储的数据,然后在边缘进行计算得到结果。就像对生鲜的烹饪是动态的,没办法事先做好缓存,因而将生鲜超市放在你家旁边,既能够送货上门,也能够现场烹饪,也是边缘计算的一种体现。 +- 另一种是 **冷链运输模式**,也即 **路径优化的模式** 。数据不是在边缘计算生成的,而是在源站生成的,但是数据的下发则可以通过CDN的网络,对路径进行优化。因为CDN节点较多,能够找到离源站很近的边缘节点,也能找到离用户很近的边缘节点。中间的链路完全由CDN来规划,选择一个更加可靠的路径,使用类似专线的方式进行访问。 对于常用的TCP连接,在公网上传输的时候经常会丢数据,导致TCP的窗口始终很小,发送速度上不去。根据前面的TCP流量控制和拥塞控制的原理,在CDN加速网络中可以调整TCP的参数,使得TCP可以更加激进地传输数据。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25421\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25421\350\256\262.md" index bad5fae6b..8d74d4498 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25421\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25421\350\256\262.md" @@ -34,7 +34,7 @@ ![img](assets/116a168c0eb55fabd7786fca728bd850-1584286230034.jpg) -交换机有一种技术叫作 **堆叠** ,所以另一种方法是,将多个交换机形成一个逻辑的交换机,服务器通过多根线分配连到多个接入层交换机上,而接入层交换机多根线分别连接到多个交换机上,并且通过堆叠的私有协议,形成 **双活** 的连接方式。 +交换机有一种技术叫作 **堆叠**,所以另一种方法是,将多个交换机形成一个逻辑的交换机,服务器通过多根线分配连到多个接入层交换机上,而接入层交换机多根线分别连接到多个交换机上,并且通过堆叠的私有协议,形成 **双活** 的连接方式。 ![img](assets/10aa7eac3fd38dfc2a09d6475ff4d93a-1584286228468.jpg) @@ -56,7 +56,7 @@ 如图,核心层和汇聚层之间通过内部的路由协议OSPF,找到最佳的路径进行访问,而且还可以通过ECMP等价路由,在多个路径之间进行负载均衡和高可用。 -但是随着数据中心里面的机器越来越多,尤其是有了云计算、大数据,集群规模非常大,而且都要求在一个二层网络里面。这就需要二层互连从 **汇聚层** 上升为 **核心层** ,也即在核心以下,全部是二层互连,全部在一个广播域里面,这就是常说的 **大二层** 。 +但是随着数据中心里面的机器越来越多,尤其是有了云计算、大数据,集群规模非常大,而且都要求在一个二层网络里面。这就需要二层互连从 **汇聚层** 上升为 **核心层**,也即在核心以下,全部是二层互连,全部在一个广播域里面,这就是常说的 **大二层** 。 ![img](assets/2aa3787c31c52defc7614c53f0a71d2c-1584286334074.jpg) @@ -66,7 +66,7 @@ 于是大二层就引入了 **TRILL** ( **Transparent Interconnection of Lots of Link** ),即 **多链接透明互联协议** 。它的基本思想是,二层环有问题,三层环没有问题,那就把三层的路由能力模拟在二层实现。 -运行TRILL协议的交换机称为 **RBridge** ,是 **具有路由转发特性的网桥设备** ,只不过这个路由是根据MAC地址来的,不是根据IP来的。 +运行TRILL协议的交换机称为 **RBridge**,是 **具有路由转发特性的网桥设备**,只不过这个路由是根据MAC地址来的,不是根据IP来的。 Rbridage之间通过 **链路状态协议** 运作。记得这个路由协议吗?通过它可以学习整个大二层的拓扑,知道访问哪个MAC应该从哪个网桥走;还可以计算最短的路径,也可以通过等价的路由进行负载均衡和高可用性。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25422\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25422\350\256\262.md" index 7d96759a9..a3afb0f5b 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25422\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25422\350\256\262.md" @@ -8,7 +8,7 @@ ![img](assets/9f797934cb5cf40543b716d97e214868.jpg) -**VPN** ,全名 **Virtual Private Network** , **虚拟专用网** ,就是利用开放的公众网络,建立专用数据传输通道,将远程的分支机构、移动办公人员等连接起来。 +**VPN**,全名 **Virtual Private Network**,**虚拟专用网**,就是利用开放的公众网络,建立专用数据传输通道,将远程的分支机构、移动办公人员等连接起来。 ## VPN是如何工作的? @@ -30,15 +30,15 @@ VPN通过隧道技术在公众网络上仿真一条点到点的专线,是通 到达之后,外部承载协议的任务就结束了,打开船舱,将车开出来,就相当于取下承载协议和隧道协议的头。接下来,在海南该怎么开车,就怎么开车,还是内部的乘客协议起作用。 -在最前面的时候说了,直接使用公网太不安全,所以接下来我们来看一种十分安全的VPN, **IPsec VPN** 。这是基于IP协议的 **安全隧道协议** ,为了保证在公网上面信息的安全,因而采取了一定的机制保证安全性。 +在最前面的时候说了,直接使用公网太不安全,所以接下来我们来看一种十分安全的VPN,**IPsec VPN** 。这是基于IP协议的 **安全隧道协议**,为了保证在公网上面信息的安全,因而采取了一定的机制保证安全性。 -- 机制一: **私密性** ,防止信息泄漏给未经授权的个人,通过加密把数据从明文变成无法读懂的密文,从而确保数据的私密性。 前面讲HTTPS的时候,说过加密可以分为对称加密和非对称加密。对称加密速度快一些。而VPN一旦建立,需要传输大量数据,因而我们采取对称加密。但是同样,对称加密还是存在加密秘钥如何传输的问题,这里需要用到因特网密钥交换(IKE,Internet Key Exchange)协议。 -- 机制二: **完整性** ,数据没有被非法篡改,通过对数据进行hash运算,产生类似于指纹的数据摘要,以保证数据的完整性。 -- 机制三: **真实性** ,数据确实是由特定的对端发出,通过身份认证可以保证数据的真实性。 +- 机制一: **私密性**,防止信息泄漏给未经授权的个人,通过加密把数据从明文变成无法读懂的密文,从而确保数据的私密性。 前面讲HTTPS的时候,说过加密可以分为对称加密和非对称加密。对称加密速度快一些。而VPN一旦建立,需要传输大量数据,因而我们采取对称加密。但是同样,对称加密还是存在加密秘钥如何传输的问题,这里需要用到因特网密钥交换(IKE,Internet Key Exchange)协议。 +- 机制二: **完整性**,数据没有被非法篡改,通过对数据进行hash运算,产生类似于指纹的数据摘要,以保证数据的完整性。 +- 机制三: **真实性**,数据确实是由特定的对端发出,通过身份认证可以保证数据的真实性。 那如何保证对方就是真正的那个人呢? -- 第一种方法就是 **预共享密钥** ,也就是双方事先商量好一个暗号,比如“天王盖地虎,宝塔镇河妖”,对上了,就说明是对的。 +- 第一种方法就是 **预共享密钥**,也就是双方事先商量好一个暗号,比如“天王盖地虎,宝塔镇河妖”,对上了,就说明是对的。 - 另外一种方法就是 **用数字签名来验证** 。咋签名呢?当然是使用私钥进行签名,私钥只有我自己有,所以如果对方能用我的数字证书里面的公钥解开,就说明我是我。 基于以上三个特性,组成了 **IPsec VPN的协议簇** 。这个协议簇内容比较丰富。 @@ -52,11 +52,11 @@ VPN通过隧道技术在公众网络上仿真一条点到点的专线,是通 在这个协议簇里面,还有两类算法,分别是 **加密算法** 和 **摘要算法** 。 -这个协议簇还包含两大组件,一个用于VPN的双方要进行对称密钥的交换的 **IKE组件** ,另一个是VPN的双方要对连接进行维护的 **SA(Security Association)组件** 。 +这个协议簇还包含两大组件,一个用于VPN的双方要进行对称密钥的交换的 **IKE组件**,另一个是VPN的双方要对连接进行维护的 **SA(Security Association)组件** 。 ## IPsec VPN的建立过程 -下面来看IPsec VPN的建立过程,这个过程分两个阶段。 **第一个阶段,建立IKE自己的SA** 。这个SA用来维护一个通过身份认证和安全保护的通道,为第二个阶段提供服务。在这个阶段,通过DH(Diffie-Hellman)算法计算出一个对称密钥K。 +下面来看IPsec VPN的建立过程,这个过程分两个阶段。**第一个阶段,建立IKE自己的SA** 。这个SA用来维护一个通过身份认证和安全保护的通道,为第二个阶段提供服务。在这个阶段,通过DH(Diffie-Hellman)算法计算出一个对称密钥K。 DH算法是一个比较巧妙的算法。客户端和服务端约定两个公开的质数p和q,然后客户端随机产生一个数a作为自己的私钥,服务端随机产生一个b作为自己的私钥,客户端可以根据p、q和a计算出公钥A,服务端根据p、q和b计算出公钥B,然后双方交换公钥A和B。 @@ -104,7 +104,7 @@ IPsec SA里面有以下内容: ATM技术虽然没有成功,但其屏弃了繁琐的路由查找,改为简单快速的标签交换,将具有全局意义的路由表改为只有本地意义的标签表,这些都可以大大提高一台路由器的转发功力。 -有没有一种方式将两者的优点结合起来呢?这就是 **多协议标签交换** ( **MPLS** , **Multi-Protocol Label Switching** )。MPLS的格式如图所示,在原始的IP头之外,多了MPLS的头,里面可以打标签。 +有没有一种方式将两者的优点结合起来呢?这就是 **多协议标签交换** ( **MPLS**,**Multi-Protocol Label Switching** )。MPLS的格式如图所示,在原始的IP头之外,多了MPLS的头,里面可以打标签。 ![img](assets/ab77ad0cec6a26f43bacb3f51b0c8d32.jpg) @@ -162,7 +162,7 @@ ATM技术虽然没有成功,但其屏弃了繁琐的路由查找,改为简 所以PE路由器之间使用特殊的MP-BGP来发布VPN路由,在相互沟通的消息中,在一般32位IPv4的地址之前加上一个客户标示的区分符用于客户地址的区分,这种称为VPN-IPv4地址族,这样PE路由器会收到如下的消息,机构A的192.168.101.0/24应该往这面走,机构B的192.168.101.0/24则应该去另外一个方向。 -另外困惑的是 **路由表** ,当两个客户的IP包到达PE的时候,PE就困惑了,因为网段是重复的。 +另外困惑的是 **路由表**,当两个客户的IP包到达PE的时候,PE就困惑了,因为网段是重复的。 如何区分哪些路由是属于哪些客户VPN内的?如何保证VPN业务路由与普通路由不相互干扰? diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25423\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25423\350\256\262.md" index 0dad97dd1..c7408a710 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25423\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25423\350\256\262.md" @@ -14,7 +14,7 @@ 手机是通过收发无线信号来通信的,专业名称是Mobile Station,简称MS,需要嵌入SIM。手机是客户端,而无线信号的服务端,就是基站子系统(BSS,Base Station SubsystemBSS)。至于什么是基站,你可以回想一下,你在爬山的时候,是不是看到过信号塔?我们平时城市里面的基站比较隐蔽,不容易看到,所以只有在山里才会注意到。正是这个信号塔,通过无线信号,让你的手机可以进行通信。 -但是你要知道一点, **无论无线通信如何无线,最终还是要连接到有线的网络里** 。前面讲\[数据中心\]的时候我也讲过,电商的应用是放在数据中心的,数据中心的电脑都是插着网线的。 +但是你要知道一点,**无论无线通信如何无线,最终还是要连接到有线的网络里** 。前面讲\[数据中心\]的时候我也讲过,电商的应用是放在数据中心的,数据中心的电脑都是插着网线的。 因而,基站子系统分两部分,一部分对外提供无线通信,叫作基站收发信台(BTS,Base Transceiver Station),另一部分对内连接有线网络,叫作基站控制器(BSC,Base Station Controller)。基站收发信台通过无线收到数据后,转发给基站控制器。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25424\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25424\350\256\262.md" index 690fcc63d..660071b45 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25424\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25424\350\256\262.md" @@ -118,7 +118,7 @@ 但是如何跨物理机互通,并且实现VLAN的隔离呢?由于brctl创建的网桥上面的tag是没办法在网桥之外的范围内起作用的,于是我们需要寻找其他的方式。 -有一个命令 **vconfig** ,可以基于物理网卡eth0创建带VLAN的虚拟网卡,所有从这个虚拟网卡出去的包,都带这个VLAN,如果这样,跨物理机的互通和隔离就可以通过这个网卡来实现。 +有一个命令 **vconfig**,可以基于物理网卡eth0创建带VLAN的虚拟网卡,所有从这个虚拟网卡出去的包,都带这个VLAN,如果这样,跨物理机的互通和隔离就可以通过这个网卡来实现。 ![img](assets/dfc95f72325ab13c2f9551cfccc073e0.jpg) diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25425\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25425\350\256\262.md" index 382ef7de0..c9c9a0a66 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25425\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25425\350\256\262.md" @@ -17,7 +17,7 @@ ![img](assets/346fe3b3dbe1024e7119ec4ffa9377f9.jpg) - **控制与转发分离** :转发平面就是一个个虚拟或者物理的网络设备,就像小区里面的一条条路。控制平面就是统一的控制中心,就像小区物业的监控室。它们原来是一起的,物业管理员要从监控室出来,到路上去管理设备,现在是分离的,路就是走人的,控制都在监控室。 -- **控制平面与转发平面之间的开放接口** :控制器向上提供接口,被应用层调用,就像总控室提供按钮,让物业管理员使用。控制器向下调用接口,来控制网络设备,就像总控室会远程控制电梯的速度。这里经常使用两个名词,前面这个接口称为 **北向接口** ,后面这个接口称为 **南向接口** ,上北下南嘛。 +- **控制平面与转发平面之间的开放接口** :控制器向上提供接口,被应用层调用,就像总控室提供按钮,让物业管理员使用。控制器向下调用接口,来控制网络设备,就像总控室会远程控制电梯的速度。这里经常使用两个名词,前面这个接口称为 **北向接口**,后面这个接口称为 **南向接口**,上北下南嘛。 - **逻辑上的集中控制** :逻辑上集中的控制平面可以控制多个转发面设备,也就是控制整个物理网络,因而可以获得全局的网络状态视图,并根据该全局网络状态视图实现对网络的优化控制,就像物业管理员在监控室能够看到整个小区的情况,并根据情况优化出入方案。 ## OpenFlow和OpenvSwitch diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25426\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25426\350\256\262.md" index b032b803d..986e8e139 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25426\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25426\350\256\262.md" @@ -14,7 +14,7 @@ 我们来复习一下,当一个网络包进入一台机器的时候,都会做什么事情。 -首先拿下MAC头看看,是不是我的。如果是,则拿下IP头来。得到目标IP之后呢,就开始进行路由判断。在路由判断之前,这个节点我们称为 **PREROUTING** 。如果发现IP是我的,包就应该是我的,就发给上面的传输层,这个节点叫作 **INPUT** 。如果发现IP不是我的,就需要转发出去,这个节点称为 **FORWARD** 。如果是我的,上层处理完毕完毕后,一般会返回一个处理结果,这个处理结果会发出去,这个节点称为 **OUTPUT** ,无论是FORWARD还是OUTPUT,都是路由判断之后发生的,最后一个节点是 **POSTROUTING** 。 +首先拿下MAC头看看,是不是我的。如果是,则拿下IP头来。得到目标IP之后呢,就开始进行路由判断。在路由判断之前,这个节点我们称为 **PREROUTING** 。如果发现IP是我的,包就应该是我的,就发给上面的传输层,这个节点叫作 **INPUT** 。如果发现IP不是我的,就需要转发出去,这个节点称为 **FORWARD** 。如果是我的,上层处理完毕完毕后,一般会返回一个处理结果,这个处理结果会发出去,这个节点称为 **OUTPUT**,无论是FORWARD还是OUTPUT,都是路由判断之后发生的,最后一个节点是 **POSTROUTING** 。 整个过程如图所示。 @@ -22,7 +22,7 @@ 整个包的处理过程还是原来的过程,只不过为什么要格外关注这 **五个节点** 呢? -是因为在Linux内核中,有一个框架叫Netfilter。它可以在这些节点插入hook函数。这些函数可以截获数据包,对数据包进行干预。例如做一定的修改,然后决策是否接着交给TCP/IP协议栈处理;或者可以交回给协议栈,那就是 **ACCEPT** ;或者过滤掉,不再传输,就是 **DROP** ;还有就是 **QUEUE** ,发送给某个用户态进程处理。 +是因为在Linux内核中,有一个框架叫Netfilter。它可以在这些节点插入hook函数。这些函数可以截获数据包,对数据包进行干预。例如做一定的修改,然后决策是否接着交给TCP/IP协议栈处理;或者可以交回给协议栈,那就是 **ACCEPT** ;或者过滤掉,不再传输,就是 **DROP** ;还有就是 **QUEUE**,发送给某个用户态进程处理。 这个比较难理解,经常用在内部负载均衡,就是过来的数据一会儿传给目标地址1,一会儿传给目标地址2,而且目标地址的个数和权重都可能变。协议栈往往处理不了这么复杂的逻辑,需要写一个函数接管这个数据,实现自己的逻辑。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25427\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25427\350\256\262.md" index d1236cf5a..c3f94b95d 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25427\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25427\350\256\262.md" @@ -18,7 +18,7 @@ ### 无类别排队规则 -第一大类称为 **无类别排队规则** (Classless Queuing Disciplines)。还记得我们讲\[ip addr\]的时候讲过的 **pfifo_fast** ,这是一种不把网络包分类的技术。 +第一大类称为 **无类别排队规则** (Classless Queuing Disciplines)。还记得我们讲\[ip addr\]的时候讲过的 **pfifo_fast**,这是一种不把网络包分类的技术。 ![img](assets/7e3218260e75bb9f18d68641928ff33e.jpg) @@ -46,7 +46,7 @@ pfifo_fast分为三个先入先出的队列,称为三个Band。根据网络包 ### 基于类别的队列规则 -另外一大类是 **基于类别的队列规则** (Classful Queuing Disciplines),其中典型的为 **分层令牌桶规则** ( **HTB** , Hierarchical Token Bucket)。 +另外一大类是 **基于类别的队列规则** (Classful Queuing Disciplines),其中典型的为 **分层令牌桶规则** ( **HTB**, Hierarchical Token Bucket)。 HTB往往是一棵树,接下来我举个具体的例子,通过TC如何构建一棵HTB树来带你理解。 @@ -60,7 +60,7 @@ HTB往往是一棵树,接下来我举个具体的例子,通过TC如何构建 tc qdisc add dev eth0 root handle 1: htb default 12 ``` -对于这个网卡,需要规定发送的速度。一般有两个速度可以配置,一个是 **rate** ,表示一般情况下的速度;一个是 **ceil** ,表示最高情况下的速度。对于根节点来讲,这两个速度是一样的,于是创建一个root class,速度为(rate=100kbps,ceil=100kbps)。 +对于这个网卡,需要规定发送的速度。一般有两个速度可以配置,一个是 **rate**,表示一般情况下的速度;一个是 **ceil**,表示最高情况下的速度。对于根节点来讲,这两个速度是一样的,于是创建一个root class,速度为(rate=100kbps,ceil=100kbps)。 ```bash tc class add dev eth0 parent 1: classid 1:1 htb rate 100kbps ceil 100kbps diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25428\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25428\350\256\262.md" index 653175574..63c5b8df7 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25428\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25428\350\256\262.md" @@ -2,21 +2,21 @@ 对于云平台中的隔离问题,前面咱们用的策略一直都是VLAN,但是我们也说过这种策略的问题,VLAN只有12位,共4096个。当时设计的时候,看起来是够了,但是现在绝对不够用,怎么办呢? -**一种方式是修改这个协议** 。这种方法往往不可行,因为当这个协议形成一定标准后,千千万万设备上跑的程序都要按这个规则来。现在说改就放,谁去挨个儿告诉这些程序呢?很显然,这是一项不可能的工程。 **另一种方式就是扩展** ,在原来包的格式的基础上扩展出一个头,里面包含足够用于区分租户的ID,外层的包的格式尽量和传统的一样,依然兼容原来的格式。一旦遇到需要区分用户的地方,我们就用这个特殊的程序,来处理这个特殊的包的格式。 +**一种方式是修改这个协议** 。这种方法往往不可行,因为当这个协议形成一定标准后,千千万万设备上跑的程序都要按这个规则来。现在说改就放,谁去挨个儿告诉这些程序呢?很显然,这是一项不可能的工程。**另一种方式就是扩展**,在原来包的格式的基础上扩展出一个头,里面包含足够用于区分租户的ID,外层的包的格式尽量和传统的一样,依然兼容原来的格式。一旦遇到需要区分用户的地方,我们就用这个特殊的程序,来处理这个特殊的包的格式。 -这个概念很像咱们\[22\]讲过的 **隧道理论** ,还记得自驾游通过摆渡轮到海南岛的那个故事吗?在那一节,我们说过,扩展的包头主要是用于加密的,而我们现在需要的包头是要能够区分用户的。 +这个概念很像咱们\[22\]讲过的 **隧道理论**,还记得自驾游通过摆渡轮到海南岛的那个故事吗?在那一节,我们说过,扩展的包头主要是用于加密的,而我们现在需要的包头是要能够区分用户的。 -底层的物理网络设备组成的网络我们称为 **Underlay网络** ,而用于虚拟机和云中的这些技术组成的网络称为 **Overlay网络** , **这是一种基于物理网络的虚拟化网络实现** 。这一节我们重点讲两个Overlay的网络技术。 +底层的物理网络设备组成的网络我们称为 **Underlay网络**,而用于虚拟机和云中的这些技术组成的网络称为 **Overlay网络**,**这是一种基于物理网络的虚拟化网络实现** 。这一节我们重点讲两个Overlay的网络技术。 ## GRE -第一个技术是 **GRE** ,全称Generic Routing Encapsulation,它是一种IP-over-IP的隧道技术。它将IP包封装在GRE包里,外面加上IP头,在隧道的一端封装数据包,并在通路上进行传输,到另外一端的时候解封装。你可以认为Tunnel是一个虚拟的、点对点的连接。 +第一个技术是 **GRE**,全称Generic Routing Encapsulation,它是一种IP-over-IP的隧道技术。它将IP包封装在GRE包里,外面加上IP头,在隧道的一端封装数据包,并在通路上进行传输,到另外一端的时候解封装。你可以认为Tunnel是一个虚拟的、点对点的连接。 ![img](assets/b189df0a6ee4b0462818bf2f154c9531.jpg) 从这个图中可以看到,在GRE头中,前32位是一定会有的,后面的都是可选的。在前4位标识位里面,有标识后面到底有没有可选项?这里面有个很重要的key字段,是一个32位的字段,里面存放的往往就是用于区分用户的Tunnel ID。32位,够任何云平台喝一壶的了! -下面的格式类型专门用于网络虚拟化的GRE包头格式,称为 **NVGRE** ,也给网络ID号24位,也完全够用了。 +下面的格式类型专门用于网络虚拟化的GRE包头格式,称为 **NVGRE**,也给网络ID号24位,也完全够用了。 除此之外,GRE还需要有一个地方来封装和解封装GRE的包,这个地方往往是路由器或者有路由功能的Linux机器。 @@ -36,9 +36,9 @@ ![img](assets/006cc8a4bf7a13fea0f456905c263afe.jpg) -其次, **GRE不支持组播** ,因此一个网络中的一个虚机发出一个广播帧后,GRE会将其广播到所有与该节点有隧道连接的节点。 +其次,**GRE不支持组播**,因此一个网络中的一个虚机发出一个广播帧后,GRE会将其广播到所有与该节点有隧道连接的节点。 -另外一个问题是目前还是 **有很多防火墙和三层网络设备无法解析GRE** ,因此它们无法对GRE封装包做合适地过滤和负载均衡。 +另外一个问题是目前还是 **有很多防火墙和三层网络设备无法解析GRE**,因此它们无法对GRE封装包做合适地过滤和负载均衡。 ## VXLAN diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25429\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25429\350\256\262.md" index 0c9dd8339..90cd5ee18 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25429\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25429\350\256\262.md" @@ -16,7 +16,7 @@ 学习集装箱,首先要有个封闭的环境,将货物封装起来,让货物之间互不干扰,互相隔离,这样装货卸货才方便。 -封闭的环境主要使用了两种技术,一种是 **看起来是隔离的技术** ,称为 **namespace** ,也即每个 namespace中的应用看到的是不同的 IP地址、用户空间、程号等。另一种是 **用起来是隔离的技术** ,称为 **cgroup** ,也即明明整台机器有很多的 CPU、内存,而一个应用只能用其中的一部分。 +封闭的环境主要使用了两种技术,一种是 **看起来是隔离的技术**,称为 **namespace**,也即每个 namespace中的应用看到的是不同的 IP地址、用户空间、程号等。另一种是 **用起来是隔离的技术**,称为 **cgroup**,也即明明整台机器有很多的 CPU、内存,而一个应用只能用其中的一部分。 有了这两项技术,就相当于我们焊好了集装箱。接下来的问题就是如何“将这个集装箱标准化”,并在哪艘船上都能运输。这里的标准首先就是 **镜像** 。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25430\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25430\350\256\262.md" index 19cebedc9..655d714ab 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25430\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25430\350\256\262.md" @@ -42,7 +42,7 @@ Kubernetes作为集团军作战管理平台,提出指导意见,说网络模 例如物理机A是网段172.17.8.0/24,物理机B是网段172.17.9.0/24,这样两台机器上启动的容器IP肯定不一样,而且就看IP地址,我们就一下子识别出,这个容器是本机的,还是远程的,如果是远程的,也能从网段一下子就识别出它归哪台物理机管,太方便了。 -接下来的问题,就是 **物理机A上的容器如何访问到物理机B上的容器呢?** 你是不是想到了熟悉的场景?虚拟机也需要跨物理机互通,往往通过Overlay的方式,容器是不是也可以这样做呢? **这里我要说Flannel使用UDP实现Overlay网络的方案。** ![img](assets/01ee306698c7dd6207e80fea0a8238c8.jpg) +接下来的问题,就是 **物理机A上的容器如何访问到物理机B上的容器呢?** 你是不是想到了熟悉的场景?虚拟机也需要跨物理机互通,往往通过Overlay的方式,容器是不是也可以这样做呢?**这里我要说Flannel使用UDP实现Overlay网络的方案。**![img](assets/01ee306698c7dd6207e80fea0a8238c8.jpg) 在物理机A上的容器A里面,能看到的容器的IP地址是172.17.8.2/24,里面设置了默认的路由规则default via 172.17.8.1 dev eth0。 @@ -66,7 +66,7 @@ Kubernetes作为集团军作战管理平台,提出指导意见,说网络模 上面的过程连通性没有问题,但是由于全部在用户态,所以性能差了一些。 -跨物理机的连通性问题,在虚拟机那里有成熟的方案,就是VXLAN,那 **能不能Flannel也用VXLAN呢** ? +跨物理机的连通性问题,在虚拟机那里有成熟的方案,就是VXLAN,那 **能不能Flannel也用VXLAN呢**? 当然可以了。如果使用VXLAN,就不需要打开一个TUN设备了,而是要建立一个VXLAN的VTEP。如何建立呢?可以通过netlink通知内核建立一个VTEP的网卡flannel.1。在我们讲OpenvSwitch的时候提过,netlink是一种用户态和内核态通信的机制。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25431\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25431\350\256\262.md" index 26e5c0fd0..8cc0b7610 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25431\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25431\350\256\262.md" @@ -18,7 +18,7 @@ 当包到达物理机B的时候,能够匹配到这条路由规则,将包发给下一跳的路由器,也即发给物理机A。在物理机A上也有路由规则,要访问172.17.8.0/24,从docker0的网卡进去即可。 -这就是 **Calico网络的大概思路** , **即不走Overlay网络,不引入另外的网络性能损耗,而是将转发全部用三层网络的路由转发来实现** ,只不过具体的实现和上面的过程稍有区别。 +这就是 **Calico网络的大概思路**,**即不走Overlay网络,不引入另外的网络性能损耗,而是将转发全部用三层网络的路由转发来实现**,只不过具体的实现和上面的过程稍有区别。 首先,如果全部走三层的路由规则,没必要每台机器都用一个docker0,从而浪费了一个IP地址,而是可以直接用路由转发到veth pair在物理机这一端的网卡。同样,在容器内,路由规则也可以这样设定:把容器外面的veth pair网卡算作默认网关,下一跳就是外面的物理机。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25433\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25433\350\256\262.md" index 5d7368230..7d28577d6 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25433\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25433\350\256\262.md" @@ -6,15 +6,15 @@ ONC RPC将客户端要发送的参数,以及服务端要发送的回复,都压缩为一个二进制串,这样固然能够解决双方的协议约定问题,但是存在一定的不方便。 -首先, **需要双方的压缩格式完全一致** ,一点都不能差。一旦有少许的差错,多一位,少一位或者错一位,都可能造成无法解压缩。当然,我们可以用传输层的可靠性以及加入校验值等方式,来减少传输过程中的差错。 +首先,**需要双方的压缩格式完全一致**,一点都不能差。一旦有少许的差错,多一位,少一位或者错一位,都可能造成无法解压缩。当然,我们可以用传输层的可靠性以及加入校验值等方式,来减少传输过程中的差错。 -其次, **协议修改不灵活** 。如果不是传输过程中造成的差错,而是客户端因为业务逻辑的改变,添加或者删除了字段,或者服务端添加或者删除了字段,而双方没有及时通知,或者线上系统没有及时升级,就会造成解压缩不成功。 +其次,**协议修改不灵活** 。如果不是传输过程中造成的差错,而是客户端因为业务逻辑的改变,添加或者删除了字段,或者服务端添加或者删除了字段,而双方没有及时通知,或者线上系统没有及时升级,就会造成解压缩不成功。 因而,当业务发生改变,需要多传输一些参数或者少传输一些参数的时候,都需要及时通知对方,并且根据约定好的协议文件重新生成双方的Stub程序。自然,这样灵活性比较差。 如果仅仅是沟通的问题也还好解决,其实更难弄的还有 **版本的问题** 。比如在服务端提供一个服务,参数的格式是版本一的,已经有50个客户端在线上调用了。现在有一个客户端有个需求,要加一个字段,怎么办呢?这可是一个大工程,所有的客户端都要适配这个,需要重新写程序,加上这个字段,但是传输值是0,不需要这个字段的客户端很“冤”,本来没我啥事儿,为啥让我也忙活? -最后, **ONC RPC的设计明显是面向函数的,而非面向对象** 。而当前面向对象的业务逻辑设计与实现方式已经成为主流。 +最后,**ONC RPC的设计明显是面向函数的,而非面向对象** 。而当前面向对象的业务逻辑设计与实现方式已经成为主流。 这一切的根源就在于压缩。这就像平时我们爱用缩略语。如果是篮球爱好者,你直接说NBA,他马上就知道什么意思,但是如果你给一个大妈说NBA,她可能就不知所云。 @@ -44,7 +44,7 @@ ONC RPC将客户端要发送的参数,以及服务端要发送的回复,都 有了这个,刚才我们说的那几个问题就都不是问题了。 -首先, **格式没必要完全一致** 。比如如果我们把price和author换个位置,并不影响客户端和服务端解析这个文本,也根本不会误会,说这个作者的名字叫68。 +首先,**格式没必要完全一致** 。比如如果我们把price和author换个位置,并不影响客户端和服务端解析这个文本,也根本不会误会,说这个作者的名字叫68。 如果有的客户端想增加一个字段,例如添加一个推荐人字段,只需要在上面的文件中加一行: @@ -100,7 +100,7 @@ HTTP协议我们学过,这个请求使用POST方法,发送一个格式为 ap 因为服务开发出来是给陌生人用的,就像上面下单的那个XML文件,对于客户端来说,它如何知道应该拼装成上面的格式呢?这就需要对于服务进行描述,因为调用的人不认识你,所以没办法找到你,问你的服务应该如何调用。 -当然你可以写文档,然后放在官方网站上,但是你的文档不一定更新得那么及时,而且你也写的文档也不一定那么严谨,所以常常会有调试不成功的情况。因而,我们需要一种相对比较严谨的 **Web服务描述语言** , **WSDL** (Web Service Description Languages)。它也是一个XML文件。 +当然你可以写文档,然后放在官方网站上,但是你的文档不一定更新得那么及时,而且你也写的文档也不一定那么严谨,所以常常会有调试不成功的情况。因而,我们需要一种相对比较严谨的 **Web服务描述语言**,**WSDL** (Web Service Description Languages)。它也是一个XML文件。 在这个文件中,要定义一个类型order,与上面的XML对应起来。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25435\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25435\350\256\262.md" index f9773e934..6eb6c2ff5 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25435\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25435\350\256\262.md" @@ -16,7 +16,7 @@ API网关用来管理API,但是API的实现一般在一个叫作 **Controller 这些服务端的状态,例如订单、库存、商品等,都是重中之重,都需要持久化到硬盘上,数据不能丢,但是由于硬盘读写性能差,因而持久化层往往吞吐量不能达到互联网应用要求的吞吐量,因而前面要有一层缓存层,使用Redis或者memcached将请求拦截一道,不能让所有的请求都进入数据库“中军大营”。 -缓存和持久化层之上一般是 **基础服务层** ,这里面提供一些原子化的接口。例如,对于用户、商品、订单、库存的增删查改,将缓存和数据库对再上层的业务逻辑屏蔽一道。有了这一层,上层业务逻辑看到的都是接口,而不会调用数据库和缓存。因而对于缓存层的扩容,数据库的分库分表,所有的改变,都截止到这一层,这样有利于将来对于缓存和数据库的运维。 +缓存和持久化层之上一般是 **基础服务层**,这里面提供一些原子化的接口。例如,对于用户、商品、订单、库存的增删查改,将缓存和数据库对再上层的业务逻辑屏蔽一道。有了这一层,上层业务逻辑看到的都是接口,而不会调用数据库和缓存。因而对于缓存层的扩容,数据库的分库分表,所有的改变,都截止到这一层,这样有利于将来对于缓存和数据库的运维。 再往上就是 **组合层** 。因为基础服务层只是提供简单的接口,实现简单的业务逻辑,而复杂的业务逻辑,比如下单,要扣优惠券,扣减库存等,就要在组合服务层实现。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25436\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25436\350\256\262.md" index a04b7ac2a..009ac577c 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25436\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25436\350\256\262.md" @@ -93,25 +93,25 @@ HTTP 2.0还将所有的传输信息分割为更小的消息和帧,并对它们 由于基于HTTP 2.0,GRPC和其他的RPC不同,可以定义四种服务方法。 -第一种,也是最常用的方式是 **单向RPC** ,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。 +第一种,也是最常用的方式是 **单向RPC**,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。 ```bash rpc SayHello(HelloRequest) returns (HelloResponse){} ``` -第二种方式是 **服务端流式RPC** ,即服务端返回的不是一个结果,而是一批。客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取,直到没有更多消息为止。 +第二种方式是 **服务端流式RPC**,即服务端返回的不是一个结果,而是一批。客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取,直到没有更多消息为止。 ```bash rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){} ``` -第三种方式为 **客户端流式RPC** ,也即客户端的请求不是一个,而是一批。客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。 +第三种方式为 **客户端流式RPC**,也即客户端的请求不是一个,而是一批。客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。 ```bash rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {} ``` -第四种方式为 **双向流式 RPC** ,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者读写相结合的其他方式。每个数据流里消息的顺序会被保持。 +第四种方式为 **双向流式 RPC**,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者读写相结合的其他方式。每个数据流里消息的顺序会被保持。 ```bash rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){} @@ -167,7 +167,7 @@ Envoy这么牛,是不是能够将服务之间的相互调用全部由它代理 如果我们的应用能够意识不到服务治理的存在,就是直接进行GRPC的调用就可以了。 -这就是未来服务治理的趋势 **Serivce Mesh** ,也即应用之间的相互调用全部由Envoy进行代理,服务之间的治理也被Envoy进行代理,完全将服务治理抽象出来,到平台层解决。 +这就是未来服务治理的趋势 **Serivce Mesh**,也即应用之间的相互调用全部由Envoy进行代理,服务之间的治理也被Envoy进行代理,完全将服务治理抽象出来,到平台层解决。 ![img](assets/15e254a8e92e031b20feb6ebdcc32402.jpg) diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25437\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25437\350\256\262.md" index 9dce70352..cad2941b0 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25437\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25437\350\256\262.md" @@ -26,7 +26,7 @@ 云平台会给每个Subnet的数据库实例分配一个域名。创建数据库实例的时候,需要你指定可用区和Subnet,这样创建出来的数据库实例可以通过这个Subnet的私网IP进行访问。 -为了分库分表实现高并发的读写,在创建的多个数据库实例之上,会 **创建一个分布式数据库的实例** ,也需要指定可用区和Subnet,还会为分布式数据库分配一个私网IP和域名。 +为了分库分表实现高并发的读写,在创建的多个数据库实例之上,会 **创建一个分布式数据库的实例**,也需要指定可用区和Subnet,还会为分布式数据库分配一个私网IP和域名。 对于数据库这种高可用性比较高的,需要进行跨机房高可用,因而两个可用区都要部署一套,但是只有一个是主,另外一个是备,云平台往往会提供数据库同步工具,将应用写入主的数据同步给备数据库集群。 diff --git "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25438\350\256\262.md" "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25438\350\256\262.md" index c0469b86f..0a136de34 100644 --- "a/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25438\350\256\262.md" +++ "b/docs/Network/\350\266\243\350\260\210\347\275\221\347\273\234\345\215\217\350\256\256/\347\254\25438\350\256\262.md" @@ -87,7 +87,7 @@ Content-Length: nnn } ``` -HTTP的报文大概分为三大部分。第一部分是 **请求行** ,第二部分是 **请求的首部** ,第三部分才是 **请求的正文实体** 。 +HTTP的报文大概分为三大部分。第一部分是 **请求行**,第二部分是 **请求的首部**,第三部分才是 **请求的正文实体** 。 在请求行中,URL就是 [www.geektime.com/purchaseOrder](https://www.geektime.com/purchaseOrder) ,版本为HTTP 1.1。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25400\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25400\350\256\262.md" index f2f93d8de..aa1a76aec 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25400\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25400\350\256\262.md" @@ -33,7 +33,7 @@ 我把这种 HTTP 学习的现状归纳为三点: **正式资料“少”、网上资料“杂”、权威资料“难”** 。 -第一个, **正式资料“少”** 。 +第一个,**正式资料“少”** 。 上购书网站,搜个 Python、Java,搜个 MySQL、Node.js,能出一大堆。但搜 HTTP,实在是少得可怜,那么几本,一只手的手指头就可以数得过来,和语言类、数据库类、框架类图书真是形成了鲜明的对比。 @@ -41,7 +41,7 @@ 而且这些书的“岁数”都很大,依据的都是 20 年前的 RFC2616,很多内容都不合时宜,而新标准 7230 已经更新了很多关键的细节。 -第二个, **网上资料“杂”** 。 +第二个,**网上资料“杂”** 。 正式的图书少,而且过时,那就求助于网络社区吧。现在的博客、论坛、搜索引擎非常发达,网上有很多 HTTP 协议相关的文章,也都是网友的实践经验分享,“干货”很多,很能解决实际问题。 @@ -51,7 +51,7 @@ 可想而知,这种“东一榔头西一棒子”的学习方式,用“碎片”拼凑出来的 HTTP 知识体系是非常不完善的,会有各种漏洞,遇到问题时基本派不上用场,还得再去找其他的“碎片”。 -第三个, **权威资料“难”** 。 +第三个,**权威资料“难”** 。 图书少,网文杂,我们还有一个终极的学习资料,那就是 RFC 文档。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25402\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25402\350\256\262.md" index 259181e9d..c89193258 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25402\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25402\350\256\262.md" @@ -60,7 +60,7 @@ HTTP 是一个“ **传输协议** ”,所谓的“传输”(Transfer)其 第一点,HTTP 协议是一个“ **双向协议** ”。 -也就是说,有两个最基本的参与者 A 和 B,从 A 开始到 B 结束,数据在 A 和 B 之间双向而不是单向流动。通常我们把先发起传输动作的 A 叫做 **请求方** ,把后接到传输的 B 叫做 **应答方** 或者 **响应方** 。拿我们最常见的上网冲浪来举例子,浏览器就是请求方 A,网易、新浪这些网站就是应答方 B。双方约定用 HTTP 协议来通信,于是浏览器把一些数据发送给网站,网站再把一些数据发回给浏览器,最后展现在屏幕上,你就可以看到各种有意思的新闻、视频了。 +也就是说,有两个最基本的参与者 A 和 B,从 A 开始到 B 结束,数据在 A 和 B 之间双向而不是单向流动。通常我们把先发起传输动作的 A 叫做 **请求方**,把后接到传输的 B 叫做 **应答方** 或者 **响应方** 。拿我们最常见的上网冲浪来举例子,浏览器就是请求方 A,网易、新浪这些网站就是应答方 B。双方约定用 HTTP 协议来通信,于是浏览器把一些数据发送给网站,网站再把一些数据发回给浏览器,最后展现在屏幕上,你就可以看到各种有意思的新闻、视频了。 第二点,数据虽然是在 A 和 B 之间传输,但并没有限制只有 A 和 B 这两个角色,允许中间有“中转”或者“接力”。 @@ -96,13 +96,13 @@ OK,经过了对 HTTP 里这三个名词的详细解释,下次当你再面对 互联网(Internet)是遍布于全球的许多网络互相连接而形成的一个巨大的国际网络,在它上面存放着各式各样的资源,也对应着各式各样的协议,例如超文本资源使用 HTTP,普通文件使用 FTP,电子邮件使用 SMTP 和 POP3 等。 -但毫无疑问,HTTP 是构建互联网的一块重要拼图,而且是占比最大的那一块。 **HTTP 不是编程语言** 。 +但毫无疑问,HTTP 是构建互联网的一块重要拼图,而且是占比最大的那一块。**HTTP 不是编程语言** 。 编程语言是人与计算机沟通交流所使用的语言,而 HTTP 是计算机与计算机沟通交流的语言,我们无法使用 HTTP 来编程,但可以反过来,用编程语言去实现 HTTP,告诉计算机如何用 HTTP 来与外界通信。 -很多流行的编程语言都支持编写 HTTP 相关的服务或应用,例如使用 Java 在 Tomcat 里编写 Web 服务,使用 PHP 在后端实现页面模板渲染,使用 JavaScript 在前端实现动态页面更新,你是否也会其中的一两种呢? **HTTP 不是 HTML** ,这个可能要特别强调一下,千万不要把 HTTP 与 HTML 混为一谈,虽然这两者经常是同时出现。 +很多流行的编程语言都支持编写 HTTP 相关的服务或应用,例如使用 Java 在 Tomcat 里编写 Web 服务,使用 PHP 在后端实现页面模板渲染,使用 JavaScript 在前端实现动态页面更新,你是否也会其中的一两种呢?**HTTP 不是 HTML**,这个可能要特别强调一下,千万不要把 HTTP 与 HTML 混为一谈,虽然这两者经常是同时出现。 -HTML 是超文本的载体,是一种标记语言,使用各种标签描述文字、图片、超链接等资源,并且可以嵌入 CSS、JavaScript 等技术实现复杂的动态效果。单论次数,在互联网上 HTTP 传输最多的可能就是 HTML,但要是论数据量,HTML 可能要往后排了,图片、音频、视频这些类型的资源显然更大。 **HTTP 不是一个孤立的协议** 。 +HTML 是超文本的载体,是一种标记语言,使用各种标签描述文字、图片、超链接等资源,并且可以嵌入 CSS、JavaScript 等技术实现复杂的动态效果。单论次数,在互联网上 HTTP 传输最多的可能就是 HTML,但要是论数据量,HTML 可能要往后排了,图片、音频、视频这些类型的资源显然更大。**HTTP 不是一个孤立的协议** 。 俗话说“一个好汉三个帮”,HTTP 也是如此。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25403\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25403\350\256\262.md" index ff8270ed8..b628fcdea 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25403\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25403\350\256\262.md" @@ -42,7 +42,7 @@ 浏览器的正式名字叫“ **Web Browser** ”,顾名思义,就是检索、查看互联网上网页资源的应用程序,名字里的 Web,实际上指的就是“World Wide Web”,也就是万维网。 -浏览器本质上是一个 HTTP 协议中的 **请求方** ,使用 HTTP 协议获取网络上的各种资源。当然,为了让我们更好地检索查看网页,它还集成了很多额外的功能。 +浏览器本质上是一个 HTTP 协议中的 **请求方**,使用 HTTP 协议获取网络上的各种资源。当然,为了让我们更好地检索查看网页,它还集成了很多额外的功能。 例如,HTML 排版引擎用来展示页面,JavaScript 引擎用来实现动态化效果,甚至还有开发者工具用来调试网页,以及五花八门的各种插件和扩展。 @@ -52,11 +52,11 @@ 刚才说的浏览器是 HTTP 里的请求方,那么在协议另一端的 **应答方** (响应方)又是什么呢? -这个你一定也很熟悉,答案就是 **服务器** , **Web Server** 。 +这个你一定也很熟悉,答案就是 **服务器**,**Web Server** 。 Web 服务器是一个很大也很重要的概念,它是 HTTP 协议里响应请求的主体,通常也把控着绝大多数的网络资源,在网络世界里处于强势地位。 -当我们谈到“Web 服务器”时有两个层面的含义:硬件和软件。 **硬件** 含义就是物理形式或“云”形式的机器,在大多数情况下它可能不是一台服务器,而是利用反向代理、负载均衡等技术组成的庞大集群。但从外界看来,它仍然表现为一台机器,但这个形象是“虚拟的”。 **软件** 含义的 Web 服务器可能我们更为关心,它就是提供 Web 服务的应用程序,通常会运行在硬件含义的服务器上。它利用强大的硬件能力响应海量的客户端 HTTP 请求,处理磁盘上的网页、图片等静态文件,或者把请求转发给后面的 Tomcat、Node.js 等业务应用,返回动态的信息。 +当我们谈到“Web 服务器”时有两个层面的含义:硬件和软件。**硬件** 含义就是物理形式或“云”形式的机器,在大多数情况下它可能不是一台服务器,而是利用反向代理、负载均衡等技术组成的庞大集群。但从外界看来,它仍然表现为一台机器,但这个形象是“虚拟的”。**软件** 含义的 Web 服务器可能我们更为关心,它就是提供 Web 服务的应用程序,通常会运行在硬件含义的服务器上。它利用强大的硬件能力响应海量的客户端 HTTP 请求,处理磁盘上的网页、图片等静态文件,或者把请求转发给后面的 Tomcat、Node.js 等业务应用,返回动态的信息。 比起层出不穷的各种 Web 浏览器,Web 服务器就要少很多了,一只手的手指头就可以数得过来。 @@ -70,7 +70,7 @@ Nginx 是 Web 服务器里的后起之秀,特点是高性能、高稳定,且 浏览器和服务器是 HTTP 协议的两个端点,那么,在这两者之间还有别的什么东西吗? -当然有了。浏览器通常不会直接连到服务器,中间会经过“重重关卡”,其中的一个重要角色就叫做 CDN。 **CDN** ,全称是“Content Delivery Network”,翻译过来就是“内容分发网络”。它应用了 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求。 +当然有了。浏览器通常不会直接连到服务器,中间会经过“重重关卡”,其中的一个重要角色就叫做 CDN。**CDN**,全称是“Content Delivery Network”,翻译过来就是“内容分发网络”。它应用了 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求。 CDN 有什么好处呢? @@ -100,15 +100,15 @@ CDN 也是现在互联网中的一项重要基础设施,除了基本的网络 ## HTML/WebService/WAF -到现在我已经说完了图中右边的五大部分,而左边的 HTML、WebService、WAF 等由于与 HTTP 技术上实质关联不太大,所以就简略地介绍一下,不再过多展开。 **HTML** 是 HTTP 协议传输的主要内容之一,它描述了超文本页面,用各种“标签”定义文字、图片等资源和排版布局,最终由浏览器“渲染”出可视化页面。 +到现在我已经说完了图中右边的五大部分,而左边的 HTML、WebService、WAF 等由于与 HTTP 技术上实质关联不太大,所以就简略地介绍一下,不再过多展开。**HTML** 是 HTTP 协议传输的主要内容之一,它描述了超文本页面,用各种“标签”定义文字、图片等资源和排版布局,最终由浏览器“渲染”出可视化页面。 HTML 目前有两个主要的标准,HTML4 和 HTML5。广义上的 HTML 通常是指 HTML、JavaScript、CSS 等前端技术的组合,能够实现比传统静态页面更丰富的动态页面。 -接下来是 **Web** **Service** ,它的名字与 Web Server 很像,但却是一个完全不同的东西。 +接下来是 **Web** **Service**,它的名字与 Web Server 很像,但却是一个完全不同的东西。 -Web Service 是一种由 W3C 定义的应用服务开发规范,使用 client-server 主从架构,通常使用 WSDL 定义服务接口,使用 HTTP 协议传输 XML 或 SOAP 消息,也就是说,它是 **一个基于 Web(HTTP)的服务架构技术** ,既可以运行在内网,也可以在适当保护后运行在外网。 +Web Service 是一种由 W3C 定义的应用服务开发规范,使用 client-server 主从架构,通常使用 WSDL 定义服务接口,使用 HTTP 协议传输 XML 或 SOAP 消息,也就是说,它是 **一个基于 Web(HTTP)的服务架构技术**,既可以运行在内网,也可以在适当保护后运行在外网。 -因为采用了 HTTP 协议传输数据,所以在 Web Service 架构里服务器和客户端可以采用不同的操作系统或编程语言开发。例如服务器端用 Linux+Java,客户端用 Windows+C#,具有跨平台跨语言的优点。 **WAF** 是近几年比较“火”的一个词,意思是“网络应用防火墙”。与硬件“防火墙”类似,它是应用层面的“防火墙”,专门检测 HTTP 流量,是防护 Web 应用的安全技术。 +因为采用了 HTTP 协议传输数据,所以在 Web Service 架构里服务器和客户端可以采用不同的操作系统或编程语言开发。例如服务器端用 Linux+Java,客户端用 Windows+C#,具有跨平台跨语言的优点。**WAF** 是近几年比较“火”的一个词,意思是“网络应用防火墙”。与硬件“防火墙”类似,它是应用层面的“防火墙”,专门检测 HTTP 流量,是防护 Web 应用的安全技术。 WAF 通常位于 Web 服务器之前,可以阻止如 SQL 注入、跨站脚本等攻击,目前应用较多的一个开源项目是 ModSecurity,它能够完全集成进 Apache 或 Nginx。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25404\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25404\350\256\262.md" index 8749cef82..298e6c18d 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25404\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25404\350\256\262.md" @@ -12,11 +12,11 @@ TCP/IP 协议是目前网络世界“事实上”的标准通信协议,即使你没有用过也一定听说过,因为它太著名了。 -TCP/IP 协议实际上是一系列网络通信协议的统称,其中最核心的两个协议是 **TCP** 和 **IP** ,其他的还有 UDP、ICMP、ARP 等等,共同构成了一个复杂但有层次的协议栈。 +TCP/IP 协议实际上是一系列网络通信协议的统称,其中最核心的两个协议是 **TCP** 和 **IP**,其他的还有 UDP、ICMP、ARP 等等,共同构成了一个复杂但有层次的协议栈。 -这个协议栈有四层,最上层是“应用层”,最下层是“链接层”,TCP 和 IP 则在中间: **TCP 属于“传输层”,IP 属于“网际层”** 。协议的层级关系模型非常重要,我会在下一讲中再专门讲解,这里先暂时放一放。 **IP 协议** 是“ **I** nternet **P** rotocol”的缩写,主要目的是解决寻址和路由问题,以及如何在两点间传送数据包。IP 协议使用“ **IP 地址** ”的概念来定位互联网上的每一台计算机。可以对比一下现实中的电话系统,你拿着的手机相当于互联网上的计算机,而要打电话就必须接入电话网,由通信公司给你分配一个号码,这个号码就相当于 IP 地址。 +这个协议栈有四层,最上层是“应用层”,最下层是“链接层”,TCP 和 IP 则在中间: **TCP 属于“传输层”,IP 属于“网际层”** 。协议的层级关系模型非常重要,我会在下一讲中再专门讲解,这里先暂时放一放。**IP 协议** 是“ **I** nternet **P** rotocol”的缩写,主要目的是解决寻址和路由问题,以及如何在两点间传送数据包。IP 协议使用“ **IP 地址** ”的概念来定位互联网上的每一台计算机。可以对比一下现实中的电话系统,你拿着的手机相当于互联网上的计算机,而要打电话就必须接入电话网,由通信公司给你分配一个号码,这个号码就相当于 IP 地址。 -现在我们使用的 IP 协议大多数是 v4 版,地址是四个用“.”分隔的数字,例如“192.168.0.1”,总共有 232,大约 42 亿个可以分配的地址。看上去好像很多,但互联网的快速发展让地址的分配管理很快就“捉襟见肘”。所以,就又出现了 v6 版,使用 8 组“:”分隔的数字作为地址,容量扩大了很多,有 2128 个,在未来的几十年里应该是足够用了。 **TCP 协议** 是“ **T** ransmission **C** ontrol **P** rotocol”的缩写,意思是“传输控制协议”,它位于 IP 协议之上,基于 IP 协议提供可靠的、字节流形式的通信,是 HTTP 协议得以实现的基础。 +现在我们使用的 IP 协议大多数是 v4 版,地址是四个用“.”分隔的数字,例如“192.168.0.1”,总共有 232,大约 42 亿个可以分配的地址。看上去好像很多,但互联网的快速发展让地址的分配管理很快就“捉襟见肘”。所以,就又出现了 v6 版,使用 8 组“:”分隔的数字作为地址,容量扩大了很多,有 2128 个,在未来的几十年里应该是足够用了。**TCP 协议** 是“ **T** ransmission **C** ontrol **P** rotocol”的缩写,意思是“传输控制协议”,它位于 IP 协议之上,基于 IP 协议提供可靠的、字节流形式的通信,是 HTTP 协议得以实现的基础。 “可靠”是指保证数据不丢失,“字节流”是指保证数据完整,所以在 TCP 协议的两端可以如同操作文件一样访问传输的数据,就像是读写在一个密闭的管道里“流动”的字节。 @@ -48,9 +48,9 @@ HTTP 协议中并没有明确要求必须使用 DNS,但实际上为了方便 还不行,DNS 和 IP 地址只是标记了互联网上的主机,但主机上有那么多文本、图片、页面,到底要找哪一个呢?就像小明管理了一大堆文档,你怎么告诉他是哪个呢? -所以就出现了 URI( **U** niform **R** esource **I** dentifier),中文名称是 **统一资源标识符** ,使用它就能够唯一地标记互联网上资源。 +所以就出现了 URI( **U** niform **R** esource **I** dentifier),中文名称是 **统一资源标识符**,使用它就能够唯一地标记互联网上资源。 -URI 另一个更常用的表现形式是 URL( **U** niform **R** esource **L** ocator), **统一资源定位符** ,也就是我们俗称的“网址”,它实际上是 URI 的一个子集,不过因为这两者几乎是相同的,差异不大,所以通常不会做严格的区分。 +URI 另一个更常用的表现形式是 URL( **U** niform **R** esource **L** ocator),**统一资源定位符**,也就是我们俗称的“网址”,它实际上是 URI 的一个子集,不过因为这两者几乎是相同的,差异不大,所以通常不会做严格的区分。 我就拿 Nginx 网站来举例,看一下 URI 是什么样子的。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25405\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25405\350\256\262.md" index f7af74e2e..41e444ef0 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25405\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25405\350\256\262.md" @@ -40,7 +40,7 @@ MAC 层的传输单位是帧(frame),IP 层的传输单位是包(packet 看完 TCP/IP 协议栈,你可能要问了,“它只有四层,那常说的七层怎么没见到呢?” -别着急,这就是今天要说的第二个网络分层模型: **OSI** ,全称是“ **开放式系统互联通信参考模型** ”(Open System Interconnection Reference Model)。 +别着急,这就是今天要说的第二个网络分层模型: **OSI**,全称是“ **开放式系统互联通信参考模型** ”(Open System Interconnection Reference Model)。 TCP/IP 发明于 1970 年代,当时除了它还有很多其他的网络协议,整个网络世界比较混乱。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25407\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25407\350\256\262.md" index f9f64e27d..c92c150fe 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25407\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25407\350\256\262.md" @@ -37,11 +37,11 @@ HTTP 协议不是一个孤立的协议,需要下层很多其他协议的配合 **Wireshark** 是著名的网络抓包工具,能够截获在 TCP/IP 协议栈中传输的所有流量,并按协议类型、地址、端口等任意过滤,功能非常强大,是学习网络协议的必备工具。 -它就像是网络世界里的一台“高速摄像机”,把只在一瞬间发生的网络传输过程如实地“拍摄”下来,事后再“慢速回放”,让我们能够静下心来仔细地分析那一瞬到底发生了什么。 **Chrome** 是 Google 开发的浏览器,是目前的主流浏览器之一。它不仅上网方便,也是一个很好的调试器,对 HTTP/1.1、HTTPS、HTTP/2、QUIC 等的协议都支持得非常好,用 F12 打开“开发者工具”还可以非常详细地观测 HTTP 传输全过程的各种数据。 +它就像是网络世界里的一台“高速摄像机”,把只在一瞬间发生的网络传输过程如实地“拍摄”下来,事后再“慢速回放”,让我们能够静下心来仔细地分析那一瞬到底发生了什么。**Chrome** 是 Google 开发的浏览器,是目前的主流浏览器之一。它不仅上网方便,也是一个很好的调试器,对 HTTP/1.1、HTTPS、HTTP/2、QUIC 等的协议都支持得非常好,用 F12 打开“开发者工具”还可以非常详细地观测 HTTP 传输全过程的各种数据。 -如果你更习惯使用 **Firefox** ,那也没问题,其实它和 Chrome 功能上都差不太多,选择自己喜欢的就好。 +如果你更习惯使用 **Firefox**,那也没问题,其实它和 Chrome 功能上都差不太多,选择自己喜欢的就好。 -与 Wireshark 不同,Chrome 和 Firefox 属于“事后诸葛亮”,不能观测 HTTP 传输的过程,只能看到结果。 **Telnet** 是一个经典的虚拟终端,基于 TCP 协议远程登录主机,我们可以使用它来模拟浏览器的行为,连接服务器后手动发送 HTTP 请求,把浏览器的干扰也彻底排除,能够从最原始的层面去研究 HTTP 协议。 **OpenResty** 你可能比较陌生,它是基于 Nginx 的一个“强化包”,里面除了 Nginx 还有一大堆有用的功能模块,不仅支持 HTTP/HTTPS,还特别集成了脚本语言 Lua 简化 Nginx 二次开发,方便快速地搭建动态网关,更能够当成应用容器来编写业务逻辑。 +与 Wireshark 不同,Chrome 和 Firefox 属于“事后诸葛亮”,不能观测 HTTP 传输的过程,只能看到结果。**Telnet** 是一个经典的虚拟终端,基于 TCP 协议远程登录主机,我们可以使用它来模拟浏览器的行为,连接服务器后手动发送 HTTP 请求,把浏览器的干扰也彻底排除,能够从最原始的层面去研究 HTTP 协议。**OpenResty** 你可能比较陌生,它是基于 Nginx 的一个“强化包”,里面除了 Nginx 还有一大堆有用的功能模块,不仅支持 HTTP/HTTPS,还特别集成了脚本语言 Lua 简化 Nginx 二次开发,方便快速地搭建动态网关,更能够当成应用容器来编写业务逻辑。 选择 OpenResty 而不直接用 Nginx 的原因是它相当于 Nginx 的“超集”,功能更丰富,安装部署更方便。我也会用 Lua 编写一些服务端脚本,实现简单的 Web 服务器响应逻辑,方便实验。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25409\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25409\350\256\262.md" index 296784ce3..6d86e897a 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25409\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25409\350\256\262.md" @@ -172,7 +172,7 @@ Host 字段告诉服务器这个请求应该由哪个主机来处理,当一台 但由于历史的原因,User-Agent 非常混乱,每个浏览器都自称是“Mozilla”“Chrome”“Safari”,企图使用这个字段来互相“伪装”,导致 User-Agent 变得越来越长,最终变得毫无意义。 -不过有的比较“诚实”的爬虫会在 User-Agent 里用“spider”标明自己是爬虫,所以可以利用这个字段实现简单的反爬虫策略。 **Date** 字段是一个通用字段,但通常出现在响应头里,表示 HTTP 报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略。 **Server** 字段是响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号,例如在我们的实验环境里它就是“Server: openresty/1.15.8.1”,即使用的是 OpenResty 1.15.8.1。 +不过有的比较“诚实”的爬虫会在 User-Agent 里用“spider”标明自己是爬虫,所以可以利用这个字段实现简单的反爬虫策略。**Date** 字段是一个通用字段,但通常出现在响应头里,表示 HTTP 报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略。**Server** 字段是响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号,例如在我们的实验环境里它就是“Server: openresty/1.15.8.1”,即使用的是 OpenResty 1.15.8.1。 Server 字段也不是必须要出现的,因为这会把服务器的一部分信息暴露给外界,如果这个版本恰好存在 bug,那么黑客就有可能利用 bug 攻陷服务器。所以,有的网站响应头里要么没有这个字段,要么就给出一个完全无关的描述信息。 @@ -180,7 +180,7 @@ Server 字段也不是必须要出现的,因为这会把服务器的一部分 ![img](assets/f1970aaecad58fb18938e262ea7f311c.png) -实体字段里要说的一个是 **Content-Length** ,它表示报文里 body 的长度,也就是请求头或响应头空行后面数据的长度。服务器看到这个字段,就知道了后续有多少数据,可以直接接收。如果没有这个字段,那么 body 就是不定长的,需要使用 chunked 方式分段传输。 +实体字段里要说的一个是 **Content-Length**,它表示报文里 body 的长度,也就是请求头或响应头空行后面数据的长度。服务器看到这个字段,就知道了后续有多少数据,可以直接接收。如果没有这个字段,那么 body 就是不定长的,需要使用 chunked 方式分段传输。 ## 小结 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25410\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25410\350\256\262.md" index 43bed0366..434d85e37 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25410\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25410\350\256\262.md" @@ -10,7 +10,7 @@ HTTP 协议里为什么要有“请求方法”这个东西呢? 很显然,需要有某种“动作的指示”,告诉操作这些资源的方式。所以,就这么出现了“请求方法”。它的实际含义就是客户端发出了一个“动作指令”,要求服务器端对 URI 定位的资源执行这个动作。 -目前 HTTP/1.1 规定了八种方法,单词 **都必须是大写的形式** ,我先简单地列把它们列出来,后面再详细讲解。 +目前 HTTP/1.1 规定了八种方法,单词 **都必须是大写的形式**,我先简单地列把它们列出来,后面再详细讲解。 1. GET:获取资源,可以理解为读取或者下载数据; 1. HEAD:获取资源的元信息; @@ -39,11 +39,11 @@ HTTP 协议里为什么要有“请求方法”这个东西呢? **GET** 方法应该是 HTTP 协议里最知名的请求方法了,也应该是用的最多的,自 0.9 版出现并一直被保留至今,是名副其实的“元老”。 -它的含义是请求 **从服务器获取资源** ,这个资源既可以是静态的文本、页面、图片、视频,也可以是由 PHP、Java 动态生成的页面或者其他格式的数据。 +它的含义是请求 **从服务器获取资源**,这个资源既可以是静态的文本、页面、图片、视频,也可以是由 PHP、Java 动态生成的页面或者其他格式的数据。 GET 方法虽然基本动作比较简单,但搭配 URI 和其他头字段就能实现对资源更精细的操作。 -例如,在 URI 后使用“#”,就可以在获取页面后直接定位到某个标签所在的位置;使用 If-Modified-Since 字段就变成了“有条件的请求”,仅当资源被修改时才会执行获取动作;使用 Range 字段就是“范围请求”,只获取资源的一部分数据。 **HEAD** 方法与 GET 方法类似,也是请求从服务器获取资源,服务器的处理机制也是一样的,但服务器不会返回请求的实体数据,只会传回响应头,也就是资源的“元信息”。 +例如,在 URI 后使用“#”,就可以在获取页面后直接定位到某个标签所在的位置;使用 If-Modified-Since 字段就变成了“有条件的请求”,仅当资源被修改时才会执行获取动作;使用 Range 字段就是“范围请求”,只获取资源的一部分数据。**HEAD** 方法与 GET 方法类似,也是请求从服务器获取资源,服务器的处理机制也是一样的,但服务器不会返回请求的实体数据,只会传回响应头,也就是资源的“元信息”。 HEAD 方法可以看做是 GET 方法的一个“简化版”或者“轻量版”。因为它的响应头与 GET 完全相同,所以可以用在很多并不真正需要资源的场合,避免传输 body 数据的浪费。 @@ -89,7 +89,7 @@ PUT DATA IS HE ## 其他方法 -讲完了 GET/HEAD/POST/PUT,还剩下四个标准请求方法,它们属于比较“冷僻”的方法,应用的不是很多。 **DELETE** 方法指示服务器删除资源,因为这个动作危险性太大,所以通常服务器不会执行真正的删除操作,而是对资源做一个删除标记。当然,更多的时候服务器就直接不处理 DELETE 请求。 **CONNECT** 是一个比较特殊的方法,要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色。 **OPTIONS** 方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回。它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持。 **TRACE** 方法多用于对 HTTP 链路的测试或诊断,可以显示出请求 - 响应的传输路径。它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。 +讲完了 GET/HEAD/POST/PUT,还剩下四个标准请求方法,它们属于比较“冷僻”的方法,应用的不是很多。**DELETE** 方法指示服务器删除资源,因为这个动作危险性太大,所以通常服务器不会执行真正的删除操作,而是对资源做一个删除标记。当然,更多的时候服务器就直接不处理 DELETE 请求。**CONNECT** 是一个比较特殊的方法,要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色。**OPTIONS** 方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回。它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持。**TRACE** 方法多用于对 HTTP 链路的测试或诊断,可以显示出请求 - 响应的传输路径。它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。 ## 扩展方法 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25411\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25411\350\256\262.md" index 5c1971717..f34efaf1e 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25411\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25411\350\256\262.md" @@ -34,7 +34,7 @@ URI 本质上是一个字符串,这个字符串的作用是 **唯一地标记 ## URI 的基本组成 -URI 第一个组成部分叫 **scheme** ,翻译成中文叫“ **方案名** ”或者“ **协议名** ”,表示 **资源应该使用哪种协议** 来访问。 +URI 第一个组成部分叫 **scheme**,翻译成中文叫“ **方案名** ”或者“ **协议名** ”,表示 **资源应该使用哪种协议** 来访问。 最常见的当然就是“http”了,表示使用 HTTP 协议。另外还有“https”,表示使用经过加密、安全的 HTTPS 协议。此外还有其他不是很常见的 scheme,例如 ftp、ldap、file、news 等。 @@ -46,11 +46,11 @@ URI 第一个组成部分叫 **scheme** ,翻译成中文叫“ **方案名** 不过这个设计已经有了三十年的历史,不管我们愿意不愿意,只能接受。 -在“://”之后,是被称为“ **authority** ”的部分,表示 **资源所在的主机名** ,通常的形式是“host:port”,即主机名加端口号。 +在“://”之后,是被称为“ **authority** ”的部分,表示 **资源所在的主机名**,通常的形式是“host:port”,即主机名加端口号。 主机名可以是 IP 地址或者域名的形式,必须要有,否则浏览器就会找不到服务器。但端口号有时可以省略,浏览器等客户端会依据 scheme 使用默认的端口号,例如 HTTP 的默认端口号是 80,HTTPS 的默认端口号是 443。 -有了协议名和主机地址、端口号,再加上后面 **标记资源所在位置** 的 **path** ,浏览器就可以连接服务器访问资源了。 +有了协议名和主机地址、端口号,再加上后面 **标记资源所在位置** 的 **path**,浏览器就可以连接服务器访问资源了。 URI 里 path 采用了类似文件系统“目录”“路径”的表示方式,因为早期互联网上的计算机多是 UNIX 系统,所以采用了 UNIX 的“/”风格。其实也比较好理解,它与 scheme 后面的“://”是一致的。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25415\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25415\350\256\262.md" index eb4d67c87..3c0f11cde 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25415\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25415\350\256\262.md" @@ -62,7 +62,7 @@ Content-Type: text/html Content-Type: image/png ``` -这样浏览器看到报文里的类型是“text/html”就知道是 HTML 文件,会调用排版引擎渲染出页面,看到“image/png”就知道是一个 PNG 文件,就会在页面上显示出图像。 **Accept-Encoding** 字段标记的是客户端支持的压缩格式,例如上面说的 gzip、deflate 等,同样也可以用“,”列出多个,服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段 **Content-Encoding** 里。 +这样浏览器看到报文里的类型是“text/html”就知道是 HTML 文件,会调用排版引擎渲染出页面,看到“image/png”就知道是一个 PNG 文件,就会在页面上显示出图像。**Accept-Encoding** 字段标记的是客户端支持的压缩格式,例如上面说的 gzip、deflate 等,同样也可以用“,”列出多个,服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段 **Content-Encoding** 里。 ```plaintext Accept-Encoding: gzip, deflate, br @@ -77,7 +77,7 @@ MIME type 和 Encoding type 解决了计算机理解 body 数据的问题,但 这实际上就是“国际化”的问题。HTTP 采用了与数据类型相似的解决方案,又引入了两个概念:语言类型与字符集。 -所谓的“ **语言类型** ”就是人类使用的自然语言,例如英语、汉语、日语等,而这些自然语言可能还有下属的地区性方言,所以在需要明确区分的时候也要使用“type-subtype”的形式,不过这里的格式与数据类型不同, **分隔符不是“/”,而是“-”** 。 +所谓的“ **语言类型** ”就是人类使用的自然语言,例如英语、汉语、日语等,而这些自然语言可能还有下属的地区性方言,所以在需要明确区分的时候也要使用“type-subtype”的形式,不过这里的格式与数据类型不同,**分隔符不是“/”,而是“-”** 。 举几个例子:en 表示任意的英语,en-US 表示美式英语,en-GB 表示英式英语,而 zh-CN 就表示我们最常使用的汉语。 @@ -89,7 +89,7 @@ MIME type 和 Encoding type 解决了计算机理解 body 数据的问题,但 ## 语言类型使用的头字段 -同样的,HTTP 协议也使用 Accept 请求头字段和 Content 实体头字段,用于客户端和服务器就语言与编码进行“ **内容协商** ”。 **Accept-Language** 字段标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型,例如: +同样的,HTTP 协议也使用 Accept 请求头字段和 Content 实体头字段,用于客户端和服务器就语言与编码进行“ **内容协商** ”。**Accept-Language** 字段标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型,例如: ```plaintext Accept-Language: zh-CN, zh, en @@ -103,7 +103,7 @@ Accept-Language: zh-CN, zh, en Content-Language: zh-CN ``` -字符集在 HTTP 里使用的请求头字段是 **Accept-Charset** ,但响应头里却没有对应的 Content-Charset,而是在 **Content-Type** 字段的数据类型后面用“charset=xxx”来表示,这点需要特别注意。 +字符集在 HTTP 里使用的请求头字段是 **Accept-Charset**,但响应头里却没有对应的 Content-Charset,而是在 **Content-Type** 字段的数据类型后面用“charset=xxx”来表示,这点需要特别注意。 例如,浏览器请求 GBK 或 UTF-8 的字符集,然后服务器返回的是 UTF-8 编码,就是下面这样: diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25416\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25416\350\256\262.md" index cc1e597fc..3fe6fdb61 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25416\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25416\350\256\262.md" @@ -36,7 +36,7 @@ 分块传输也可以用于“流式数据”,例如由数据库动态生成的表单页面,这种情况下 body 数据的长度是未知的,无法在头字段“ **Content-Length** ”里给出确切的长度,所以也只能用 chunked 方式分块发送。 -“Transfer-Encoding: chunked”和“Content-Length”这两个字段是 **互斥的** ,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked),这一点你一定要记住。 +“Transfer-Encoding: chunked”和“Content-Length”这两个字段是 **互斥的**,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked),这一点你一定要记住。 下面我们来看一下分块传输的编码规则,其实也很简单,同样采用了明文的方式,很类似响应头。 @@ -89,11 +89,11 @@ Range 的格式也很灵活,起点 x 和终点 y 可以省略,能够很方 服务器收到 Range 字段后,需要做四件事。 -第一,它必须检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码 **416** ,意思是“你的范围请求有误,我无法处理,请再检查一下”。 +第一,它必须检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码 **416**,意思是“你的范围请求有误,我无法处理,请再检查一下”。 第二,如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码“ **206 Partial Content** ”,和 200 的意思差不多,但表示 body 只是原数据的一部分。 -第三,服务器要添加一个响应头字段 **Content-Range** ,告诉片段的实际偏移量和资源的总大小,格式是“ **bytes x-y/length** ”,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。 +第三,服务器要添加一个响应头字段 **Content-Range**,告诉片段的实际偏移量和资源的总大小,格式是“ **bytes x-y/length** ”,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。 最后剩下的就是发送数据了,直接把片段用 TCP 发给客户端,一个范围请求就算是处理完了。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25417\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25417\350\256\262.md" index e113cec05..fd32c3ab1 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25417\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25417\350\256\262.md" @@ -50,7 +50,7 @@ HTTP 协议最初(0.9/1.0)是个非常简单的协议,通信过程也采 由于长连接对性能的改善效果非常显著,所以在 HTTP/1.1 中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接,也就是长连接,在这个连接上收发数据。 -当然,我们也可以在请求头里明确地要求使用长连接机制,使用的字段是 **Connection** ,值是“ **keep-alive** ”。 +当然,我们也可以在请求头里明确地要求使用长连接机制,使用的字段是 **Connection**,值是“ **keep-alive** ”。 不过不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个“ **Connection: keep-alive** ”字段,告诉客户端:“我是支持长连接的,接下来就用这个 TCP 一直收发数据吧”。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25418\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25418\350\256\262.md" index 88610d73a..988adbd03 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25418\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25418\350\256\262.md" @@ -34,7 +34,7 @@ http://nginx.org/en/download.html 这里出现了一个新的头字段“Location: /index.html”,它就是 301/302 重定向跳转的秘密所在。 -“ **Location** ”字段属于响应字段,必须出现在响应报文里。但只有配合 301/302 状态码才有意义,它 **标记了服务器要求重定向的 URI** ,这里就是要求浏览器跳转到“index.html”。 +“ **Location** ”字段属于响应字段,必须出现在响应报文里。但只有配合 301/302 状态码才有意义,它 **标记了服务器要求重定向的 URI**,这里就是要求浏览器跳转到“index.html”。 浏览器收到 301/302 报文,会检查响应头里有没有“Location”。如果有,就从字段值里提取出 URI,发出新的 HTTP 请求,相当于自动替我们点击了这个链接。 @@ -80,7 +80,7 @@ http://www.chrono.com/12-1?code=302 **301** 俗称“永久重定向”(Moved Permanently),意思是原 URI 已经“永久”性地不存在了,今后的所有请求都必须改用新的 URI。 -浏览器看到 301,就知道原来的 URI“过时”了,就会做适当的优化。比如历史记录、更新书签,下次可能就会直接用新的 URI 访问,省去了再次跳转的成本。搜索引擎的爬虫看到 301,也会更新索引库,不再使用老的 URI。 **302** 俗称“临时重定向”(“Moved Temporarily”),意思是原 URI 处于“临时维护”状态,新的 URI 是起“顶包”作用的“临时工”。 +浏览器看到 301,就知道原来的 URI“过时”了,就会做适当的优化。比如历史记录、更新书签,下次可能就会直接用新的 URI 访问,省去了再次跳转的成本。搜索引擎的爬虫看到 301,也会更新索引库,不再使用老的 URI。**302** 俗称“临时重定向”(“Moved Temporarily”),意思是原 URI 处于“临时维护”状态,新的 URI 是起“顶包”作用的“临时工”。 浏览器或者爬虫看到 302,会认为原来的 URI 仍然有效,但暂时不可用,所以只会执行简单的跳转页面,不记录新的 URI,也不会有其他的多余动作,下次访问还是用原 URI。 @@ -94,7 +94,7 @@ http://www.chrono.com/12-1?code=302 ## 重定向的应用场景 -理解了重定向的工作原理和状态码的含义,我们就可以 **在服务器端拥有主动权** ,控制浏览器的行为,不过要怎么利用重定向才好呢? +理解了重定向的工作原理和状态码的含义,我们就可以 **在服务器端拥有主动权**,控制浏览器的行为,不过要怎么利用重定向才好呢? 使用重定向跳转,核心是要理解“ **重定向** ”和“ **永久 / 临时** ”这两个关键词。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25419\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25419\350\256\262.md" index 860d46a66..391bfbab1 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25419\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25419\350\256\262.md" @@ -64,7 +64,7 @@ HTTP 的 Cookie 机制也是一样的道理,既然服务器记不住,那就 ![img](assets/9dbb8b490714360475911ca04134df5d.png) -首先,我们应该 **设置 Cookie 的生存周期** ,也就是它的有效期,让它只能在一段时间内可用,就像是食品的“保鲜期”,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。 +首先,我们应该 **设置 Cookie 的生存周期**,也就是它的有效期,让它只能在一段时间内可用,就像是食品的“保鲜期”,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。 Cookie 的有效期可以使用 Expires 和 Max-Age 两个属性来设置。 @@ -74,7 +74,7 @@ Expires 和 Max-Age 可以同时出现,两者的失效时间可以一致,也 比如在这个例子里,Expires 标记的过期时间是“GMT 2019 年 6 月 7 号 8 点 19 分”,而 Max-Age 则只有 10 秒,如果现在是 6 月 6 号零点,那么 Cookie 的实际有效期就是“6 月 6 号零点过 10 秒”。 -其次,我们需要 **设置 Cookie 的作用域** ,让浏览器仅发送给特定的服务器和 URI,避免被其他网站盗用。 +其次,我们需要 **设置 Cookie 的作用域**,让浏览器仅发送给特定的服务器和 URI,避免被其他网站盗用。 作用域的设置比较简单,“ **Domain** ”和“ **Path** ”指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。 @@ -100,7 +100,7 @@ Chrome 开发者工具是查看 Cookie 的有力工具,在“Network-Cookies 现在回到我们最开始的话题,有了 Cookie,服务器就有了“记忆能力”,能够保存“状态”,那么应该如何使用 Cookie 呢? -Cookie 最基本的一个用途就是 **身份识别** ,保存用户的登录信息,实现会话事务。 +Cookie 最基本的一个用途就是 **身份识别**,保存用户的登录信息,实现会话事务。 比如,你用账号和密码登录某电商,登录成功后网站服务器就会发给浏览器一个 Cookie,内容大概是“name=yourid”,这样就成功地把身份标签贴在了你身上。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25420\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25420\350\256\262.md" index 86536560a..fb658bf8d 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25420\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25420\350\256\262.md" @@ -42,8 +42,8 @@ “max-age”是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存: -- no_store: **不允许缓存** ,用于某些变化非常频繁的数据,例如秒杀页面; -- no_cache:它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是 **可以缓存** ,但在使用之前必须要去服务器验证是否过期,是否有最新的版本; +- no_store: **不允许缓存**,用于某些变化非常频繁的数据,例如秒杀页面; +- no_cache:它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是 **可以缓存**,但在使用之前必须要去服务器验证是否过期,是否有最新的版本; - must-revalidate:又是一个和 no_cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。 听的有点糊涂吧。没关系,我拿生鲜速递来举例说明一下: @@ -110,7 +110,7 @@ http://www.chrono.com/18-1?dst=20-1 “Last-modified”很好理解,就是文件的最后修改时间。ETag 是什么呢? -ETag 是“实体标签”(Entity Tag)的缩写, **是资源的一个唯一标识** ,主要是用来解决修改时间无法准确区分文件变化的问题。 +ETag 是“实体标签”(Entity Tag)的缩写,**是资源的一个唯一标识**,主要是用来解决修改时间无法准确区分文件变化的问题。 比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25423\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25423\350\256\262.md" index 33b769c31..babc892a9 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25423\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25423\350\256\262.md" @@ -28,11 +28,11 @@ **机密性** (Secrecy/Confidentiality)是指对数据的“保密”,只能由可信的人访问,对其他人是不可见的“秘密”,简单来说就是不能让不相关的人看到不该看的东西。 -比如小明和小红私下聊天,但“隔墙有耳”,被小强在旁边的房间里全偷听到了,这就是没有机密性。我们之前一直用的 Wireshark ,实际上也是利用了 HTTP 的这个特点,捕获了传输过程中的所有数据。 **完整性** (Integrity,也叫一致性)是指数据在传输过程中没有被窜改,不多也不少,“完完整整”地保持着原状。 +比如小明和小红私下聊天,但“隔墙有耳”,被小强在旁边的房间里全偷听到了,这就是没有机密性。我们之前一直用的 Wireshark ,实际上也是利用了 HTTP 的这个特点,捕获了传输过程中的所有数据。**完整性** (Integrity,也叫一致性)是指数据在传输过程中没有被窜改,不多也不少,“完完整整”地保持着原状。 机密性虽然可以让数据成为“秘密”,但不能防止黑客对数据的修改,黑客可以替换数据,调整数据的顺序,或者增加、删除部分数据,破坏通信过程。 -比如,小明给小红写了张纸条:“明天公园见”。小强把“公园”划掉,模仿小明的笔迹把这句话改成了“明天广场见”。小红收到后无法验证完整性,信以为真,第二天的约会就告吹了。 **身份认证** (Authentication)是指确认对方的真实身份,也就是“证明你真的是你”,保证消息只能发送给可信的人。 +比如,小明给小红写了张纸条:“明天公园见”。小强把“公园”划掉,模仿小明的笔迹把这句话改成了“明天广场见”。小红收到后无法验证完整性,信以为真,第二天的约会就告吹了。**身份认证** (Authentication)是指确认对方的真实身份,也就是“证明你真的是你”,保证消息只能发送给可信的人。 如果通信时另一方是假冒的网站,那么数据再保密也没有用,黑客完全可以使用冒充的身份“套”出各种信息,加密和没加密一样。 @@ -50,7 +50,7 @@ 说到这里,终于轮到今天的主角 HTTPS 出场了,它为 HTTP 增加了刚才所说的四大安全特性。 -HTTPS 其实是一个“非常简单”的协议,RFC 文档很小,只有短短的 7 页,里面规定了 **新的协议名“https”,默认端口号 443** ,至于其他的什么请求 - 应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。 +HTTPS 其实是一个“非常简单”的协议,RFC 文档很小,只有短短的 7 页,里面规定了 **新的协议名“https”,默认端口号 443**,至于其他的什么请求 - 应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。 也就是说,除了协议名“http”和端口号 80 这两点不同,HTTPS 协议在语法、语义上和 HTTP 完全一样,优缺点也“照单全收”(当然要除去“明文”和“不安全”)。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25425\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25425\350\256\262.md" index a9c54a6af..f6aa3fa08 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25425\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25425\350\256\262.md" @@ -104,7 +104,7 @@ DV 是最低的,只是域名级别的可信,背后是谁不知道。EV 是 不过,CA 怎么证明自己呢? -这还是信任链的问题。小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 **Root CA** ,就只能自己证明自己了,这个就叫“ **自签名证书** ”(Self-Signed Certificate)或者“ **根证书** ”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了。 +这还是信任链的问题。小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 **Root CA**,就只能自己证明自己了,这个就叫“ **自签名证书** ”(Self-Signed Certificate)或者“ **根证书** ”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了。 ![img](assets/8f0813e9555ba1a40bd2170734aced9c.png) diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25426\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25426\350\256\262.md" index 0ed79e1e5..a9d7de721 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25426\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25426\350\256\262.md" @@ -20,7 +20,7 @@ TLS 包含几个子协议,你也可以理解为它是由几个不同职责的模块组成,比较常用的有记录协议、警报协议、握手协议、变更密码规范协议等。 -**记录协议** (Record Protocol)规定了 TLS 收发数据的基本单位:记录(record)。它有点像是 TCP 里的 segment,所有的其他子协议都需要通过记录协议发出。但多个记录数据可以在一个 TCP 包里一次性发出,也并不需要像 TCP 那样返回 ACK。 **警报协议** (Alert Protocol)的职责是向对方发出警报信息,有点像是 HTTP 协议里的状态码。比如,protocol_version 就是不支持旧版本,bad_certificate 就是证书有问题,收到警报后另一方可以选择继续,也可以立即终止连接。 **握手协议** (Handshake Protocol)是 TLS 里最复杂的子协议,要比 TCP 的 SYN/ACK 复杂的多,浏览器和服务器会在握手过程中协商 TLS 版本号、随机数、密码套件等信息,然后交换证书和密钥参数,最终双方协商得到会话密钥,用于后续的混合加密系统。 +**记录协议** (Record Protocol)规定了 TLS 收发数据的基本单位:记录(record)。它有点像是 TCP 里的 segment,所有的其他子协议都需要通过记录协议发出。但多个记录数据可以在一个 TCP 包里一次性发出,也并不需要像 TCP 那样返回 ACK。**警报协议** (Alert Protocol)的职责是向对方发出警报信息,有点像是 HTTP 协议里的状态码。比如,protocol_version 就是不支持旧版本,bad_certificate 就是证书有问题,收到警报后另一方可以选择继续,也可以立即终止连接。**握手协议** (Handshake Protocol)是 TLS 里最复杂的子协议,要比 TCP 的 SYN/ACK 复杂的多,浏览器和服务器会在握手过程中协商 TLS 版本号、随机数、密码套件等信息,然后交换证书和密钥参数,最终双方协商得到会话密钥,用于后续的混合加密系统。 最后一个是 **变更密码规范协议** (Change Cipher Spec Protocol),它非常简单,就是一个“通知”,告诉对方,后续的数据都将使用加密保护。那么反过来,在它之前,数据都是明文的。 @@ -52,7 +52,7 @@ TLS 包含几个子协议,你也可以理解为它是由几个不同职责的 ![img](assets/9caba6d4b527052bbe7168ed4013011e.png) -在 TCP 建立连接之后,浏览器会首先发一个“ **Client Hello** ”消息,也就是跟服务器“打招呼”。里面有客户端的版本号、支持的密码套件,还有一个 **随机数(Client Random)** ,用于后续生成会话密钥。 +在 TCP 建立连接之后,浏览器会首先发一个“ **Client Hello** ”消息,也就是跟服务器“打招呼”。里面有客户端的版本号、支持的密码套件,还有一个 **随机数(Client Random)**,用于后续生成会话密钥。 ```plaintext Handshake Protocol: Client Hello @@ -65,7 +65,7 @@ Handshake Protocol: Client Hello 这个的意思就是:“我这边有这些这些信息,你看看哪些是能用的,关键的随机数可得留着。” -作为“礼尚往来”,服务器收到“Client Hello”后,会返回一个“Server Hello”消息。把版本号对一下,也给出一个 **随机数(Server Random)** ,然后从客户端的列表里选一个作为本次通信使用的密码套件,在这里它选择了“TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384”。 +作为“礼尚往来”,服务器收到“Client Hello”后,会返回一个“Server Hello”消息。把版本号对一下,也给出一个 **随机数(Server Random)**,然后从客户端的列表里选一个作为本次通信使用的密码套件,在这里它选择了“TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384”。 ```plaintext Handshake Protocol: Server Hello @@ -78,7 +78,7 @@ Handshake Protocol: Server Hello 然后,服务器为了证明自己的身份,就把证书也发给了客户端(Server Certificate)。 -接下来是一个关键的操作,因为服务器选择了 ECDHE 算法,所以它会在证书后发送“ **Server Key Exchange** ”消息,里面是 **椭圆曲线的公钥(Server Params)** ,用来实现密钥交换算法,再加上自己的私钥签名认证。 +接下来是一个关键的操作,因为服务器选择了 ECDHE 算法,所以它会在证书后发送“ **Server Key Exchange** ”消息,里面是 **椭圆曲线的公钥(Server Params)**,用来实现密钥交换算法,再加上自己的私钥签名认证。 ```plaintext Handshake Protocol: Server Key Exchange @@ -100,7 +100,7 @@ Handshake Protocol: Server Key Exchange 这就要用到第 25 讲里的知识了,开始走证书链逐级验证,确认证书的真实性,再用证书公钥验证签名,就确认了服务器的身份:“刚才跟我打招呼的不是骗子,可以接着往下走。” -然后,客户端按照密码套件的要求,也生成一个 **椭圆曲线的公钥(Client Params)** ,用“ **Client Key Exchange** ”消息发给服务器。 +然后,客户端按照密码套件的要求,也生成一个 **椭圆曲线的公钥(Client Params)**,用“ **Client Key Exchange** ”消息发给服务器。 ```plaintext Handshake Protocol: Client Key Exchange diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25427\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25427\350\256\262.md" index f8f4bad1e..b9cc42e39 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25427\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25427\350\256\262.md" @@ -145,7 +145,7 @@ Handshake Protocol: Server Hello “还真让你给猜对了,虽然还是按老规矩打招呼,但咱们来个‘旧瓶装新酒’。刚才你给的我都用上了,我再给几个你缺的参数,这次加密就这么定了。” -这时只交换了两条消息,客户端和服务器就拿到了四个共享信息: **Client Random** 和 **Server Random** 、 **Client Params** 和 **Server Params** ,两边就可以各自用 ECDHE 算出“ **Pre-Master** ”,再用 HKDF 生成主密钥“ **Master Secret** ”,效率比 TLS1.2 提高了一大截。 +这时只交换了两条消息,客户端和服务器就拿到了四个共享信息: **Client Random** 和 **Server Random** 、 **Client Params** 和 **Server Params**,两边就可以各自用 ECDHE 算出“ **Pre-Master** ”,再用 HKDF 生成主密钥“ **Master Secret** ”,效率比 TLS1.2 提高了一大截。 在算出主密钥后,服务器立刻发出“ **Change Cipher Spec** ”消息,比 TLS1.2 提早进入加密通信,后面的证书等就都是加密的了,减少了握手时的明文信息泄露。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25428\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25428\350\256\262.md" index acdeeaf16..d452bba6b 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25428\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25428\350\256\262.md" @@ -2,7 +2,7 @@ 你可能或多或少听别人说过,“HTTPS 的连接很慢”。那么“慢”的原因是什么呢? -通过前两讲的学习,你可以看到,HTTPS 连接大致上可以划分为两个部分,第一个是建立连接时的 **非对称加密握手** ,第二个是握手后的 **对称加密报文传输** 。 +通过前两讲的学习,你可以看到,HTTPS 连接大致上可以划分为两个部分,第一个是建立连接时的 **非对称加密握手**,第二个是握手后的 **对称加密报文传输** 。 由于目前流行的 AES、ChaCha20 性能都很好,还有硬件优化,报文传输的性能损耗可以说是非常地小,小到几乎可以忽略不计了。所以,通常所说的“HTTPS 连接慢”指的就是刚开始建立连接的那段时间。 @@ -30,7 +30,7 @@ HTTPS 连接是计算密集型,而不是 I/O 密集型。所以,如果你花 那该用什么样的硬件来做优化呢? -首先,你可以选择 **更快的 CPU** ,最好还内建 AES 优化,这样即可以加速握手,也可以加速传输。 +首先,你可以选择 **更快的 CPU**,最好还内建 AES 优化,这样即可以加速握手,也可以加速传输。 其次,你可以选择“ **SSL 加速卡** ”,加解密时调用它的 API,让专门的硬件来做非对称加解密,分担 CPU 的计算压力。 @@ -44,7 +44,7 @@ HTTPS 连接是计算密集型,而不是 I/O 密集型。所以,如果你花 所以,软件优化的方式相对来说更可行一些,性价比高,能够“少花钱,多办事”。 -软件方面的优化还可以再分成两部分:一个是 **软件升级** ,一个是 **协议优化** 。 +软件方面的优化还可以再分成两部分:一个是 **软件升级**,一个是 **协议优化** 。 软件升级实施起来比较简单,就是把现在正在使用的软件尽量升级到最新版本,比如把 Linux 内核由 2.x 升级到 4.x,把 Nginx 由 1.6 升级到 1.16,把 OpenSSL 由 1.0.1 升级到 1.1.0/1.1.1。 @@ -75,7 +75,7 @@ ssl_ecdh_curve X25519:P-256; 除了密钥交换,握手过程中的证书验证也是一个比较耗时的操作,服务器需要把自己的证书链全发给客户端,然后客户端接收后再逐一验证。 -这里就有两个优化点,一个是 **证书传输** ,一个是 **证书验证** 。 +这里就有两个优化点,一个是 **证书传输**,一个是 **证书验证** 。 服务器的证书可以选择椭圆曲线(ECDSA)证书而不是 RSA 证书,因为 224 位的 ECC 相当于 2048 位的 RSA,所以椭圆曲线证书的“个头”要比 RSA 小很多,即能够节约带宽也能减少客户端的运算量,可谓“一举两得”。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25430\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25430\350\256\262.md" index 1848f776d..869a03d3e 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25430\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25430\350\256\262.md" @@ -70,7 +70,7 @@ 消息的“碎片”到达目的地后应该怎么组装起来呢? -HTTP/2 为此定义了一个“ **流** ”(Stream)的概念, **它是二进制帧的双向传输序列** ,同一个消息往返的帧会分配一个唯一的流 ID。你可以想象把它成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1 里的请求报文和响应报文。 +HTTP/2 为此定义了一个“ **流** ”(Stream)的概念,**它是二进制帧的双向传输序列**,同一个消息往返的帧会分配一个唯一的流 ID。你可以想象把它成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1 里的请求报文和响应报文。 因为“流”是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用“ **流** ”同时发送多个“碎片化”的消息,这就是常说的“ **多路复用** ”( Multiplexing)——多个往返通信都复用一个连接来处理。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25431\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25431\350\256\262.md" index c1a7a0416..ce3ce197d 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25431\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25431\350\256\262.md" @@ -62,13 +62,13 @@ HTTP/2 的帧结构有点类似 TCP 的段或者 TLS 里的记录,但报头很 帧开头是 3 个字节的 **长度** (但不包括头的 9 个字节),默认上限是 2^14,最大是 2^24,也就是说 HTTP/2 的帧通常不超过 16K,最大是 16M。 -长度后面的一个字节是 **帧类型** ,大致可以分成 **数据帧** 和 **控制帧** 两类,HEADERS 帧和 DATA 帧属于数据帧,存放的是 HTTP 报文,而 SETTINGS、PING、PRIORITY 等则是用来管理流的控制帧。 +长度后面的一个字节是 **帧类型**,大致可以分成 **数据帧** 和 **控制帧** 两类,HEADERS 帧和 DATA 帧属于数据帧,存放的是 HTTP 报文,而 SETTINGS、PING、PRIORITY 等则是用来管理流的控制帧。 HTTP/2 总共定义了 10 种类型的帧,但一个字节可以表示最多 256 种,所以也允许在标准之外定义其他类型实现功能扩展。这就有点像 TLS 里扩展协议的意思了,比如 Google 的 gRPC 就利用了这个特点,定义了几种自用的新帧类型。 -第 5 个字节是非常重要的 **帧标志** 信息,可以保存 8 个标志位,携带简单的控制信息。常用的标志位有 **END_HEADERS** 表示头数据结束,相当于 HTTP/1 里头后的空行(“\\r\\n”), **END_STREAM** 表示单方向数据发送结束(即 EOS,End of Stream),相当于 HTTP/1 里 Chunked 分块结束标志(“0\\r\\n\\r\\n”)。 +第 5 个字节是非常重要的 **帧标志** 信息,可以保存 8 个标志位,携带简单的控制信息。常用的标志位有 **END_HEADERS** 表示头数据结束,相当于 HTTP/1 里头后的空行(“\\r\\n”),**END_STREAM** 表示单方向数据发送结束(即 EOS,End of Stream),相当于 HTTP/1 里 Chunked 分块结束标志(“0\\r\\n\\r\\n”)。 -报文头里最后 4 个字节是 **流标识符** ,也就是帧所属的“流”,接收方使用它就可以从乱序的帧里识别出具有相同流 ID 的帧序列,按顺序组装起来就实现了虚拟的“流”。 +报文头里最后 4 个字节是 **流标识符**,也就是帧所属的“流”,接收方使用它就可以从乱序的帧里识别出具有相同流 ID 的帧序列,按顺序组装起来就实现了虚拟的“流”。 流标识符虽然有 4 个字节,但最高位被保留不用,所以只有 31 位可以使用,也就是说,流标识符的上限是 231,大约是 21 亿。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25432\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25432\350\256\262.md" index 1407a5a04..c226a80d6 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25432\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25432\350\256\262.md" @@ -22,7 +22,7 @@ Google 在推 SPDY 的时候就已经意识到了这个问题,于是就又发明了一个新的“QUIC”协议,让 HTTP 跑在 QUIC 上而不是 TCP 上。 -而这个“HTTP over QUIC”就是 HTTP 协议的下一个大版本, **HTTP/3** 。它在 HTTP/2 的基础上又实现了质的飞跃,真正“完美”地解决了“队头阻塞”问题。 +而这个“HTTP over QUIC”就是 HTTP 协议的下一个大版本,**HTTP/3** 。它在 HTTP/2 的基础上又实现了质的飞跃,真正“完美”地解决了“队头阻塞”问题。 不过 HTTP/3 目前还处于草案阶段,正式发布前可能会有变动,所以今天我尽量不谈那些不稳定的细节。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25433\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25433\350\256\262.md" index e2c95cbbb..47fc00590 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25433\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25433\350\256\262.md" @@ -16,7 +16,7 @@ 前面的几讲主要关注了 HTTP/2 的内部实现,今天我们就来看看它有哪些优点和缺点。 -首先要说的是,HTTP/2 最大的一个优点是 **完全保持了与 HTTP/1 的兼容** ,在语义上没有任何变化,之前在 HTTP 上的所有投入都不会浪费。 +首先要说的是,HTTP/2 最大的一个优点是 **完全保持了与 HTTP/1 的兼容**,在语义上没有任何变化,之前在 HTTP 上的所有投入都不会浪费。 因为兼容 HTTP/1,所以 HTTP/2 也具有 HTTP/1 的所有优点,并且“基本”解决了 HTTP/1 的所有缺点,安全与性能兼顾,可以认为是“更安全的 HTTP、更快的 HTTPS”。 @@ -30,7 +30,7 @@ 节约带宽的基本手段就是压缩,在 HTTP/1 里只能压缩 body,而 HTTP/2 则可以用 HPACK 算法压缩 header,这对高流量的网站非常有价值,有数据表明能节省大概 5%~10% 的流量,这是实实在在的“真金白银”。 -与 HTTP/1“并发多个连接”不同,HTTP/2 的“多路复用”特性要求对 **一个域名(或者 IP)只用一个 TCP 连接** ,所有的数据都在这一个连接上传输,这样不仅节约了客户端、服务器和网络的资源,还可以把带宽跑满,让 TCP 充分“吃饱”。 +与 HTTP/1“并发多个连接”不同,HTTP/2 的“多路复用”特性要求对 **一个域名(或者 IP)只用一个 TCP 连接**,所有的数据都在这一个连接上传输,这样不仅节约了客户端、服务器和网络的资源,还可以把带宽跑满,让 TCP 充分“吃饱”。 这是为什么呢? diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25434\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25434\350\256\262.md" index 5a2495751..b25c57d9a 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25434\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25434\350\256\262.md" @@ -50,7 +50,7 @@ Nginx 在启动的时候会预先创建好固定数量的 worker 进程,在之 那么,疑问也就产生了:为什么单线程的 Nginx,处理能力却能够超越其他多线程的服务器呢? -这要归功于 Nginx 利用了 Linux 内核里的一件“神兵利器”, **I/O 多路复用接口** ,“大名鼎鼎”的 epoll。 +这要归功于 Nginx 利用了 Linux 内核里的一件“神兵利器”,**I/O 多路复用接口**,“大名鼎鼎”的 epoll。 “多路复用”这个词我们已经在之前的 HTTP/2、HTTP/3 里遇到过好几次,如果你理解了那里的“多路复用”,那么面对 Nginx 的 epoll“多路复用”也就好办了。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25435\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25435\350\256\262.md" index ee2dc5b05..3263ee281 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25435\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25435\350\256\262.md" @@ -18,7 +18,7 @@ Nginx 的服务管理思路延续了当时的流行做法,使用磁盘上的 其实你对 OpenResty 并不陌生,这个专栏的实验环境就是用 OpenResty 搭建的,这么多节课程下来,你应该或多或少对它有了一些印象吧。 -OpenResty 诞生于 2009 年,到现在刚好满 10 周岁。它的创造者是当时就职于某宝的“神级”程序员 **章亦春** ,网名叫“agentzh”。 +OpenResty 诞生于 2009 年,到现在刚好满 10 周岁。它的创造者是当时就职于某宝的“神级”程序员 **章亦春**,网名叫“agentzh”。 OpenResty 并不是一个全新的 Web 服务器,而是基于 Nginx,它利用了 Nginx 模块化、可扩展的特性,开发了一系列的增强模块,并把它们打包整合,形成了一个 **“一站式”的 Web 开发平台** 。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25437\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25437\350\256\262.md" index e413aa129..c5462a19a 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25437\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25437\350\256\262.md" @@ -56,7 +56,7 @@ CDN,正是把“数据传输”这件看似简单的事情“做大做强” ## CDN 的负载均衡 -我们再来看看 CDN 是具体怎么运行的,它有两个关键组成部分: **全局负载均衡** 和 **缓存系统** ,对应的是 DNS(\[第 6 讲\])和缓存代理(\[第 21 讲\]、\[第 22 讲\])技术。 +我们再来看看 CDN 是具体怎么运行的,它有两个关键组成部分: **全局负载均衡** 和 **缓存系统**,对应的是 DNS(\[第 6 讲\])和缓存代理(\[第 21 讲\]、\[第 22 讲\])技术。 全局负载均衡(Global Sever Load Balance)一般简称为 GSLB,它是 CDN 的“大脑”,主要的职责是当用户接入网络的时候在 CDN 专网中挑选出一个“最佳”节点提供服务,解决的是用户如何找到“最近的”边缘节点,对整个 CDN 网络进行“负载均衡”。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25438\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25438\350\256\262.md" index 103f88955..16b768767 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25438\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25438\350\256\262.md" @@ -56,7 +56,7 @@ wss://www.chrono.com:445/im?user_id=xxx 刚才说了,WebSocket 用的也是二进制帧,有之前 HTTP/2、HTTP/3 的经验,相信你这次也能很快掌握 WebSocket 的报文结构。 -不过 WebSocket 和 HTTP/2 的关注点不同,WebSocket 更 **侧重于“实时通信”** ,而 HTTP/2 更侧重于提高传输效率,所以两者的帧结构也有很大的区别。 +不过 WebSocket 和 HTTP/2 的关注点不同,WebSocket 更 **侧重于“实时通信”**,而 HTTP/2 更侧重于提高传输效率,所以两者的帧结构也有很大的区别。 WebSocket 虽然有“帧”,但却没有像 HTTP/2 那样定义“流”,也就不存在“多路复用”“优先级”等复杂的特性,而它自身就是“全双工”的,也就不需要“服务器推送”。所以综合起来,WebSocket 的帧学习起来会简单一些。 diff --git "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25440\350\256\262.md" "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25440\350\256\262.md" index 5dded995c..9afa8487f 100644 --- "a/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25440\350\256\262.md" +++ "b/docs/Network/\351\200\217\350\247\206 HTTP \345\215\217\350\256\256/\347\254\25440\350\256\262.md" @@ -6,9 +6,9 @@ 知道了大致的方向,HTTP 性能优化具体应该怎么做呢? -总的来说,任何计算机系统的优化都可以分成这么几类:硬件软件、内部外部、花钱不花钱。 **投资购买现成的硬件** 最简单的优化方式,比如换上更强的 CPU、更快的网卡、更大的带宽、更多的服务器,效果也会“立竿见影”,直接提升网站的服务能力,也就实现了 HTTP 优化。 +总的来说,任何计算机系统的优化都可以分成这么几类:硬件软件、内部外部、花钱不花钱。**投资购买现成的硬件** 最简单的优化方式,比如换上更强的 CPU、更快的网卡、更大的带宽、更多的服务器,效果也会“立竿见影”,直接提升网站的服务能力,也就实现了 HTTP 优化。 -另外, **花钱购买外部的软件或者服务** 也是一种行之有效的优化方式,最“物有所值”的应该算是 CDN 了(参见\[第 37 讲\])。CDN 专注于网络内容交付,帮助网站解决“中间一公里”的问题,还有很多其他非常专业的优化功能。把网站交给 CDN 运营,就好像是“让网站坐上了喷气飞机”,能够直达用户,几乎不需要费什么力气就能够达成很好的优化效果。 +另外,**花钱购买外部的软件或者服务** 也是一种行之有效的优化方式,最“物有所值”的应该算是 CDN 了(参见\[第 37 讲\])。CDN 专注于网络内容交付,帮助网站解决“中间一公里”的问题,还有很多其他非常专业的优化功能。把网站交给 CDN 运营,就好像是“让网站坐上了喷气飞机”,能够直达用户,几乎不需要费什么力气就能够达成很好的优化效果。 不过这些“花钱”的手段实在是太没有“技术含量”了,属于“懒人”(无贬义)的做法,所以我就不再细说,接下来重点就讲讲在网站内部、“不花钱”的软件优化。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25400\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25400\350\256\262.md" index 12f5459f7..a38b1b53c 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25400\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25400\350\256\262.md" @@ -1,6 +1,6 @@ # 00 开篇词 Go 为开发者的需求设计,带你实现高效工作 -你好,我是 **飞雪无情** ,在技术领域从业近 10 年,目前在一家互联网公司担任技术总监,负责技术管理和架构设计。 +你好,我是 **飞雪无情**,在技术领域从业近 10 年,目前在一家互联网公司担任技术总监,负责技术管理和架构设计。 2014 年,我因为 Docker 接触了 Go 语言,其简洁的语法、高效的开发效率和语言层面上的并发支持深深地吸引了我。经过不断地学习和实践,我对 Go 语言有了更深入的了解,不久后,便带领团队转型 Go 语言开发,提升了团队开发效率和系统性能,降低了用人成本。 @@ -23,7 +23,7 @@ K8s、Docker、etcd 这类耳熟能详的工具,就是用 Go 语言开发的 如今微服务和云原生已经成为一种趋势,而 Go 作为一款高性能的编译型语言,最适合承载落地微服务的实现 ,又容易生成跨平台的可执行文件,相比其他编程语言更容易部署在 Docker 容器中,实现灵活的自动伸缩服务。 -总体来看, **Go 语言的整体设计理念就是以软件工程为目的的,也就是说它不是为了编程语言本身多么强大而设计,而是为了开发者更好地研发、管理软件工程,一切都是为了开发者着想。** 如果你是有 1~3 年经验的其他语言开发者(如 Python、PHP、C/C++),Go 的学习会比较容易,因为编程语言的很多概念相通。而如果你是有基本计算机知识但无开发经验的小白,Go 也适合尽早学习,吃透它有助于加深你对编程语言的理解,也更有职业竞争力。 +总体来看,**Go 语言的整体设计理念就是以软件工程为目的的,也就是说它不是为了编程语言本身多么强大而设计,而是为了开发者更好地研发、管理软件工程,一切都是为了开发者着想。** 如果你是有 1~3 年经验的其他语言开发者(如 Python、PHP、C/C++),Go 的学习会比较容易,因为编程语言的很多概念相通。而如果你是有基本计算机知识但无开发经验的小白,Go 也适合尽早学习,吃透它有助于加深你对编程语言的理解,也更有职业竞争力。 而在我与 Go 语言学习者进行交流,以及面试的过程中,也 **发现了一些典型问题,可概括为如下三点** : @@ -41,7 +41,7 @@ K8s、Docker、etcd 这类耳熟能详的工具,就是用 Go 语言开发的 - **系统性设计** :从基础知识、底层原理到实战,让你不仅可以学会使用,还能从语言自身的逻辑、框架层面分析问题,并做到能上手项目。这样当出现问题时,你可以不再盲目地搜索知识点。 - **案例实操** :我设计了很多便于运用知识点的代码示例,还特意站在学习者的视角,演示了一些容易出 Bug 的场景,帮你避雷。我还引入了很多生活化的场景,比如用枪响后才能赛跑的例子演示 sync.Cond 的使用,帮你加深印象,缓解语言学习的枯燥感。 -- **贴近实际** :我所策划的内容来源于众多学习者的反馈,在不断地交流中,我总结了他们问题的共性和不同,并有针对性地融入专栏。 **那我是怎么划分这门课的呢?** +- **贴近实际** :我所策划的内容来源于众多学习者的反馈,在不断地交流中,我总结了他们问题的共性和不同,并有针对性地融入专栏。**那我是怎么划分这门课的呢?** 作为初学者,不管你是否有编程经验,都需要先学习 Go 语言的基本语法,然后我会在此基础上再向你介绍 Go 语言的核心特性——并发,这也是 Go 最自豪的功能。其基于协程的并发,比我们平时使用的线程并发更轻量,可以随意地在一台普通的电脑上启动成百上千个协程,成本非常低。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25401\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25401\350\256\262.md" index f255b1957..1923e1c62 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25401\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25401\350\256\262.md" @@ -43,7 +43,7 @@ run 在这里是 go 命令的子命令,表示要运行 Go 语言代码的意 - **第四行的 fmt.Println("Hello, 世界")** 是通过 fmt 包的 Println 函数打印“Hello, 世界”这段文本。其中 fmt 是刚刚导入的包,要想使用一个包,必须先导入。Println 函数是属于包 fmt 的函数,这里我需要它打印输出一段文本,也就是“Hello, 世界”。 - **第五行的大括号 }** 表示 main 函数体的结束。现在整个代码片段已经分析完了,运行就可以看到“Hello, 世界”结果的输出。 -从以上分析来看, **Go 语言的代码是非常简洁、完整的核心程序,只需要 package、import、func main 这些核心概念就可以实现。** 在后面的课时中,我还会讲如何使用变量,如何自定义函数等,这里先略过不讲,我们先来看看 Go 语言的开发环境是如何搭建的,这样才能运行上面的 Go 语言代码,让整个程序跑起来。 +从以上分析来看,**Go 语言的代码是非常简洁、完整的核心程序,只需要 package、import、func main 这些核心概念就可以实现。** 在后面的课时中,我还会讲如何使用变量,如何自定义函数等,这里先略过不讲,我们先来看看 Go 语言的开发环境是如何搭建的,这样才能运行上面的 Go 语言代码,让整个程序跑起来。 ### Go 语言环境搭建 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25404\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25404\350\256\262.md" index 070b12ee6..f6d3a3da3 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25404\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25404\350\256\262.md" @@ -134,7 +134,7 @@ slice:=array[start:end] 切片和数组一样,也可以通过索引定位元素。这里以新获取的 slice 切片为例,slice\[0\] 的值为 c,slice\[1\] 的值为 d。 -有没有发现,在数组 array 中,元素 c 的索引其实是 2,但是对数组切片后,在新生成的切片 slice 中,它的索引是 0,这就是切片。虽然切片底层用的也是 array 数组, **但是经过切片后,切片的索引范围改变了** 。 +有没有发现,在数组 array 中,元素 c 的索引其实是 2,但是对数组切片后,在新生成的切片 slice 中,它的索引是 0,这就是切片。虽然切片底层用的也是 array 数组,**但是经过切片后,切片的索引范围改变了** 。 通过下图可以看出,切片是一个具备三个字段的数据结构,分别是指向数组的指针 data,长度 len 和容量 cap: @@ -374,6 +374,6 @@ for i,r:=range s{ 这节课到这里就要结束了,在这节课里我讲解了数组、切片和映射的声明和使用,有了这些集合类型,你就可以把你需要的某一类数据放到集合类型中了,比如获取用户列表、商品列表等。 -数组、切片还可以分为二维和多维,比如二维字节切片就是 \[\]\[\]byte,三维就是 \[\]\[\]\[\]byte,因为不常用,所以本节课中没有详细介绍,你可以结合我讲的一维 \[\]byte 切片自己尝试练习,这也是本节课要给你留的 **思考题** ,创建一个二维数组并使用它。 +数组、切片还可以分为二维和多维,比如二维字节切片就是 \[\]\[\]byte,三维就是 \[\]\[\]\[\]byte,因为不常用,所以本节课中没有详细介绍,你可以结合我讲的一维 \[\]byte 切片自己尝试练习,这也是本节课要给你留的 **思考题**,创建一个二维数组并使用它。 此外,如果 map 的 Key 的类型是整型,并且集合中的元素比较少,应该尽量选择切片,因为效率更高。在实际的项目开发中,数组并不常用,尤其是在函数间作为参数传递的时候,用得最多的是切片,它更灵活,并且内存占用少。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25405\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25405\350\256\262.md" index 22830ae93..b830bd8dc 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25405\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25405\350\256\262.md" @@ -240,7 +240,7 @@ fmt.Println(sum1(1,2,3,4)) 不管是自定义的函数 sum、sum1,还是我们使用到的函数 Println,都会从属于一个包,也就是 package。sum 函数属于 main 包,Println 函数属于 fmt 包。 -同一个包中的函数哪怕是私有的(函数名称首字母小写)也可以被调用。如果不同包的函数要被调用,那么函数的作用域必须是公有的,也就是 **函数名称的首字母要大写** ,比如 Println。 +同一个包中的函数哪怕是私有的(函数名称首字母小写)也可以被调用。如果不同包的函数要被调用,那么函数的作用域必须是公有的,也就是 **函数名称的首字母要大写**,比如 Println。 在后面的包、作用域和模块化的课程中我会详细讲解,这里可以先记住: diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25406\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25406\350\256\262.md" index ca86c196b..b682c1dca 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25406\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25406\350\256\262.md" @@ -88,7 +88,7 @@ fmt.Println(p.name,p.age) 在 person 这个结构体中,第一个字段是 string 类型的 name,第二个字段是 uint 类型的 age,所以在初始化的时候,初始化值的类型顺序必须一一对应,才能编译通过。也就是说,在示例 {"飞雪无情",30} 中,表示 name 的字符串飞雪无情必须在前,表示年龄的数字 30 必须在后。 -那么是否可以不按照顺序初始化呢? **当然可以,只不过需要指出字段名称** ,如下所示: +那么是否可以不按照顺序初始化呢?**当然可以,只不过需要指出字段名称**,如下所示: ```plaintext p:=person{age:30,name:"飞雪无情"} diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25407\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25407\350\256\262.md" index daa622ef4..865630edf 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25407\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25407\350\256\262.md" @@ -168,7 +168,7 @@ type MyError struct { } ``` -这个结构体有两个字段,其中 error 类型的 err 字段用于存放已存在的 error,string 类型的 msg 字段用于存放新的错误信息, **这种方式就是 error 的嵌套** 。 +这个结构体有两个字段,其中 error 类型的 err 字段用于存放已存在的 error,string 类型的 msg 字段用于存放新的错误信息,**这种方式就是 error 的嵌套** 。 现在让 MyError 这个 struct 实现 error 接口,然后在初始化 MyError 的时候传递存在的 error 和新的错误信息,如下面的代码所示: @@ -309,7 +309,7 @@ func panic(v interface{}) > 小提示:interface{} 是空接口的意思,在 Go 语言中代表任意类型。 -panic 异常是一种非常严重的情况,会让程序中断运行,使程序崩溃,所以 **如果是不影响程序运行的错误,不要使用 panic,使用普通错误 error 即可。** ![pDE7ppQNyfRSIn1Q__thumbnail.png](assets/CgqCHl-15ZSAAsw5AAUnpsfN34w061.png) +panic 异常是一种非常严重的情况,会让程序中断运行,使程序崩溃,所以 **如果是不影响程序运行的错误,不要使用 panic,使用普通错误 error 即可。**![pDE7ppQNyfRSIn1Q__thumbnail.png](assets/CgqCHl-15ZSAAsw5AAUnpsfN34w061.png) ### Recover 捕获 Panic 异常 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25409\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25409\350\256\262.md" index c74253ae9..02f3eeca6 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25409\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25409\350\256\262.md" @@ -303,9 +303,9 @@ func race(){ sync.Cond 有三个方法,它们分别是: -1. **Wait** ,阻塞当前协程,直到被其他协程调用 Broadcast 或者 Signal 方法唤醒,使用的时候需要加锁,使用 sync.Cond 中的锁即可,也就是 L 字段。 - 2. **Signal** ,唤醒一个等待时间最长的协程。 - 3. **Broadcast** ,唤醒所有等待的协程。 +1. **Wait**,阻塞当前协程,直到被其他协程调用 Broadcast 或者 Signal 方法唤醒,使用的时候需要加锁,使用 sync.Cond 中的锁即可,也就是 L 字段。 + 2. **Signal**,唤醒一个等待时间最长的协程。 + 3. **Broadcast**,唤醒所有等待的协程。 > 注意:在调用 Signal 或者 Broadcast 之前,要确保目标协程处于 Wait 阻塞状态,不然会出现死锁问题。 @@ -315,6 +315,6 @@ sync.Cond 有三个方法,它们分别是: 这节课主要讲解 Go 语言的同步原语使用,通过它们可以更灵活地控制多协程的并发。从使用上讲,Go 语言还是更推荐 channel 这种更高级别的并发控制方式,因为它更简洁,也更容易理解和使用。 -当然本节课讲的这些比较基础的同步原语也很有用。 **同步原语通常用于更复杂的并发控制,如果追求更灵活的控制方式和性能,你可以使用它们。** +当然本节课讲的这些比较基础的同步原语也很有用。**同步原语通常用于更复杂的并发控制,如果追求更灵活的控制方式和性能,你可以使用它们。** 本节课到这里就要结束了,sync 包里还有一个同步原语我没有讲,它就是 sync.Map。sync.Map 的使用和内置的 map 类型一样,只不过它是并发安全的,所以这节课的作业就是练习使用 sync.Map。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25411\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25411\350\256\262.md" index 35a346a3c..96f927dd1 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25411\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25411\350\256\262.md" @@ -96,7 +96,7 @@ Pipeline 模式也称为流水线模式,模拟的就是现实世界中的流 通过以上流水线模式示意图,可以看到从最开始的生产,经过工序 1、2、3、4 到最终成品,这就是一条比较形象的流水线,也就是 Pipeline。 -现在我以组装手机为例,讲解流水线模式的使用。假设一条组装手机的流水线有 3 道工序,分别是 **配件采购** 、 **配件组装** 、 **打包成品** ,如图所示: +现在我以组装手机为例,讲解流水线模式的使用。假设一条组装手机的流水线有 3 道工序,分别是 **配件采购** 、 **配件组装** 、 **打包成品**,如图所示: ![图片2.png](assets/Ciqc1F_HcfGAWb6pAABvGsG8s_o830.png) @@ -213,8 +213,8 @@ func main() { 从改造后的流水线示意图可以看到,工序 2 共有工序 2-1、工序 2-2、工序 2-3 三班人手,工序 1 采购的配件会被工序 2 的三班人手同时组装,这三班人手组装好的手机会同时传给 **merge 组件** 汇聚,然后再传给工序 3 打包成品。在这个流程中,会产生两种模式: **扇出和扇入** 。 -- 示意图中红色的部分是 **扇出** ,对于工序 1 来说,它同时为工序 2 的三班人手传递数据(采购配件)。以工序 1 为中点,三条传递数据的线发散出去,就像一把打开的扇子一样,所以叫扇出。 -- 示意图中蓝色的部分是 **扇入** ,对于 merge 组件来说,它同时接收工序 2 三班人手传递的数据(组装的手机)进行汇聚,然后传给工序 3。以 merge 组件为中点,三条传递数据的线汇聚到 merge 组件,也像一把打开的扇子一样,所以叫扇入。 +- 示意图中红色的部分是 **扇出**,对于工序 1 来说,它同时为工序 2 的三班人手传递数据(采购配件)。以工序 1 为中点,三条传递数据的线发散出去,就像一把打开的扇子一样,所以叫扇出。 +- 示意图中蓝色的部分是 **扇入**,对于 merge 组件来说,它同时接收工序 2 三班人手传递的数据(组装的手机)进行汇聚,然后传给工序 3。以 merge 组件为中点,三条传递数据的线汇聚到 merge 组件,也像一把打开的扇子一样,所以叫扇入。 > 小提示:扇出和扇入都像一把打开的扇子,因为数据传递的方向不同,所以叫法也不一样,扇出的数据流向是发散传递出去,是输出流;扇入的数据流向是汇聚进来,是输入流。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25413\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25413\350\256\262.md" index 6b8e5bd73..abf6ec5a6 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25413\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25413\350\256\262.md" @@ -264,7 +264,7 @@ func makechan(t *chantype, size int64) *hchan { } ``` -**严格来说,Go 语言没有引用类型** ,但是我们可以把 map、chan 称为引用类型,这样便于理解。除了 map、chan 之外,Go 语言中的函数、接口、slice 切片都可以称为引用类型。 +**严格来说,Go 语言没有引用类型**,但是我们可以把 map、chan 称为引用类型,这样便于理解。除了 map、chan 之外,Go 语言中的函数、接口、slice 切片都可以称为引用类型。 > 小提示:指针类型也可以理解为是一种引用类型。 @@ -280,7 +280,7 @@ func makechan(t *chantype, size int64) *hchan { ### 总结 -在 Go 语言中, **函数的参数传递只有值传递** ,而且传递的实参都是原始数据的一份拷贝。如果拷贝的内容是值类型的,那么在函数中就无法修改原始数据;如果拷贝的内容是指针(或者可以理解为引用类型 map、chan 等),那么就可以在函数中修改原始数据。 +在 Go 语言中,**函数的参数传递只有值传递**,而且传递的实参都是原始数据的一份拷贝。如果拷贝的内容是值类型的,那么在函数中就无法修改原始数据;如果拷贝的内容是指针(或者可以理解为引用类型 map、chan 等),那么就可以在函数中修改原始数据。 ![Lark20201209-184447.png](assets/CgqCHl_QqryAEqYQAAVkYmbnDIM013.png) diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25417\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25417\350\256\262.md" index 34bac3c9d..08ddf5390 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25417\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25417\350\256\262.md" @@ -23,7 +23,7 @@ array{ } ``` -比如变量 a1 的大小是 1,内部元素的类型是 string,也就是说 a1 最多只能存储 1 个类型为 string 的元素。而 a2 的大小是 2,内部元素的类型也是 string,所以 a2 最多可以存储 2 个类型为 string 的元素。 **一旦一个数组被声明,它的大小和内部元素的类型就不能改变** ,你不能随意地向数组添加任意多个元素。这是数组的第一个限制。 +比如变量 a1 的大小是 1,内部元素的类型是 string,也就是说 a1 最多只能存储 1 个类型为 string 的元素。而 a2 的大小是 2,内部元素的类型也是 string,所以 a2 最多可以存储 2 个类型为 string 的元素。**一旦一个数组被声明,它的大小和内部元素的类型就不能改变**,你不能随意地向数组添加任意多个元素。这是数组的第一个限制。 既然数组的大小是固定的,如果需要使用数组存储大量的数据,就需要提前指定一个合适的大小,比如 10 万,代码如下所示: @@ -31,7 +31,7 @@ array{ a10:=[100000]string{"飞雪无情"} ``` -这样虽然可以解决问题,但又带来了另外的问题,那就是内存占用。因为在 Go 语言中,函数间的传参是值传递的,数组作为参数在各个函数之间被传递的时候,同样的内容就会被一遍遍地复制, **这就会造成大量的内存浪费** ,这是数组的第二个限制。 +这样虽然可以解决问题,但又带来了另外的问题,那就是内存占用。因为在 Go 语言中,函数间的传参是值传递的,数组作为参数在各个函数之间被传递的时候,同样的内容就会被一遍遍地复制,**这就会造成大量的内存浪费**,这是数组的第二个限制。 虽然数组有限制,但是它是 Go 非常重要的底层数据结构,比如 slice 切片的底层数据就存储在数组中。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25418\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25418\350\256\262.md" index 91004c8d2..fa19bbb63 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25418\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25418\350\256\262.md" @@ -10,7 +10,7 @@ ### 什么是单元测试 -顾名思义,单元测试强调的是对单元进行测试。在开发中,一个单元可以是一个函数、一个模块等。一般情况下,你要测试的单元应该是一个完整的 **最小单元** ,比如 Go 语言的函数。这样的话,当每个最小单元都被验证通过,那么整个模块、甚至整个程序就都可以被验证通过。 +顾名思义,单元测试强调的是对单元进行测试。在开发中,一个单元可以是一个函数、一个模块等。一般情况下,你要测试的单元应该是一个完整的 **最小单元**,比如 Go 语言的函数。这样的话,当每个最小单元都被验证通过,那么整个模块、甚至整个程序就都可以被验证通过。 单元测试由开发者自己编写,也就是谁改动了代码,谁就要编写相应的单元测试代码以验证本次改动的正确性。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25419\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25419\350\256\262.md" index 629b33d4c..b8e812c46 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25419\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25419\350\256\262.md" @@ -10,7 +10,7 @@ 今天要讲的内容是 Go 语言的代码检查和优化,下面我们开始本讲内容的讲解。 -在项目开发中, **保证代码质量和性能的手段不只有单元测试和基准测试** , **还有代码规范检查和性能优化** 。 +在项目开发中,**保证代码质量和性能的手段不只有单元测试和基准测试**,**还有代码规范检查和性能优化** 。 - **代码规范检查** 是对单元测试的一种补充,它可以从非业务的层面检查你的代码是否还有优化的空间,比如变量是否被使用、是否是死代码等等。 - **性能优化** 是通过基准测试来衡量的,这样我们才知道优化部分是否真的提升了程序的性能。 @@ -19,7 +19,7 @@ #### 什么是代码规范检查 -代码规范检查,顾名思义,是从 Go 语言层面出发,依据 Go 语言的规范,对你写的代码进行的 **静态扫描检查** ,这种检查和你的业务无关。 +代码规范检查,顾名思义,是从 Go 语言层面出发,依据 Go 语言的规范,对你写的代码进行的 **静态扫描检查**,这种检查和你的业务无关。 比如你定义了个常量,从未使用过,虽然对代码运行并没有造成什么影响,但是这个常量是可以删除的,代码如下所示: @@ -55,7 +55,7 @@ func main() { 可用于 Go 语言代码分析的工具有很多,比如 golint、gofmt、misspell 等,如果一一引用配置,就会比较烦琐,所以通常我们不会单独地使用它们,而是使用 golangci-lint。 -[golangci-lint](https://github.com/golangci/golangci-lint) 是一个 **集成工具** ,它集成了很多静态代码分析工具,便于我们使用。通过配置这一工具,我们可以很灵活地启用需要的代码规范检查。 +[golangci-lint](https://github.com/golangci/golangci-lint) 是一个 **集成工具**,它集成了很多静态代码分析工具,便于我们使用。通过配置这一工具,我们可以很灵活地启用需要的代码规范检查。 如果要使用 golangci-lint,首先需要安装。因为 golangci-lint 本身就是 Go 语言编写的,所以我们可以从源代码安装它,打开终端,输入如下命令即可安装。 @@ -162,7 +162,7 @@ service: golangci-lint-version: 1.32.2 # use the fixed version to not introduce new linters unexpectedly ``` -#### 集成 golangci-lint 到 CI **代码检查一定要集成到 CI 流程中** ,效果才会更好,这样开发者提交代码的时候,CI 就会自动检查代码,及时发现问题并进行修正 +#### 集成 golangci-lint 到 CI **代码检查一定要集成到 CI 流程中**,效果才会更好,这样开发者提交代码的时候,CI 就会自动检查代码,及时发现问题并进行修正 不管你是使用 Jenkins,还是 Gitlab CI,或者 Github Action,都可以通过 **Makefile** 的方式运行 golangci-lint。现在我在项目根目录下创建一个 Makefile 文件,并添加如下代码: @@ -189,14 +189,14 @@ make verifiers ### 性能优化 -性能优化的目的是让程序更好、更快地运行,但是它不是必要的,这一点一定要记住。所以在程序开始的时候,你不必刻意追求性能优化,先大胆地写你的代码就好了, **写正确的代码是性能优化的前提** 。 ![19金句.png](assets/CgpVE1_sUrmATX3ZAAU9yqs6HjM626.png) +性能优化的目的是让程序更好、更快地运行,但是它不是必要的,这一点一定要记住。所以在程序开始的时候,你不必刻意追求性能优化,先大胆地写你的代码就好了,**写正确的代码是性能优化的前提** 。 ![19金句.png](assets/CgpVE1_sUrmATX3ZAAU9yqs6HjM626.png) #### 堆分配还是栈 在比较古老的 C 语言中,内存分配是手动申请的,内存释放也需要手动完成。 - 手动控制有一个很大的 **好处** 就是你需要多少就申请多少,可以最大化地 **利用内存** ; -- 但是这种方式也有一个明显的 **缺点** ,就是如果忘记释放内存,就会导致 **内存泄漏** 。 +- 但是这种方式也有一个明显的 **缺点**,就是如果忘记释放内存,就会导致 **内存泄漏** 。 所以,为了让程序员更好地专注于业务代码的实现,Go 语言增加了垃圾回收机制,自动地回收不再使用的内存。 @@ -235,7 +235,7 @@ func newString() *string{ ch19/main.go:16:8: new(string) escapes to heap ``` -在这一命令中,-m 表示打印出逃逸分析信息,-l 表示禁止内联,可以更好地观察逃逸。从以上输出结果可以看到,发生了逃逸, **也就是说指针作为函数返回值的时候** , **一定会发生逃逸** 。 +在这一命令中,-m 表示打印出逃逸分析信息,-l 表示禁止内联,可以更好地观察逃逸。从以上输出结果可以看到,发生了逃逸,**也就是说指针作为函数返回值的时候**,**一定会发生逃逸** 。 逃逸到堆内存的变量不能马上被回收,只能通过垃圾回收标记清除,增加了垃圾回收的压力,所以要尽可能地避免逃逸,让变量分配在栈内存上,这样函数返回时就可以回收资源,提升效率。 @@ -309,7 +309,7 @@ ch19/main.go:16:2: moved to heap: s ch19/main.go:15:20: map[int]*string literal does not escape ``` -从这一结果可以看到,变量 m 没有逃逸,反而被变量 m 引用的变量 s 逃逸到了堆上。 **所以被map** 、 **slice 和 chan 这三种类型引用的指针一定会发生逃逸的** 。 +从这一结果可以看到,变量 m 没有逃逸,反而被变量 m 引用的变量 s 逃逸到了堆上。**所以被map** 、 **slice 和 chan 这三种类型引用的指针一定会发生逃逸的** 。 逃逸分析是判断变量是分配在堆上还是栈上的一种方法,在实际的项目中要尽可能避免逃逸,这样就不会被 GC 拖慢速度,从而提升效率。 @@ -319,7 +319,7 @@ ch19/main.go:15:20: map[int]*string literal does not escape 通过前面小节的介绍,相信你已经了解了栈内存和堆内存,以及变量什么时候会逃逸,那么在优化的时候思路就比较清晰了,因为都是基于以上原理进行的。下面我总结几个优化的小技巧: **第 1 个** 需要介绍的技巧是尽可能避免逃逸,因为栈内存效率更高,还不用 GC。比如小对象的传参,array 要比 slice 效果好。 -如果避免不了逃逸,还是在堆上分配了内存,那么对于频繁的内存申请操作,我们要学会重用内存,比如使用 sync.Pool,这是 **第 2 个** 技巧。 **第 3 个** 技巧就是选用合适的算法,达到高性能的目的,比如空间换时间。 +如果避免不了逃逸,还是在堆上分配了内存,那么对于频繁的内存申请操作,我们要学会重用内存,比如使用 sync.Pool,这是 **第 2 个** 技巧。**第 3 个** 技巧就是选用合适的算法,达到高性能的目的,比如空间换时间。 > 小提示:性能优化的时候,要结合基准测试,来验证自己的优化是否有提升。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25420\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25420\350\256\262.md" index 406fd7b0b..1ec0ca0a4 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25420\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25420\350\256\262.md" @@ -1,6 +1,6 @@ # 20 协作开发:模块化管理为什么能够提升研发效能? -任何业务,都是从简单向复杂演进的。而在业务演进的过程中,技术是从单体向多模块、多服务演进的。技术的这种演进方式的核心目的是 **复用代码、提高效率** ,这一讲,我会为你介绍 Go 语言是如何通过模块化的管理,提升开发效率的。 +任何业务,都是从简单向复杂演进的。而在业务演进的过程中,技术是从单体向多模块、多服务演进的。技术的这种演进方式的核心目的是 **复用代码、提高效率**,这一讲,我会为你介绍 Go 语言是如何通过模块化的管理,提升开发效率的。 ### Go 语言中的包 @@ -8,7 +8,7 @@ 在业务非常简单的时候,你甚至可以把代码写到一个 Go 文件中。但随着业务逐渐复杂,你会发现,如果代码都放在一个 Go 文件中,会变得难以维护,这时候你就需要抽取代码,把相同业务的代码放在一个目录中。在 Go 语言中,这个目录叫作包。 -在 Go 语言中,一个包是通过 **package 关键字定义** 的,最常见的就是 **main 包** ,它的定义如下所示: +在 Go 语言中,一个包是通过 **package 关键字定义** 的,最常见的就是 **main 包**,它的定义如下所示: ```plaintext package main @@ -16,11 +16,11 @@ package main 此外,前面章节演示示例经常使用到的 fmt 包,也是通过 package 关键字声明的。 -一个包就是一个 **独立的空间** ,你可以在这个包里 **定义函数** 、 **结构体** 等。这时,我们认为这些函数、结构体是属于这个包的。 +一个包就是一个 **独立的空间**,你可以在这个包里 **定义函数** 、 **结构体** 等。这时,我们认为这些函数、结构体是属于这个包的。 #### 使用包 -如果你想使用一个包里的函数或者结构体,就需要先 **导入这个包** ,才能使用,比如常用的 fmt包,代码示例如下所示。 +如果你想使用一个包里的函数或者结构体,就需要先 **导入这个包**,才能使用,比如常用的 fmt包,代码示例如下所示。 ```plaintext package main @@ -76,13 +76,13 @@ _ch20/util/string.go_ package util ``` -可以看到,Go 语言中的包是代码的一种 **组织形式** ,通过包把相同业务或者相同职责的代码放在一起。通过包对代码进行归类,便于代码维护以及被其他包调用,提高团队协作效率。 +可以看到,Go 语言中的包是代码的一种 **组织形式**,通过包把相同业务或者相同职责的代码放在一起。通过包对代码进行归类,便于代码维护以及被其他包调用,提高团队协作效率。 #### init 函数 除了 main 这个特殊的函数外,Go 语言还有一个特殊的函数——init,通过它可以 **实现包级别的一些初始化操作** 。 -init 函数没有返回值,也没有参数,它 **先于 main 函数执行** ,代码如下所示: +init 函数没有返回值,也没有参数,它 **先于 main 函数执行**,代码如下所示: ```plaintext func init() { @@ -100,7 +100,7 @@ func init() { 在 Go 语言中: -- 一个模块通常是 **一个项目** ,比如这个专栏实例中使用的 gotour 项目; +- 一个模块通常是 **一个项目**,比如这个专栏实例中使用的 gotour 项目; - 也可以是 **一个框** 架,比如常用的 Web 框架 gin。 #### go mod @@ -119,14 +119,14 @@ module gotour go 1.15 ``` -- 第一句是该项目的 **模块名** ,也就是 gotour; +- 第一句是该项目的 **模块名**,也就是 gotour; - 第二句表示要编译该模块至少需要 **Go 1.15 版本的 SDK** 。 > 小提示:模块名最好是以自己的域名开头,比如 flysnow.org/gotour,这样就可以很大程度上保证模块名的唯一,不至于和其他模块重名。 #### 使用第三方模块 -模块化为什么可以提高开发效率?最重要的原因就是 **复用了现有的模块** ,Go 语言也不例外。比如你可以把项目中的公共代码抽取为一个模块,这样就可以供其他项目使用,不用再重复开发;同理,在 Github 上也有很多开源的 Go 语言项目,它们都是一个个独立的模块,也可以被我们直接使用,提高我们的开发效率,比如 Web 框架 [gin-gonic/gin](https://github.com/gin-gonic/gin)。 +模块化为什么可以提高开发效率?最重要的原因就是 **复用了现有的模块**,Go 语言也不例外。比如你可以把项目中的公共代码抽取为一个模块,这样就可以供其他项目使用,不用再重复开发;同理,在 Github 上也有很多开源的 Go 语言项目,它们都是一个个独立的模块,也可以被我们直接使用,提高我们的开发效率,比如 Web 框架 [gin-gonic/gin](https://github.com/gin-gonic/gin)。 众所周知,在使用第三方模块之前,需要先设置下 Go 代理,也就是 GOPROXY,这样我们就可以获取到第三方模块了。 @@ -139,7 +139,7 @@ go env -w GOPROXY=https://goproxy.io,direct 打开终端,输入这一命令回车即可设置成功。 -在实际的项目开发中,除了第三方模块外,还有我们 **自己开发的模块** ,放在了公司的 GitLab上,这时候就要把公司 Git 代码库的域名排除在 Go PROXY 之外,为此 Go 语言提供了GOPRIVATE 这个环境变量帮助我们达到目的。通过如下命令即可设置 GOPRIVATE: +在实际的项目开发中,除了第三方模块外,还有我们 **自己开发的模块**,放在了公司的 GitLab上,这时候就要把公司 Git 代码库的域名排除在 Go PROXY 之外,为此 Go 语言提供了GOPRIVATE 这个环境变量帮助我们达到目的。通过如下命令即可设置 GOPRIVATE: ```plaintext # 设置不走 proxy 的私有仓库,多个用逗号相隔(可选) @@ -169,7 +169,7 @@ func main() { } ``` -以上代码现在还 **无法编译通过** ,因为还没有同步 Gin 这个模块的依赖,也就是没有把它添加到go.mod 文件中。通过如下命令可以添加缺失的模块: +以上代码现在还 **无法编译通过**,因为还没有同步 Gin 这个模块的依赖,也就是没有把它添加到go.mod 文件中。通过如下命令可以添加缺失的模块: ```plaintext go mod tidy @@ -206,6 +206,6 @@ require ( 所以对于你的项目(模块)来说,它具有 **模块 ➡ 包 ➡ 函数类型** 这样三层结构,同一个模块中,可以通过包组织代码,达到代码复用的目的;在不同模块中,就需要通过模块的引入,达到这个目的。 -编程界有个谚语:不要重复 **造轮子** ,使用现成的轮子,可以提高开发效率,降低 Bug 率。Go 语言提供的模块、包这些能力,就可以很好地让我们使用现有的轮子,在多人协作开发中,更好地提高工作效率。 +编程界有个谚语:不要重复 **造轮子**,使用现成的轮子,可以提高开发效率,降低 Bug 率。Go 语言提供的模块、包这些能力,就可以很好地让我们使用现有的轮子,在多人协作开发中,更好地提高工作效率。 最后,为你留个作业:基于模块化拆分你所做的项目,提取一些公共的模块,以供更多项目使用。相信这样你们的开发效率会大大提升的。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25421\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25421\350\256\262.md" index c1cd340c5..bf97366da 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25421\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25421\350\256\262.md" @@ -8,7 +8,7 @@ RESTful API 是一套规范,它可以规范我们如何对服务器上的资源进行操作。在了解 RESTful API 之前,我先为你介绍下 HTTP Method,因为 RESTful API 和它是密不可分的。 -说起 HTTP Method,最常见的就是 **POST** 和 **GET** ,其实最早在 HTTP 0.9 版本中,只有一个 **GET** 方法,该方法是一个 **幂等方法** ,用于获取服务器上的资源,也就是我们在浏览器中直接输入网址回车请求的方法。 +说起 HTTP Method,最常见的就是 **POST** 和 **GET**,其实最早在 HTTP 0.9 版本中,只有一个 **GET** 方法,该方法是一个 **幂等方法**,用于获取服务器上的资源,也就是我们在浏览器中直接输入网址回车请求的方法。 在 HTTP 1.0 版本中又增加了 **HEAD** 和 **POST** 方法,其中常用的是 POST 方法,一般用于给服务端提交一个资源,导致服务器的资源发生变化。 @@ -24,7 +24,7 @@ RESTful API 是一套规范,它可以规范我们如何对服务器上的资 1. TRACE 方法用于沿着到目标资源的路径执行一个消息环回测试。 1. PATCH 方法用于对资源应用部分修改。 -从以上每个方法的介绍可以看到,HTTP 规范针对每个方法都给出了明确的定义,所以我们使用的时候也要尽可能地 **遵循这些定义** ,这样我们在开发中才可以更好地协作。 +从以上每个方法的介绍可以看到,HTTP 规范针对每个方法都给出了明确的定义,所以我们使用的时候也要尽可能地 **遵循这些定义**,这样我们在开发中才可以更好地协作。 理解了这些 HTTP 方法,就可以更好地理解 RESTful API 规范了,因为 RESTful API 规范就是基于这些 HTTP 方法规范我们对服务器资源的操作,同时规范了 URL 的样式和 HTTP Status Code。 diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25422\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25422\350\256\262.md" index f90afcdea..86493bb0d 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25422\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25422\350\256\262.md" @@ -69,9 +69,9 @@ func updateUserName(c *gin.Context) { ### 什么是RPC 服务 -RPC,也就是 **远程过程调用** ,是分布式系统中不同节点调用的方式(进程间通信),属于 C/S 模式。RPC 由客户端发起,调用服务端的方法进行通信,然后服务端把结果返回给客户端。 +RPC,也就是 **远程过程调用**,是分布式系统中不同节点调用的方式(进程间通信),属于 C/S 模式。RPC 由客户端发起,调用服务端的方法进行通信,然后服务端把结果返回给客户端。 -RPC的核心有两个: **通信协议** 和 **序列化** 。在 HTTP 2 之前,一般采用自定义 TCP 协议的方式进行通信,HTTP 2 出来后,也有采用该协议的,比如流行的gRPC。 **序列化** 和 **反序列化** 是一种把传输内容编码和解码的方式,常见的编解码方式有 JSON、Protobuf 等。 +RPC的核心有两个: **通信协议** 和 **序列化** 。在 HTTP 2 之前,一般采用自定义 TCP 协议的方式进行通信,HTTP 2 出来后,也有采用该协议的,比如流行的gRPC。**序列化** 和 **反序列化** 是一种把传输内容编码和解码的方式,常见的编解码方式有 JSON、Protobuf 等。 在大多数 RPC的架构设计中,都有 **Client** 、 **Client Stub** 、 **Server** 、 **Server Stub** 这四个组件,Client 和 Server 之间通过 Socket 进行通信。RPC 架构如下图所示: @@ -114,7 +114,7 @@ func (m *MathService) Add(args Args, reply *int) error { 在以上代码中: -- 定义了 **MathService** ,用于表示一个远程服务对象; +- 定义了 **MathService**,用于表示一个远程服务对象; - Args 结构体用于表示参数; - Add 这个方法实现了加法的功能,加法的结果通过 replay这个指针变量返回。 @@ -147,7 +147,7 @@ func main() { 然后通过 net.Listen 函数建立一个TCP 链接,在 1234 端口进行监听,最后通过 rpc.Accept 函数在该 TCP 链接上提供 MathService 这个 RPC 服务。现在客户端就可以看到MathService这个服务以及它的Add 方法了。 -任何一个框架都有自己的规则,net/rpc 这个 Go 语言提供的RPC 框架也不例外。要想把一个对象注册为 RPC 服务,可以让 **客户端远程访问** ,那么该对象(类型)的方法必须满足如下条件: +任何一个框架都有自己的规则,net/rpc 这个 Go 语言提供的RPC 框架也不例外。要想把一个对象注册为 RPC 服务,可以让 **客户端远程访问**,那么该对象(类型)的方法必须满足如下条件: - 方法的类型是可导出的(公开的); - 方法本身也是可导出的; @@ -196,7 +196,7 @@ func main() { TCP 链接建立成功后,就需要准备远程方法需要的参数,也就是示例中的args 和 reply。参数准备好之后,就可以通过 Call 方法调用远程的RPC 服务了。Call 方法有 3 个参数,它们的作用分别如下所示: -1. 调用的远程方法的名字,这里是MathService.Add,点前面的部分是 **注册的服务的名称** ,点后面的部分是 **该服务的方法** ; +1. 调用的远程方法的名字,这里是MathService.Add,点前面的部分是 **注册的服务的名称**,点后面的部分是 **该服务的方法** ; 1. 客户端为了 **调用远程方法** 提供的参数,示例中是args; 1. 为了接收远程方法返回的结果,必须是一个指针,也就是示例中的& replay,这样客户端就可以获得服务端返回的结果了。 @@ -255,7 +255,7 @@ func main() { ![image](assets/Ciqc1F_7zbWAb5PXAAA7zm9tcRE148.png) -如上图所示, **注册的 RPC 服务** 、 **方法的签名** 、 **已经被调用的次数** 都可以看到。 +如上图所示,**注册的 RPC 服务** 、 **方法的签名** 、 **已经被调用的次数** 都可以看到。 ### JSON RPC 跨平台通信 @@ -263,7 +263,7 @@ func main() { #### 基于 TCP 的 JSON RPC -实现跨语言 RPC 服务的核心在于选择一个 **通用的编码** ,这样大多数语言都支持,比如常用的JSON。在 Go 语言中,实现一个 JSON RPC 服务非常简单,只需要使用 net/rpc/jsonrpc 包即可。 +实现跨语言 RPC 服务的核心在于选择一个 **通用的编码**,这样大多数语言都支持,比如常用的JSON。在 Go 语言中,实现一个 JSON RPC 服务非常简单,只需要使用 net/rpc/jsonrpc 包即可。 同样以上面的示例为例,我把它改造成支持 JSON的RPC 服务,服务端代码如下所示: diff --git "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25423\350\256\262.md" "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25423\350\256\262.md" index 5f530472a..81859e027 100644 --- "a/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25423\350\256\262.md" +++ "b/docs/Other/22 \350\256\262\351\200\232\345\205\263 Go \350\257\255\350\250\200/\347\254\25423\350\256\262.md" @@ -26,7 +26,7 @@ CNCF(云原生计算基金会)对云原生的定义是: 基于官方文档入门后,你就可以参考一些第三方大牛写的相关书籍了。阅读不同人写的 Go 语言书籍,你可以融会贯通,更好地理解 Go 语言的知识点。比如在其他书上看不懂的内容,换一本你可能就看懂了。 -阅读书籍还有一个好处是让你的学习具备 **系统性** ,而非零散的。现在大部分的我们都选择碎片化学习,其实通过碎片化的时间,系统地学习才是正确的方式。 +阅读书籍还有一个好处是让你的学习具备 **系统性**,而非零散的。现在大部分的我们都选择碎片化学习,其实通过碎片化的时间,系统地学习才是正确的方式。 不管是通过书籍、官网文档,还是视频、专栏的学习,我们都要结合示例进行练习,不能只用眼睛看,这样的学习效率很低,一定要将代码动手写出来,这样你对知识的理解程度和只看是完全不一样的,在这个过程中你可以 **通过写加深记忆** 、 **通过调试加深理解** 、 **通过结果验证你的知识** 。 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25401\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25401\350\256\262.md" index 99b0a9eef..2be17f83d 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25401\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25401\350\256\262.md" @@ -34,7 +34,7 @@ ### 2019 年——云原生技术普及元年 -为什么说 2019 年很可能是一个关键节点呢? **我们认为 2019 年是云原生技术的普及元年。** 首先大家可以看到,在 2019 年,阿里巴巴宣布要全面上云,而且“上云就要上云原生”。我们还可以看到,以“云”为核心的软件研发思想,正逐步成为所有开发者的默认选项。像 Kubernetes 等云原生技术正在成为技术人员的必修课,大量的工作岗位正在涌现出来。 +为什么说 2019 年很可能是一个关键节点呢?**我们认为 2019 年是云原生技术的普及元年。** 首先大家可以看到,在 2019 年,阿里巴巴宣布要全面上云,而且“上云就要上云原生”。我们还可以看到,以“云”为核心的软件研发思想,正逐步成为所有开发者的默认选项。像 Kubernetes 等云原生技术正在成为技术人员的必修课,大量的工作岗位正在涌现出来。 这种背景下,“会 Kubernetes”已经远远不够了,“懂 Kubernetes”、“会云原生架构”的重要性正日益凸显出来。 从 2019 年开始,云原生技术将会大规模普及,这也是为什么大家都要在这个时间点上学习和投资云原生技术的重要原因。 @@ -70,7 +70,7 @@ ### 云原生的定义 -很多人都会问“到底什么是云原生?” 实际上,云原生是一条最佳路径或者最佳实践。更详细的说, **云原生为用户指定了一条低心智负担的、敏捷的、能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径。** 因此,云原生其实是一套指导进行软件架构设计的思想。按照这样的思想而设计出来的软件:首先,天然就“生在云上,长在云上”;其次,能够最大化地发挥云的能力,使得我们开发的软件和“云”能够天然地集成在一起,发挥出“云”的最大价值。 所以,云原生的最大价值和愿景,就是认为未来的软件,会从诞生起就生长在云上,并且遵循一种新的软件开发、发布和运维模式,从而使得软件能够最大化地发挥云的能力。说到了这里,大家可以思考一下为什么容器技术具有革命性? +很多人都会问“到底什么是云原生?” 实际上,云原生是一条最佳路径或者最佳实践。更详细的说,**云原生为用户指定了一条低心智负担的、敏捷的、能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径。** 因此,云原生其实是一套指导进行软件架构设计的思想。按照这样的思想而设计出来的软件:首先,天然就“生在云上,长在云上”;其次,能够最大化地发挥云的能力,使得我们开发的软件和“云”能够天然地集成在一起,发挥出“云”的最大价值。 所以,云原生的最大价值和愿景,就是认为未来的软件,会从诞生起就生长在云上,并且遵循一种新的软件开发、发布和运维模式,从而使得软件能够最大化地发挥云的能力。说到了这里,大家可以思考一下为什么容器技术具有革命性? 其实,容器技术和集装箱技术的革命性非常类似,即:容器技术使得应用具有了一种“自包含”的定义方式。所以,这样的应用才能以敏捷的、以可扩展可复制的方式发布在云上,发挥出云的能力。这也就是容器技术对云发挥出的革命性影响所在,所以说,容器技术正是云原生技术的核心底盘。 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25402\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25402\350\256\262.md" index 2318994eb..77901042d 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25402\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25402\350\256\262.md" @@ -28,7 +28,7 @@ 那么,应该如何定义这样的进程集合呢? -其实, **容器就是一个视图隔离、资源可限制、独立文件系统的进程集合。** 所谓“视图隔离”就是能够看到部分进程以及具有独立的主机名等;控制资源使用率则是可以对于内存大小以及 CPU 使用个数等进行限制。容器就是一个进程集合,它将系统的其他资源隔离开来,具有自己独立的资源视图。 +其实,**容器就是一个视图隔离、资源可限制、独立文件系统的进程集合。** 所谓“视图隔离”就是能够看到部分进程以及具有独立的主机名等;控制资源使用率则是可以对于内存大小以及 CPU 使用个数等进行限制。容器就是一个进程集合,它将系统的其他资源隔离开来,具有自己独立的资源视图。 容器具有一个独立的文件系统,因为使用的是系统的资源,所以在独立的文件系统内不需要具备内核相关的代码或者工具,我们只需要提供容器所需的二进制文件、配置文件以及依赖即可。只要容器运行时所需的文件集合都能够具备,那么这个容器就能够运行起来。 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25403\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25403\350\256\262.md" index 97a62f27f..d70d8c5d1 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25403\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25403\350\256\262.md" @@ -70,13 +70,13 @@ Kubernetes 的 Master 包含四个主要的组件:API Server、Controller、Sc ### Kubernetes 的架构:Node -Kubernetes 的 Node 是真正运行业务负载的,每个业务负载会以 Pod 的形式运行。等一下我会介绍一下 Pod 的概念。一个 Pod 中运行的一个或者多个容器,真正去运行这些 Pod 的组件的是叫做 **kubelet** ,也就是 Node 上最为关键的组件,它通过 API Server 接收到所需要 Pod 运行的状态,然后提交到我们下面画的这个 Container Runtime 组件中。 +Kubernetes 的 Node 是真正运行业务负载的,每个业务负载会以 Pod 的形式运行。等一下我会介绍一下 Pod 的概念。一个 Pod 中运行的一个或者多个容器,真正去运行这些 Pod 的组件的是叫做 **kubelet**,也就是 Node 上最为关键的组件,它通过 API Server 接收到所需要 Pod 运行的状态,然后提交到我们下面画的这个 Container Runtime 组件中。 ![avatar](assets/Fp29--O3Bo8y2VsC3C3SkwLILJ4-) -在 OS 上去创建容器所需要运行的环境,最终把容器或者 Pod 运行起来,也需要对存储跟网络进行管理。Kubernetes 并不会直接进行网络存储的操作,他们会靠 Storage Plugin 或者是网络的 Plugin 来进行操作。用户自己或者云厂商都会去写相应的 **Storage Plugin** 或者 **Network Plugin** ,去完成存储操作或网络操作。 +在 OS 上去创建容器所需要运行的环境,最终把容器或者 Pod 运行起来,也需要对存储跟网络进行管理。Kubernetes 并不会直接进行网络存储的操作,他们会靠 Storage Plugin 或者是网络的 Plugin 来进行操作。用户自己或者云厂商都会去写相应的 **Storage Plugin** 或者 **Network Plugin**,去完成存储操作或网络操作。 -在 Kubernetes 自己的环境中,也会有 Kubernetes 的 Network,它是为了提供 Service network 来进行搭网组网的。(等一下我们也会去介绍“service”这个概念。)真正完成 service 组网的组件的是 **Kube-proxy** ,它是利用了 iptable 的能力来进行组建 Kubernetes 的 Network,就是 cluster network,以上就是 Node 上面的四个组件。 +在 Kubernetes 自己的环境中,也会有 Kubernetes 的 Network,它是为了提供 Service network 来进行搭网组网的。(等一下我们也会去介绍“service”这个概念。)真正完成 service 组网的组件的是 **Kube-proxy**,它是利用了 iptable 的能力来进行组建 Kubernetes 的 Network,就是 cluster network,以上就是 Node 上面的四个组件。 Kubernetes 的 Node 并不会直接和 user 进行 interaction,它的 interaction 只会通过 Master。而 User 是通过 Master 向节点下发这些信息的。Kubernetes 每个 Node 上,都会运行我们刚才提到的这几个组件。 @@ -160,11 +160,11 @@ Kubernetes 的 kubectl 也就是 command tool,Kubernetes UI,或者有时候 如果我们去提交一个 Pod,或者 get 一个 Pod 的时候,它的 content 内容都是用 JSON 或者是 YAML 表达的。上图中有个 yaml 的例子,在这个 yaml file 中,对 Pod 资源的描述也分为几个部分。 -第一个部分,一般来讲会是 API 的 **version** 。比如在这个例子中是 V1,它也会描述我在操作哪个资源;比如说我的 **kind** 如果是 pod,在 Metadata 中,就写上这个 Pod 的名字;比如说 nginx,我们也会给它打一些 **label** ,我们等下会讲到 label 的概念。在 Metadata 中,有时候也会去写 **annotation** ,也就是对资源的额外的一些用户层次的描述。 +第一个部分,一般来讲会是 API 的 **version** 。比如在这个例子中是 V1,它也会描述我在操作哪个资源;比如说我的 **kind** 如果是 pod,在 Metadata 中,就写上这个 Pod 的名字;比如说 nginx,我们也会给它打一些 **label**,我们等下会讲到 label 的概念。在 Metadata 中,有时候也会去写 **annotation**,也就是对资源的额外的一些用户层次的描述。 -比较重要的一个部分叫做 **Spec** ,Spec 也就是我们希望 Pod 达到的一个预期的状态。比如说它内部需要有哪些 container 被运行;比如说这里面有一个 nginx 的 container,它的 image 是什么?它暴露的 port 是什么? +比较重要的一个部分叫做 **Spec**,Spec 也就是我们希望 Pod 达到的一个预期的状态。比如说它内部需要有哪些 container 被运行;比如说这里面有一个 nginx 的 container,它的 image 是什么?它暴露的 port 是什么? -当我们从 Kubernetes API 中去获取这个资源的时候,一般来讲在 Spec 下面会有一个项目叫 **status** ,它表达了这个资源当前的状态;比如说一个 Pod 的状态可能是正在被调度、或者是已经 running、或者是已经被 terminates,就是被执行完毕了。 +当我们从 Kubernetes API 中去获取这个资源的时候,一般来讲在 Spec 下面会有一个项目叫 **status**,它表达了这个资源当前的状态;比如说一个 Pod 的状态可能是正在被调度、或者是已经 running、或者是已经被 terminates,就是被执行完毕了。 刚刚在 API 之中,我们讲了一个比较有意思的 metadata 叫做“ **label** ”,这个 label 可以是一组 KeyValuePair。 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25404\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25404\350\256\262.md" index b5b4cb4f6..d89c5b1ab 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25404\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25404\350\256\262.md" @@ -40,7 +40,7 @@ 而反过来真的把这个应用本身改成了 systemd,或者在容器里面运行了一个 systemd,将会导致另外一个问题:使得管理容器,不再是管理应用本身了,而等于是管理 systemd,这里的问题就非常明显了。比如说我这个容器里面 run 的程序或者进程是 systemd,那么接下来,这个应用是不是退出了?是不是 fail 了?是不是出现异常失败了?实际上是没办法直接知道的,因为容器管理的是 systemd。这就是为什么在容器里面运行一个复杂程序往往比较困难的一个原因。 -这里再帮大家梳理一下: **由于容器实际上是一个“单进程”模型** ,所以如果你在容器里启动多个进程,只有一个可以作为 PID=1 的进程,而这时候,如果这个 PID=1 的进程挂了,或者说失败退出了,那么其他三个进程就会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资源,这是一个非常不好的情况。 +这里再帮大家梳理一下: **由于容器实际上是一个“单进程”模型**,所以如果你在容器里启动多个进程,只有一个可以作为 PID=1 的进程,而这时候,如果这个 PID=1 的进程挂了,或者说失败退出了,那么其他三个进程就会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资源,这是一个非常不好的情况。 > 注意:Linux 容器的“单进程”模型,指的是容器的生命周期等同于 PID=1 的进程(容器应用进程)的生命周期,而不是说容器里不能创建多进程。当然,一般情况下,容器应用进程并不具备进程管理能力,所以你通过 exec 或者 ssh 在容器里创建的其他进程,一旦异常退出(比如 ssh 终止)是很容易变成孤儿进程的。 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25405\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25405\350\256\262.md" index 8761ba7a3..99fe7d668 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25405\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25405\350\256\262.md" @@ -42,7 +42,7 @@ 我们当时讲到最后一个元数据叫做 Ownereference,所谓所有者,一般就是指集合类的资源,比如说 Pod 集合,就有 replicaset、statefulset,这个将在后序的课程中讲到。 -集合类资源的控制器会创建对应的归属资源。比如:replicaset 控制器在操作中会创建 Pod,被创建 Pod 的 Ownereference 就指向了创建 Pod 的 replicaset,Ownereference 使得用户可以方便地查找一个创建资源的对象,另外,还可以用来实现级联删除的效果。 **** +集合类资源的控制器会创建对应的归属资源。比如:replicaset 控制器在操作中会创建 Pod,被创建 Pod 的 Ownereference 就指向了创建 Pod 的 replicaset,Ownereference 使得用户可以方便地查找一个创建资源的对象,另外,还可以用来实现级联删除的效果。**** ## 操作演示 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25407\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25407\350\256\262.md" index ebf05bd4d..640b12098 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25407\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25407\350\256\262.md" @@ -43,7 +43,7 @@ ![avatar](assets/Fo9k3bh5T3hAJxqA-3hFXVRkYF70) -Job 创建完成之后,我们就可以通过 kubectl get jobs 这个命令,来查看当前 job 的运行状态。得到的值里面,基本就有 Job 的名称、当前完成了多少个 Pod,进行多长时间。 **AGE** 的含义是指这个 Pod 从当前时间算起,减去它当时创建的时间。这个时长主要用来告诉你 Pod 的历史、Pod 距今创建了多长时间。 **DURATION** 主要来看我们 Job 里面的实际业务到底运行了多长时间,当我们的性能调优的时候,这个参数会非常的有用。 **COMPLETIONS** 主要来看我们任务里面这个 Pod 一共有几个,然后它其中完成了多少个状态,会在这个字段里面做显示。 +Job 创建完成之后,我们就可以通过 kubectl get jobs 这个命令,来查看当前 job 的运行状态。得到的值里面,基本就有 Job 的名称、当前完成了多少个 Pod,进行多长时间。**AGE** 的含义是指这个 Pod 从当前时间算起,减去它当时创建的时间。这个时长主要用来告诉你 Pod 的历史、Pod 距今创建了多长时间。**DURATION** 主要来看我们 Job 里面的实际业务到底运行了多长时间,当我们的性能调优的时候,这个参数会非常的有用。**COMPLETIONS** 主要来看我们任务里面这个 Pod 一共有几个,然后它其中完成了多少个状态,会在这个字段里面做显示。 #### 查看 Pod @@ -51,7 +51,7 @@ Job 创建完成之后,我们就可以通过 kubectl get jobs 这个命令, ![avatar](assets/FhXtzQPMBAE-mI_CZZ1pNtldCOHY) -它比普通的 Pod 多了一个叫 **ownerReferences** ,这个东西来声明此 pod 是归哪个上一层 controller 来管理。可以看到这里的 ownerReferences 是归 batch/v1,也就是上一个 Job 来管理的。这里就声明了它的 controller 是谁,然后可以通过 pod 返查到它的控制器是谁,同时也能根据 Job 来查一下它下属有哪些 Pod。 +它比普通的 Pod 多了一个叫 **ownerReferences**,这个东西来声明此 pod 是归哪个上一层 controller 来管理。可以看到这里的 ownerReferences 是归 batch/v1,也就是上一个 Job 来管理的。这里就声明了它的 controller 是谁,然后可以通过 pod 返查到它的控制器是谁,同时也能根据 Job 来查一下它下属有哪些 Pod。 #### 并行运行 Job @@ -213,7 +213,7 @@ DaemonSet 也是 Kubernetes 提供的一个 default controller,它实际是做 ![avatar](assets/FgVuatnFylUD_LRK6upX1PxBbwIQ) -其实 DaemonSet 和 deployment 特别像,它也有两种更新策略:一个是 **RollingUpdate** ,另一个是 **OnDelete** 。 +其实 DaemonSet 和 deployment 特别像,它也有两种更新策略:一个是 **RollingUpdate**,另一个是 **OnDelete** 。 - RollingUpdate 其实比较好理解,就是会一个一个的更新。先更新第一个 pod,然后老的 pod 被移除,通过健康检查之后再去见第二个 pod,这样对于业务上来说会比较平滑地升级,不会中断; - OnDelete 其实也是一个很好的更新策略,就是模板更新之后,pod 不会有任何变化,需要我们手动控制。我们去删除某一个节点对应的 pod,它就会重建,不删除的话它就不会重建,这样的话对于一些我们需要手动控制的特殊需求也会有特别好的作用。 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25410\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25410\350\256\262.md" index 792348043..a1d16a695 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25410\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25410\350\256\262.md" @@ -31,8 +31,8 @@ 常见的有三种,这三种在使用时经常会遇到的: -- 第一种,在使用云存储服务的时候,经常会遇到 **region** ,也就是地区的概念,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/region 来标识。这个是为了标识单个 K8s 集群管理的跨 region 的 nodes 到底属于哪个地区; -- 第二种,比较常用的是可用区,也就是 available **zone** ,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/zone 来标识。这个是为了标识单个 K8s 集群管理的跨 zone 的 nodes 到底属于哪个可用区; +- 第一种,在使用云存储服务的时候,经常会遇到 **region**,也就是地区的概念,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/region 来标识。这个是为了标识单个 K8s 集群管理的跨 region 的 nodes 到底属于哪个地区; +- 第二种,比较常用的是可用区,也就是 available **zone**,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/zone 来标识。这个是为了标识单个 K8s 集群管理的跨 zone 的 nodes 到底属于哪个可用区; - 第三种,是 **hostname,** 就是单机维度,是拓扑域为 node 范围,在 K8s 中常通过 label kubernetes.io/hostname 来标识,这个在文章的最后讲 local pv 的时候,会再详细描述。 上面讲到的三个拓扑是比较常用的,而拓扑其实是可以自己定义的。可以定义一个字符串来表示一个拓扑域,这个 key 所对应的值其实就是拓扑域下不同的拓扑位置。 @@ -47,7 +47,7 @@ 因为在 K8s 中创建 pod 的流程和创建 PV 的流程,其实可以认为是并行进行的,这样的话,就没有办法来保证 pod 最终运行的 node 是能访问到 有位置限制的 PV 对应的存储,最终导致 pod 没法正常运行。这里来举两个经典的例子: -首先来看一下 **Local PV 的例子** ,Local PV 是将一个 node 上的本地存储封装为 PV,通过使用 PV 的方式来访问本地存储。为什么会有 Local PV 的需求呢?简单来说,刚开始使用 PV 或 PVC 体系的时候,主要是用来针对分布式存储的,分布式存储依赖于网络,如果某些业务对 I/O 的性能要求非常高,可能通过网络访问分布式存储没办法满足它的性能需求。这个时候需要使用本地存储,刨除了网络的 overhead,性能往往会比较高。但是用本地存储也是有坏处的!分布式存储可以通过多副本来保证高可用,但本地存储就需要业务自己用类似 Raft 协议来实现多副本高可用。 +首先来看一下 **Local PV 的例子**,Local PV 是将一个 node 上的本地存储封装为 PV,通过使用 PV 的方式来访问本地存储。为什么会有 Local PV 的需求呢?简单来说,刚开始使用 PV 或 PVC 体系的时候,主要是用来针对分布式存储的,分布式存储依赖于网络,如果某些业务对 I/O 的性能要求非常高,可能通过网络访问分布式存储没办法满足它的性能需求。这个时候需要使用本地存储,刨除了网络的 overhead,性能往往会比较高。但是用本地存储也是有坏处的!分布式存储可以通过多副本来保证高可用,但本地存储就需要业务自己用类似 Raft 协议来实现多副本高可用。 接下来看一下 Local PV 场景可能如果没有对PV做“访问位置”的限制会遇到什么问题? @@ -102,7 +102,7 @@ Local PV 大部分使用的时候都是通过静态创建的方式,也就是要先去声明 PV 对象,既然 Local PV 只能是本地访问,就需要在声明 PV 对象的,在 PV 对象中通过 nodeAffinity 来限制我这个 PV 只能在单 node 上访问,也就是给这个 PV 加上拓扑限制。如上图拓扑的 key 用 kubernetes.io/hostname 来做标记,也就是只能在 node1 访问。如果想用这个 PV,你的 pod 必须要调度到 node1 上。 -既然是静态创建 PV 的方式,这里为什么还需要 storageClassname 呢?前面也说了,在 Local PV 中,如果要想让它正常工作,需要用到延迟绑定特性才行,那既然是延迟绑定,当用户在写完 PVC 提交之后,即使集群中有相关的 PV 能跟它匹配,它也暂时不能做匹配,也就是说 PV controller 不能马上去做 binding,这个时候你就要通过一种手段来告诉 PV controller,什么情况下是不能立即做 binding。这里的 storageClass 就是为了起到这个副作用,我们可以看到 storageClass 里面的 provisioner 指定的是 **no-provisioner** ,其实就是相当于告诉 K8s 它不会去动态创建 PV,它主要用到 storageclass 的 VolumeBindingMode 字段,叫 WaitForFirstConsumer,可以先简单地认为它是延迟绑定。 +既然是静态创建 PV 的方式,这里为什么还需要 storageClassname 呢?前面也说了,在 Local PV 中,如果要想让它正常工作,需要用到延迟绑定特性才行,那既然是延迟绑定,当用户在写完 PVC 提交之后,即使集群中有相关的 PV 能跟它匹配,它也暂时不能做匹配,也就是说 PV controller 不能马上去做 binding,这个时候你就要通过一种手段来告诉 PV controller,什么情况下是不能立即做 binding。这里的 storageClass 就是为了起到这个副作用,我们可以看到 storageClass 里面的 provisioner 指定的是 **no-provisioner**,其实就是相当于告诉 K8s 它不会去动态创建 PV,它主要用到 storageclass 的 VolumeBindingMode 字段,叫 WaitForFirstConsumer,可以先简单地认为它是延迟绑定。 当用户开始提交 PVC 的时候,pv controller 在看到这个 pvc 的时候,它会找到相应的 storageClass,发现这个 BindingMode 是延迟绑定,它就不会做任何事情。 @@ -118,7 +118,7 @@ Local PV 大部分使用的时候都是通过静态创建的方式,也就是 首先在 storageclass 还是需要指定 BindingMode,就是 WaitForFirstConsumer,就是延迟绑定。 -其次特别重要的一个字段就是 **allowedTopologies** ,限制就在这个地方。上图中可以看到拓扑限制是可用区的级别,这里其实有两层意思: +其次特别重要的一个字段就是 **allowedTopologies**,限制就在这个地方。上图中可以看到拓扑限制是可用区的级别,这里其实有两层意思: 1. 第一层意思就是说我在动态创建 PV 的时候,创建出来的 PV 必须是在这个可用区可以访问的; 1. 第二层含义是因为声明的是延迟绑定,那调度器在发现使用它的 PVC 正好对应的是该 storageclass 的时候,调度 pod 就要选择位于该可用区的 nodes。 @@ -255,7 +255,7 @@ csi 实现存储扩展主要包含两部分: 经过这上面步骤之后,就找到了所有即满足 pod 计算资源需求又满足 pod 存储资源需求的所有 nodes。 -当 node 选出来之后, **第三个步骤** 就是调度器内部做的一个优化。这里简单过一下,就是更新经过预选和优选之后,pod 的 node 信息,以及 PV 和 PVC 在 scheduler 中做的一些 cache 信息。 +当 node 选出来之后,**第三个步骤** 就是调度器内部做的一个优化。这里简单过一下,就是更新经过预选和优选之后,pod 的 node 信息,以及 PV 和 PVC 在 scheduler 中做的一些 cache 信息。 **第四个步骤** 也是重要的一步,已经选择出来 node 的 Pod,不管其使用的 PVC 是要 binding 已经存在的 PV,还是要做动态创建 PV,这时就可以开始做。由调度器来触发,调度器它就会去更新 PVC 对象和 PV 对象里面的相关信息,然后去触发 PV controller 去做 binding 操作,或者是由 csi-provisioner 去做动态创建流程。 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25412\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25412\350\256\262.md" index bd2abc409..c483288d8 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25412\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25412\350\256\262.md" @@ -7,7 +7,7 @@ ## 背景 -监控和日志是大型分布式系统的重要 **基础设施** ,监控可以帮助开发者查看系统的运行状态,而日志可以协助问题的排查和诊断。 +监控和日志是大型分布式系统的重要 **基础设施**,监控可以帮助开发者查看系统的运行状态,而日志可以协助问题的排查和诊断。 在 Kubernetes 中,监控和日志属于生态的一部分,它并不是核心组件,因此大部分的能力依赖上层的云厂商的适配。Kubernetes 定义了介入的接口标准和规范,任何符合接口标准的组件都可以快速集成。 diff --git "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25413\350\256\262.md" "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25413\350\256\262.md" index c838c9b50..e42595b5e 100644 --- "a/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25413\350\256\262.md" +++ "b/docs/Other/CNCF X \351\230\277\351\207\214\345\267\264\345\267\264\344\272\221\345\216\237\347\224\237\346\212\200\346\234\257\345\205\254\345\274\200\350\257\276/\347\254\25413\350\256\262.md" @@ -77,7 +77,7 @@ - **Flannel** 是一个比较大一统的方案,它提供了多种的网络 backend。不同的 backend 实现了不同的拓扑,它可以覆盖多种场景; - **Calico** 主要是采用了策略路由,节点之间采用 BGP 的协议,去进行路由的同步。它的特点是功能比较丰富,尤其是对 Network Point 支持比较好,大家都知道 Calico 对底层网络的要求,一般是需要 mac 地址能够直通,不能跨二层域; - 当然也有一些社区的同学会把 Flannel 的优点和 Calico 的优点做一些集成。我们称之为嫁接型的创新项目 **Cilium** ; -- 最后讲一下 **WeaveNet** ,如果大家在使用中需要对数据做一些加密,可以选择用 WeaveNet,它的动态方案可以实现比较好的加密。 +- 最后讲一下 **WeaveNet**,如果大家在使用中需要对数据做一些加密,可以选择用 WeaveNet,它的动态方案可以实现比较好的加密。 ### Flannel 方案 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25400\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25400\350\256\262.md" index 6d8e8ae48..424952927 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25400\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25400\350\256\262.md" @@ -15,13 +15,13 @@ 诸如此类的问题,在 OKR 应用后都得到了有效解决。一位经理人曾跟我说,团队中竟然有人在用 OKR 自发做着老板没有要求的事情,感觉团队更有干劲儿了;也有团队成员找到我说,用了 OKR 感觉自己工作更加有意义,知道组织中的战略是什么,也知道自己所做的工作竟然可以支撑组织战略的实现,非常开心。 -OKR 就是有着这样的魔力,为什么呢?从本质上来说, **OKR 是一套目标管理方法论,是一个包括了从目标的制定,目标实现过程的管理,再到目标完成的效果评价的系统工程** 。这个闭环的目标管理过程几乎涵盖了工作中的方方面面,覆盖到了组织中各个层级的人。也就是说,用好 OKR 能给工作的各个环节以及各类人员都能带来不同的价值。 +OKR 就是有着这样的魔力,为什么呢?从本质上来说,**OKR 是一套目标管理方法论,是一个包括了从目标的制定,目标实现过程的管理,再到目标完成的效果评价的系统工程** 。这个闭环的目标管理过程几乎涵盖了工作中的方方面面,覆盖到了组织中各个层级的人。也就是说,用好 OKR 能给工作的各个环节以及各类人员都能带来不同的价值。 而在互联网的下半场,这把组织目标管理的神器能帮助我们解决的问题还有更多。 ### OKR 这把神器还“神”在哪些地方? -我们大部分人都是在组织中工作和成长,即使你去创业,本身也是在创立一个组织, **而一个组织所能成立的理由就是去实现目标** 。所以管理好目标,对于组织发展和在组织中成长的人来说都非常重要。 +我们大部分人都是在组织中工作和成长,即使你去创业,本身也是在创立一个组织,**而一个组织所能成立的理由就是去实现目标** 。所以管理好目标,对于组织发展和在组织中成长的人来说都非常重要。 此外,每个组织都面临内外部环境的挑战,一方面是如何灵活应对不断变化的外部经营环境,另一方面是如何建设好一个具备竞争力的内在组织。学好用好 OKR,这些问题都能迎刃而解。 @@ -59,13 +59,13 @@ OKR 持续的协同制定过程,以及团队 OKR 的通晒机制,有助于 ### 我是如何设计这门课程的? -在这个专栏中,我依托 **京东和行业上多个 OKR 实战案例(字节、百度、快手等)** ,帮你系统化理解 OKR 方法的运用,不仅有理论也有实操,不仅有案例也有提炼的方法论。 +在这个专栏中,我依托 **京东和行业上多个 OKR 实战案例(字节、百度、快手等)**,帮你系统化理解 OKR 方法的运用,不仅有理论也有实操,不仅有案例也有提炼的方法论。 **模块一,全局:OKR工作法** 。我将介绍 OKR 与其他目标管理方法的差异和联系,并从业务、经营、组织管理和人四个维度,全盘解读 OKR 火起来的真正原因,再通过老牌互联网公司百度李彦宏的 OKR 案例,帮你加深理解 OKR 与组织战略的关系,最后带你掌握组织中各个层级和个人 OKR 的生成规律。 -当你理清了 OKR 的发展背景、运用的特殊意义、与战略的映射关系,以及组织中各个层级的生成规律,也就理清了 OKR 工作法是如何在组织目标管理中发挥整体作用的。 **模块二,实操:O 和 KR 怎么写** 。实际写 OKR 时,相信你遇到过很多实操的问题,比如什么样的 O 才是有价值的?O 写了改、改了删,为什么总感觉没法一步到位写清楚呢?KR 需要怎么量化?到底写几个 O、 几个 KR 合适? +当你理清了 OKR 的发展背景、运用的特殊意义、与战略的映射关系,以及组织中各个层级的生成规律,也就理清了 OKR 工作法是如何在组织目标管理中发挥整体作用的。**模块二,实操:O 和 KR 怎么写** 。实际写 OKR 时,相信你遇到过很多实操的问题,比如什么样的 O 才是有价值的?O 写了改、改了删,为什么总感觉没法一步到位写清楚呢?KR 需要怎么量化?到底写几个 O、 几个 KR 合适? -以上这些问题,我都会进行详细说明,并给你一个万能公式,助你下笔如有神。 **模块三,落地:OKR 的流程和变革管理** 。这部分我将教会你一套建立 OKR 的流程机制,告别“OKR 根本就落不了地,用着用着就变成 KPI”的担心。 +以上这些问题,我都会进行详细说明,并给你一个万能公式,助你下笔如有神。**模块三,落地:OKR 的流程和变革管理** 。这部分我将教会你一套建立 OKR 的流程机制,告别“OKR 根本就落不了地,用着用着就变成 KPI”的担心。 我会结合京东的案例,介绍 OKR 的设定和管理流程中核心环节的有效实践,保证你学完马上就能用,还会通过对比 KPI 和 OKR 的异同,帮你彻底搞懂这两者的区别和联系。 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25402\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25402\350\256\262.md" index 0badcbb25..284407697 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25402\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25402\350\256\262.md" @@ -32,7 +32,7 @@ OKR 源于英特尔,发扬光大于谷歌,其前期发展都离不开约翰 OKR 的工作方式,建立起了基于目标从制定、过程评审,再到闭环的管理流程。通过该流程,拉动了上下级对于目标制定和实现的沟通节奏,定期跟进目标完成过程,互相探讨对于实现工作目标的期望和问题,打造了一个良好的上下级工作关系,提升管理者领导力。 -此外,OKR 鼓励个体自驱对组织目标进行思考和制定,在目标制定维度充分融入个人的想法,其实是对个体的最大激励,由 **“要我干”变成“我要干”** ,就提升了员工执行力。最后,应用 OKR 的组织,都会把 OKR 通晒在某个系统内,大家彼此都能看到,方便沟通目标对齐以便达成合力,在制定OKR时也会横向拉着多个职能部门一起协商和确认,这些都有助于组织沉淀协作、透明、诚信的文化。 +此外,OKR 鼓励个体自驱对组织目标进行思考和制定,在目标制定维度充分融入个人的想法,其实是对个体的最大激励,由 **“要我干”变成“我要干”**,就提升了员工执行力。最后,应用 OKR 的组织,都会把 OKR 通晒在某个系统内,大家彼此都能看到,方便沟通目标对齐以便达成合力,在制定OKR时也会横向拉着多个职能部门一起协商和确认,这些都有助于组织沉淀协作、透明、诚信的文化。 所以,通过 OKR 的应用,可以帮助一个组织中的员工提升执行力,管理者提升领导力,并沉淀优秀文化,使得组织能力全面提升。 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25403\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25403\350\256\262.md" index 3bbfda07c..2301e5df4 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25403\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25403\350\256\262.md" @@ -14,7 +14,7 @@ - 二是组织的特殊使命是什么? - 三是完成组织使命所需的核心能力是什么? -> **愿景:组织最终想要去往的目的地,走向的远方** ,在大环境中想要的角色和定位,就是组织的愿景。 **使命:是一个组织因为什么而存在** ,是一个组织存在的价值理由,是支撑愿景实现的价值取向。 **核心能力** :一个组织会面临具体的行业领域的细分, **细分领域所拥有的知识和技术沉淀** ,就是一个组织的核心能力,正是因为拥有了核心能力,组织履行使命才能有保障。 +> **愿景:组织最终想要去往的目的地,走向的远方**,在大环境中想要的角色和定位,就是组织的愿景。**使命:是一个组织因为什么而存在**,是一个组织存在的价值理由,是支撑愿景实现的价值取向。**核心能力** :一个组织会面临具体的行业领域的细分,**细分领域所拥有的知识和技术沉淀**,就是一个组织的核心能力,正是因为拥有了核心能力,组织履行使命才能有保障。 给你举个京东案例。 @@ -67,7 +67,7 @@ Objective 就是目标,Key Results 就是多个关键结果。结合本课时 通过李彦宏的 OKR,我帮你做以下三点分析: 1. 作为百度创始人兼 CEO,李彦宏的目标(O)就是百度的战略方向,一共包括了三个战略:打造移动生态、跑通 AI 商业模式以及提升组织能力。 -1. 李彦宏的三个目标(O)中, **不仅仅包括了移动生态和 AI 赛道的业务目标,也包括了提升组织能力维度的非业务目标。** 换句话说,组织战略方向的选择不能仅仅局限于业务方向,也需要综合考虑组织能力的方向。 +1. 李彦宏的三个目标(O)中,**不仅仅包括了移动生态和 AI 赛道的业务目标,也包括了提升组织能力维度的非业务目标。** 换句话说,组织战略方向的选择不能仅仅局限于业务方向,也需要综合考虑组织能力的方向。 1. 李彦宏的每个目标(O),都有对应的 3 个 KR,3 个 KR 就是 3 个维度的增长量化结果。也就是说,每个战略方向,都需要制定多维度的增长指标。 这就是 OKR 与组织战略目标的关系,O 是选择的战略方向,KR 是所选方向的增长量化结果。每个组织会有多个战略方向(O),每个战略方向会有多个增长指标(KR)。 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25404\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25404\350\256\262.md" index ba1045d21..0e35e7060 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25404\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25404\350\256\262.md" @@ -50,7 +50,7 @@ 我们一起回想下,在上一课时我列举的百度李彦宏的 OKR 案例中,百度的三大战略方向,不仅包括移动生态和 AI 商业模式 2 个业务类 OKR,也包括组织能力提升的非业务类 OKR,同样也是这个道理。 -所以,OKR 生成的另外一个规律,就是组织中 OKR 的类型不仅包括了业务型,也包括了非业务型。但无论是哪种类型 OKR,最终落地都要靠人去执行。那么,个人 OKR 的生成包括哪些方面呢? **个人 OKR 的生成规律** 个人 OKR 的制定,可以有上下对齐、自驱以及外部支撑三种来源。为了帮你更好地理解这个生成规律,我依旧列举个人的案例来进行说明。 **我的上级 OKR 示例** O1:产研效能提升 +所以,OKR 生成的另外一个规律,就是组织中 OKR 的类型不仅包括了业务型,也包括了非业务型。但无论是哪种类型 OKR,最终落地都要靠人去执行。那么,个人 OKR 的生成包括哪些方面呢?**个人 OKR 的生成规律** 个人 OKR 的制定,可以有上下对齐、自驱以及外部支撑三种来源。为了帮你更好地理解这个生成规律,我依旧列举个人的案例来进行说明。**我的上级 OKR 示例** O1:产研效能提升 KR1:敏捷成熟度达到85分 @@ -86,13 +86,13 @@ KR2:对京东内,以线上/线下的方式,给京东兄弟部门分享培 我的 OKR 则包含了 3 个 O,每个 O 继续拆解成了 2 个 KR。从上级和我的 OKR 中,我们可以得出以下个人 OKR 的生成结论: -1、 **个人的 OKR 部分来源于上级的 OKR 拆解** ,在这个案例中,上级 O1 中 KR1 的敏捷成熟度和 KR2 的 OKR 覆盖度对应着我的 O1 和 O2,我从上往下对齐形成的个人 O1 和 O2 继续拆解成能支撑这两个目标完成的 KR。 +1、 **个人的 OKR 部分来源于上级的 OKR 拆解**,在这个案例中,上级 O1 中 KR1 的敏捷成熟度和 KR2 的 OKR 覆盖度对应着我的 O1 和 O2,我从上往下对齐形成的个人 O1 和 O2 继续拆解成能支撑这两个目标完成的 KR。 -2、 **个人的 OKR 部分来源于个体职责的自驱** ,就像我 O3 中提到的提升敏捷影响力,上级的 O是没有该方向的,所以个人 O3 中的敏捷影响力的塑造是基于本人能力以及组织的发展,完完全全是自驱希望给组织带来的绩效价值。 +2、 **个人的 OKR 部分来源于个体职责的自驱**,就像我 O3 中提到的提升敏捷影响力,上级的 O是没有该方向的,所以个人 O3 中的敏捷影响力的塑造是基于本人能力以及组织的发展,完完全全是自驱希望给组织带来的绩效价值。 -3、 **个人的 OKR 部分来源于对外部的支持** ,在京东内部有很多部门会找我去培训敏捷和 OKR 方法,对应着我在 O3 中提及的赋能其他部门能力提升方向,这一类的 OKR 就属于外部支撑型。 +3、 **个人的 OKR 部分来源于对外部的支持**,在京东内部有很多部门会找我去培训敏捷和 OKR 方法,对应着我在 O3 中提及的赋能其他部门能力提升方向,这一类的 OKR 就属于外部支撑型。 -这就是生成个人 OKR 的规律,当工作中个人去制定 OKR 时, **需要注意从上往下的对齐拆解以及横向的支持** ,并能基于自己的能力自驱给组织创造额外的绩效价值,通过个人自驱型 OKR 的设立,就能把 OKR 鼓励挑战和自驱的理念落地。 +这就是生成个人 OKR 的规律,当工作中个人去制定 OKR 时,**需要注意从上往下的对齐拆解以及横向的支持**,并能基于自己的能力自驱给组织创造额外的绩效价值,通过个人自驱型 OKR 的设立,就能把 OKR 鼓励挑战和自驱的理念落地。 ### 总结 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25405\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25405\350\256\262.md" index d032c9b6e..9220428e1 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25405\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25405\350\256\262.md" @@ -6,9 +6,9 @@ 2019 年 6 月 18 日,一场在快手内部被称作“K3”的战役正式打响,创始人宿华和程一笑在全员内部信中表达了对公司现状的不满,并给出了“战斗”的明确目标:2020 年春节前后,3 亿 DAU。随后,快手采取了一系列的行动来去完成 K3 目标,我梳理了下,如果把快手的 K3 目标 OKR 化,可以梳理成如下所示: -> O:通过“K3”战役,在 2020 年春节前后,快手达 3 亿 DAU。 KR1:依靠 **极速版** ,春节前 DAU 峰值突破 **3 亿。** KR2:通过 **丰富垂类内容、大量签约 MCN、进行活动策划和运营、给予流量扶持** 等做法来保持留存率,保证春节前 DAU 峰值突破 **3 亿。** KR3:依靠 **春晚红包** ,春节后三个月 DAU 平均值达到 **3 亿** 。 +> O:通过“K3”战役,在 2020 年春节前后,快手达 3 亿 DAU。 KR1:依靠 **极速版**,春节前 DAU 峰值突破 **3 亿。** KR2:通过 **丰富垂类内容、大量签约 MCN、进行活动策划和运营、给予流量扶持** 等做法来保持留存率,保证春节前 DAU 峰值突破 **3 亿。** KR3:依靠 **春晚红包**,春节后三个月 DAU 平均值达到 **3 亿** 。 -这是一个典型的 OKR 写法, **通过 O 来描述我想要做什么,然后对应着 3 个具体的 KR 来支撑 O 的实现。** 我相信,看过 2020 年春晚的同学一定会知道,快手和春晚确实是通过红包的形式展开了合作,并且给快手拉新了大量的用户,这就是其中 KR3 的落地。 +这是一个典型的 OKR 写法,**通过 O 来描述我想要做什么,然后对应着 3 个具体的 KR 来支撑 O 的实现。** 我相信,看过 2020 年春晚的同学一定会知道,快手和春晚确实是通过红包的形式展开了合作,并且给快手拉新了大量的用户,这就是其中 KR3 的落地。 回到我们工作当中,O 和 KR 的写法有没有一些基本遵循的原则和规律呢?我们首先来看 O,对于目标,我们很少有人能在一开始就思考得非常清楚,总是感觉做着做着,目标才渐进明细,这其实就是 O 的基本属性——迭代属性。 @@ -42,7 +42,7 @@ #### 2. 本季度切实可行 -从 OKR 的制定节奏上来说,国内大部分包括京东在内的组织都是按照季度来制定的,所以我们每季度中所写的 O 要能在本季度可执行,不能执行的就不要写。比如,公司定了一个年度销售1个亿的挑战目标,我们在定第一季度 OKR 时,就要对这一个亿进行拆解,写一个 Q1 能完成的目标放到 OKR 中,而不是把一个亿直接作为 Q1 的 OKR。也就是说,在定季度 OKR 时,O 一定是切实可行的, **本季度努力一把是够得着、达得到的** ,而不是制定根本就完成不了的“虚荣目标”。 +从 OKR 的制定节奏上来说,国内大部分包括京东在内的组织都是按照季度来制定的,所以我们每季度中所写的 O 要能在本季度可执行,不能执行的就不要写。比如,公司定了一个年度销售1个亿的挑战目标,我们在定第一季度 OKR 时,就要对这一个亿进行拆解,写一个 Q1 能完成的目标放到 OKR 中,而不是把一个亿直接作为 Q1 的 OKR。也就是说,在定季度 OKR 时,O 一定是切实可行的,**本季度努力一把是够得着、达得到的**,而不是制定根本就完成不了的“虚荣目标”。 #### 3. 聚焦性 @@ -52,7 +52,7 @@ #### 4. 融入自驱&挑战理念 -OKR 带给组织的其中一个价值,就是希望能激发组织中个体的活力,让每个人都能为组织创造价值,贡献力量。所以我们在写 O 时,就要能落地 OKR 的这种理念,也就是说,在 O 中倡导鼓励包含自驱&挑战的方向, **不能说老板让我做什么我才做什么,只完成老板布置的任务,要能自发着去挑战其他一些额外的对组织有价值有突破的工作。** +OKR 带给组织的其中一个价值,就是希望能激发组织中个体的活力,让每个人都能为组织创造价值,贡献力量。所以我们在写 O 时,就要能落地 OKR 的这种理念,也就是说,在 O 中倡导鼓励包含自驱&挑战的方向,**不能说老板让我做什么我才做什么,只完成老板布置的任务,要能自发着去挑战其他一些额外的对组织有价值有突破的工作。** 比如,在我们部门制定 2020 年 Q3 OKR 目标时,测试团队的某个人的 OKR 中不仅定了上级所要求的既定测试任务的 O,还自驱制定了“精准测试”这一测试专业化能力提升的 O。当把 OKR 所倡导的自驱&挑战理念落地,你会发现整个组织中的所有个体都被盘活了。 @@ -99,13 +99,13 @@ O 遵循了这四个原则,就确保了我们在组织中制定 OKR 的有效 3. **效率型:** 比如案例 2 中提到的开发效率。 4. **能力提升型:** 比如案例 1 中提到的提升业务能力以及案例 2 中提到的提升员工能力。 -这 4 个类型,其实就是组织绩效的构成。 **一个商业组织,不仅仅要能营收,也要关注用户价值,不仅仅要内部效率,也要关注能力的沉淀和提升** 。而这 4 个类型的选择也对应了我开篇提到的,战略的选择要包括业务型以及非业务型的方向。业务型的方向就是营收、用户价值和业务能力沉淀的类型。非业务型的方向就是效率和组织中人员能力提升的类型。 +这 4 个类型,其实就是组织绩效的构成。**一个商业组织,不仅仅要能营收,也要关注用户价值,不仅仅要内部效率,也要关注能力的沉淀和提升** 。而这 4 个类型的选择也对应了我开篇提到的,战略的选择要包括业务型以及非业务型的方向。业务型的方向就是营收、用户价值和业务能力沉淀的类型。非业务型的方向就是效率和组织中人员能力提升的类型。 如果我们要从这 4 种 O 的类型中进行优先级排序,显而易见,对于商业组织而言,营收是第一位的。如果没有营收,那么一个组织就很难长期存活,从而就没有足够的资金投入来持续提升用户体验,满足用户更多需求,更没有资金投到持续效率提升和能力沉淀方面。 排在第二位的是用户型的 O,一个商业组织能营收是因为持续解决了用户的问题,给用户带来了持续的价值和好的体验,从而用户才愿意持续为组织提供的产品&服务付费,这些会反应在用户满意度、用户量等方面。 -最后,在有了营收和能持续为用户提供价值的基础上,组织需要不断关注效率和组织能力的提升,成本效率决定了组织的市场竞争力,而能力的提升则有助于效率提升。但是,能力的沉淀和培养是长期的,朝夕间并不能看到效果。那么我们在选择 O 时,如果立马能给组织带来效率提高,优先级一定高于长期的能力培养。 **所以,当你在写 O 时,选择从营收、用户、效率和能力提升 4 个类型入手,一定对组织的经营和发展有价值。如果在 4 个类型中,再做出取舍,我建议你 O 选择的优先级是:营收型>用户型>效率型>能力提升型。** +最后,在有了营收和能持续为用户提供价值的基础上,组织需要不断关注效率和组织能力的提升,成本效率决定了组织的市场竞争力,而能力的提升则有助于效率提升。但是,能力的沉淀和培养是长期的,朝夕间并不能看到效果。那么我们在选择 O 时,如果立马能给组织带来效率提高,优先级一定高于长期的能力培养。**所以,当你在写 O 时,选择从营收、用户、效率和能力提升 4 个类型入手,一定对组织的经营和发展有价值。如果在 4 个类型中,再做出取舍,我建议你 O 选择的优先级是:营收型>用户型>效率型>能力提升型。** #### 结语 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25406\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25406\350\256\262.md" index cb385adcb..7776e7836 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25406\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25406\350\256\262.md" @@ -20,7 +20,7 @@ 比如,从微软发布的 2019 年第四财季财报上我们可以知道,其生产率和业务流程业务收入增长至 110 亿美元,智能云业务收入增长至 114 亿美元,个人计算业务收入增长达到 113 亿美元。这些增长结果,如果用 OKR 来管理微软的战略目标,就需要体现在 KR 中,这就是 KR 最重要的增长属性。 -KR 的增长属性对于组织发展毫无疑问是重要的, **一个组织失去了增长性,就代表着财务、用户的增长停滞,内部效率和组织能力的提升停滞** ,如此在竞争中就会失去优势,渐渐被市场淘汰。 +KR 的增长属性对于组织发展毫无疑问是重要的,**一个组织失去了增长性,就代表着财务、用户的增长停滞,内部效率和组织能力的提升停滞**,如此在竞争中就会失去优势,渐渐被市场淘汰。 所以,我们在写 KR 时,需要牢记 KR 要能回答组织增长,这个增长属性不能丢。此外,为了让所写 KR 有效,我们还需要遵循 4 个原则。 @@ -50,7 +50,7 @@ KR 的量化不仅包括了过程量化,也包括了关键结果的量化。 第二个原则就是时限性。KR 中需要有时限来对产出进行约束,在上述这 3 个案例中,时限就是指“每两周”“Q2”“春节前”。结合目标 O 制定的节奏,KR 的时限会以季度为基本的最大限制周期。 -也就是说,每个季度,我们在制定 OKR 时,KR 会有按周、按月或是按具体时间节点的阶段性完成时限,但最长的完成周期不要超过一个季度,超过一个季度的待完成项我们可以写到下一个季度的 OKR 中。 **KR 需要遵守时限原则的意义是为了效率。** 完成目标的具体实现过程有了时间的约束就会带来压力,有压力才有动力,动力就会带来效率的提高。 +也就是说,每个季度,我们在制定 OKR 时,KR 会有按周、按月或是按具体时间节点的阶段性完成时限,但最长的完成周期不要超过一个季度,超过一个季度的待完成项我们可以写到下一个季度的 OKR 中。**KR 需要遵守时限原则的意义是为了效率。** 完成目标的具体实现过程有了时间的约束就会带来压力,有压力才有动力,动力就会带来效率的提高。 试想我们做任何事情都没有完成的时间概念,那将会一事无成,就像阿里内部流传的那句话:“没有结果的过程就是放屁”,这是任何组织和团队都不想看到的。 @@ -78,7 +78,7 @@ O:主流AI赛道模式跑通,实现可持续增长 #### (4)具备挑战性 -KR 的挑战性,在上述的这 3 个案例中可以体现在“UV 300”“拉新 1000 万”“DAU 突破 3 亿”的数值上。 **那到底定什么样的数值才具备挑战呢?我们可以从两个方面进行把控,一个是跟行业比,一个是跟自己比。** +KR 的挑战性,在上述的这 3 个案例中可以体现在“UV 300”“拉新 1000 万”“DAU 突破 3 亿”的数值上。**那到底定什么样的数值才具备挑战呢?我们可以从两个方面进行把控,一个是跟行业比,一个是跟自己比。** 我拿快手的案例来说明,在 2018 年年中,抖音的日活用户超过了快手,快手的增长一度陷入停滞,甚至出现了负增长。后来在竞品抖音 2019 年年中 DAU 突破 3.2 亿的大背景下,快手在 2019 年 6 月 18 日下定决心要告别“慢公司”, 这才有了 3 亿 DAU 的 KR 制定。而 3 亿DAU 的关键结果的数值,也建立在快手截至当年 5 月 DAU 达到 2.5 亿的背景下。虽然 3 亿 DAU 与抖音还有些差距,但对于快手自身而言已经算是一个挑战和突破型的关键结果 。 @@ -111,9 +111,9 @@ KR 遵循了以上四个原则,确保了我们在组织中制定 KR 的有效 通过这个结构,就可以满足我上述所说 KR 需要遵守原则中的过程和结果量化以及时限性。如果在量化的数值中,团队或者上下级讨论共识了所写量化效果的数值确实具有挑战,那么就确保了所写 KR 的有效。 -这个万能公式暂时满足了我们写 KR 的基本要求,但是从内容上缺少了对于组织经营的关注,这个关注点是什么呢?在 2019 年开年,京东零售 CEO 徐雷提出一条经营理念“以信赖为基础,以客户为中心的价值创造”, **这句话的核心是在说明组织中经营的方方面面要能给客户解决问题,创造价值。** 2019 年 11 月 11 日,腾讯发布了文化 3.0,在其使命愿景中说到“用户为本,科技向善,一切以用户价值为归依”,这句话的核心又是在说明组织在制定的目标中要能给用户解决问题,提供用户价值。 +这个万能公式暂时满足了我们写 KR 的基本要求,但是从内容上缺少了对于组织经营的关注,这个关注点是什么呢?在 2019 年开年,京东零售 CEO 徐雷提出一条经营理念“以信赖为基础,以客户为中心的价值创造”,**这句话的核心是在说明组织中经营的方方面面要能给客户解决问题,创造价值。** 2019 年 11 月 11 日,腾讯发布了文化 3.0,在其使命愿景中说到“用户为本,科技向善,一切以用户价值为归依”,这句话的核心又是在说明组织在制定的目标中要能给用户解决问题,提供用户价值。 -把京东经营理念和腾讯文化的使命愿景结合在一起,我们需要了解组织经营目标的制定中要能蕴含给用户/客户创造的价值点。用再简单的话来说, **目标的制定,必须首先考虑给用户/客户带来什么价值,解决什么问题,这是企业经营能立足的根本。** 所以,结合组织经营目标需要给用户/客户带来价值,解决问题的理念,我把 KR 的万能公式进行了升级: +把京东经营理念和腾讯文化的使命愿景结合在一起,我们需要了解组织经营目标的制定中要能蕴含给用户/客户创造的价值点。用再简单的话来说,**目标的制定,必须首先考虑给用户/客户带来什么价值,解决什么问题,这是企业经营能立足的根本。** 所以,结合组织经营目标需要给用户/客户带来价值,解决问题的理念,我把 KR 的万能公式进行了升级: > 通过XXX方法, 解决用户/客户XXX问题, 在XXX时间点, 达到XXX成效。 @@ -125,7 +125,7 @@ KR 遵循了以上四个原则,确保了我们在组织中制定 KR 的有效 但是,非业务型的 KR,比如提升组织效率以及提升组织中人能力的 KR,会离业务较远,属于支撑业务发展的工作,所以在非业务型的 KR 中无法直接说明所提供的用户/客户价值。 -但我们需要明确的是, **提升效率是因为组织效率一定面临了问题,需要提升人员能力也是因为能力出现跟不上的问题** ,所以当我们在写非业务型 KR 时,需要把解决了什么组织问题纳入其中。因此,对于上述的 KR 万能公式,我们进行简单调整就能适用于组织中的所有人: +但我们需要明确的是,**提升效率是因为组织效率一定面临了问题,需要提升人员能力也是因为能力出现跟不上的问题**,所以当我们在写非业务型 KR 时,需要把解决了什么组织问题纳入其中。因此,对于上述的 KR 万能公式,我们进行简单调整就能适用于组织中的所有人: > 通过XXX方法, 解决(用户/客户)XXX问题, 在XXX时间点, 达到XXX成效。 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25407\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25407\350\256\262.md" index 3d99eec70..aae54d8ba 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25407\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25407\350\256\262.md" @@ -14,7 +14,7 @@ **点评** :在百度对外公布的 OKR 案例中,由于商业数据的敏感性,并没有把相关量化的数据结果透明出来。但我相信心细的小伙伴还是能发现百度老板 OKR 的其他 **问题** 。比如: -- O1 的 KR3 根本就 **没有量化** ,而是写了一个范范的描述, **到底创新什么,怎么创新都不清楚;** - O2 的 KR1 没有写具体的实现路径,但如果看过 2020 年《奔跑吧 第四季》的小伙伴应该知道百度“小度”在节目里投了广告,还请沙溢做了代言,所以这个 KR 写成“小度通过邀请沙溢代言并与《奔跑吧》节目的合作,提升用户对小度的认知和使用率,达到日交互次数超过\*亿”就是一个好的 KR 呈现。 +- O1 的 KR3 根本就 **没有量化**,而是写了一个范范的描述,**到底创新什么,怎么创新都不清楚;** - O2 的 KR1 没有写具体的实现路径,但如果看过 2020 年《奔跑吧 第四季》的小伙伴应该知道百度“小度”在节目里投了广告,还请沙溢做了代言,所以这个 KR 写成“小度通过邀请沙溢代言并与《奔跑吧》节目的合作,提升用户对小度的认知和使用率,达到日交互次数超过\*亿”就是一个好的 KR 呈现。 在这个案例中,除了整体上大部分 KR 都缺少量化实现目标的过程路径之外,也有写得比较 **好的 O 和 KR** 。比如: @@ -27,13 +27,13 @@ **点评** :快手 K3 战役的 OKR 化,从目标 O 的制定上,充满了挑战,不仅是行业中抖音带来的竞争压力迫使快手需要加速争夺市场的脚步,同时 DAU3 亿的增长目标也是对当时快手 DAU 达到 2.5 亿的突破。 -此外,对于整个 K3 战役,快手有着明确完成 DAU3 亿的实现路径和时限,在 KR 中就需要把这些量化的关键要素体现出来。 **这样的战略级 OKR 在从上往下对齐的时候,会让人一目了然,减少理解歧义和沟通成本,团队和组织形成合力的速度才更快** ,有了“春节前后”的时限,完成目标的效率也能更有保证。 +此外,对于整个 K3 战役,快手有着明确完成 DAU3 亿的实现路径和时限,在 KR 中就需要把这些量化的关键要素体现出来。**这样的战略级 OKR 在从上往下对齐的时候,会让人一目了然,减少理解歧义和沟通成本,团队和组织形成合力的速度才更快**,有了“春节前后”的时限,完成目标的效率也能更有保证。 所以,我给你所举的快手 K3 战役 OKR 化的案例,是典型写得好的 OKR。但是,如果参照我教给你的写好 KR 万能公式的最终形态,好像在快手 KR 中还缺了对于用户维度的关注。 -在这一点,一位快手高管曾向记者表示:“快手在战略上最值得反思的不是数据,不是打法,而是战略目标的设立。K3 战役的目标,更多只是公司视角的目标,不是用户视角的目标。 **未来快手在战略思考时,会更加回归初心,思考用户真正需要什么** ”,也就是说,快手在制定 K3 目标中, **缺少了对用户问题的洞察和解决** ,在 K3 战役的 OKR 化中缺少的也正是这部分内容。 +在这一点,一位快手高管曾向记者表示:“快手在战略上最值得反思的不是数据,不是打法,而是战略目标的设立。K3 战役的目标,更多只是公司视角的目标,不是用户视角的目标。**未来快手在战略思考时,会更加回归初心,思考用户真正需要什么** ”,也就是说,快手在制定 K3 目标中,**缺少了对用户问题的洞察和解决**,在 K3 战役的 OKR 化中缺少的也正是这部分内容。 -从这个案例,我想再次提醒你, **对于偏向业务型目标的 OKR 制定,必须要回归用户/客户视角** ,在 KR 中,得把到底解决了用户/客户的问题思考清楚,说得明白,才是对组织经营有价值的目标制定。 +从这个案例,我想再次提醒你,**对于偏向业务型目标的 OKR 制定,必须要回归用户/客户视角**,在 KR 中,得把到底解决了用户/客户的问题思考清楚,说得明白,才是对组织经营有价值的目标制定。 解读完几个业务型 OKR,我再带你来看看技术型 OKR 的案例。 @@ -43,17 +43,17 @@ (说明:该前端研发负责人的 OKR 是 Q2 制定的,所以在时限上,整个 OKR 是在 Q2 完成。) **点评** :首先,我带你来看该前端开发团队的 O 的制定,这里有“能力”“效率”“用户体验”三个关键字眼,对应了我先前讲过的 O 需要围绕营收、用户、效率和能力这四种类型来制定,这样对于组织才有价值,所以该 O 是一个典型的以价值导向 O 的写法。 -接下来,在所有的 KR 制定中,不仅把具体的实现路径明确了, **更重要的是每个 KR 我们从内容上就能看出都是想要解决用户的问题,从而来提升用户体验** 。这个 KR 案例对于技术研发的 OKR 制定非常具有参考性,研发同学平时可能过于关注功能开发和上线,长期就会导致缺乏经营意识,也会让研发自我感觉沦为了工具般存在。 +接下来,在所有的 KR 制定中,不仅把具体的实现路径明确了,**更重要的是每个 KR 我们从内容上就能看出都是想要解决用户的问题,从而来提升用户体验** 。这个 KR 案例对于技术研发的 OKR 制定非常具有参考性,研发同学平时可能过于关注功能开发和上线,长期就会导致缺乏经营意识,也会让研发自我感觉沦为了工具般存在。 而组织中方方面面的工作都需要紧紧围绕经营的用户/客户价值来做才有意义,这样组织才能立足,人也能感知在创造价值,所以在研发同学的 OKR 制定中,不仅仅要有功能上线的说明,也要能回到用户/客户视角,结合我给你的“万能公式”,把解决的组织经营问题写明才行。 -该 OKR 制定的另外一个优点,就是在 KR3 的具体实现方法中,采用了新的 PWA 前端技术, **挑战型的 KR 制定就是要回到行业上来看,把行业中的新技术、新方法能够引入到组织工作当中,这样才能为组织创造突破的绩效结果。** 当然,KR3 的不足点是没能把具体解决的用户问题说清,同时和 KR2 一样,缺乏了通过什么量化的数值结果来度量用户体验的提升,KR2 和 KR3 量化的用户体验结果,可以仿照 KR1 的满意度提升来进行改善设计。 +该 OKR 制定的另外一个优点,就是在 KR3 的具体实现方法中,采用了新的 PWA 前端技术,**挑战型的 KR 制定就是要回到行业上来看,把行业中的新技术、新方法能够引入到组织工作当中,这样才能为组织创造突破的绩效结果。** 当然,KR3 的不足点是没能把具体解决的用户问题说清,同时和 KR2 一样,缺乏了通过什么量化的数值结果来度量用户体验的提升,KR2 和 KR3 量化的用户体验结果,可以仿照 KR1 的满意度提升来进行改善设计。 那么,在我们看完了研发类的 OKR 之后,我们再来看一个探索型项目的 OKR 案例。 ### 案例 4:京东某商品管理项目 OKR -> _ **O:打造行业领先的商品运营阵地,为商家降本增效** _ _KR1: **8 月份中旬** ,通过对行业调研及竞品分析、商家和事业部调研,产出调研报告和结论,精准识别商家商品发布的痛点问题;KR2: **8 月底** ,通过统计商品发布页商家使用时长,用数据来验证识别的商家商品发布痛点问题的假设合理性;KR3: **9.30 前** ,圈定商品发布改造的试点商家,通过试点类目的智能商品发布流程简化版MVP上线,灰度覆盖至少个三级类目,保证商品信息自动填充率达%以上;KR4: **在 10.31 前** ,完成个三级类目的智能简化版商品发布的扩充和应用,确保商品信息自动填充率均达_\*%以上。\* +> _ **O:打造行业领先的商品运营阵地,为商家降本增效** _ _KR1: **8 月份中旬**,通过对行业调研及竞品分析、商家和事业部调研,产出调研报告和结论,精准识别商家商品发布的痛点问题;KR2: **8 月底**,通过统计商品发布页商家使用时长,用数据来验证识别的商家商品发布痛点问题的假设合理性;KR3: **9.30 前**,圈定商品发布改造的试点商家,通过试点类目的智能商品发布流程简化版MVP上线,灰度覆盖至少个三级类目,保证商品信息自动填充率达%以上;KR4: **在 10.31 前**,完成个三级类目的智能简化版商品发布的扩充和应用,确保商品信息自动填充率均达_\*%以上。\* (说明:为了项目脱敏,我已将关键数据和方法进行了模糊处理,学习该案例,更多要关注 OKR 如何结合探索型项目进行应用。) @@ -65,7 +65,7 @@ 且从 KR1 的制定内容上,我们就能知道该项目是通过和行业竞品对标展开,在整个 KR 的实现过程中,团队几乎是以内部创业的玩法,在进行探索式的尝试和验证,所以完成整个 OKR 都充满了挑战性。 -最后想特殊说明的是,在时限上,项目型 OKR 没有完全遵循季度 OKR 的制定规则,也就是没按照季度的周期来进行 OKR 的拆解, **为了项目管理的连贯和持续性** ,以项目的起止时间来展示一个完整的 OKR 会更加合理,比如这个项目就是横跨了 8~10 月,跨了不同季度。 +最后想特殊说明的是,在时限上,项目型 OKR 没有完全遵循季度 OKR 的制定规则,也就是没按照季度的周期来进行 OKR 的拆解,**为了项目管理的连贯和持续性**,以项目的起止时间来展示一个完整的 OKR 会更加合理,比如这个项目就是横跨了 8~10 月,跨了不同季度。 我和你分享的这 4 个案例,都特别具有参考性,包括了 **创始人的 OKR、公司战略型 OKR、研发负责人型 OKR 和项目型 OKR** 。我在具体的点评过程中,都参照了上两讲 O 和 KR 写法的知识点,对这 4 个 OKR 案例的优缺点进行了说明,解读完整的案例会让你对如何写好 OKR 有更加全面、深刻的理解,避免你在实际应用中踩坑。 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25408\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25408\350\256\262.md" index b34ee54e1..a845c6bc6 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25408\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25408\350\256\262.md" @@ -37,9 +37,9 @@ 为了方便你理解,我贴出了当时我们现场的一张照片。可以看到,在工作坊时,我们把京东零售的战略意图明确写出并贴在了看板上,对应着就是图中我标识的“ **战略方向** ”,这就是对于方向 O 制定层面上的输入。在集团战略方向的指引下,让团队成员先自己产出对业务条线未来想要做的方向 O,这样就保证了团队中产出 O 的方向是聚焦的,是能支撑战略的。 -在团队成员各自产出了 O 后,我们会对每个 O 进行澄清,把属于表达同一个意思的 O 放在一起,也就是我图中的“ **归类区** ”。在这个案例中,我们当时一共归类了四个团队业务方向。有了四个业务方向的归类后, **继续在每个方向上进行概念化的提炼** ,提炼的过程就是对团队未来业务方向 O 共识的过程。 +在团队成员各自产出了 O 后,我们会对每个 O 进行澄清,把属于表达同一个意思的 O 放在一起,也就是我图中的“ **归类区** ”。在这个案例中,我们当时一共归类了四个团队业务方向。有了四个业务方向的归类后,**继续在每个方向上进行概念化的提炼**,提炼的过程就是对团队未来业务方向 O 共识的过程。 -有了对于 O 的共识后,接下来就是产出每个 O 的 KR,在 KR 这个部分,需要不断地沟通和确认。这个团队后续在对于 KR 的共识上开了近 3 次的正式沟通会,非正式的沟通次数则更多, **直到团队和上下级都对最终 OKR 共识了为止** 。 +有了对于 O 的共识后,接下来就是产出每个 O 的 KR,在 KR 这个部分,需要不断地沟通和确认。这个团队后续在对于 KR 的共识上开了近 3 次的正式沟通会,非正式的沟通次数则更多,**直到团队和上下级都对最终 OKR 共识了为止** 。 在帮助该业务条线团队制定 OKR 的过程中,我们积累了很多宝贵的经验教训,在这也一同分享给你,期望你在制定 OKR 时能少踩坑,避免这些问题。 @@ -52,7 +52,7 @@ #### 2. OKR 的过程检视&调整机制 -我们通常是 **建立以日、周、季中三个维度来针对目标完成过程的检视和调整机制** ,从而把控问题和风险,应对目标变化。 **日:每日站会** 每日站会的意义在于跟进从 KR 拆解出来的工作项的进度、问题和风险。 +我们通常是 **建立以日、周、季中三个维度来针对目标完成过程的检视和调整机制**,从而把控问题和风险,应对目标变化。**日:每日站会** 每日站会的意义在于跟进从 KR 拆解出来的工作项的进度、问题和风险。 在京东内部,团队会把工作项任务写在便签纸上并贴在物理看板透明出来,物理看板上会有三个命名为 **to do | doing | done** 纵向泳道(下图),然后站会时团队会围绕着该物理看板,并针对每个工作任务基于以下提问展开每日站会。 @@ -62,7 +62,7 @@ 每日站会时,团队成员需要在看板上及时更新工作项的状态。比如处于 doing 列的工作项如果做完了,就会移动到 done 列,然后会把 to do 列的高优先级的工作项再移至 doing 列。 -此外,我们更需要关注暴露的问题,并针对每个问题圈定负责人,跟进问题的解决情况。这样,每天一个闭环,不仅有序地推进了 OKR 完成的进度,也能及时同步和解决问题。 **周:周会/周报** +此外,我们更需要关注暴露的问题,并针对每个问题圈定负责人,跟进问题的解决情况。这样,每天一个闭环,不仅有序地推进了 OKR 完成的进度,也能及时同步和解决问题。**周:周会/周报** 周会/周报的意义在于跟进既定 O 和 KR 的进度和风险,并管理目标的变化情况,看目标是否需要调整,是否有新出现的目标。 @@ -74,7 +74,7 @@ - **说明每个 KR 以周为单位,具体拆分出来的工作任务项完成情况。** 这里就对应了上述"每日站会"的内容,可以包括本周完成的工作项,如我周报里的 1)和 2),以及下周待做的工作任务,如我这里的 3),每天的工作紧紧围绕完成 KR 展开,就有力确保了 KR 的实现落地。 - **跟进变化和新增的 OKR。** 在周报里,我还会呈现不在原有计划 OKR 中的工作内容,一旦出现这样的情况,就需要跟团队、该方向的依赖方以及上下级去沟通达成共识。共识后,就更新调整现有的 OKR 内容或是新增 OKR,然后就把这些 OKR 再次作为既定的 OKR 来进行管理和跟进。 -我们可以看出,基于 OKR 的工作周报呈现三级对应关系,即多个工作任务支撑某个 KR 的实现,多个 KR 支撑某个 O 的实现,这三个层级组成了我们每周的具体工作内容。其中,KR 的完成进度百分比和信心指数是两个非常好用的实践,相当于是进度和风险的量化。最后,在周报中关注 OKR 的变化情况,就是对经营环境变化的及时响应。这样,每周一个闭环,有序推进了所有 OKR 完成进度,并体现了在日常工作中对变化的管理。 **季中:整体 OKR 盘点** 季中 OKR 盘点的意义就是对个人、团队和部门的 OKR 进行回顾并把控整体进度和风险。在季中这个节点, **需要组织中的管理者能与团队再次确认和共识目标** ,这样对于最终季末产出的实际绩效结果,才更有保障。 +我们可以看出,基于 OKR 的工作周报呈现三级对应关系,即多个工作任务支撑某个 KR 的实现,多个 KR 支撑某个 O 的实现,这三个层级组成了我们每周的具体工作内容。其中,KR 的完成进度百分比和信心指数是两个非常好用的实践,相当于是进度和风险的量化。最后,在周报中关注 OKR 的变化情况,就是对经营环境变化的及时响应。这样,每周一个闭环,有序推进了所有 OKR 完成进度,并体现了在日常工作中对变化的管理。**季中:整体 OKR 盘点** 季中 OKR 盘点的意义就是对个人、团队和部门的 OKR 进行回顾并把控整体进度和风险。在季中这个节点,**需要组织中的管理者能与团队再次确认和共识目标**,这样对于最终季末产出的实际绩效结果,才更有保障。 在京东内部,由于组织结构和部门管理幅度很大的原因,我带领部门进行 OKR 季中盘点时,分为了两个层级,一个是部门管理者与经理层维度,一个是经理层与一线团队维度。 @@ -115,7 +115,7 @@ > 比如某个 O 包含 3 个 KR,KR1、KR2 和 KR3 的优先级权重分别是 5、3、2,共有 5 个人进行了所有 KR 的评分,那么 O 得分的数值计算公式为:(0.5_K1 的评价分数总和+0.3_KR2 的得分总和+0.2\*KR3 的得分总和)/5,而每个 O 的最终得分范围就在 0~1 分之间。 -还有一点需要注意, **在实际的 OKR 评分过程中,我们不能仅按照完成进度来打分高低。** +还有一点需要注意,**在实际的 OKR 评分过程中,我们不能仅按照完成进度来打分高低。** > 比如某个人的 OKR 内容毫无挑战性但是最终完成进度是 100%,那么我们在评价的时候可能给予是 0.3 分,也就是做得一般;而某个员工写的 OKR 很有挑战,完成率 70%,那么评价的分数反而是 0.7 分,因为在挑战的情况下能做到 70% 的进度已经很优秀了。如果这个员工的 OKR 很有挑战,完成率又是 100%,那么就可以评 1 分,远超预期。 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25409\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25409\350\256\262.md" index a60d3d52c..b0fe44bf7 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25409\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25409\350\256\262.md" @@ -12,9 +12,9 @@ > OKR:Objectives and Key Results,目标和关键成果 KPI:Key Performance Indicator,关键绩效指标 -**KPI 从概念上就给了我们错误的引导** ,以至于让很多使用 KPI 的人 **误认为只要把这个“关键绩效指标”拿出来,目标管理就完成了** 。长此以往,就出现了“唯 KPI 论”和“交数文化”,只看数字,只交付数字,严重忽略了过程。 +**KPI 从概念上就给了我们错误的引导**,以至于让很多使用 KPI 的人 **误认为只要把这个“关键绩效指标”拿出来,目标管理就完成了** 。长此以往,就出现了“唯 KPI 论”和“交数文化”,只看数字,只交付数字,严重忽略了过程。 -而 OKR 从概念上,首先就把“目标”拎出来作为了我们需要关注的第一维度。 **先有方向,继而关注该方向所期望获得的“关键成果”** ,这至少能让我们清晰地知道所从事工作的上下文,了解产出和目标的对应关系。而且,通过上一课时的学习,相信你已经知道基于 OKR 的目标管理是一个系统的跟踪和管理过程。 +而 OKR 从概念上,首先就把“目标”拎出来作为了我们需要关注的第一维度。**先有方向,继而关注该方向所期望获得的“关键成果”**,这至少能让我们清晰地知道所从事工作的上下文,了解产出和目标的对应关系。而且,通过上一课时的学习,相信你已经知道基于 OKR 的目标管理是一个系统的跟踪和管理过程。 因此,由概念出发,OKR 和 KPI 在展现形式上也就有了差异,这个差异是什么呢? @@ -28,13 +28,13 @@ OKR 展现形式 -KPI 展现形式 **O:让孩子快乐、健康的成长到10 岁** **KR1:** 今年陪伴孩子至少出去旅游一次,让孩子了解大千世界。 **KR2:** 今年让孩子掌握游泳体育运动,并和宝宝每个月能一起去游泳馆游一次。 **KR3:** 今年鼓励宝宝读 3 本关于名人传记的书,开拓宝宝的思维和认知。 +KPI 展现形式 **O:让孩子快乐、健康的成长到10 岁** **KR1:** 今年陪伴孩子至少出去旅游一次,让孩子了解大千世界。**KR2:** 今年让孩子掌握游泳体育运动,并和宝宝每个月能一起去游泳馆游一次。**KR3:** 今年鼓励宝宝读 3 本关于名人传记的书,开拓宝宝的思维和认知。 孩子长到 **10岁** > 我使用生活中的例子,是因为我们每个人都是从小孩子长起来的。孩子需要有陪伴过程,尤其在开始形成价值观和世界观的时候,通过父母的陪伴孩子才能真正身心健康地成长。 -通过对于该目标的 KPI 化,可以发现 **KPI 的展现形式太过偏向于数字结果** ,只要孩子能长到“关键绩效指标10岁”就行,无所谓怎么去长。 +通过对于该目标的 KPI 化,可以发现 **KPI 的展现形式太过偏向于数字结果**,只要孩子能长到“关键绩效指标10岁”就行,无所谓怎么去长。 -而该目标的 **OKR 展现形式既有过程也有结果** ,孩子的成长过程错过了真的一辈子就错过了,没有再来的机会,在过程中需要通过各种活动的陪伴负起对孩子成长的责任。 +而该目标的 **OKR 展现形式既有过程也有结果**,孩子的成长过程错过了真的一辈子就错过了,没有再来的机会,在过程中需要通过各种活动的陪伴负起对孩子成长的责任。 同理,用只看数字的 KPI 来管理组织中的目标和绩效结果,就会造成像阿里内部流传的那句话“没有过程的结果是垃圾”。所以目标管理 **一定是一个工作过程,不能只看结果数字。** 在 OKR 应用的展示形式中,KR 的过程量化就要求我们不断对实现目标的具体路径和方式进行讨论和共识,并要能把控按路径实现过程的进度、风险和变化情况,这样才能产出高质量的实际绩效结果。 @@ -44,27 +44,27 @@ KPI 展现形式 **O:让孩子快乐、健康的成长到10 岁** **KR1:** KPI 出现的 90 年代,国内改革开放才 10 多年,大部分企业正是改革开放以后才建立起来的,那个时候的企业经营环境任何行业都是供小于求,蓝海一片。只要你能做出个不差的产品,或者能提供一个较好的服务,都会有人要,也都会有人买。我还记得小时候家里安装固定电话,那是求着移动专员要名额,否则都没有安装的机会。 -在这里,我想说明的是, **在那个产品稀缺的时代,组织绩效的获得是由组织自己决定和可掌控的** 。这里我还是用我家安装电话这个例子帮你理解,比如固话公司打算在我家区域安装 10 个电话,那么公司就期望在该区域有 10 个电话的营收绩效,因为产品稀缺,大家都抢着要,这 10 个电话名额一定都会卖出去。 +在这里,我想说明的是,**在那个产品稀缺的时代,组织绩效的获得是由组织自己决定和可掌控的** 。这里我还是用我家安装电话这个例子帮你理解,比如固话公司打算在我家区域安装 10 个电话,那么公司就期望在该区域有 10 个电话的营收绩效,因为产品稀缺,大家都抢着要,这 10 个电话名额一定都会卖出去。 此时,用 KPI 来管理组织绩效:某区域装 10 个固话,直截了当,简单可实现。组织还因此能持续不断地获得成功,长此以往,就形成了 KPI 式的经营固化思维。 -然而,当把时间线拨回当下,我们去满足个人工作、生活和学习的需求时可以有大量的产品&服务供我们选择,产品稀缺的时代结束了。此时, **对于产品的选择权正在从企业转为用户,由用户来决定用不用你的产品,组织绩效便开始由外部决定,而用户的需求是多元易变的** 。 +然而,当把时间线拨回当下,我们去满足个人工作、生活和学习的需求时可以有大量的产品&服务供我们选择,产品稀缺的时代结束了。此时,**对于产品的选择权正在从企业转为用户,由用户来决定用不用你的产品,组织绩效便开始由外部决定,而用户的需求是多元易变的** 。 这个时候,如果我们还期望用一个定完就放任不管的 KPI 数值来取得组织经营成功,就变得越来越不可能。OKR 从制定完成,就要进入持续的过程检视&调整的原因,就是期望及时管理好过程,拥抱过程中的变化,通过 OKR 管理的灵活性,来适应经营环境的 VUCA 化。 -所以,我们可以看到, **KPI 式的固化实现绩效方式,根植于对组织经营的外部环境是确定的,是可以掌控的假设。而OKR 的灵活实现绩效方式,是基于对当下组织经营环境是不确定的,是需要探索式前行的假设。** 在这样不同经营假设的思维影响下,就带来了不同的分别基于 OKR 和 KPI 的管理方式,具体体现在哪里呢? +所以,我们可以看到,**KPI 式的固化实现绩效方式,根植于对组织经营的外部环境是确定的,是可以掌控的假设。而OKR 的灵活实现绩效方式,是基于对当下组织经营环境是不确定的,是需要探索式前行的假设。** 在这样不同经营假设的思维影响下,就带来了不同的分别基于 OKR 和 KPI 的管理方式,具体体现在哪里呢? ### 4. 管理方式 以确定性思维来经营组织,带来的就是对目标管控,甚至拒绝目标变化,这一管理理念会反映在组织中的 KPI 绩效管理系统上。 -在执行 KPI 的组织中,当你的 KPI 一旦在系统里填写完成提交后,任何关于 KPI 的改动都必须要上级管理者审批。比如发现了其中某个地方写错或者想要调整,此时就必须让管理者对你先前提交的 KPI 先进行“打回”操作,然后你才能改。要知道, **KPI 管的是目标,目标指导的是个人行为** ,管理者一直没有点击“打回”按钮,就预示着目标还没变,组织中的个人行为就不能随意乱动。 +在执行 KPI 的组织中,当你的 KPI 一旦在系统里填写完成提交后,任何关于 KPI 的改动都必须要上级管理者审批。比如发现了其中某个地方写错或者想要调整,此时就必须让管理者对你先前提交的 KPI 先进行“打回”操作,然后你才能改。要知道,**KPI 管的是目标,目标指导的是个人行为**,管理者一直没有点击“打回”按钮,就预示着目标还没变,组织中的个人行为就不能随意乱动。 久而久之,倒也不是员工不想自主自发的去做事,而是受制于这种教条的管理机制,被动接受了“ **上级动,我才动;上级不动,我就在那等着** ”的工作模式。没有人再去主动思考如何为组织创造更好的绩效,抹杀了积极性和创造性,带来了组织僵化,活力丧失。 -更可怕的是,由于 **基于 KPI 的绩效管理系统的设计不能及时地响应外部环境的变化** ,无论员工还是管理者,每次修改目标会特别麻烦,变更成本极高。这就导致大家不愿意在制定和管理 KPI 上花太多时间,流于形式。 +更可怕的是,由于 **基于 KPI 的绩效管理系统的设计不能及时地响应外部环境的变化**,无论员工还是管理者,每次修改目标会特别麻烦,变更成本极高。这就导致大家不愿意在制定和管理 KPI 上花太多时间,流于形式。 -这样下来,实际工作与写到 KPI 系统里的工作内容早就大相径庭。试想, **一个组织中的绩效管理系统不能如实地反映组织真实的经营绩效结果,我们就没有真实可靠的依据来进行人才盘点和公平的激励。这个时候,当然是唯上和领导一言堂,无法建立真正绩效导向的文化,也就没有人真的去挑战去突破** 。 +这样下来,实际工作与写到 KPI 系统里的工作内容早就大相径庭。试想,**一个组织中的绩效管理系统不能如实地反映组织真实的经营绩效结果,我们就没有真实可靠的依据来进行人才盘点和公平的激励。这个时候,当然是唯上和领导一言堂,无法建立真正绩效导向的文化,也就没有人真的去挑战去突破** 。 既然 KPI 给我们带来了这么多管理问题,那我们来看看 OKR 是如何改善的呢? @@ -73,7 +73,7 @@ KPI 出现的 90 年代,国内改革开放才 10 多年,大部分企业正 在落地 OKR 的组织,首先会按照短周期的月度或者季度来制定 OKR。此外,一线员工可以在OKR 的管理系统中随时对 O 和 KR 进行及时新增、修改和删除操作,而且这些操作并不需要任何人的审批。OKR 系统的设计就是更加在意自驱和自主性的目标管理,以及对目标完成过程的问题和变化的及时跟进和把控。这样的 OKR 管理方式有以下两个核心理由: - **如今市场环境变化多端,无法通过预定义的方式来做很长周期的计划,这时就要求我们把计划周期变短,设定的目标变小,才能够及时调整目标的打法和资源配置,** 所以这也是为什么 OKR 的目标管理实践都是以月度或是季度来进行制定的原因。 -- VUCA 化的经营环境,其实和战场的环境一样, **计划一旦做好,就像战斗一旦打响一样。这时,就必须让一线的人员能够拥有及时响应变化、及时做决策的权利去应对突发的事情,而不是出现任何问题都是先向上层层汇报,然后层层审批,再向下层层传递,继而一线的人员才能动,这样根本无法做到快速响应外部环境的变化,** 所以这也是为什么 OKR 的目标管理实践是授权给到一线,可以自主进行目标及时调整做法的原因。 **为了能建立灵活响应外部 VUCA 经营环境的管理模式,让听得见炮火的人及时做决策。** KPI 式的“命令+控制”管理方式突显死板和僵化,长期不仅带来“唯 KPI 论”,更会丧失组织活力和创新。而 OKR 管理的灵活和敏捷性,不仅体现在建立短期迭代式小目标的 OKR 制定周期上,也体现在响应目标的变化和授权给到一线拥有自主及时调整目标的权利上,最后 **再结合上一课时我和你讲的 OKR 过程管理流程,就能做到上下级对变更信息的及时同步和共识,这样也会避免一线的随意决策导致的组织风险。** +- VUCA 化的经营环境,其实和战场的环境一样,**计划一旦做好,就像战斗一旦打响一样。这时,就必须让一线的人员能够拥有及时响应变化、及时做决策的权利去应对突发的事情,而不是出现任何问题都是先向上层层汇报,然后层层审批,再向下层层传递,继而一线的人员才能动,这样根本无法做到快速响应外部环境的变化,** 所以这也是为什么 OKR 的目标管理实践是授权给到一线,可以自主进行目标及时调整做法的原因。**为了能建立灵活响应外部 VUCA 经营环境的管理模式,让听得见炮火的人及时做决策。** KPI 式的“命令+控制”管理方式突显死板和僵化,长期不仅带来“唯 KPI 论”,更会丧失组织活力和创新。而 OKR 管理的灵活和敏捷性,不仅体现在建立短期迭代式小目标的 OKR 制定周期上,也体现在响应目标的变化和授权给到一线拥有自主及时调整目标的权利上,最后 **再结合上一课时我和你讲的 OKR 过程管理流程,就能做到上下级对变更信息的及时同步和共识,这样也会避免一线的随意决策导致的组织风险。** ### 小结 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25410\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25410\350\256\262.md" index 77cb9a72e..c95fac1e9 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25410\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25410\350\256\262.md" @@ -12,13 +12,13 @@ 这说明: **激励可以引导群体行为。** 游戏玩家一起产生了去发弹幕的这种行为的原因,是因为游戏给予了特殊的激励。其实,这样的例子我们在平时的工作和生活中也能找到,拼多多就是靠砍价激励引导人在微信群转发,从而达到了裂变疯传的目的,硬生生从阿里和京东主导的市场中挤出了一条下沉之路。 -那么回到组织里,我们期望所做的激励,一定是围绕在如何更好地取得绩效结果上,也就是说, **组织里的激励是为了引导全体员工更愿意产生获得组织绩效的行为。而 OKR 就是管理组织目标和绩效结果,所以组织中的激励一定要基于 OKR 来做。** 然而,我常常看到一些团队和组织在引入 OKR 后,依旧还是基于 KPI 来进行考核和激励,那么长此以往,组织中引导的群体行为还是会基于 KPI 来运转,而在上一篇我已经和你说明了 KPI 会给组织带来很多管理弊端。 +那么回到组织里,我们期望所做的激励,一定是围绕在如何更好地取得绩效结果上,也就是说,**组织里的激励是为了引导全体员工更愿意产生获得组织绩效的行为。而 OKR 就是管理组织目标和绩效结果,所以组织中的激励一定要基于 OKR 来做。** 然而,我常常看到一些团队和组织在引入 OKR 后,依旧还是基于 KPI 来进行考核和激励,那么长此以往,组织中引导的群体行为还是会基于 KPI 来运转,而在上一篇我已经和你说明了 KPI 会给组织带来很多管理弊端。 -所以说,组织中的激励机制需要升级基于 OKR 来做, **只有激励设计和 OKR 相关,组织中的人才认 OKR,才会用 OKR,才会通过 OKR 来产生更好的绩效结果。** 而作为组织中的管理者以及 HR 侧负责绩效管理的同学,就需要牵引整个组织来基于 OKR 打绩效、做评价。 +所以说,组织中的激励机制需要升级基于 OKR 来做,**只有激励设计和 OKR 相关,组织中的人才认 OKR,才会用 OKR,才会通过 OKR 来产生更好的绩效结果。** 而作为组织中的管理者以及 HR 侧负责绩效管理的同学,就需要牵引整个组织来基于 OKR 打绩效、做评价。 -比如,京东内部每个季末,HR 侧都会发起面向整个一级部门的绩效评估,内容包括了本季度的绩效评价及下个季度的目标设定,而绩效评价会跟员工的绩点相关,被打更高等级绩点的员工就会有更高的绩效奖金激励。在该场景下,如果组织中导入了 OKR, **HR 就要表态整个绩效评估必须要转移到 OKR 上来进行,管理者也是参考 OKR 中的实际绩效产出及 OKR 闭环管理时的评分来给下属打绩点** ,这样的激励机制才能起到利用 OKR 来引导组织中的群体产生更高绩效。 +比如,京东内部每个季末,HR 侧都会发起面向整个一级部门的绩效评估,内容包括了本季度的绩效评价及下个季度的目标设定,而绩效评价会跟员工的绩点相关,被打更高等级绩点的员工就会有更高的绩效奖金激励。在该场景下,如果组织中导入了 OKR,**HR 就要表态整个绩效评估必须要转移到 OKR 上来进行,管理者也是参考 OKR 中的实际绩效产出及 OKR 闭环管理时的评分来给下属打绩点**,这样的激励机制才能起到利用 OKR 来引导组织中的群体产生更高绩效。 -在理解了我们需要基于 OKR 来进行组织激励,通过 OKR 来引导组织中产生更能取得绩效的行为后,我们再来看看,OKR 在组织中是如何让人更有工作动力的呢? **通过 OKR 让个人目标和组织目标合二为一** 在 OKR 的制定过程中,个人的 OKR 生成主要来自 3 个方面: +在理解了我们需要基于 OKR 来进行组织激励,通过 OKR 来引导组织中产生更能取得绩效的行为后,我们再来看看,OKR 在组织中是如何让人更有工作动力的呢?**通过 OKR 让个人目标和组织目标合二为一** 在 OKR 的制定过程中,个人的 OKR 生成主要来自 3 个方面: > 1、和上级对齐 2、自身岗位职责自驱 3、外部支撑相关 @@ -26,11 +26,11 @@ 而在自身岗位职责自驱生成的 OKR 中,无论 O 还是 KR 则完完全全都是由自己来确定,只要满足能支撑组织的发展就行,如此个人也会更加有动力去实现自定义的目标,尽最大力量去取得绩效结果。 -相反, **如果在一个组织中,被分配的工作大部分都不是自己想做的,就会导致个人目标和组织目标产生偏离** 。举个极端例子,你让项目经理去干产品经理的活,项目经理不仅可能胜任不了,而且会导致项目经理想要做的事情和组织给他分配的事情产生了冲突,这时不仅没有任何激励,更有可能会带来人才流失的问题。 +相反,**如果在一个组织中,被分配的工作大部分都不是自己想做的,就会导致个人目标和组织目标产生偏离** 。举个极端例子,你让项目经理去干产品经理的活,项目经理不仅可能胜任不了,而且会导致项目经理想要做的事情和组织给他分配的事情产生了冲突,这时不仅没有任何激励,更有可能会带来人才流失的问题。 -再比如, **阿里的合伙人机制** ,就是让合伙人在定战略时拥有决策权,而不是仅仅听创始人一个人的,这时, **合伙人的想法会融合在组织战略方向的制定中,就会更有动力为组织付出** ,这是国内最出名的激励优秀个体为组织长期贡献的案例,本质还是在让个人目标和组织目标进行融合。 +再比如,**阿里的合伙人机制**,就是让合伙人在定战略时拥有决策权,而不是仅仅听创始人一个人的,这时,**合伙人的想法会融合在组织战略方向的制定中,就会更有动力为组织付出**,这是国内最出名的激励优秀个体为组织长期贡献的案例,本质还是在让个人目标和组织目标进行融合。 -我在京东又是怎么做的呢?在季度初带领团队制定 OKR 时,包括 **O 和 KR 都是让团队先自行产出** ,然后我会和团队进行归纳总结,讨论共识,这时就 **充分考虑了团队中所有人的意见** 。而且,我会鼓励个人制定完全自驱的OKR。比如,某人定了 3 个 OKR,那么我会鼓励该同学在这 3 个 OKR 中包含一个完全自主想做的 O。 +我在京东又是怎么做的呢?在季度初带领团队制定 OKR 时,包括 **O 和 KR 都是让团队先自行产出**,然后我会和团队进行归纳总结,讨论共识,这时就 **充分考虑了团队中所有人的意见** 。而且,我会鼓励个人制定完全自驱的OKR。比如,某人定了 3 个 OKR,那么我会鼓励该同学在这 3 个 OKR 中包含一个完全自主想做的 O。 在组织中,通过 OKR 让个人目标与组织目标更好地合二为一,就是对个体工作最大的激励。 @@ -38,9 +38,9 @@ 如果你问身边的人为什么要工作,那么得到的答案肯定是多种多样,有的为了钱,有的为了荣誉,有的为了地位,有的甚至为了消耗时间。所以,人要工作的原因是复杂的,而为了满足这些需求,我们常常会看到外在的诸如金钱、荣誉证书等激励,目的就是希望让员工更有工作动力。 -然而,长期使用外激励,会让我们逐渐忽略了人要工作的另一个因素: **内在动力** 。我们不得不承认的是,外在激励总是少数的,比如股权、荣誉,总不可能让人人都拿到。所以说,我们不能只做外激励, **更要做好内激励,让更多人自发,从内在就愿意为工作而付出** ,就会对组织绩效的获取更有益。 **OKR 则弥补了组织中经常缺失的内在激励。** 为什么呢?OKR 制定的过程,紧紧围绕组织目标展开,提倡自主性,更去鼓励个体制定自驱的沉淀方法论或者影响力的方向,基于 OKR 的管理流程则不断地会跟进完成目标的进度和问题,从而让影响目标完成的问题能够及时得到反馈和解决。 **明确的目标、工作更加自主、人能够获得成长、做事情有明显的推动进展感,这些内在动力都能极大地促成人对工作的热情,从而更好去完成工作目标,取得更高绩效结果** 。 +然而,长期使用外激励,会让我们逐渐忽略了人要工作的另一个因素: **内在动力** 。我们不得不承认的是,外在激励总是少数的,比如股权、荣誉,总不可能让人人都拿到。所以说,我们不能只做外激励,**更要做好内激励,让更多人自发,从内在就愿意为工作而付出**,就会对组织绩效的获取更有益。**OKR 则弥补了组织中经常缺失的内在激励。** 为什么呢?OKR 制定的过程,紧紧围绕组织目标展开,提倡自主性,更去鼓励个体制定自驱的沉淀方法论或者影响力的方向,基于 OKR 的管理流程则不断地会跟进完成目标的进度和问题,从而让影响目标完成的问题能够及时得到反馈和解决。**明确的目标、工作更加自主、人能够获得成长、做事情有明显的推动进展感,这些内在动力都能极大地促成人对工作的热情,从而更好去完成工作目标,取得更高绩效结果** 。 -当然,像好奇、社交、稳定等内在诉求也有可能是人要工作的内在需求,但 **并不是所有内在需求都和组织绩效强相关** 。和你举个例子,有的人工作就是想拓展自己的社交人脉,交更多朋友才是他更强烈的工作原因,但是这一点对组织绩效而言,没有多大促进作用。所以, **内在激励的应用一定要围绕组织目标的制定和工作过程展开,也就是围绕 OKR 制定和实现过程来做** ,激发出真正能让人产生工作意愿、更好产生绩效结果的内在动力,才是有效内激励。 +当然,像好奇、社交、稳定等内在诉求也有可能是人要工作的内在需求,但 **并不是所有内在需求都和组织绩效强相关** 。和你举个例子,有的人工作就是想拓展自己的社交人脉,交更多朋友才是他更强烈的工作原因,但是这一点对组织绩效而言,没有多大促进作用。所以,**内在激励的应用一定要围绕组织目标的制定和工作过程展开,也就是围绕 OKR 制定和实现过程来做**,激发出真正能让人产生工作意愿、更好产生绩效结果的内在动力,才是有效内激励。 在京东内部,基于 OKR 工作方式的内激励,我们做了如下实践: @@ -58,19 +58,19 @@ 我常听到在某一线互联网公司的朋友吐槽:“老板平时对我不闻不问,一有事了才想起我,我工位还和他挨着,平时基本上都不说话”。出现诸如这样的状况,最核心的问题就是没有建立一个良好的上下级的工作方式和习惯,长期跟着这样的老板,难免有很多抱怨,甚至选择离开。 -那么,我们如何来构建这个良好的工作方式呢?在京东内部,从高管到一线员工都有开早会的习惯,每天早上固定时间点,上级就会带着所管理的团队一起就实现目标的进度、问题和风险进行沟通和探讨,并会及时把公司的一些重要信息进行同步。除了早会,还有按周开展的周会,按重要项目开展的月会, **开这些会的目的,是给了上下级能够正式沟通的机会,互相探讨问题,交换意见,保持信息上下流动,从而带来工作过程的信任** 。 +那么,我们如何来构建这个良好的工作方式呢?在京东内部,从高管到一线员工都有开早会的习惯,每天早上固定时间点,上级就会带着所管理的团队一起就实现目标的进度、问题和风险进行沟通和探讨,并会及时把公司的一些重要信息进行同步。除了早会,还有按周开展的周会,按重要项目开展的月会,**开这些会的目的,是给了上下级能够正式沟通的机会,互相探讨问题,交换意见,保持信息上下流动,从而带来工作过程的信任** 。 -看到这,相信你一定会联想到 OKR 的制定流程和过程管理,在应用 OKR 的团队和组织,需要建立以日、周、季中三个维度的针对目标完成过程的检视和调整机制,从而来把控过程的问题和风险,并应对目标的变化,那么 **上下级就可以在 OKR 拉动的这个工作方式中,来达到与上述京东各类会议同样的目的和效果,建立起良好的上下级工作关系,管理者可以在这种工作方式中充分影响他人去达成群体目标,也就塑造了我们常说的领导力。** 所以,基于 OKR 建立起来的工作方式, **作为管理者,可以给你提高领导力,及时发现下属面临的问题,给予工作意见和指导;作为员工,可以在这类工作方式中及时汇报自己的工作情况,争取资源,获得帮助。** 上下级建立的这种彼此信任、及时同步信息、以更好取得绩效结果为目的的良好工作关系,有效激励了下属更有动力去为工作付出。 **通过 OKR 打造绩效导向的文化** 在 **面向结果的激励** 中,无论是给予奖金、晋升还是荣誉等,在组织里,我们唯一的导向就是绩效。 +看到这,相信你一定会联想到 OKR 的制定流程和过程管理,在应用 OKR 的团队和组织,需要建立以日、周、季中三个维度的针对目标完成过程的检视和调整机制,从而来把控过程的问题和风险,并应对目标的变化,那么 **上下级就可以在 OKR 拉动的这个工作方式中,来达到与上述京东各类会议同样的目的和效果,建立起良好的上下级工作关系,管理者可以在这种工作方式中充分影响他人去达成群体目标,也就塑造了我们常说的领导力。** 所以,基于 OKR 建立起来的工作方式,**作为管理者,可以给你提高领导力,及时发现下属面临的问题,给予工作意见和指导;作为员工,可以在这类工作方式中及时汇报自己的工作情况,争取资源,获得帮助。** 上下级建立的这种彼此信任、及时同步信息、以更好取得绩效结果为目的的良好工作关系,有效激励了下属更有动力去为工作付出。**通过 OKR 打造绩效导向的文化** 在 **面向结果的激励** 中,无论是给予奖金、晋升还是荣誉等,在组织里,我们唯一的导向就是绩效。 -OKR 从制定时如何确保支撑战略落地、如何进行流程的过程管理,再到 O 和 KR 的写法,完完全全都是围绕如何高效地制定和实现目标在做。 **尤其在 O 的选择中,聚焦营收、用户、效率和能力维度,就是在让制定的目标都是以价值导向,因为这些就是组织绩效的构成维度** 。所以, OKR 所有的理念和相关实践都是为了更好地获得绩效而在努力。 **此外,激励在以绩效导向时,还要注重公平。** 基于 OKR 的结果评分机制,通过多个相关方的评价,则体现了相对公平性,避免了唯上和一言堂。并且 OKR 系统通晒了所有人的 OKR和 评价结果,则有力保证了绩效评价的公开透明。 +OKR 从制定时如何确保支撑战略落地、如何进行流程的过程管理,再到 O 和 KR 的写法,完完全全都是围绕如何高效地制定和实现目标在做。**尤其在 O 的选择中,聚焦营收、用户、效率和能力维度,就是在让制定的目标都是以价值导向,因为这些就是组织绩效的构成维度** 。所以, OKR 所有的理念和相关实践都是为了更好地获得绩效而在努力。**此外,激励在以绩效导向时,还要注重公平。** 基于 OKR 的结果评分机制,通过多个相关方的评价,则体现了相对公平性,避免了唯上和一言堂。并且 OKR 系统通晒了所有人的 OKR和 评价结果,则有力保证了绩效评价的公开透明。 -然而,我特别担心很多组织内部形成的“没有功劳也有苦劳”的激励思维, **兼顾苦劳的激励,就是在看工作量的多少,不仅不是绩效导向,而且会带来不公平,会让真正产生组织绩效的人失望和不满意,这时伤的不仅是绩效,更是组织文化。** 这种文化过滤下来的人,尤其擅长“划水”,以及“养老式”的工作,而人才则会选择离开。 +然而,我特别担心很多组织内部形成的“没有功劳也有苦劳”的激励思维,**兼顾苦劳的激励,就是在看工作量的多少,不仅不是绩效导向,而且会带来不公平,会让真正产生组织绩效的人失望和不满意,这时伤的不仅是绩效,更是组织文化。** 这种文化过滤下来的人,尤其擅长“划水”,以及“养老式”的工作,而人才则会选择离开。 ![Lark20201123-180305.png](assets/CgqCHl-7iVqAE0GWAAV5HpLNMWg136.png) 比如,我们在季度打绩效时,不能看一个人天天在加班,上班不迟到也不早退,就给出高绩点。而是要结合 OKR,看他从事的工作所带来的效果,是帮助组织赚了多少钱、提升了多少用户增量、提高了什么组织效率,还是为组织培养了多少个人才,以这个作为绩效的评判标准,才会告别兼顾苦劳的“平均主义”激励。 -所以,真正把 OKR 用好后,组织中的管理者参考 OKR 来进行员工的晋升、股权分配、奖金分发、荣誉的获得等,就是完全在以绩效导向来进行激励,通过 OKR 通晒和多方评分的方式,也更好地保证了公平性。 **只有基于绩效的公平的激励,才能让人真正满意** ,让人更有持续的工作动力。 +所以,真正把 OKR 用好后,组织中的管理者参考 OKR 来进行员工的晋升、股权分配、奖金分发、荣誉的获得等,就是完全在以绩效导向来进行激励,通过 OKR 通晒和多方评分的方式,也更好地保证了公平性。**只有基于绩效的公平的激励,才能让人真正满意**,让人更有持续的工作动力。 ### 小结 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25411\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25411\350\256\262.md" index 1eb127f34..9b186c985 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25411\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25411\350\256\262.md" @@ -24,11 +24,11 @@ 文化为什么和人的思维方式相关呢?这里和你说个关于我的小故事。 -我曾在一家某军工集团下属的研究所工作过,在这样的环境中,我会 **严格遵守瀑布式产品开发的交付流程来管理整个软件项目,严控甚至拒绝需求变更** ,这种交付模式也深得客户方认可,所以我对于产品开发的思维方式一直都是传统的瀑布模式,而且我也并不认为这有什么问题。 +我曾在一家某军工集团下属的研究所工作过,在这样的环境中,我会 **严格遵守瀑布式产品开发的交付流程来管理整个软件项目,严控甚至拒绝需求变更**,这种交付模式也深得客户方认可,所以我对于产品开发的思维方式一直都是传统的瀑布模式,而且我也并不认为这有什么问题。 > 瀑布式产品开发:软件开发中的一种模式,前期会花很长时间来确定大量需求范围,并通过文档驱动,严格遵循预先计划的需求、分析、设计、编码、测试的步骤顺序进行,过程中做严格的需求变更控制,一般一个交付周期半年~1 年,甚至也有更长的。 -后来投身到互联网的世界,才发现瀑布式的开发方法在一个变化非常快的互联网环境中根本不适用, **只有迭代式、及时响应需求变化的敏捷产品开发才能让组织更好地活下来** ,我的整个思维方式都被颠覆了。 +后来投身到互联网的世界,才发现瀑布式的开发方法在一个变化非常快的互联网环境中根本不适用,**只有迭代式、及时响应需求变化的敏捷产品开发才能让组织更好地活下来**,我的整个思维方式都被颠覆了。 > 敏捷产品开发:2001 年的敏捷宣言,开启了软件敏捷开发的时代。该模式强调进行短周期迭代满足用户需求,边开发边求证,一般一个迭代的开发周期为 2~3 周,甚至还有更短的,一周就迭代出一个小版本进行发布验证。 @@ -43,19 +43,19 @@ 通过调整人的思维来进行文化升级,就让 OKR 文化具备了沙因文化中的基本假设层次,这样的基本假设强调拥抱不确定性、以人为本、增长导向、重视过程,组织才能获得成功。 -在组织内部,我们可以 **采用持续的培训、分享、讲座、思维大赛等方式来传播 OKR 思想。** ![Drawing 1.png](assets/CgqCHl--G1mAN_evABpXI4MAK7M917.png) +在组织内部,我们可以 **采用持续的培训、分享、讲座、思维大赛等方式来传播 OKR 思想。**![Drawing 1.png](assets/CgqCHl--G1mAN_evABpXI4MAK7M917.png) (左侧为本人 OKR 培训现场,右侧为 OKR 思维大赛奖杯) 比如,为了让大家更好地理解 OKR 是什么,前期在部门内部导入 OKR 时,我每周二都会给团队中的经理、Leader 做培训,目的就是给大家布道 OKR 的理念及所代表的核心价值观。我还联合 HR 侧举办过“OKR 思维”大赛,通过比赛的形式,拉动大家来学习 OKR 的理念,如此反复向群体中注入 OKR 思维。 -只有组织中人的思维能长期基于 OKR 所代表的基本假设来思考和决策, **OKR 文化的内核** 才可以塑造起来。 +只有组织中人的思维能长期基于 OKR 所代表的基本假设来思考和决策,**OKR 文化的内核** 才可以塑造起来。 #### 2. 做管理 -当我们去往不同的地域,看到不同人群的生活方式,会常说这是当地的文化使然。文化在生活中是一种生活方式,在组织中就是一种管理方式。而 **管理是一种实践** ,有着什么样的管理实践,对应就会有什么样的管理和被管理的行为,从而就会产生相应的文化。 +当我们去往不同的地域,看到不同人群的生活方式,会常说这是当地的文化使然。文化在生活中是一种生活方式,在组织中就是一种管理方式。而 **管理是一种实践**,有着什么样的管理实践,对应就会有什么样的管理和被管理的行为,从而就会产生相应的文化。 -想要生成 OKR 文化,就需要 **找到从 OKR 基本假设中衍生出来的具体落地 OKR 的管理实践,而文化的外显价值观正是由管理实践来体现的。** 比如,不同组织都写在墙上的“敏捷”价值观,从文化的人工饰物层来看,是同一种表现形式。 **但和不同组织中的人详细沟通交流后会发现** ,在有的组织敏捷代表的是敏捷产品开发,有的组织则代表的是与客户的沟通响应速度要快,对应着, **会有不同的管理实践来承载这两种不同的价值主张,代表敏捷开发的管理实践会是迭代,而代表沟通响应速度要快的管理实践可能是客户的问题必须半小时内给予响应** 。 +想要生成 OKR 文化,就需要 **找到从 OKR 基本假设中衍生出来的具体落地 OKR 的管理实践,而文化的外显价值观正是由管理实践来体现的。** 比如,不同组织都写在墙上的“敏捷”价值观,从文化的人工饰物层来看,是同一种表现形式。**但和不同组织中的人详细沟通交流后会发现**,在有的组织敏捷代表的是敏捷产品开发,有的组织则代表的是与客户的沟通响应速度要快,对应着,**会有不同的管理实践来承载这两种不同的价值主张,代表敏捷开发的管理实践会是迭代,而代表沟通响应速度要快的管理实践可能是客户的问题必须半小时内给予响应** 。 所以,塑造 OKR 文化的外显价值观层,可以从管理的四个核心实践角度来做。 @@ -80,23 +80,23 @@ 而在国外,比如欧洲的一些国家,在路上开车是遵循着“靠左行驶”的交通规则,如果我们去到这些国家依旧采用国内的“靠右行驶”,就会产生很多交通矛盾和事故,这也是基于不同交通规则的交通文化背景带来的冲突。 -同理,在组织中,有什么样的规则就会产生什么样的约定行为,从而就会带来什么样的文化,或者说文化的建立深受规则的影响, **我们常常把这类规则称为规章制度或运行机制** 。 +同理,在组织中,有什么样的规则就会产生什么样的约定行为,从而就会带来什么样的文化,或者说文化的建立深受规则的影响,**我们常常把这类规则称为规章制度或运行机制** 。 -那么,我们想要塑造 OKR 文化,就需要产生 OKR 执行和落地的规则, **建立两个核心机制。** +那么,我们想要塑造 OKR 文化,就需要产生 OKR 执行和落地的规则,**建立两个核心机制。** - **流程机制:基于 OKR 打造组织中目标管理的流程。** - **激励机制:基于 OKR 来进行组织中的激励设计。** 只有组织中的工作流程、引导群体行为的激励全部基于 OKR 展开和制定,且能刚性执行,OKR 文化才能立起来。 -有了基于 OKR 的流程和激励机制的升级保障,就让 OKR 文化具备了沙因文化中的人工饰物层次。 **这些人工饰物体现在,当你初入一个群体,会看到落在纸面上的关于 OKR 的流程和激励制度,也会看到人们进行 OKR 制定-过程检视&调整-OKR 闭环管理的各种行为,还会听到含有 OKR 的言语(如下图)等** 。 +有了基于 OKR 的流程和激励机制的升级保障,就让 OKR 文化具备了沙因文化中的人工饰物层次。**这些人工饰物体现在,当你初入一个群体,会看到落在纸面上的关于 OKR 的流程和激励制度,也会看到人们进行 OKR 制定-过程检视&调整-OKR 闭环管理的各种行为,还会听到含有 OKR 的言语(如下图)等** 。 ![Drawing 3.png](assets/Ciqc1F--HECATCAGAA7IDVwrWWc579.png) (团队按照 OKR 的流程跑起来,就会带来各种制定和讨论 OKR 的行为和言语,外人从这些表面上看到的听到的都是 OKR 文化的人工饰物层) -同时, **流程和激励机制会和管理实践交融在一起发挥作用** ,比如在 OKR 流程的目标设定环节,我们就会采用小目标、优先级的管理实践,在 OKR 的过程检视&调整时采用每日站会结合物理看板的管理实践;基于 OKR 的激励,就会用到通晒、评分、目标合二为一的管理实践。这样,就会 **让 OKR 文化的人工饰物背后,都能找到与之匹配的 OKR 外显价值观,彼此互相支撑** 。 +同时,**流程和激励机制会和管理实践交融在一起发挥作用**,比如在 OKR 流程的目标设定环节,我们就会采用小目标、优先级的管理实践,在 OKR 的过程检视&调整时采用每日站会结合物理看板的管理实践;基于 OKR 的激励,就会用到通晒、评分、目标合二为一的管理实践。这样,就会 **让 OKR 文化的人工饰物背后,都能找到与之匹配的 OKR 外显价值观,彼此互相支撑** 。 然而我常常看到,很多推行 OKR 的组织,仅仅在团队层面基于 OKR 来进行工作的展开,管理者和高层从来不用,组织中从高层开始就无视规则、挑战规则、不遵守规则,团队中的规则执行也就可想而知,这也是国内很多组织 OKR 落地生根不了的重要原因之一。 -所以, **涉及 OKR 的流程和激励规则一旦制定,必须要刚性执行,甚至组织中的高管也不能例外** 。我们可以看到字节跳动和百度在推动 OKR 时,创始人张一鸣和李彦宏就开始写 OKR 的原因,就是从高管开始执行 OKR 制定的流程,才能保证流程机制不被挑战,否则一旦例外过多, OKR 文化就树立不起来。 +所以,**涉及 OKR 的流程和激励规则一旦制定,必须要刚性执行,甚至组织中的高管也不能例外** 。我们可以看到字节跳动和百度在推动 OKR 时,创始人张一鸣和李彦宏就开始写 OKR 的原因,就是从高管开始执行 OKR 制定的流程,才能保证流程机制不被挑战,否则一旦例外过多, OKR 文化就树立不起来。 通过调思维、做管理、定规则三步走,来调整组织中群体的行为习惯,就可以塑造出 OKR 文化拥有沙因文化的三个层次。这样 OKR 文化的雏形也就出来了,但文化建设一定是一个长期坚持的事情,我们该如何把这些好的做法沉淀下来呢? diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25412\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25412\350\256\262.md" index 5d2de3cfc..6991b4dcb 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25412\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25412\350\256\262.md" @@ -16,7 +16,7 @@ ![Drawing 0.png](assets/CgqCHl_EyaOABIKvAACSkhByQ4Y572.png) -首先,我们把一个组织抽象成一个 **系统** ,在这个系统中,还包括了 **人以及人与人之间的交互** 。 +首先,我们把一个组织抽象成一个 **系统**,在这个系统中,还包括了 **人以及人与人之间的交互** 。 那么,当一个变化,如 OKR 转型,进入到这个组织系统,就会对这三个部分带来调整,同时也会面临这三个部分的落地挑战,这些难点在于: @@ -34,7 +34,7 @@ 我们很多人都喜欢在豆瓣、微信社群、小红书聚集,这些基于情感、兴趣、爱好组成的社区形式,就是非正式的组织。在非正式组织里,没有所谓的压力和效率,想聊什么就聊,想来就来,想走就走。 -但是正式组织则不同,正式组织是为了 **实现目标而存在** ,而为了实现目标,继而会 **设计很多角色来承担实现目标的职责** ,然后 **通过分工给到具体的执行人** 。 +但是正式组织则不同,正式组织是为了 **实现目标而存在**,而为了实现目标,继而会 **设计很多角色来承担实现目标的职责**,然后 **通过分工给到具体的执行人** 。 ![Drawing 1.png](assets/CgqCHl_Eya-AacJcAAEEaWTmYiA820.png) @@ -42,7 +42,7 @@ 举个例子。我在京东某部门推动落地 OKR 时,首先定义了一个名为“OKR Master”的角色,并赋予了该角色需要落地 OKR 的职责,然后在该部门以业务条线的维度找承担该职责的人。当时,我一共找到了 20 多个团队的 Leader 来进行分工承接。 -接下来,我自己和这 20 多位 OKR Master 成立了 **OKR 变革小组** ,通过设立 **OKR 变革小组的工作目标** ,来带领部门 200 多人进行 OKR 转型。在这里,我把变革小组的阶段性目标中的关键量化指标分享给你(如下),你可以参考作为 OKR 变革时的核心过程指标。 +接下来,我自己和这 20 多位 OKR Master 成立了 **OKR 变革小组**,通过设立 **OKR 变革小组的工作目标**,来带领部门 200 多人进行 OKR 转型。在这里,我把变革小组的阶段性目标中的关键量化指标分享给你(如下),你可以参考作为 OKR 变革时的核心过程指标。 - **OKR 覆盖度** - **OKR NPS** - **OKR 实现过程管理能力水平** OKR 实现过程管理能力水平,就是 OKR 的文化监控做法《具体参考 11 课时 OKR 文化建设里所讲的如何沉淀 OKR 文化》,这里着重介绍下 OKR 覆盖度和 OKR NPS。 @@ -54,7 +54,7 @@ NPS(Net Promoter Score,净推荐值)原本是用来衡量用户向其他人推荐某个产品或服务可能性程度的指标,该指标可以用来说明用户对于某产品使用的满意度、喜好情况。 -OKR 作为一个新的目标管理方法,到底能不能帮助组织解决问题,到底好不好,这真得听听长期在应用这套方法的当事人怎么说才行。就像产品真正解决了用户痛点问题后,用户就会非常喜欢并乐于推荐一样,OKR 若是真的非常有价值,那么运用这套方法的团队就会越来越接受,也会越来越满意,并乐于向其他人推荐这套工作方法。所以,OKR NPS 设立的目的,就是用来收集使用 OKR 这个方法的用户好差反馈,我们可以按照月度或者季度来收集该数据。 **有了 OKR 覆盖度,可以确保组织中所有人都能把 OKR 用起来。然后通过 OKR NPS 来收集使用过程中的问题,做持续的问题识别和改进,最后再结合 OKR 实现过程管理能力水平,来塑造和沉淀 OKR 文化,这样就能全方位地推 OKR。** 所以在推动 OKR 转型时,我们需要成立 OKR 变革小组,定角色、定职责,然后制定变革小组的工作目标。这样做就是在一个组织中,在以组织形式来推动 OKR,提高落地 OKR 的效率和成功率。 +OKR 作为一个新的目标管理方法,到底能不能帮助组织解决问题,到底好不好,这真得听听长期在应用这套方法的当事人怎么说才行。就像产品真正解决了用户痛点问题后,用户就会非常喜欢并乐于推荐一样,OKR 若是真的非常有价值,那么运用这套方法的团队就会越来越接受,也会越来越满意,并乐于向其他人推荐这套工作方法。所以,OKR NPS 设立的目的,就是用来收集使用 OKR 这个方法的用户好差反馈,我们可以按照月度或者季度来收集该数据。**有了 OKR 覆盖度,可以确保组织中所有人都能把 OKR 用起来。然后通过 OKR NPS 来收集使用过程中的问题,做持续的问题识别和改进,最后再结合 OKR 实现过程管理能力水平,来塑造和沉淀 OKR 文化,这样就能全方位地推 OKR。** 所以在推动 OKR 转型时,我们需要成立 OKR 变革小组,定角色、定职责,然后制定变革小组的工作目标。这样做就是在一个组织中,在以组织形式来推动 OKR,提高落地 OKR 的效率和成功率。 变革小组成立后,在按照计划推动 OKR 时,往往面临最大的问题,还是回到组织中的人身上,在人这个维度,我们又可以采用什么方法来降低 OKR 变革阻力呢? @@ -64,15 +64,15 @@ OKR 作为一个新的目标管理方法,到底能不能帮助组织解决问 > **B** :Behaviour,行为 **M** :Motivation,动机 **A** :Ability,能力 **T** :Trigger,触发 -也就是说,个体的行为受到动机、能力和触发三个变量的综合影响。那么,在组织里,我们就需要从这三个变量切入,来解决人怎么才能更容易产生应用 OKR 的行为。 **在动机维度** ,最重要的激励,我在\<10 课时>和你分享过,组织中的激励机制和激励设计围绕 OKR 来做,就可以引导群体更有动力来使用 OKR。除了结合激励,变革的引入,还要说明能帮助组织解决什么问题,或者通过变革可以解决未来什么危机,这样的问题和危机感,要能从上往下地传递出来,增加组织中群体拥抱变化的紧迫感。 +也就是说,个体的行为受到动机、能力和触发三个变量的综合影响。那么,在组织里,我们就需要从这三个变量切入,来解决人怎么才能更容易产生应用 OKR 的行为。**在动机维度**,最重要的激励,我在\<10 课时>和你分享过,组织中的激励机制和激励设计围绕 OKR 来做,就可以引导群体更有动力来使用 OKR。除了结合激励,变革的引入,还要说明能帮助组织解决什么问题,或者通过变革可以解决未来什么危机,这样的问题和危机感,要能从上往下地传递出来,增加组织中群体拥抱变化的紧迫感。 -京东内部在启动 Big Boss 变革时,京东创始人刘强东曾说:“……在复杂的、多业态、多场景、碎片化、去中心化、还要满足客户极致的个性化需求下,必须有一套适应业务的管理体系和方法论。为了未来的发展做好准备,我们 **需要组织先行、方法先行** , **找到适合未来企业形态、业务形态的组织管理方法** ,所以我们要做 Big Boss, **这才是 Big Boss 产生的背景** ……” +京东内部在启动 Big Boss 变革时,京东创始人刘强东曾说:“……在复杂的、多业态、多场景、碎片化、去中心化、还要满足客户极致的个性化需求下,必须有一套适应业务的管理体系和方法论。为了未来的发展做好准备,我们 **需要组织先行、方法先行**,**找到适合未来企业形态、业务形态的组织管理方法**,所以我们要做 Big Boss,**这才是 Big Boss 产生的背景** ……” -京东 Big Boss 变革体系,其中涉及的 Big Boss 工作法就是指 OKR 工作法。有了创始人从上往下带来的组织需要进行变革的意志,就可以更好激发公司全体人员做出改变的动力。 **在能力维度** ,首先必须要让人具备 OKR 的整套理论知识和实践方法,也就是本专栏所提供的非常系统化的 OKR 落地管理法。此外, **组织中的学习不是让人学多少,而是用多少** 。所以,除了做好培训,我们注意力一定是回到应用上,再根据“学习金字塔”(该方法把学习方式分为听课、阅读、现场示范、讨论、践行和教别人,不同的学习方式带来不同的学习效果转化,而教是最好的学),一个人对某个知识理解最为深刻,就是能讲出来或是教别人。 +京东 Big Boss 变革体系,其中涉及的 Big Boss 工作法就是指 OKR 工作法。有了创始人从上往下带来的组织需要进行变革的意志,就可以更好激发公司全体人员做出改变的动力。**在能力维度**,首先必须要让人具备 OKR 的整套理论知识和实践方法,也就是本专栏所提供的非常系统化的 OKR 落地管理法。此外,**组织中的学习不是让人学多少,而是用多少** 。所以,除了做好培训,我们注意力一定是回到应用上,再根据“学习金字塔”(该方法把学习方式分为听课、阅读、现场示范、讨论、践行和教别人,不同的学习方式带来不同的学习效果转化,而教是最好的学),一个人对某个知识理解最为深刻,就是能讲出来或是教别人。 -结合以上,我在京东内部会让实践 OKR 的团队,把自己团队实践的案例进行总结分享, **一是看用 OKR 的情况,二是看理解 OKR 的深度** 。然后根据团队的 OKR 实践水平,进行专门的辅导,让组织中的人具备正确应用 OKR 的能力。 **在触发维度** ,让人长期能被触发使用 OKR,靠的就是管理流程和机制,这一点我在 OKR 文化的建设和沉淀中已经详细进行了说明\<参考 11 课时>。除此之外, **在组织里讲究责任和权利,组织结构中职位越高的人,拥有越高的权利,相应也就拥有越大的影响力,所以在触发全员进行变革时,需要高层的加入和支持,利用高层来影响人群接受和使用 OKR** 。 +结合以上,我在京东内部会让实践 OKR 的团队,把自己团队实践的案例进行总结分享,**一是看用 OKR 的情况,二是看理解 OKR 的深度** 。然后根据团队的 OKR 实践水平,进行专门的辅导,让组织中的人具备正确应用 OKR 的能力。**在触发维度**,让人长期能被触发使用 OKR,靠的就是管理流程和机制,这一点我在 OKR 文化的建设和沉淀中已经详细进行了说明\<参考 11 课时>。除此之外,**在组织里讲究责任和权利,组织结构中职位越高的人,拥有越高的权利,相应也就拥有越大的影响力,所以在触发全员进行变革时,需要高层的加入和支持,利用高层来影响人群接受和使用 OKR** 。 -在京东内部推 OKR 时,为了让高层支持 OKR 转型,我个人联合 HR 一起给部门 VP 进行了多次汇报,得到了 VP 的认可,并去影响 HR 侧的老板“游说”部门 VP 来支持 OKR 的推动。在后续战略会上,大领导表态用 OKR 来拆解战略,用 OKR 来管理项目过程,结合 OKR 来进行绩效和项目的复盘。如此在不同场合下,通过高层的影响力来触发人员要使用 OKR,就会让人倍增需要进行 OKR 变革的压力。 **在搞定了上层,有了一把手支持,推动 OKR 就会顺畅很多。** 所以,从动机、能力和触发三个变量切入,运用好的实践,激发让人更能使用 OKR 的动机,提升人应用 OKR 的能力,持续触发影响人形成应用 OKR 的思维和习惯,这样就可以让组织中的群体更容易产生应用 OKR 的行为,促成 OKR 变革更易成功。 +在京东内部推 OKR 时,为了让高层支持 OKR 转型,我个人联合 HR 一起给部门 VP 进行了多次汇报,得到了 VP 的认可,并去影响 HR 侧的老板“游说”部门 VP 来支持 OKR 的推动。在后续战略会上,大领导表态用 OKR 来拆解战略,用 OKR 来管理项目过程,结合 OKR 来进行绩效和项目的复盘。如此在不同场合下,通过高层的影响力来触发人员要使用 OKR,就会让人倍增需要进行 OKR 变革的压力。**在搞定了上层,有了一把手支持,推动 OKR 就会顺畅很多。** 所以,从动机、能力和触发三个变量切入,运用好的实践,激发让人更能使用 OKR 的动机,提升人应用 OKR 的能力,持续触发影响人形成应用 OKR 的思维和习惯,这样就可以让组织中的群体更容易产生应用 OKR 的行为,促成 OKR 变革更易成功。 解决了人的行为维度,接下来,我们还需要思考的是,在一个组织中怎么更有效地进行 OKR 扩散和传播呢? @@ -82,7 +82,7 @@ OKR 作为一个新的目标管理方法,到底能不能帮助组织解决问 我们要学习的是,40 年前国家推动改革开放的变革手段,就是用深圳作为试点,进而总结经济特区建设经验,提炼了方法论“实践是检验真理的唯一标准”后,才开始更广泛地推广改革开放,然后开始复制出苏南模式、乡镇企业模式、珠三角模式等等。 -所以,在做变革时, **为了避免上来就“一刀切”给组织带来的风险,我们需要先做试点,然后总结试点的成功经验,继而再规模化铺开。** 同理,在组织中推动 OKR 变革,我们也需要按照这个步骤来实施,而作为 OKR 变革的试点,又该如何选择呢? +所以,在做变革时,**为了避免上来就“一刀切”给组织带来的风险,我们需要先做试点,然后总结试点的成功经验,继而再规模化铺开。** 同理,在组织中推动 OKR 变革,我们也需要按照这个步骤来实施,而作为 OKR 变革的试点,又该如何选择呢? ![Drawing 3.png](assets/CgqCHl_EyeSAf16iAATIRfQr138513.png) @@ -90,7 +90,7 @@ OKR 作为一个新的目标管理方法,到底能不能帮助组织解决问 上图的创新扩散模型,由美国埃弗雷特·罗杰斯(E.M.Rogers)提出。从该模型中,我们可以学习到,当一个变化引入一个群体后,群体中的个体拥抱变化的态度上会有明显差别,一般可分为创新者、初期采用者、早期大众、后期大众和落后者五类人。 -由于群体中这五类人的存在, **对于一个变化,进入一个群体后的扩散速度,就会有先后、快慢之分。** 所以,为了降低变革阻力,我们不能上来就找落后者或后期大众来推动 OKR,那将会困难重重,在成本和效率上就会让变革失败。而是应该重点找到并激活整个组织中的创新者和初期采用者,通过他们作为 OKR 变革试点,然后总结试点的成功经验,联合团结他们把这些成功经验推广至其他群体,以此来更快地进行全组织有效的 OKR 变革扩张。 +由于群体中这五类人的存在,**对于一个变化,进入一个群体后的扩散速度,就会有先后、快慢之分。** 所以,为了降低变革阻力,我们不能上来就找落后者或后期大众来推动 OKR,那将会困难重重,在成本和效率上就会让变革失败。而是应该重点找到并激活整个组织中的创新者和初期采用者,通过他们作为 OKR 变革试点,然后总结试点的成功经验,联合团结他们把这些成功经验推广至其他群体,以此来更快地进行全组织有效的 OKR 变革扩张。 ![Drawing 4.png](assets/Ciqc1F_EyfCAQtdNABPHV5hCckc260.png) @@ -98,7 +98,7 @@ OKR 作为一个新的目标管理方法,到底能不能帮助组织解决问 京东从 2019 年年中开始,选择了对 OKR 感兴趣和有热情的京东零售前台 3C 事业部、中台平台生态部等部门进行 OKR 试点。试点推广实践了半年之后,在 2020 年才开始大面积地铺向更多部门。而我部门作为实践 OKR 的排头兵,我就把实际在京东场景下推广 OKR 成功的方法论和经验带给了其他兄弟部门,从而更加规模化地去做京东 OKR 变革。 -在这里,我特别想提醒你的是,每个组织的文化不同,制定目标的周期不同,人员水平不同,行业背景不同,所以我们不能完全“照抄”复制其他企业的 OKR 落地方法。一定要基于自己企业试点团队的实际情况,参考本专栏的 OKR 方法论和要素,总结形成适合自己企业落地 OKR 的经验,然后再铺向全组织,这样才是对于你的组织最有效的 OKR 变革扩散。而且, **一定要能确保 OKR 在试点团队获得成功,如果连试点都失败,OKR 这种新兴的目标管理方法,想在组织中全面推动几乎是不可能的** 。 +在这里,我特别想提醒你的是,每个组织的文化不同,制定目标的周期不同,人员水平不同,行业背景不同,所以我们不能完全“照抄”复制其他企业的 OKR 落地方法。一定要基于自己企业试点团队的实际情况,参考本专栏的 OKR 方法论和要素,总结形成适合自己企业落地 OKR 的经验,然后再铺向全组织,这样才是对于你的组织最有效的 OKR 变革扩散。而且,**一定要能确保 OKR 在试点团队获得成功,如果连试点都失败,OKR 这种新兴的目标管理方法,想在组织中全面推动几乎是不可能的** 。 ### 小结 diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25413\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25413\350\256\262.md" index c3fa3a9b1..3d040b49c 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25413\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25413\350\256\262.md" @@ -12,7 +12,7 @@ ![Drawing 0.png](assets/CgqCHl_HVz6AE74tAABZREKSNJY393.png) -**使用场景** :按照组织中绩效制定节奏,在开始制定或是过程中需要新增 OKR 时使用该模板(这里所提供的模板均是基于季度的 OKR 制定节奏来呈现的,下同)。 **操作流程** :先确定方向 O,再生成每个 O 的 KR,所有 OKR 都需要上下级共识(\[可参考 08 课时 OKR 制定流程\],我列举了详细案例说明)。 **注意点** : +**使用场景** :按照组织中绩效制定节奏,在开始制定或是过程中需要新增 OKR 时使用该模板(这里所提供的模板均是基于季度的 OKR 制定节奏来呈现的,下同)。**操作流程** :先确定方向 O,再生成每个 O 的 KR,所有 OKR 都需要上下级共识(\[可参考 08 课时 OKR 制定流程\],我列举了详细案例说明)。**注意点** : - O 和 KR 要遵循自上往下以及自下而上的生成规律(\[参考 04 课时\]); - O 和 KR 的制定需要遵循我先前讲过的原则(\[具体参考 05 课时\]、\[06 课时\]); @@ -24,7 +24,7 @@ ![Drawing 1.png](assets/Ciqc1F_HV0qASM8oAAB_ZwsxnGA883.png) -**使用场景** :每天站会时使用该 OKR 物理看板。 **操作流程** :首先在团队工作现场搭建该物理看板,然后在团队内制定使用该看板的规则,也就是每日固定时间,来看板前基于每日站会三问过每个工作任务的进展。 **注意点** : +**使用场景** :每天站会时使用该 OKR 物理看板。**操作流程** :首先在团队工作现场搭建该物理看板,然后在团队内制定使用该看板的规则,也就是每日固定时间,来看板前基于每日站会三问过每个工作任务的进展。**注意点** : - 团队基于看板的每日站会,规则一旦制定必须刚性执行,否则看板就白搭了; - 建议通过横向泳道区分出不同的项目或业务; @@ -36,7 +36,7 @@ ![Drawing 2.png](assets/Ciqc1F_HV1OAfuobAABSqW0ub0E809.png) -**使用场景** :每周写周报时。 **操作流程** :O 和 KR 都是制定时生成的,可以直接复制到周报里,但需要更新每个 KR 的进展和信心指数。遇到的问题、阻碍越多,信心指数就标注越低,对应着 KR 下方的问题&风险就要重点描述,反之信心指数越高,说明完成该 KR 没有阻碍,问题&风险可以写无。除此之外,周报中还需要体现完成每个 KR 所做的日常工作,包括本周所做工作(较详细)以及下周工作计划(简写)。 **注意点** : +**使用场景** :每周写周报时。**操作流程** :O 和 KR 都是制定时生成的,可以直接复制到周报里,但需要更新每个 KR 的进展和信心指数。遇到的问题、阻碍越多,信心指数就标注越低,对应着 KR 下方的问题&风险就要重点描述,反之信心指数越高,说明完成该 KR 没有阻碍,问题&风险可以写无。除此之外,周报中还需要体现完成每个 KR 所做的日常工作,包括本周所做工作(较详细)以及下周工作计划(简写)。**注意点** : - 1)2)3)对应的就是每日站会上的工作任务; - 彻底完成的 KR,建议在周报中可以用灰色来标记; @@ -48,7 +48,7 @@ ![Drawing 3.png](assets/CgqCHl_HV1yADnmRAABryUxGza8525.png) -**使用场景** :过程中对 OKR 完成情况盘点时使用该模板,盘点节奏可以每月或者在一个季度的季中来进行。 **操作流程** :盘点时,个人要更新完 OKR 的整体进度,然后主动约上级时间进行 OKR 盘点,或者上级主动发起,来组织整个团队的过程盘点。 **注意点** : +**使用场景** :过程中对 OKR 完成情况盘点时使用该模板,盘点节奏可以每月或者在一个季度的季中来进行。**操作流程** :盘点时,个人要更新完 OKR 的整体进度,然后主动约上级时间进行 OKR 盘点,或者上级主动发起,来组织整个团队的过程盘点。**注意点** : - OKR 过程中的盘点,一定要正式化,也就是上级管理者必须要参与; - 盘点时发现的问题,先做现场共识,及时记录并做后续跟进; @@ -60,7 +60,7 @@ ![Drawing 4.png](assets/Ciqc1F_HV2SAR-cKAABXaMKPNfE009.png) -**使用场景** :组织中绩效闭环管理时。 **操作流程** :一般由 HR 侧发起整个 OKR 绩效闭环评估,团队中个体先进行自评,然后拉起 OKR 实现过程的相关方,对每个 KR 进行评分,O 的得分自动计算(参考\[08 课时 OKR 闭环评分中的 O 计算公式\])。 **注意点** : +**使用场景** :组织中绩效闭环管理时。**操作流程** :一般由 HR 侧发起整个 OKR 绩效闭环评估,团队中个体先进行自评,然后拉起 OKR 实现过程的相关方,对每个 KR 进行评分,O 的得分自动计算(参考\[08 课时 OKR 闭环评分中的 O 计算公式\])。**注意点** : - OKR 实现过程的依赖方,需要参与进行评分,并给予评估意见; - 上级必须要对下属进行评分,给予评估意见,并关注其他相关方评估意见; diff --git "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25414\350\256\262.md" "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25414\350\256\262.md" index b36ac8c4a..c647c5692 100644 --- "a/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25414\350\256\262.md" +++ "b/docs/Other/OKR \347\273\204\347\273\207\346\225\217\346\215\267\347\233\256\346\240\207\345\222\214\347\273\251\346\225\210\347\256\241\347\220\206/\347\254\25414\350\256\262.md" @@ -20,13 +20,13 @@ 从目标制定,到最终获得绩效,在这个过程中,还有很多影响绩效获得的关键变量,这些变量包括哪些呢? -组织中的目标,一定源于 **战略** 。战略的制定,会带来两个方面的变化,要么产生了基于现有业务的增长目标,要么产生了基于新业务的创新目标,所以战略和源于战略的 **变革** ,影响了一个组织目标的制定。 +组织中的目标,一定源于 **战略** 。战略的制定,会带来两个方面的变化,要么产生了基于现有业务的增长目标,要么产生了基于新业务的创新目标,所以战略和源于战略的 **变革**,影响了一个组织目标的制定。 -目标设定之后,承接目标的首先是 **组织结构** ,而不是人。通过组织结构,我们把实现目标的责任和权力,放到结构对应角色上,然后通过分工再放到人的身上。 +目标设定之后,承接目标的首先是 **组织结构**,而不是人。通过组织结构,我们把实现目标的责任和权力,放到结构对应角色上,然后通过分工再放到人的身上。 这个时候确定的组织结构,会带来上下层级和左右职能的分工,这些设计会影响绩效的产出。而一旦目标和组织结构设定完成,是通过 **管理手段** 来实现目标的,所以管理的目的就是去高效取得绩效结果。 -有了管理,就有管理者和被管理者,这两种角色的行为,形成了组织的 **文化** ,而文化则反过来深深影响所有人的行为。管理者的 **领导能力** 和 **激励风格** ,是能否激活团队的核心,也会影响被管理团队的行为,而被管理的一线 **团队** 的分工合作最终产出绩效。 +有了管理,就有管理者和被管理者,这两种角色的行为,形成了组织的 **文化**,而文化则反过来深深影响所有人的行为。管理者的 **领导能力** 和 **激励风格**,是能否激活团队的核心,也会影响被管理团队的行为,而被管理的一线 **团队** 的分工合作最终产出绩效。 所以,组织中的 **战略、变革、组织结构、管理、激励、领导、团队和文化** 这八个没有变的核心变量,深深影响着每个目标的最终绩效达成。 @@ -38,9 +38,9 @@ 如果你打开本专栏的课程目录,再仔细从上往下再浏览一遍就可以发现,我就是围绕 OKR 是如何提升组织中的各项能力,在设计本专栏课时结构。 -在模块一,全局中,我介绍了 **OKR 与战略的关系** ,让 OKR 这种新的目标管理方法要能和组织的战略管理融合。因为有 **组织结构层级的存在** ,所以在该模块,我还介绍了如何用 OKR 来做好各个层级的目标对齐和生成,确保战略能在多层级的组织中落地。 +在模块一,全局中,我介绍了 **OKR 与战略的关系**,让 OKR 这种新的目标管理方法要能和组织的战略管理融合。因为有 **组织结构层级的存在**,所以在该模块,我还介绍了如何用 OKR 来做好各个层级的目标对齐和生成,确保战略能在多层级的组织中落地。 -在模块二,实操中,我详细介绍了 O 和 KR 的写法方法论,有了方法论的指导可以提高我们在写 OKR 时的效率,也就提高了组织中绩效制定的效率,而组织管理就一个目的,就是如何去高效地实现组织绩效。所以, **掌握 O 和 KR 的实操写法,就可以实打实地提高我们做绩效管理的能力** 。 +在模块二,实操中,我详细介绍了 O 和 KR 的写法方法论,有了方法论的指导可以提高我们在写 OKR 时的效率,也就提高了组织中绩效制定的效率,而组织管理就一个目的,就是如何去高效地实现组织绩效。所以,**掌握 O 和 KR 的实操写法,就可以实打实地提高我们做绩效管理的能力** 。 在模块三,落地中,我讲解了如何通过 OKR 来细化组织中 **目标管理的流程,如何通过 OKR 来激励团队** (还涉及了 **领导力** ),如何通过 OKR 来建设组织中优秀的 **文化,以及如何通过 OKR 的变革来提高组织变革** 的能力 **。** 可以看到,OKR 落地过程,提升了组织中流程管理、激励、领导、文化和变革建设的能力。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25400\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25400\350\256\262.md" index 62b5c3257..b165838a5 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25400\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25400\350\256\262.md" @@ -36,7 +36,7 @@ - **第二部分:构建效率。** 如果说开发是我们日常投入最多的工作,那么等待构建结果就是日常耗费最多的非开发时间了。在这一模块,我会分析那些影响到 webpack 构建时间的关键因素,并详细分析对应的解决方案和工具。此外,我还会进一步讲解 webpack 5 中新的效率提升方案,并带你了解 no-bundle 类构建工具的优缺点。希望通过这些内容的学习,来帮助你建立完整的构建工具优化思路,进一步优化你的项目构建效率 **,** 最大程度消灭那些无谓的等待时间。 - **第三部分:部署效率。** 代码从构建到部署是前端能力的延伸。许多企业日常工作中的代码部署使用的是前后端通用的 CI/CD 系统,而前端开发人员在使用过程中较少能对其中的流程效率进行优化。在这一模块,我将介绍那些业界常用的 CI/CD 系统 ,并分析其中前端项目的效率优化点,以及从打包机方案到容器化方案、前端项目在部署时的注意点和优化空间。 希望学习完这部分内容,你能结合所在企业的技术特点,来打造或优化适合你前端项目的部署流程。 -### 你将获得 **全面、系统的效率工程化知识体系** 。我会带你系统学习相关知识,而不是碎片化获取,让你补全短板,提升个人技术实力。 **对实际项目输出针对性优化方案的能力** 。正确的方法比努力更重要,有了正确的思路方法,才能在实际工作中快速定位症结、避免跑偏,避免把力气花在一些细枝末节上。 **丰富的实战经验分享** 。我将从常用的开发效率提升工具讲到 webpack 底层的技术细节,再到部署工具中的效率优化分析,高度还原真实的业务场景,带你了解前端效率工程优化的全过程。 **面试 Offer 收割利器** 。课程中的许多案例,都是前端工程化方向面试题的重灾区,我将指出容易被忽略的内容考点,让你既能在整体上对效率工程化有一个由点到面的认识,也能深入掌握关键的技术细节 +### 你将获得 **全面、系统的效率工程化知识体系** 。我会带你系统学习相关知识,而不是碎片化获取,让你补全短板,提升个人技术实力。**对实际项目输出针对性优化方案的能力** 。正确的方法比努力更重要,有了正确的思路方法,才能在实际工作中快速定位症结、避免跑偏,避免把力气花在一些细枝末节上。**丰富的实战经验分享** 。我将从常用的开发效率提升工具讲到 webpack 底层的技术细节,再到部署工具中的效率优化分析,高度还原真实的业务场景,带你了解前端效率工程优化的全过程。**面试 Offer 收割利器** 。课程中的许多案例,都是前端工程化方向面试题的重灾区,我将指出容易被忽略的内容考点,让你既能在整体上对效率工程化有一个由点到面的认识,也能深入掌握关键的技术细节 ### 作者寄语 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25401\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25401\350\256\262.md" index 48d478a5a..b8bae2ad8 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25401\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25401\350\256\262.md" @@ -8,13 +8,13 @@ 1. 一个完备的项目基础环境就像一个精密的仪器,只有各部分都充分协调后才能运转正常。要在较短时间内配置一个技术栈完整、辅助功能丰富、兼顾不同环境下构建优化目标的项目基础代码,通常需要开发人员在工程领域长久的知识储备与实践总结,而这对于经验相对较少的开发人员而言是一个不小的挑战。 1. 不同的项目需求和团队情况,对应我们在使用基础设施时的选择可能也各不相同,因此我们并不能依靠一套固定不变的模板,而是需要根据不同的现状来使用不同的基础设施。这又增加了整体时间成本。 -而 **脚手架工具** ,正是为了解决这些问题而诞生的。 +而 **脚手架工具**,正是为了解决这些问题而诞生的。 - 利用脚手架工具,我们可以经过几个简单的选项 **快速生成** 项目的基础代码。 -- 使用脚手架工具生成的项目模板通常是经过经验丰富的开发者提炼和检验的,很大程度上代表某一类项目开发的 **最佳实践** ,相较于让开发者自行配置提供了更优选择。 -- 同时,脚手架工具也支持使用 **自定义模板** ,我们也可以根据项目中的实际经验总结、定制一个脚手架模板。 +- 使用脚手架工具生成的项目模板通常是经过经验丰富的开发者提炼和检验的,很大程度上代表某一类项目开发的 **最佳实践**,相较于让开发者自行配置提供了更优选择。 +- 同时,脚手架工具也支持使用 **自定义模板**,我们也可以根据项目中的实际经验总结、定制一个脚手架模板。 -因此,对于一个熟练的前端工程师来说,要掌握的基本能力之一就是通过技术选型来确定所需要使用的 **技术栈** ,然后根据技术栈选择合适的 **脚手架工具** ,来做项目代码的初始化。一个合适的脚手架,可以为开发人员提供反复优化后的开发流程配置,高效地解决开发中涉及的流程问题,使得工程师能够快速上手,并提升整个开发流程的效率和体验。当然,前提是建立在选择对了脚手架工具并深入掌握其工作细节的基础上。 +因此,对于一个熟练的前端工程师来说,要掌握的基本能力之一就是通过技术选型来确定所需要使用的 **技术栈**,然后根据技术栈选择合适的 **脚手架工具**,来做项目代码的初始化。一个合适的脚手架,可以为开发人员提供反复优化后的开发流程配置,高效地解决开发中涉及的流程问题,使得工程师能够快速上手,并提升整个开发流程的效率和体验。当然,前提是建立在选择对了脚手架工具并深入掌握其工作细节的基础上。 那么下面我们先来谈谈脚手架工具究竟是什么。 @@ -24,18 +24,18 @@ ![Drawing 0.png](assets/Ciqc1F8w7KGAc5KTAFjMHp-GUzQ575.png) -而在 **软件开发领域** ,脚手架是指通过各种工具来生成项目基础代码的技术。通过脚手架工具生成后的代码,通常已包含了项目开发流程中所需的 **工作目录内的通用基础设施** ,使开发者可以方便地将注意力集中到业务开发本身。 +而在 **软件开发领域**,脚手架是指通过各种工具来生成项目基础代码的技术。通过脚手架工具生成后的代码,通常已包含了项目开发流程中所需的 **工作目录内的通用基础设施**,使开发者可以方便地将注意力集中到业务开发本身。 那么对于日常的前端开发流程来说,项目内究竟有哪些部分属于通用基础设施呢?让我们从项目创建的流程说起。对于一个前端项目来说,一般在进入开发之前我们需要做的准备有: -1. 首先我们需要有 **package.json** ,它是 npm 依赖管理体系下的基础配置文件。 -1. 然后 **选择使用 npm 或 Yarn 作为包管理器** ,这会在项目里添加上对应的 **lock 文件** ,来确保在不同环境下部署项目时的依赖稳定性。 - 3. **确定项目技术栈** ,团队习惯的技术框架是哪种?使用哪一种数据流模块?是否使用 TypeScript?使用哪种 CSS 预处理器?等等。在明确选择后安装相关依赖包并在 **src** 目录中建立入口源码文件。 - 4. **选择构建工具** ,目前来说,构建工具的主流选择还是 webpack (除非项目已先锋性地考虑尝试 nobundle 方案),对应项目里就需要增加相关的 **webpack 配置文件** ,可以考虑针对开发/生产环境使用不同配置文件。 - 5. **打通构建流程** ,通过安装与配置各种 **Loader 、插件和其他配置项** ,来确保开发和生产环境能正常构建代码和预览效果。 - 6. **优化构建流程** ,针对开发/生产环境的不同特点进行各自优化。例如,开发环境更关注构建效率和调试体验,而生产环境更关注访问性能等。 - 7. **选择和调试辅助工具** ,例如代码检查工具和单元测试工具,安装相应依赖并调试配置文件。 -1. 最后是 **收尾工作** ,检查各主要环节的脚本是否工作正常,编写说明文档 README.md,将不需要纳入版本管理的文件目录记入 .gitignore 等。 +1. 首先我们需要有 **package.json**,它是 npm 依赖管理体系下的基础配置文件。 +1. 然后 **选择使用 npm 或 Yarn 作为包管理器**,这会在项目里添加上对应的 **lock 文件**,来确保在不同环境下部署项目时的依赖稳定性。 + 3. **确定项目技术栈**,团队习惯的技术框架是哪种?使用哪一种数据流模块?是否使用 TypeScript?使用哪种 CSS 预处理器?等等。在明确选择后安装相关依赖包并在 **src** 目录中建立入口源码文件。 + 4. **选择构建工具**,目前来说,构建工具的主流选择还是 webpack (除非项目已先锋性地考虑尝试 nobundle 方案),对应项目里就需要增加相关的 **webpack 配置文件**,可以考虑针对开发/生产环境使用不同配置文件。 + 5. **打通构建流程**,通过安装与配置各种 **Loader 、插件和其他配置项**,来确保开发和生产环境能正常构建代码和预览效果。 + 6. **优化构建流程**,针对开发/生产环境的不同特点进行各自优化。例如,开发环境更关注构建效率和调试体验,而生产环境更关注访问性能等。 + 7. **选择和调试辅助工具**,例如代码检查工具和单元测试工具,安装相应依赖并调试配置文件。 +1. 最后是 **收尾工作**,检查各主要环节的脚本是否工作正常,编写说明文档 README.md,将不需要纳入版本管理的文件目录记入 .gitignore 等。 正如下面简单的示例项目模板,经历了上面这些步骤后我们的项目目录下就新增了这些相关的文件: @@ -109,9 +109,9 @@ Vue CLI 工具在设计上吸取了 CRA 工具的教训,在保留了创建项 ### 了解脚手架模板中的技术细节 -刚上手开发项目时,我们通过上述脚手架提供的开箱即用的能力可以很容易地上手开发项目,但是往往在开发过程中遇到问题时又需要回过头来查询文档,看脚手架中是否已有相应解决方案。而 **如果我们对该脚手架足够熟悉** ,就能 **减少这类情况下所花费的时间** , **提升开发效率** 。所以在这里,我们先来聊一下该如何了解一个脚手架。 +刚上手开发项目时,我们通过上述脚手架提供的开箱即用的能力可以很容易地上手开发项目,但是往往在开发过程中遇到问题时又需要回过头来查询文档,看脚手架中是否已有相应解决方案。而 **如果我们对该脚手架足够熟悉**,就能 **减少这类情况下所花费的时间**,**提升开发效率** 。所以在这里,我们先来聊一下该如何了解一个脚手架。 -要了解一个脚手架,除了学会如何使用脚手架来创建项目外,我们还需要了解它提供的 **具体功能边界** ,提供了 **哪些功能、哪些优化** 。这样我们才能在后续的开发中更得心应手,后续的优化也更有的放矢。 +要了解一个脚手架,除了学会如何使用脚手架来创建项目外,我们还需要了解它提供的 **具体功能边界**,提供了 **哪些功能、哪些优化** 。这样我们才能在后续的开发中更得心应手,后续的优化也更有的放矢。 还是以上面的 CRA 和 Vue CLI 为例,除了通过脚手架模板生成项目之外,项目内部分别使用 react-scripts 和 vue-cli-service 作为开发流程的集成工具。接下来,我们先来对比下这两个工具在开发与生产环境命令中都使用了哪些配置项,其中一些涉及效率的优化项在后面的课程中还会详细介绍。 @@ -155,7 +155,7 @@ Vue CLI 工具在设计上吸取了 CRA 工具的教训,在保留了创建项 脚手架模板在 Yeoman 中对应的是生成器 (Generator)。作为主打自由制作和分享脚手架生成器的开源工具, Yeoman 为制作生成器提供了丰富的 API 和 [详细的文档](https://yeoman.io/authoring/index.html)。在这里,我们简单概述一下,一个基本的复制已有项目模板的生成器包含了: -1. 生成器描述文件 **package.json** ,其中限定了 name、file、keywords 等对应字段的规范赋值。 +1. 生成器描述文件 **package.json**,其中限定了 name、file、keywords 等对应字段的规范赋值。 1. 作为主体的 **generators/app** 目录,包含生成器的核心文件。该目录是执行 yo 命令时的默认查找目录, Yeoman 支持多目录的方式集成多个子生成器,篇幅原因我就不在这里展开了。 3. **app/index.js** 是生成器的核心控制模块,其内容是导出一个继承自 yeoman-generator 的类,并由后者提供运行时上下文、用户交互、生成器组合等功能。 4. **app/templates/** 目录是我们需要复制到新项目中的脚手架模板目录。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25402\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25402\350\256\262.md" index 12c01859b..35bd5900f 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25402\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25402\350\256\262.md" @@ -58,7 +58,7 @@ package.json } ``` -当我们执行 npm run build:watch,webpack 同样执行一次打包过程,但在打包结束后并未退出当前进程,而是继续 **监控源文件内容是否发生变化** ,当源文件发生变更后将再次执行该流程,直到用户主动退出(除了在配置文件中加入参数外,也可以在 webpack 命令中增加 --watch 来实现)。 +当我们执行 npm run build:watch,webpack 同样执行一次打包过程,但在打包结束后并未退出当前进程,而是继续 **监控源文件内容是否发生变化**,当源文件发生变更后将再次执行该流程,直到用户主动退出(除了在配置文件中加入参数外,也可以在 webpack 命令中增加 --watch 来实现)。 有了 watch 模式之后,我们在开发时就不用每次手动执行打包脚本了。但问题并未解决,为了看到执行效果,我们需要在浏览器中进行预览,但在预览时我们会发现,即使产物文件发生了变化,在浏览器里依然需要手动点击刷新才能看到变更后的效果。那么这个问题又该如何解决呢? @@ -167,7 +167,7 @@ webpack 的打包思想可以简化为 3 点: 1. 一切源代码文件均可通过各种 Loader 转换为 JS 模块 ( **module** ),模块之间可以互相引用。 1. webpack 通过入口点( **entry point** )递归处理各模块引用关系,最后输出为一个或多个产物包 js(bundle) 文件。 -1. 每一个入口点都是一个块组( **chunk group** ),在不考虑分包的情况下,一个 chunk group 中只有一个 **chunk** ,该 chunk 包含递归分析后的所有模块。每一个 chunk 都有对应的一个打包后的输出文件( **asset/bundle** )。 +1. 每一个入口点都是一个块组( **chunk group** ),在不考虑分包的情况下,一个 chunk group 中只有一个 **chunk**,该 chunk 包含递归分析后的所有模块。每一个 chunk 都有对应的一个打包后的输出文件( **asset/bundle** )。 ![4.png](assets/CgqCHl82OaWAMXDLAABdNTOV1dY952.png) @@ -216,7 +216,7 @@ module.hot.dispose(function() { 上面的 module.hot 实际上是一个来自 webpack 的基础插件 HotModuleReplacementPlugin,该插件作为热替换功能的基础插件,其 API 方法导出到了 module.hot 的属性中。 -在上面代码的两个 API 中, **hot.accept** 方法传入依赖模块名称和回调方法,当依赖模块发生更新时,其回调方法就会被执行,而开发者就可以在回调中实现对应的替换逻辑,即上面的用更新的样式替换原标签中的样式。另一个 **hot.dispose** 方法则是传入一个回调,当代码上下文的模块被移除时,其回调方法就会被执行。例如当我们在源代码中移除导入的 CSS 模块时,运行时原有的模块中的 update() 就会被执行,从而在页面移除对应的 style 标签。 +在上面代码的两个 API 中,**hot.accept** 方法传入依赖模块名称和回调方法,当依赖模块发生更新时,其回调方法就会被执行,而开发者就可以在回调中实现对应的替换逻辑,即上面的用更新的样式替换原标签中的样式。另一个 **hot.dispose** 方法则是传入一个回调,当代码上下文的模块被移除时,其回调方法就会被执行。例如当我们在源代码中移除导入的 CSS 模块时,运行时原有的模块中的 update() 就会被执行,从而在页面移除对应的 style 标签。 module.hot 中还包含了该插件提供的其他热更新相关的 API 方法,这里就不再赘述了,感兴趣的同学可以从 [官方文档](https://webpack.js.org/api/hot-module-replacement/)中进一步了解。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25403\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25403\350\256\262.md" index aeb13f3e9..e73f28593 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25403\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25403\350\256\262.md" @@ -10,14 +10,14 @@ ![Drawing 0.png](assets/Ciqc1F85_FmAA4UeAABWNiHqsWQ595.png) -因此,我们需要一种在调试时将产物代码显示回源代码的功能, **source map** 就是实现这一目标的工具。 +因此,我们需要一种在调试时将产物代码显示回源代码的功能,**source map** 就是实现这一目标的工具。 source-map 的基本原理是,在编译处理的过程中,在生成产物代码的同时生成产物代码中被转换的部分与源代码中相应部分的映射关系表。有了这样一张完整的映射表,我们就可以通过 Chrome 控制台中的"Enable Javascript source map"来实现调试时的显示与定位源代码功能。 对于同一个源文件,根据不同的目标,可以生成不同效果的 source map。它们在 **构建速度** 、 **质量** (反解代码与源代码的接近程度以及调试时行号列号等辅助信息的对应情况)、 **访问方式** (在产物文件中或是单独生成 source map 文件)和 **文件大小** 等方面各不相同。在开发环境和生产环境下,我们对于 source map 功能的期望也有所不同: -- **在开发环境中** ,通常我们关注的是构建速度快,质量高,以便于提升开发效率,而不关注生成文件的大小和访问方式。 -- **在生产环境中** ,通常我们更关注是否需要提供线上 source map , 生成的文件大小和访问方式是否会对页面性能造成影响等,其次才是质量和构建速度。 +- **在开发环境中**,通常我们关注的是构建速度快,质量高,以便于提升开发效率,而不关注生成文件的大小和访问方式。 +- **在生产环境中**,通常我们更关注是否需要提供线上 source map , 生成的文件大小和访问方式是否会对页面性能造成影响等,其次才是质量和构建速度。 ### Webpack 中的 source map 预设 @@ -64,8 +64,8 @@ if (options.devtool.includes("source-map")) { - **false** :即不开启 source map 功能,其他不符合上述规则的赋值也等价于 false。 - **eval** :是指在编译器中使用 EvalDevToolModulePlugin 作为 source map 的处理插件。 - **[xxx-...]source-map** :根据 devtool 对应值中是否有 **eval** 关键字来决定使用 EvalSourceMapDevToolPlugin 或 SourceMapDevToolPlugin 作为 source map 的处理插件,其余关键字则决定传入到插件的相关字段赋值。 -- **inline** :决定是否传入插件的 filename 参数,作用是决定单独生成 source map 文件还是在行内显示, **该参数在 eval- 参数存在时无效** 。 -- **hidden** :决定传入插件 append 的赋值,作用是判断是否添加 SourceMappingURL 的注释, **该参数在 eval- 参数存在时无效** 。 +- **inline** :决定是否传入插件的 filename 参数,作用是决定单独生成 source map 文件还是在行内显示,**该参数在 eval- 参数存在时无效** 。 +- **hidden** :决定传入插件 append 的赋值,作用是判断是否添加 SourceMappingURL 的注释,**该参数在 eval- 参数存在时无效** 。 - **module** :为 true 时传入插件的 module 为 true ,作用是为加载器(Loaders)生成 source map。 - **cheap** :这个关键字有两处作用。首先,当 module 为 false 时,它决定插件 module 参数的最终取值,最终取值与 cheap 相反。其次,它决定插件 columns 参数的取值,作用是决定生成的 source map 中是否包含列信息,在不包含列信息的情况下,调试时只能定位到指定代码所在的行而定位不到所在的列。 - **nosource** :nosource 决定了插件中 noSource 变量的取值,作用是决定生成的 source map 中是否包含源代码信息,不包含源码情况下只能显示调用堆栈信息。 @@ -122,7 +122,7 @@ if (options.devtool.includes("source-map")) { - 通常来说,开发环境首选哪一种预设取决于 source map 对于我们的帮助程度。 - 如果对项目代码了如指掌,查看产物代码也可以无障碍地了解对应源代码的部分,那就可以关闭 devtool 或使用 eval 来获得最快构建速度。 -- 反之如果在调试时,需要通过 source map 来快速定位到源代码,则优先考虑使用 **eval-cheap-modulesource-map** ,它的质量与初次/再次构建速度都属于次优级,以牺牲定位到列的功能为代价换取更快的构建速度通常也是值得的。 +- 反之如果在调试时,需要通过 source map 来快速定位到源代码,则优先考虑使用 **eval-cheap-modulesource-map**,它的质量与初次/再次构建速度都属于次优级,以牺牲定位到列的功能为代价换取更快的构建速度通常也是值得的。 - 在其他情况下,根据对质量要求更高或是对速度要求更高的不同情况,可以分别考虑使用 **eval-source-map** 或 **eval-cheap-source-map** 。 了解了开发环境下如何选择 source map 预设后,我们再来补充几种工具和脚手架中的默认预设: diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25404\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25404\350\256\262.md" index e01c4e286..a0227bf60 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25404\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25404\350\256\262.md" @@ -41,7 +41,7 @@ [Mock.js](https://github.com/nuysoft/Mock) 是前端领域流行的 Mock 数据生成工具之一,后续许多功能更丰富的工具和系统在各自的 Mock 功能部分都将它作为基础设施。 -Mock.js 的核心能力是定义了两类生成模拟数据的规范: **数据模板定义规范** (Data Template Definition, DTD)与 **数据占位符定义规范** (Data Placeholder Definition, DPD),以及实现了应用相应规范生成模拟数据的方法。 **数据模板定义规范(DTD)** **数据模板定义规范** 约定了可以通过“属性名|生成规则:属性值”这样的格式来生成模拟数据,例如(完整示例代码参见 [04_mock](https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/04_mock)): +Mock.js 的核心能力是定义了两类生成模拟数据的规范: **数据模板定义规范** (Data Template Definition, DTD)与 **数据占位符定义规范** (Data Placeholder Definition, DPD),以及实现了应用相应规范生成模拟数据的方法。**数据模板定义规范(DTD)** **数据模板定义规范** 约定了可以通过“属性名|生成规则:属性值”这样的格式来生成模拟数据,例如(完整示例代码参见 [04_mock](https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/04_mock)): ```plaintext Mock.mock({ @@ -58,7 +58,7 @@ Mock.mock({ //Result: str为1-100个随机长度的字符串'1'。例如{str: '11111'} ``` -从上面的例子可以看到,属性名只是作为生成数据的固定名称,而同样的生成规则下,随着属性值的不同,生成规则对应的内部处理逻辑也不同。在 Mock.js 中,共定义了 7 种生成规则:min-max、min-max.dmin-dmax、min-max.dcount、count、count.dmin-dmax、count.dcount、+step。根据这 7 种规则,再结合不同数据类型的属性值,就可以定义出任意我们所需要的随机数据生成逻辑。 **数据占位符定义规范 (DPD)** **数据占位符定义规范** 则是对于随机数据的一系列常用类型预设,书写格式是'@占位符(参数 \[, 参数\] )'。如以下例子: +从上面的例子可以看到,属性名只是作为生成数据的固定名称,而同样的生成规则下,随着属性值的不同,生成规则对应的内部处理逻辑也不同。在 Mock.js 中,共定义了 7 种生成规则:min-max、min-max.dmin-dmax、min-max.dcount、count、count.dmin-dmax、count.dcount、+step。根据这 7 种规则,再结合不同数据类型的属性值,就可以定义出任意我们所需要的随机数据生成逻辑。**数据占位符定义规范 (DPD)** **数据占位符定义规范** 则是对于随机数据的一系列常用类型预设,书写格式是'@占位符(参数 \[, 参数\] )'。如以下例子: ```plaintext Mock.mock('@email') @@ -71,7 +71,7 @@ Random.image('200x100', '#894FC4', '#FFF', 'png', '!') //Result: 利用dummyimage库生成的图片url, "http://dummyimage.com/200x100/894FC4/FFF.png" ``` -从这些例子中可以看到,占位符既可以用于单独返回指定类型的随机数据,又能结合数据模板,作为模板中属性值的部分来生成更复杂的数据类型。Mock.js 中定义了 9 大类共 42 种占位符,相关更多占位符的说明和示例可以从官网中查找和使用。 **其他功能** 除了提供生成模拟数据的规范和方法外,Mock.js 还提供了一些辅助功能,包括: +从这些例子中可以看到,占位符既可以用于单独返回指定类型的随机数据,又能结合数据模板,作为模板中属性值的部分来生成更复杂的数据类型。Mock.js 中定义了 9 大类共 42 种占位符,相关更多占位符的说明和示例可以从官网中查找和使用。**其他功能** 除了提供生成模拟数据的规范和方法外,Mock.js 还提供了一些辅助功能,包括: 1. **Ajax 请求拦截** :Mock.mock 方法中支持传入 Ajax 请求的 url 和 type,用于拦截特定 url 的请求,直接将模拟数据作为响应值返回。这一功能方便我们在项目本地中使用 Mock 数据做调试,其原理是覆盖了原生的 XMLHttpRequest 对象,因此对于使用 fetch 作为接口请求的 API 的项目还不能适用。此外,提供了 Mock.setup 方法来设置拦截 Ajax 请求后的响应时间。 2. **数据验证** :Mock.valid 方法,验证指定数据和数据模板是否匹配。这一功能可以用于验证后端 API 接口的返回值与对应 Mock 数据的规则描述是否冲突。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25405\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25405\350\256\262.md" index f28fca215..1f77c6b5c 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25405\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25405\350\256\262.md" @@ -35,7 +35,7 @@ 在 **实现的功能** 方面:这三种 CSS 的预处理语言都实现了变量(Variables)、嵌套(Nesting)、混合 (Mixins)、运算(Operators)、父选择器引用(Parent Reference)、扩展(Extend)和大量内建函数(Build-in Functions)。但是与另外两种语言相比,Less 缺少自定义函数的功能(可以使用 Mixins 结合 Guard 实现类似效果),而 Stylus 提供了超过 60 个内建函数,更有利于编写复杂的计算函数。 -在 **语法方面** : **Sass** 支持 .scss 与 .sass 两种文件格式。差异点是 .scss 在语法上更接近 CSS,需要括号、分号等标识符,而 Sass 相比之下,在语法上做了简化,去掉了 CSS 规则的括号分号等 (增加对应标识符会导致报错) 。 **Less** 的整体语法更接近 .scss。 **Stylus** 则同时支持类似 .sass 的精简语法和普通 CSS 语法。语法细节上也各不相同,示例如下: +在 **语法方面** : **Sass** 支持 .scss 与 .sass 两种文件格式。差异点是 .scss 在语法上更接近 CSS,需要括号、分号等标识符,而 Sass 相比之下,在语法上做了简化,去掉了 CSS 规则的括号分号等 (增加对应标识符会导致报错) 。**Less** 的整体语法更接近 .scss。**Stylus** 则同时支持类似 .sass 的精简语法和普通 CSS 语法。语法细节上也各不相同,示例如下: ```plaintext //scss @@ -65,7 +65,7 @@ div #### Pug -对于 HTML 模板的预处理语言选择而言,目前主流的是[Pug](https://pugjs.org/) **** (这里考虑的是预处理语言对于效率的提升,因此一些纯模板语言,如 **EJS** ,则不在讨论范围内。此外,基于其他技术栈的模板语言,例如 Ruby 的 **Haml** 和 **Slim** ,在前端工程化中的应用也并不多,因此也不在这里讨论)。 +对于 HTML 模板的预处理语言选择而言,目前主流的是[Pug](https://pugjs.org/) **** (这里考虑的是预处理语言对于效率的提升,因此一些纯模板语言,如 **EJS**,则不在讨论范围内。此外,基于其他技术栈的模板语言,例如 Ruby 的 **Haml** 和 **Slim**,在前端工程化中的应用也并不多,因此也不在这里讨论)。 Pug 的前身名叫 **Jade** (2010),2016 年时因为和同名软件商标冲突而改名为了 Pug。语法示例如下: @@ -184,7 +184,7 @@ bdrs1e => border-radius: 1em; Html 语言在如今组件化的前端项目中是作为一个组件的模板存在的。而编写组件模板与纯 Html 的区别在于,组件模板中通常已经由框架提供了 **数据注入** (Interpolation)以及循环、条件等语法,组件化本身也解决了包含、混入等代码复用的问题。因此,在使用提效工具时,我们用到的主要还是简化标签书写的功能,而不太涉及工具本身提供的上述逻辑功能(混用逻辑功能可能反而导致代码的混乱和调试的困难)。当然,简化标签书写既可以选择使用 Pug 语言,也可以使用 Emmet。 -使用 Pug 的好处主要在于,对于习惯书写带缩进的 html 标签的同学而言 **上手更快** ,迁移成本低,且整体上阅读体验会更好一些。 +使用 Pug 的好处主要在于,对于习惯书写带缩进的 html 标签的同学而言 **上手更快**,迁移成本低,且整体上阅读体验会更好一些。 而 Emmet 则相反,取消缩进后作为替代需要通过关系标识符来作为连接,书写习惯上 **迁移成本略高** 一些,且由于是即时转换,转后代码的阅读体验与 Html 没有区别。相对而言,由于可以自定义 Snippet 来使用常用缩写,因此使用熟练后实际效率提升会更明显一些。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25406\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25406\350\256\262.md" index aaf711d92..549e002fa 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25406\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25406\350\256\262.md" @@ -39,7 +39,7 @@ 云开发模式是在上述远程开发模式的基础之上发展而来的,将开发环境托管,由远程开发服务器变更为云服务。个人电脑通过 IDE 或云服务提供的浏览器界面访问云端工作区进行开发。云开发模式在继承远程开发模式优点的基础上,更能提升效率的原因在于: 1. 通过容器化技术,将开发环境所需基础设施(应用程序、配置文件、IDE 插件、IDE 设定项等)集成到基础镜像中,大大 **提升开发环境准备的效率** 。同时,同样的基础环境也避免了相同项目不同开发集成环境导致的环境差异类问题。 -1. 通过服务化的云开发平台, **简化使用流程** ,解决个人使用远程开发时可能遇到的技术困难,使得刚入职的新人也能够快速上手。 +1. 通过服务化的云开发平台,**简化使用流程**,解决个人使用远程开发时可能遇到的技术困难,使得刚入职的新人也能够快速上手。 1. 对于团队而言,能够 **提升团队协作效率** 。云开发模式有利于流程规范的统一,有利于团队成员共享开发工具,同时支持多人访问相同开发环境,有助于结对编程等协作流程。 1. 对于公司而言,使用弹性化的云端容器环境有利于资源利用率的提升和硬件资产成本的降低。 @@ -121,7 +121,7 @@ Web #### Eclipse: Theia -[Eclipse Theia](https://theia-ide.org/) **(以下简称 Theia)** ,是 Eclipse 基金会推出的 VS Code 的替代产品,它的定位是以 NodeJS 和 TS 为技术栈开发的云端和桌面端的 IDE 基础框架,于 2017 年启动, 2018 年发布了对应的 Web 端 IDE 产品 [Gitpod](https://github.com/gitpod-io/gitpod)。 +[Eclipse Theia](https://theia-ide.org/) **(以下简称 Theia)**,是 Eclipse 基金会推出的 VS Code 的替代产品,它的定位是以 NodeJS 和 TS 为技术栈开发的云端和桌面端的 IDE 基础框架,于 2017 年启动, 2018 年发布了对应的 Web 端 IDE 产品 [Gitpod](https://github.com/gitpod-io/gitpod)。 Theia 和 VS Code 的技术相同点有: @@ -135,24 +135,24 @@ Theia 和 VS Code 的技术相同点有: - 架构上更模块化,更易于自定义。 - 从一开始就被设计成同时运行于桌面和云端。 - 由厂商中立的开源基金会开发维护。 -- 开发独立的 WebIDE 是云开发产品的 **首选** ,目前 VS Code 并未开源功能完整的 Web 版本(目前开源的 Web 版本仅可用于预览功能),但 Thiea 有开源可定制化的版本。 +- 开发独立的 WebIDE 是云开发产品的 **首选**,目前 VS Code 并未开源功能完整的 Web 版本(目前开源的 Web 版本仅可用于预览功能),但 Thiea 有开源可定制化的版本。 ### 云开发模式的技术要素 -一般来说,云开发模式依赖的技术要素主要有三个方面: **WebIDE** , **容器化** ,以及能够 **对接其他云服务** 。 +一般来说,云开发模式依赖的技术要素主要有三个方面: **WebIDE**,**容器化**,以及能够 **对接其他云服务** 。 #### WebIDE 继 VS Code 2019 年发布 Codespace 后, Eclipse 基金会于 2020 年初也发布了 Theia 1.0 版本。 WebIDE 在功能体验上已达到和桌面 IDE 相同的水平(尽管在初始化阶段会有不同程度的额外耗时)。同时, WebIDE 还具有以下优点: -- 便于 **平台化定制** :在团队使用时可通过定制 WebIDE 来实现 **通用的功能扩展和升级** ,而无须变更团队成员的桌面 IDE(例如,使用微信开发者工具软件的同学,在工具发布新版本时需要各自处理升级,而 Web 版则无须如此)。 +- 便于 **平台化定制** :在团队使用时可通过定制 WebIDE 来实现 **通用的功能扩展和升级**,而无须变更团队成员的桌面 IDE(例如,使用微信开发者工具软件的同学,在工具发布新版本时需要各自处理升级,而 Web 版则无须如此)。 - **流程体验上更平滑** :虽然基本使用仍然是打开一个包含源代码的工作空间容器进行开发,但是通过和代码仓库以及 CI/CD 工具的对接,可以在很多流程节点上做到平滑的体验(例如,测试环境下修复 Bug,可以通过工具,在查找到对应的提交版本后点击进入到 IDE 界面进行修复、测试和提交,相比于原先需要线下操作的流程而言,效率会上升一个台阶)。 #### 容器化 容器化以往在服务部署中应用较多。在云开发中的用途主要有: -- 为每个用户的每个项目创建 **独立的工作空间。** - 基于容器化的分层结构,可以方便地在基础环境、项目、用户等维度做镜像继承,便于团队成员维护相同项目时 **提升环境创建效率。** - 相比个人虚拟机,有利于 **提升资源利用率** ,同时环境搭建更便捷。 +- 为每个用户的每个项目创建 **独立的工作空间。** - 基于容器化的分层结构,可以方便地在基础环境、项目、用户等维度做镜像继承,便于团队成员维护相同项目时 **提升环境创建效率。** - 相比个人虚拟机,有利于 **提升资源利用率**,同时环境搭建更便捷。 #### 云服务对接 @@ -181,7 +181,7 @@ Theia 和 VS Code 的技术相同点有: 尽管随着 WebIDE 的兴起,越来越多的云开发产品开始呈现,但是作为一种新兴的工作模式,在尝试规模化使用前还是需要考虑到可能出现的一些问题: -1. **代码安全问题** :代码安全是首先需要考虑的问题。通常在代码仓库中我们会设置具体项目的 **访问权限** ,云开发模式下的镜像与空间访问设计上也应当注意对这部分权限的验证。此外,对于公司内部的项目,在使用云开发模式时应当首选 **支持内部部署** 的云服务或 **搭建自维护** 的云服务,而非将代码上传到外部云空间中。 +1. **代码安全问题** :代码安全是首先需要考虑的问题。通常在代码仓库中我们会设置具体项目的 **访问权限**,云开发模式下的镜像与空间访问设计上也应当注意对这部分权限的验证。此外,对于公司内部的项目,在使用云开发模式时应当首选 **支持内部部署** 的云服务或 **搭建自维护** 的云服务,而非将代码上传到外部云空间中。 2. **服务搭建与维护** :要在团队内使用云开发的功能,需要考虑服务搭建的方式和成本。对于大厂而言,云服务资源和技术建设比较丰富,搭建自维护的云开发服务可以提供更多灵活的功能;而对于中小规模的技术团队而言,购买使用一些支持内部部署的现有云开发服务是更好的选择。 3. **服务降级与备份** :由于云开发模式下将开发环境与工作代码都存储于云端,需要考虑当云端服务异常时的降级策略。例如是否有独立的环境镜像可供下载后离线使用,以及工作空间内的暂存代码是否有备份,可供独立下载使用。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25407\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25407\350\256\262.md" index 62b850274..9cc9ecef9 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25407\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25407\350\256\262.md" @@ -4,13 +4,13 @@ ### 什么是低代码开发 -**低代码开发** (Low-Code Development, **LCD** ),是一种[很早被提出(2011)](https://en.wikipedia.org/wiki/Low-code_development_platform)的开发模式,开发者 **主要通过图形化用户界面和配置** 来创建应用软件,而不是像传统模式那样 **主要依靠手写代码** 。对应的,提供给开发者的这类低代码开发功能实现的软件,称为 **低代码开发平台** (Low-Code Development Platform, **LCDP** )。低代码开发模式的开发者,通常不需要具备非常专业的编码技能,或者不需要某一专门领域的编码技能,而是可以通过平台的功能和约束来实现专业代码的产出。 +**低代码开发** (Low-Code Development,**LCD** ),是一种[很早被提出(2011)](https://en.wikipedia.org/wiki/Low-code_development_platform)的开发模式,开发者 **主要通过图形化用户界面和配置** 来创建应用软件,而不是像传统模式那样 **主要依靠手写代码** 。对应的,提供给开发者的这类低代码开发功能实现的软件,称为 **低代码开发平台** (Low-Code Development Platform,**LCDP** )。低代码开发模式的开发者,通常不需要具备非常专业的编码技能,或者不需要某一专门领域的编码技能,而是可以通过平台的功能和约束来实现专业代码的产出。 从定义中我们可以看到,低代码开发的工作方式主要依赖操作图形化的用户界面,包括拖拽控件,以及修改其中可被编辑区域的配置。这种可视化的开发方式,可以追溯到更早的 Dreamwaver 时期。而随着前端项目的日趋复杂,这种方式已不再适应现代项目的需求,于是渐渐被更专业的工程化的开发模式所取代。 但是,快速生成项目代码的诉求从未消失。人们也慢慢找到了实现这个目的的两种路径: -- 一种是在高度定制化的场景中,基于经验总结,找到那些相对固定的产品形态,例如公司介绍、产品列表、活动页面等,开放少量的编辑入口,让非专业开发者也能使用。下一课介绍的 **无代码开发** ,主要就是面向这样的场景需求。 +- 一种是在高度定制化的场景中,基于经验总结,找到那些相对固定的产品形态,例如公司介绍、产品列表、活动页面等,开放少量的编辑入口,让非专业开发者也能使用。下一课介绍的 **无代码开发**,主要就是面向这样的场景需求。 - 另一类则相反,顺着早期可视化开发的思路,尝试以组件化和数据绑定为基础,通过抽象语法或 IDE 来实现自由度更高、交互复杂度上限更高的页面搭建流程。这种项目开发方式通常需要一定的开发经验与编码能力,只是和普通编码开发方式相比,更多通过操作可视化工具的方式来达到整体效率的提升,因此被称为 **低代码开发。** 在实际场景中,尤其是商用的低代码平台产品,往往提供的是上面两种开发方式的结合。 #### 低代码开发的典型应用场景 @@ -34,7 +34,7 @@ 1. 页面组件结构模板和相应数据模型的代码组织,可以替换为 **更高效** 的 JSON 语法树描述。 -1. 通过制定用于编写的 **JSON 语法图式(JSON Schema)** ,以及封装能够渲染对应 JSON 语法树的运行时工具集,就可以提升开发效率,降低开发技术要求。 +1. 通过制定用于编写的 **JSON 语法图式(JSON Schema)**,以及封装能够渲染对应 JSON 语法树的运行时工具集,就可以提升开发效率,降低开发技术要求。 下图中的代码就是组件语法树示例(完整的示例代码参见 [07_low_code](https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/07_low_code)),我们通过编写一个简单的 JSON 语法树以及对应的编译器,来展示低代码开发的模式。 @@ -67,7 +67,7 @@ #### 可视化操作平台的基本使用方式 -绝大部分的可视化操作平台都将界面布局分为三个区域:左侧的 **组件选择区** ,中部的 **预览交互区** 以及右侧的 **属性编辑区** 。这三个区域的排布所对应的,也是用户 **生成页面的操作流程** : +绝大部分的可视化操作平台都将界面布局分为三个区域:左侧的 **组件选择区**,中部的 **预览交互区** 以及右侧的 **属性编辑区** 。这三个区域的排布所对应的,也是用户 **生成页面的操作流程** : - 首先,在左侧面板中选择组件。 - 然后,拖入中间预览区域,并放置到合适的容器块内。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25408\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25408\350\256\262.md" index 82317862c..4fdf01bb4 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25408\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25408\350\256\262.md" @@ -19,7 +19,7 @@ #### 无代码开发和低代码开发的区别 -从下面的表格中可以看到无代码开发和低代码开发的主要区别,包括目标人群、目标产品、开发模式等 7 个维度。 **区别维度** **低代码开发** **无代码开发(面向非开发)** **无代码开发(面向准开发)** **目标人群** 主要面向有一定技术基础的开发人员 +从下面的表格中可以看到无代码开发和低代码开发的主要区别,包括目标人群、目标产品、开发模式等 7 个维度。**区别维度** **低代码开发** **无代码开发(面向非开发)** **无代码开发(面向准开发)** **目标人群** 主要面向有一定技术基础的开发人员 主要面向非开发岗位人员(例如运营人员,设计人员) @@ -51,8 +51,8 @@ 其中,可以把市面上的 **无代码开发模式** 进一步 **细分为两类** : -- **一类完全面向非开发人员** ,如百度 H5,对应开发的目标产品主要是模式化的 C 端活动与其他 H5 页面类型(例如招聘页面,测试小游戏等); -- **另一类面向准开发人员** ,即缺少代码经验且希望开发全栈产品的个人或团队,在目标产品和定制功能上更全面,但是相应的学习使用成本也更高,这类产品以 iVX 为代表。 +- **一类完全面向非开发人员**,如百度 H5,对应开发的目标产品主要是模式化的 C 端活动与其他 H5 页面类型(例如招聘页面,测试小游戏等); +- **另一类面向准开发人员**,即缺少代码经验且希望开发全栈产品的个人或团队,在目标产品和定制功能上更全面,但是相应的学习使用成本也更高,这类产品以 iVX 为代表。 下面,我们就来进一步了解下这两种开发模式的异同。 @@ -60,7 +60,7 @@ #### 面向非开发人员的无代码开发产品 -这类产品的设计目标就是将一些固定类型的项目生产流程,由代码开发转变为操作图形化交互工具。 **企业内部的定制化搭建平台** 例如面向 C 端的企业经常会有推广拉新的开发需求,以红包活动为例,如果按照普通的代码开发流程,需要经历以下 6 个流程才能最终上线: +这类产品的设计目标就是将一些固定类型的项目生产流程,由代码开发转变为操作图形化交互工具。**企业内部的定制化搭建平台** 例如面向 C 端的企业经常会有推广拉新的开发需求,以红包活动为例,如果按照普通的代码开发流程,需要经历以下 6 个流程才能最终上线: 1. 产品确定活动流程,交付产品文档与原型。 1. 设计师设计页面,交付设计稿。 @@ -79,7 +79,7 @@ 这种方法通过一次开发,即可让运营人员长期重复使用,解放了后续的开发资源,并且从流程上将普通项目开发的 6 个环节简化为两个环节:设计师设计页面,以及运营人员无代码地编辑内容。这将原先以天为单位的开发部署时间,缩短为以分钟为单位的编辑生成时间。 -以上便是 **企业内部无代码开发** 的一类应用场景。 **外部无代码搭建平台** 另一类面向非开发人员的无代码开发产品,针对的是缺乏开发资源的企业和部门。对于一些常见的小型项目需求,例如招聘页面、报名页面等,它们往往需要借助 **外部提供的无代码开发平台** 。这类无代码开发平台包括百度 H5、MAKA、易企秀等。 +以上便是 **企业内部无代码开发** 的一类应用场景。**外部无代码搭建平台** 另一类面向非开发人员的无代码开发产品,针对的是缺乏开发资源的企业和部门。对于一些常见的小型项目需求,例如招聘页面、报名页面等,它们往往需要借助 **外部提供的无代码开发平台** 。这类无代码开发平台包括百度 H5、MAKA、易企秀等。 ![image](assets/Ciqc1F9R0u-ABNY5ABlh1mN5IXo611.png) diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25410\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25410\350\256\262.md" index dd6bf798d..95516638a 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25410\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25410\350\256\262.md" @@ -56,7 +56,7 @@ const webpack = (options, callback) => { compiler.run(callback) 中的执行逻辑较为复杂,我们把它按流程抽象一下。抽象后的执行流程如下: 1. **readRecords** :读取[构建记录](https://webpack.js.org/configuration/other-options/#recordspath),用于分包缓存优化,在未设置 recordsPath 时直接返回。 - 2. **compile 的主要构建过程** ,涉及以下几个环节: + 2. **compile 的主要构建过程**,涉及以下几个环节: 1. **newCompilationParams** :创建 NormalModule 和 ContextModule 的工厂实例,用于创建后续模块实例。 2. **newCompilation** :创建编译过程 Compilation 实例,传入上一步的两个工厂实例作为参数。 3. **compiler.hooks.make.callAsync** :触发 make 的 Hook,执行所有监听 make 的插件(例如 [SingleEntryPlugin.js](https://github.com/webpack/webpack/blob/webpack-4/lib/SingleEntryPlugin.js) 中,会在相应的监听中触发 compilation 的 addEntry 方法)。其中,Hook 的作用,以及其他 Hook 会在下面的小节中再谈到。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25412\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25412\350\256\262.md" index 4493c8a60..74065c4d5 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25412\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25412\350\256\262.md" @@ -101,7 +101,7 @@ function(module,exports){function HelloWorld(){var foo="1234";console.log(HelloW ![Lark20200918-161934.png](assets/Ciqc1F9kbdCALcuwAABCdtCwxuY965.png) -从结果中可以看到,当 **compress** 参数为 false 时,压缩阶段的效率有明显提升,同时对压缩的质量影响较小。在需要对压缩阶段的效率进行优化的情况下, **可以优先选择设置该参数** 。 +从结果中可以看到,当 **compress** 参数为 false 时,压缩阶段的效率有明显提升,同时对压缩的质量影响较小。在需要对压缩阶段的效率进行优化的情况下,**可以优先选择设置该参数** 。 #### 面向 CSS 的压缩工具 @@ -109,7 +109,7 @@ CSS 同样有几种压缩工具可供选择:[OptimizeCSSAssetsPlugin](https:// 这三个插件在压缩 CSS 代码功能方面,都默认基于 [cssnano](https://cssnano.co/) 实现,因此在压缩质量方面没有什么差别。 -在压缩效率方面,首先值得一提的是最新发布的 CSSMinimizerWebpackPlugin,它 **支持缓存和多进程** ,这是另外两个工具不具备的。而在非缓存的普通压缩过程方面,整体上 3 个工具相差不大,不同的参数结果略有不同,如下面的表格所示(下面结果为示例代码中 example-css 的执行构建结果)。 +在压缩效率方面,首先值得一提的是最新发布的 CSSMinimizerWebpackPlugin,它 **支持缓存和多进程**,这是另外两个工具不具备的。而在非缓存的普通压缩过程方面,整体上 3 个工具相差不大,不同的参数结果略有不同,如下面的表格所示(下面结果为示例代码中 example-css 的执行构建结果)。 ![Lark20200918-161938.png](assets/CgqCHl9kbb6AI7F5AABRRdbprbU989.png) @@ -145,7 +145,7 @@ optimization: { ... ``` -在这个示例中,有两个入口文件引入了相同的依赖包 lodash,在没有额外设置分包的情况下, lodash 被同时打入到两个产物文件中,在后续的压缩代码阶段耗时 1740ms。 **而在设置分包规则为 chunks:'all' 的情况下** ,通过分离公共依赖到单独的 Chunk,使得在后续压缩代码阶段,只需要压缩一次 lodash 的依赖包代码,从而减少了压缩时长,总耗时为 1036ms。通过下面两张图片也可以看出这样的变化。 +在这个示例中,有两个入口文件引入了相同的依赖包 lodash,在没有额外设置分包的情况下, lodash 被同时打入到两个产物文件中,在后续的压缩代码阶段耗时 1740ms。**而在设置分包规则为 chunks:'all' 的情况下**,通过分离公共依赖到单独的 Chunk,使得在后续压缩代码阶段,只需要压缩一次 lodash 的依赖包代码,从而减少了压缩时长,总耗时为 1036ms。通过下面两张图片也可以看出这样的变化。 ![Drawing 3.png](assets/Ciqc1F9kWAWANNLZAAGM4v1icLA197.png) ![Drawing 4.png](assets/CgqCHl9kWAqAELXZAAG5xisRryc225.png) @@ -161,7 +161,7 @@ optimization: { 可以看到,引入不同的依赖包(lodash vs lodash-es)、不同的引入方式,以及是否使用 babel 等,都会对 Tree Shaking 的效果产生影响。下面我们就来分析具体原因。 -1. **ES6 模块** : 首先,只有 ES6 类型的模块才能进行Tree Shaking。因为 ES6 模块的依赖关系是确定的,因此可以进行不依赖运行时的 **静态分析** ,而 CommonJS 类型的模块则不能。因此,CommonJS 类型的模块 lodash,在无论哪种引入方式下都不能实现 Tree Shaking,而需要依赖第三方提供的插件(例如 babel-plugin-lodash 等)才能实现动态删除无效代码。而 ES6 风格的模块 lodash-es,则可以进行 Tree Shaking 优化。 +1. **ES6 模块** : 首先,只有 ES6 类型的模块才能进行Tree Shaking。因为 ES6 模块的依赖关系是确定的,因此可以进行不依赖运行时的 **静态分析**,而 CommonJS 类型的模块则不能。因此,CommonJS 类型的模块 lodash,在无论哪种引入方式下都不能实现 Tree Shaking,而需要依赖第三方提供的插件(例如 babel-plugin-lodash 等)才能实现动态删除无效代码。而 ES6 风格的模块 lodash-es,则可以进行 Tree Shaking 优化。 2. **引入方式** :以 default 方式引入的模块,无法被 Tree Shaking;而引入单个导出对象的方式,无论是使用 import * as xxx 的语法,还是 import {xxx} 的语法,都可以进行 Tree Shaking。 3\. **sideEffects** :在 Webpack 4 中,会根据依赖模块 package.json 中的 sideEffects 属性来确认对应的依赖包代码是否会产生副作用。只有 sideEffects 为 false 的依赖包(或不在 sideEffects 对应数组中的文件),才可以实现安全移除未使用代码的功能。在上面的例子中,如果我们查看 lodash-es 的 package.json 文件,可以看到其中包含了 "sideEffects":false 的描述。此外,在 Webpack 配置的加载器规则和优化配置项中,分别有 rule.sideEffects(默认为 false)和 optimization.sideEffects(默认为 true)选项,前者指代在要处理的模块中是否有副作用,后者指代在优化过程中是否遵循依赖模块的副作用描述。尤其前者,常用于对 CSS 文件模块开启副作用模式,以防止被移除。 4. **Babel** :在 Babel 7 之前的 **babel-preset-env** 中,modules 的默认选项为 ' **commonjs** ',因此在使用 babel 处理模块时,即使模块本身是 ES6 风格的,也会在转换过程中,因为被转换而导致无法在后续优化阶段应用 Tree Shaking。而在 Babel 7 之后的 @babel/preset-env 中,modules 选项默认为 ‘ **auto** ’,它的含义是对 ES6 风格的模块不做转换(等同于 modules: false),而将其他类型的模块默认转换为 CommonJS 风格。因此我们会看到,后者即使经过 babel 处理,也能应用 Tree Shaking。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25413\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25413\350\256\262.md" index 1925a0c70..23baaf36d 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25413\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25413\350\256\262.md" @@ -85,7 +85,7 @@ module: { ![Drawing 4.png](assets/CgqCHl9kXOqAGqBaAAB8XJNiH2c187.png) ![Drawing 5.png](assets/Ciqc1F9kXO-Ae1fcAABt0doSQD0218.png) -主要原因是 babel-loader 中的 **缓存信息较少** ,而 cache-loader 中存储的 **Buffer 形式的数据处理效率更高** 。下面的示例代码,是 babel-loader 和 cache-loader 入口模块的缓存信息对比: +主要原因是 babel-loader 中的 **缓存信息较少**,而 cache-loader 中存储的 **Buffer 形式的数据处理效率更高** 。下面的示例代码,是 babel-loader 和 cache-loader 入口模块的缓存信息对比: ```python //babel-loader中的缓存数据 @@ -112,13 +112,13 @@ TerserWebpackPlugin 插件的效果在本节课的开头部分我们已经演示 ### 缓存的失效 -尽管上面示例所显示的再次构建时间要比初次构建时间快很多,但前提是两次构建没有任何代码发生变化,也就是说,最佳效果是在缓存完全命中的情况下。而现实中,通常需要重新构建的原因是代码发生了变化。因此 **如何最大程度地让缓存命中** ,成为我们选择缓存方案后首先要考虑的事情。 +尽管上面示例所显示的再次构建时间要比初次构建时间快很多,但前提是两次构建没有任何代码发生变化,也就是说,最佳效果是在缓存完全命中的情况下。而现实中,通常需要重新构建的原因是代码发生了变化。因此 **如何最大程度地让缓存命中**,成为我们选择缓存方案后首先要考虑的事情。 #### 缓存标识符发生变化导致的缓存失效 -在上面介绍的支持缓存的 Loader 和插件中,会根据一些固定字段的值加上所处理的模块或 Chunk 的数据 hash 值来生成对应缓存的标识符,例如特定依赖包的版本、对应插件的配置项信息、环境变量等。一旦其中的值发生变化,对应缓存标识符就会发生改变。这也意味着对应工具中, **所有之前的缓存都将失效** 。因此,通常情况下我们需要尽可能少地变更会影响到缓存标识符生成的字段。 +在上面介绍的支持缓存的 Loader 和插件中,会根据一些固定字段的值加上所处理的模块或 Chunk 的数据 hash 值来生成对应缓存的标识符,例如特定依赖包的版本、对应插件的配置项信息、环境变量等。一旦其中的值发生变化,对应缓存标识符就会发生改变。这也意味着对应工具中,**所有之前的缓存都将失效** 。因此,通常情况下我们需要尽可能少地变更会影响到缓存标识符生成的字段。 -其中 **尤其需要注意的是** ,在许多项目的集成构建环境中,特定依赖包由于安装时所生成的语义化版本,导致构建版本时常自动更新,并造成缓存失效。因此,建议大家还是在使用缓存时根据项目的构建使用场景来合理设置对应缓存标识符的计算属性,从而尽可能地减少因为标识符变化而导致缓存失效的情况。 +其中 **尤其需要注意的是**,在许多项目的集成构建环境中,特定依赖包由于安装时所生成的语义化版本,导致构建版本时常自动更新,并造成缓存失效。因此,建议大家还是在使用缓存时根据项目的构建使用场景来合理设置对应缓存标识符的计算属性,从而尽可能地减少因为标识符变化而导致缓存失效的情况。 #### 编译阶段的缓存失效 @@ -132,7 +132,7 @@ TerserWebpackPlugin 插件的效果在本节课的开头部分我们已经演示 之所以会出现这样的结果,是因为,尽管在模块编译阶段每个模块是单独执行编译的,但是当进入到代码压缩环节时,各模块已经被组织到了相关联的 Chunk 中。如上面的示例,4 个模块最后只生成了一个 Chunk,任何一个模块发生变化都会导致整个 Chunk 的内容发生变化,而使之前保存的缓存失效。 -在知道了失效原因后, **对应的优化思路也就显而易见了** :尽可能地把那些不变的处理成本高昂的模块打入单独的 Chunk 中。这就涉及了 Webpack 中的分包配置——[splitChunks](https://webpack.js.org/configuration/optimization/#optimizationsplitchunks)。 +在知道了失效原因后,**对应的优化思路也就显而易见了** :尽可能地把那些不变的处理成本高昂的模块打入单独的 Chunk 中。这就涉及了 Webpack 中的分包配置——[splitChunks](https://webpack.js.org/configuration/optimization/#optimizationsplitchunks)。 #### 使用 splitChunks 优化缓存利用率 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25414\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25414\350\256\262.md" index 90c6dfc88..e720a8645 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25414\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25414\350\256\262.md" @@ -30,7 +30,7 @@ #### cache 配置 -仔细查阅 Webpack 的配置项文档,会在菜单最下方的“其他选项”一栏中找到 [cache](https://v4.webpack.js.org/configuration/other-options/#cache) 选项(需要注意的是我们查阅的是 **Webpack 4 版本的文档** ,Webpack 5 中这一选项会有大的改变,会在下一节课中展开讨论)。这一选项的值有两种类型:布尔值和对象类型。一般情况下默认为 **false** ,即不使用缓存,但在开发模式开启 watch 配置的情况下,cache 的默认值变更为 **true** 。此外,如果 cache 传值为对象类型,则表示使用该对象来作为缓存对象,这往往用于多个编译器 compiler 的调用情况。 +仔细查阅 Webpack 的配置项文档,会在菜单最下方的“其他选项”一栏中找到 [cache](https://v4.webpack.js.org/configuration/other-options/#cache) 选项(需要注意的是我们查阅的是 **Webpack 4 版本的文档**,Webpack 5 中这一选项会有大的改变,会在下一节课中展开讨论)。这一选项的值有两种类型:布尔值和对象类型。一般情况下默认为 **false**,即不使用缓存,但在开发模式开启 watch 配置的情况下,cache 的默认值变更为 **true** 。此外,如果 cache 传值为对象类型,则表示使用该对象来作为缓存对象,这往往用于多个编译器 compiler 的调用情况。 下面我们就来看一下,在生产模式下,如果watch 和 cache 都为 true,结果会如何(npm run build:watch-cache)?如下面的图片所示: @@ -83,8 +83,8 @@ compiler.hooks.thisCompilation.tap("CachePlugin", compilation => { 而在 Compilation.js 中,运用 cache 的地方有两处: -1. 在 **编译阶段添加模块时** ,若命中缓存 **module** ,则直接跳过该模块的编译过程(与 cache-loader 等作用于加载器的缓存不同,此处的缓存可直接跳过 Webpack 内置的编译阶段)。 -1. 在创建 Chunk 产物代码阶段,若命中缓存 **Chunk** ,则直接跳过该 Chunk 的产物代码生成过程。 +1. 在 **编译阶段添加模块时**,若命中缓存 **module**,则直接跳过该模块的编译过程(与 cache-loader 等作用于加载器的缓存不同,此处的缓存可直接跳过 Webpack 内置的编译阶段)。 +1. 在创建 Chunk 产物代码阶段,若命中缓存 **Chunk**,则直接跳过该 Chunk 的产物代码生成过程。 如下面的代码所示: @@ -142,9 +142,9 @@ createChunkAssets() { 而生产环境下的构建通常在集成部署系统中进行。对于管理多项目的构建系统而言,构建过程是任务式的:任务结束后即结束进程并回收系统资源。对于这样的系统而言,增量构建所需的保留进程与长时间占用内存,通常都是 **不可接受的** 。 -因此,基于内存的缓存数据注定无法运用到生产环境中。要想在生产环境下提升构建速度, **首要条件是将缓存写入到文件系统中** 。只有将文件系统中的缓存数据持久化,才能脱离对保持进程的依赖,你只需要在每次构建时将缓存数据读取到内存中进行处理即可。事实上,这也是上一课时中讲到的那些 Loader 与插件中的缓存数据的存储方式。 +因此,基于内存的缓存数据注定无法运用到生产环境中。要想在生产环境下提升构建速度,**首要条件是将缓存写入到文件系统中** 。只有将文件系统中的缓存数据持久化,才能脱离对保持进程的依赖,你只需要在每次构建时将缓存数据读取到内存中进行处理即可。事实上,这也是上一课时中讲到的那些 Loader 与插件中的缓存数据的存储方式。 -遗憾的是,Webpack 4 中的 cache 配置 **只支持基于内存的缓存** ,并不支持文件系统的缓存。因此,我们只能通过上节课讲到的一些支持缓存的第三方处理插件将局部的构建环节应用“ **增量处理** ”。 +遗憾的是,Webpack 4 中的 cache 配置 **只支持基于内存的缓存**,并不支持文件系统的缓存。因此,我们只能通过上节课讲到的一些支持缓存的第三方处理插件将局部的构建环节应用“ **增量处理** ”。 不过好消息是 Webpack 5 中 **正式支持基于文件系统的持久化缓存** (Persistent Cache)。我们会在下一课时详细讨论包括这一特性在内的 Webpack 5 中的优化点。 diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25415\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25415\350\256\262.md" index 7171355f9..f43dc83da 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25415\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25415\350\256\262.md" @@ -63,7 +63,7 @@ Webpack 5 会跟踪每个模块的依赖项:fileDependencies、contextDependen 它的默认选项是{defaultWebpack: \["webpack/lib"\]}。这一选项的含义是,当 node_modules 中的 Webpack 或 Webpack 的依赖项(例如 watchpack 等)发生变化时,当前的构建缓存即失效。 -上述选项是默认内置的,无须写在项目配置文件中。配置文件中的 buildDenpendencies 还支持增加另一种选项 {config: \[\_\_filename\]},它的作用是当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。 **version** 第二种配置是 cache.version。当配置文件和代码都没有发生变化,但是构建的外部依赖(如环境变量)发生变化时,预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。 **name** +上述选项是默认内置的,无须写在项目配置文件中。配置文件中的 buildDenpendencies 还支持增加另一种选项 {config: \[\_\_filename\]},它的作用是当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。**version** 第二种配置是 cache.version。当配置文件和代码都没有发生变化,但是构建的外部依赖(如环境变量)发生变化时,预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。**name** 缓存的名称除了作为默认的缓存目录下的子目录名称外,也起到区分缓存数据的作用。例如,可以传入 cache: {name: process.env.NODE_ENV}。这里有两点需要补充说明: @@ -74,7 +74,7 @@ Webpack 5 会跟踪每个模块的依赖项:fileDependencies、contextDependen 除了上述介绍的配置项外,cache 还支持其他属性:managedPath、hashAlgorithm、store、idleTimeout 等,具体功能可以通过[官方文档](https://webpack.js.org/configuration/other-options/#cache)进行查询。 -此外,在 Webpack 4 中,部分插件是默认启用缓存功能的(例如压缩代码的 Terser 插件等),项目在生产环境下构建时,可能无意识地享受缓存带来的效率提升,但是在 Webpack 5 中则不行。无论是否设置 cache 配置,Webpack 5 都将忽略各插件的缓存设置(例如 [TerserWebpackPlugin](https://webpack.js.org/plugins/terser-webpack-plugin/#cache)),而由引擎自身提供构建各环节的缓存读写逻辑。 **因此,项目在迁移到 Webpack 5 时都需要通过上面介绍的 cache 属性来单独配置缓存。** +此外,在 Webpack 4 中,部分插件是默认启用缓存功能的(例如压缩代码的 Terser 插件等),项目在生产环境下构建时,可能无意识地享受缓存带来的效率提升,但是在 Webpack 5 中则不行。无论是否设置 cache 配置,Webpack 5 都将忽略各插件的缓存设置(例如 [TerserWebpackPlugin](https://webpack.js.org/plugins/terser-webpack-plugin/#cache)),而由引擎自身提供构建各环节的缓存读写逻辑。**因此,项目在迁移到 Webpack 5 时都需要通过上面介绍的 cache 属性来单独配置缓存。** ### Tree Shaking diff --git "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25416\350\256\262.md" "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25416\350\256\262.md" index dc8d819f2..a2fa50faf 100644 --- "a/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25416\350\256\262.md" +++ "b/docs/Other/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\262\276\350\256\262/\347\254\25416\350\256\262.md" @@ -17,9 +17,9 @@ 什么是无包构建呢?这是一个与基于模块化打包的构建方案相对的概念。 -在“ 第 9 课时|构建总览:前端构建工具的演进”中谈到过,目前主流的构建工具,例如 Webpack、Rollup 等都是基于一个或多个入口点模块,通过依赖分析将有依赖关系的模块 **打包到一起** ,最后形成少数几个产物代码包,因此这些工具也被称为 **打包工具** 。只不过,这些工具的构建过程除了打包外,还包括了模块编译和代码优化等,因此称为打包式构建工具或许更恰当。 +在“ 第 9 课时|构建总览:前端构建工具的演进”中谈到过,目前主流的构建工具,例如 Webpack、Rollup 等都是基于一个或多个入口点模块,通过依赖分析将有依赖关系的模块 **打包到一起**,最后形成少数几个产物代码包,因此这些工具也被称为 **打包工具** 。只不过,这些工具的构建过程除了打包外,还包括了模块编译和代码优化等,因此称为打包式构建工具或许更恰当。 -而 **无包构建** 是指这样一类构建方式:在构建时只需处理模块的编译而 **无须打包** ,把模块间的 **依赖关系完全交给浏览器来处理。** 浏览器会加载入口模块,分析依赖后,再通过网络请求加载被依赖的模块。通过这样的方式简化构建时的处理过程,提升构建效率。 +而 **无包构建** 是指这样一类构建方式:在构建时只需处理模块的编译而 **无须打包**,把模块间的 **依赖关系完全交给浏览器来处理。** 浏览器会加载入口模块,分析依赖后,再通过网络请求加载被依赖的模块。通过这样的方式简化构建时的处理过程,提升构建效率。 这种通过浏览器原生的模块进行解析的方式又称为 Native-ESM(Native ES Module)。下面我们就通过一个简单示例来展示这种基于浏览器的模块加载过程([16_nobundle](https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/16_nobundle)/simple-esm),如下面的代码和图片所示: @@ -93,7 +93,7 @@ npm run dev #### 对导入模块的解析 -**对 HTML 文件的预处理** 当启动 Vite 时,会通过 [serverPluginHtml](https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginHtml.ts).ts 注入 [/vite/client](https://github.com/vitejs/vite/blob/master/src/client/client.ts) 运行时的依赖模块,该模块用于处理热更新,以及提供更新 CSS 的方法 updateStyle。 **对外部依赖包的解析** 首先是对不带路径前缀的外部依赖包(也称为 **Bare Modules** )的解析,例如上图中在示例源代码中导入了 'vue' 模块,但是在浏览器的网络请求中变为了请求 /@module/vue。 +**对 HTML 文件的预处理** 当启动 Vite 时,会通过 [serverPluginHtml](https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginHtml.ts).ts 注入 [/vite/client](https://github.com/vitejs/vite/blob/master/src/client/client.ts) 运行时的依赖模块,该模块用于处理热更新,以及提供更新 CSS 的方法 updateStyle。**对外部依赖包的解析** 首先是对不带路径前缀的外部依赖包(也称为 **Bare Modules** )的解析,例如上图中在示例源代码中导入了 'vue' 模块,但是在浏览器的网络请求中变为了请求 /@module/vue。 这个解析过程在 Vite 中主要通过三个文件来处理: @@ -101,7 +101,7 @@ npm run dev - [serverPluginModuleRewrite](https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginModuleRewrite.ts).ts 负责把源码中的 bare modules 加上 /@module/ 前缀。 - [serverPluginModuleResolve](https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginModuleResolve.ts).ts 负责解析加上前缀后的模块。 -**对 Vue文件的解析** 对 Vue 文件的解析是通过 [serverPluginVue](https://github.com/vitejs/vite/blob/master/src/node/server/serverPluginVue.ts).ts 处理的,分离出 Vue 代码中的 script/template/style 代码片段,并分别转换为 JS 模块,然后将 template/style 模块的 import写到script 模块代码的头部。因此在浏览器访问时,一个 Vue 源代码文件会分裂为 2~3 的关联请求(例如上面的 /src/App.vue 和 /src/App.vue?type=template,如果 App.vue 中包含 `